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:
Matt Hess
2026-02-10 06:31:28 +00:00
parent 4eb3062d9c
commit c931290f10
6 changed files with 154 additions and 9 deletions
+33
View File
@@ -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",
+1
View File
@@ -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
+79
View File
@@ -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)
+6
View File
@@ -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
View File
@@ -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');
}