Add Argon2id to Rust crypto crate, fix CARROT-era mining address
- Add argon2 = "0.5" dependency and salvium_argon2id FFI function for
native key derivation (wallet PIN → encryption key). Eliminates the
pure-JS Argon2id bottleneck on QuickJS/mobile (~minutes → ~1-2s).
- C header and JSI backend wired for Flutter FFI integration.
- 33/33 Rust tests pass (2 new: RFC-params test + bad-params test).
- Fix testnet suite: switch from legacy to CARROT address at HF10
boundary (height 1100) for mining. Phase 6 CARROT TX tests passing
(3 transfers, B→A, stake, burn all succeeded on-chain).
This commit is contained in:
Generated
+33
@@ -2,6 +2,18 @@
|
||||
# It is not intended for manual editing.
|
||||
version = 4
|
||||
|
||||
[[package]]
|
||||
name = "argon2"
|
||||
version = "0.5.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3c3610892ee6e0cbce8ae2700349fcf8f98adb0dbfbee85aec3c9179d29cc072"
|
||||
dependencies = [
|
||||
"base64ct",
|
||||
"blake2",
|
||||
"cpufeatures",
|
||||
"password-hash",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "arrayref"
|
||||
version = "0.3.9"
|
||||
@@ -32,6 +44,15 @@ version = "1.8.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2af50177e190e07a26ab74f8b1efbfe2ef87da2116221318cb1c2e82baf7de06"
|
||||
|
||||
[[package]]
|
||||
name = "blake2"
|
||||
version = "0.10.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "46502ad458c9a52b69d4d4d32775c788b7a1b85e8bc9d482d92250fc0e3f8efe"
|
||||
dependencies = [
|
||||
"digest",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "blake2b_simd"
|
||||
version = "1.0.4"
|
||||
@@ -354,6 +375,17 @@ dependencies = [
|
||||
"sha2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "password-hash"
|
||||
version = "0.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "346f04948ba92c43e8469c1ee6736c7563d71012b17d40745260fe106aac2166"
|
||||
dependencies = [
|
||||
"base64ct",
|
||||
"rand_core",
|
||||
"subtle",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pem-rfc7468"
|
||||
version = "0.7.0"
|
||||
@@ -467,6 +499,7 @@ checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d"
|
||||
name = "salvium-crypto"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"argon2",
|
||||
"blake2b_simd",
|
||||
"curve25519-dalek",
|
||||
"der",
|
||||
|
||||
@@ -16,6 +16,7 @@ curve25519-dalek = { version = "4", features = ["alloc"] }
|
||||
# Oracle signature verification (SHA-256, ECDSA P-256, DSA)
|
||||
# Only for native targets — WASM uses the JS crypto shim
|
||||
sha2 = "0.10"
|
||||
argon2 = "0.5"
|
||||
|
||||
[target.'cfg(not(target_arch = "wasm32"))'.dependencies]
|
||||
p256 = { version = "0.13", features = ["ecdsa"] }
|
||||
|
||||
@@ -153,6 +153,21 @@ int32_t salvium_verify_signature(
|
||||
const uint8_t *signature, size_t sig_len,
|
||||
const uint8_t *pubkey_der, size_t key_len);
|
||||
|
||||
/* ─── Key Derivation ───────────────────────────────────────────────────────── */
|
||||
|
||||
/**
|
||||
* Argon2id key derivation.
|
||||
* Returns 0 on success, -1 on error.
|
||||
*/
|
||||
int32_t salvium_argon2id(
|
||||
const uint8_t *password, size_t password_len,
|
||||
const uint8_t *salt, size_t salt_len,
|
||||
uint32_t t_cost, /* time cost (iterations/passes) */
|
||||
uint32_t m_cost, /* memory cost in KiB */
|
||||
uint32_t parallelism, /* number of lanes */
|
||||
size_t out_len, /* desired output length in bytes (typically 32) */
|
||||
uint8_t *out);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
@@ -385,6 +385,40 @@ pub unsafe extern "C" fn salvium_verify_signature(
|
||||
crate::verify_signature(message, signature, pubkey_der)
|
||||
}
|
||||
|
||||
// ─── Key Derivation ─────────────────────────────────────────────────────────
|
||||
|
||||
/// Argon2id key derivation.
|
||||
/// Returns 0 on success, -1 on error.
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn salvium_argon2id(
|
||||
password: *const u8,
|
||||
password_len: usize,
|
||||
salt: *const u8,
|
||||
salt_len: usize,
|
||||
t_cost: u32,
|
||||
m_cost: u32,
|
||||
parallelism: u32,
|
||||
out_len: usize,
|
||||
out: *mut u8,
|
||||
) -> i32 {
|
||||
use argon2::{Argon2, Algorithm, Version, Params};
|
||||
|
||||
let password = slice::from_raw_parts(password, password_len);
|
||||
let salt = slice::from_raw_parts(salt, salt_len);
|
||||
let out = std::slice::from_raw_parts_mut(out, out_len);
|
||||
|
||||
let params = match Params::new(m_cost, t_cost, parallelism, Some(out_len)) {
|
||||
Ok(p) => p,
|
||||
Err(_) => return -1,
|
||||
};
|
||||
let argon2 = Argon2::new(Algorithm::Argon2id, Version::V0x13, params);
|
||||
|
||||
match argon2.hash_password_into(password, salt, out) {
|
||||
Ok(_) => 0,
|
||||
Err(_) => -1,
|
||||
}
|
||||
}
|
||||
|
||||
// ─── Tests ──────────────────────────────────────────────────────────────────
|
||||
|
||||
#[cfg(test)]
|
||||
@@ -798,6 +832,51 @@ mod tests {
|
||||
assert_eq!(rc, 0); // invalid → 0
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_argon2id() {
|
||||
// Argon2id test: same params as RFC 9106 Section 4 but without
|
||||
// secret/AD (which our FFI doesn't expose, matching wallet usage).
|
||||
let password = [0x01u8; 32];
|
||||
let salt = [0x02u8; 16];
|
||||
// Known-good output from argon2 crate v0.5 (Argon2id, t=3, m=32, p=4)
|
||||
let expected: [u8; 32] = [
|
||||
0x03, 0xaa, 0xb9, 0x65, 0xc1, 0x20, 0x01, 0xc9,
|
||||
0xd7, 0xd0, 0xd2, 0xde, 0x33, 0x19, 0x2c, 0x04,
|
||||
0x94, 0xb6, 0x84, 0xbb, 0x14, 0x81, 0x96, 0xd7,
|
||||
0x3c, 0x1d, 0xf1, 0xac, 0xaf, 0x6d, 0x0c, 0x2e,
|
||||
];
|
||||
let mut out = [0u8; 32];
|
||||
let rc = unsafe {
|
||||
salvium_argon2id(
|
||||
password.as_ptr(), password.len(),
|
||||
salt.as_ptr(), salt.len(),
|
||||
3, // t_cost
|
||||
32, // m_cost (32 KiB — tiny, just for test)
|
||||
4, // parallelism
|
||||
32, // out_len
|
||||
out.as_mut_ptr(),
|
||||
)
|
||||
};
|
||||
assert_eq!(rc, 0);
|
||||
assert_eq!(out, expected, "Argon2id FFI output mismatch");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_argon2id_bad_params() {
|
||||
// Invalid params (m_cost=0) should return -1
|
||||
let password = b"test";
|
||||
let salt = [0u8; 16];
|
||||
let mut out = [0u8; 32];
|
||||
let rc = unsafe {
|
||||
salvium_argon2id(
|
||||
password.as_ptr(), password.len(),
|
||||
salt.as_ptr(), salt.len(),
|
||||
1, 0, 1, 32, out.as_mut_ptr(),
|
||||
)
|
||||
};
|
||||
assert_eq!(rc, -1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_verify_signature_ecdsa_p256() {
|
||||
// Testnet oracle public key (ECDSA P-256, DER-encoded SPKI)
|
||||
|
||||
@@ -135,4 +135,10 @@ export class JsiCryptoBackend {
|
||||
// Native module returns 1 for valid, 0 for invalid
|
||||
return this.native.verifySignature(message, signature, pubkeyDer) === 1;
|
||||
}
|
||||
|
||||
// ─── Key Derivation ─────────────────────────────────────────────────────
|
||||
|
||||
async argon2id(password, salt, tCost, mCost, parallelism, outLen) {
|
||||
return this.native.argon2id(password, salt, tCost, mCost, parallelism, outLen);
|
||||
}
|
||||
}
|
||||
|
||||
+20
-9
@@ -395,18 +395,28 @@ async function phase4_sal1TxTests(daemon, daemonUrl, walletA, walletB) {
|
||||
async function phase5_mineToCarrot(daemon, daemonUrl, walletA) {
|
||||
console.log(`\n═══ Phase 5: Mine to CARROT (target height ${TARGET.CARROT_MATURE}) ═══`);
|
||||
const t0 = performance.now();
|
||||
const address = walletA.getLegacyAddress();
|
||||
const legacyAddr = walletA.getLegacyAddress();
|
||||
const carrotAddr = walletA.getCarrotAddress();
|
||||
|
||||
// Rust fill to CARROT activation + maturity
|
||||
await mineTo(daemon, TARGET.CARROT_MATURE, address, daemonUrl, 'rust');
|
||||
// Mine up to HF10 boundary (1100) with legacy address
|
||||
const HF10_HEIGHT = 1100;
|
||||
const currentHeight = await getDaemonHeight(daemon);
|
||||
if (currentHeight < HF10_HEIGHT) {
|
||||
console.log(` Mining to HF10 boundary (${HF10_HEIGHT}) with legacy address...`);
|
||||
await mineTo(daemon, HF10_HEIGHT, legacyAddr, daemonUrl, 'rust');
|
||||
}
|
||||
|
||||
// After HF10: daemon requires CARROT address for coinbase outputs
|
||||
console.log(` Switching to CARROT address for post-HF10 mining`);
|
||||
await mineTo(daemon, TARGET.CARROT_MATURE, carrotAddr, daemonUrl, 'rust');
|
||||
|
||||
// WASM probe 2: mine 3 blocks in the CARROT era to validate WASM still works
|
||||
const currentHeight = await getDaemonHeight(daemon);
|
||||
const postHeight = await getDaemonHeight(daemon);
|
||||
console.log('\n ── WASM Probe 2 (CARROT era) ──');
|
||||
await mineTo(daemon, currentHeight + WASM_PROBE_BLOCKS, address, daemonUrl, 'wasm');
|
||||
await mineTo(daemon, postHeight + WASM_PROBE_BLOCKS, carrotAddr, daemonUrl, 'wasm');
|
||||
|
||||
// Mine a few more for maturity
|
||||
await mineTo(daemon, (await getDaemonHeight(daemon)) + MATURITY_BLOCKS + 2, address, daemonUrl, 'rust');
|
||||
await mineTo(daemon, (await getDaemonHeight(daemon)) + MATURITY_BLOCKS + 2, carrotAddr, daemonUrl, 'rust');
|
||||
|
||||
const finalHeight = await getDaemonHeight(daemon);
|
||||
const hfVer = getHfVersionForHeight(finalHeight, NETWORK_ID.TESTNET);
|
||||
@@ -429,8 +439,8 @@ async function phase6_carrotTxTests(daemon, daemonUrl, walletA, walletB) {
|
||||
await doTransfer(walletA, walletB, sal(2), 'CARROT A→B 2 SAL');
|
||||
await doTransfer(walletA, walletB, sal(5), 'CARROT A→B 5 SAL');
|
||||
|
||||
// Mine maturity so B can spend
|
||||
const address = walletA.getLegacyAddress();
|
||||
// Mine maturity so B can spend (CARROT address required post-HF10)
|
||||
const address = walletA.getCarrotAddress();
|
||||
await mineTo(daemon, (await getDaemonHeight(daemon)) + MATURITY_BLOCKS, address, daemonUrl, 'rust');
|
||||
await syncWallet(walletA, daemon, 'A');
|
||||
await syncWallet(walletB, daemon, 'B');
|
||||
@@ -551,7 +561,8 @@ async function phaseExtra_mine(daemon, daemonUrl, walletA) {
|
||||
return;
|
||||
}
|
||||
console.log(`\n═══ Extra: Mine to ${TARGET.FINAL} ═══`);
|
||||
const address = walletA.getLegacyAddress();
|
||||
// Post-HF10: use CARROT address for coinbase outputs
|
||||
const address = currentHeight >= 1100 ? walletA.getCarrotAddress() : walletA.getLegacyAddress();
|
||||
await mineTo(daemon, TARGET.FINAL, address, daemonUrl, 'rust');
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user