Wire oracle.js to crypto provider, add QuickJS compatibility
- Oracle: replace Node.js crypto (createHash/createVerify) with crypto
provider's verifySignature, eliminating the 'crypto' module dependency.
verifyPricingRecordSignature and validatePricingRecord are now async.
- Rust crate: add SHA-256 (all targets) and verify_signature with ECDSA
P-256 + DSA support (native-only) to salvium-crypto. FFI bindings and
C header updated. All 31 Rust tests pass.
- Crypto backends: add sha256/verifySignature to JS (noble + WebCrypto
fallback), WASM (noble + WebCrypto fallback), and JSI (native FFI).
- QuickJS compat (6 issues):
· catch {} → catch (_e) {} across 10 files
· Buffer → bytesToHex/hexToBytes in consensus.js and validation.js
· AbortController → Promise.race timeout in rpc/client, connection-
manager, and oracle
· URLSearchParams → manual query string in rpc/client and oracle
· Promise.allSettled → Promise.all wrapper in connection-manager
· btoa → _toBase64 with manual fallback in rpc/client
· pemToDer base64 decoder with 3-tier fallback (Buffer/atob/manual)
- Fix GammaPicker exclusion zone (60 → 10) matching C++ gamma_picker
- Fix testnet TARGET heights for coinbase maturity
This commit is contained in:
Generated
+429
@@ -14,6 +14,24 @@ version = "0.7.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50"
|
||||
|
||||
[[package]]
|
||||
name = "autocfg"
|
||||
version = "1.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8"
|
||||
|
||||
[[package]]
|
||||
name = "base16ct"
|
||||
version = "0.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4c7f02d4ea65f2c1853089ffd8d2787bdbc63de2f0d29dedbcf8ccdfa0ccd4cf"
|
||||
|
||||
[[package]]
|
||||
name = "base64ct"
|
||||
version = "1.8.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2af50177e190e07a26ab74f8b1efbfe2ef87da2116221318cb1c2e82baf7de06"
|
||||
|
||||
[[package]]
|
||||
name = "blake2b_simd"
|
||||
version = "1.0.4"
|
||||
@@ -25,6 +43,15 @@ dependencies = [
|
||||
"constant_time_eq",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "block-buffer"
|
||||
version = "0.10.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71"
|
||||
dependencies = [
|
||||
"generic-array",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "bumpalo"
|
||||
version = "3.19.1"
|
||||
@@ -37,6 +64,12 @@ version = "1.0.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801"
|
||||
|
||||
[[package]]
|
||||
name = "const-oid"
|
||||
version = "0.9.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8"
|
||||
|
||||
[[package]]
|
||||
name = "constant_time_eq"
|
||||
version = "0.4.2"
|
||||
@@ -58,6 +91,28 @@ version = "0.2.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "460fbee9c2c2f33933d720630a6a0bac33ba7053db5344fac858d4b8952d77d5"
|
||||
|
||||
[[package]]
|
||||
name = "crypto-bigint"
|
||||
version = "0.5.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0dc92fb57ca44df6db8059111ab3af99a63d5d0f8375d9972e319a379c6bab76"
|
||||
dependencies = [
|
||||
"generic-array",
|
||||
"rand_core",
|
||||
"subtle",
|
||||
"zeroize",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "crypto-common"
|
||||
version = "0.1.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3"
|
||||
dependencies = [
|
||||
"generic-array",
|
||||
"typenum",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "curve25519-dalek"
|
||||
version = "4.1.3"
|
||||
@@ -84,24 +139,258 @@ dependencies = [
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "der"
|
||||
version = "0.7.10"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e7c1832837b905bbfb5101e07cc24c8deddf52f93225eee6ead5f4d63d53ddcb"
|
||||
dependencies = [
|
||||
"const-oid",
|
||||
"pem-rfc7468",
|
||||
"zeroize",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "digest"
|
||||
version = "0.10.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292"
|
||||
dependencies = [
|
||||
"block-buffer",
|
||||
"const-oid",
|
||||
"crypto-common",
|
||||
"subtle",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "dsa"
|
||||
version = "0.6.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "48bc224a9084ad760195584ce5abb3c2c34a225fa312a128ad245a6b412b7689"
|
||||
dependencies = [
|
||||
"digest",
|
||||
"num-bigint-dig",
|
||||
"num-traits",
|
||||
"pkcs8",
|
||||
"rfc6979",
|
||||
"sha2",
|
||||
"signature",
|
||||
"zeroize",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ecdsa"
|
||||
version = "0.16.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ee27f32b5c5292967d2d4a9d7f1e0b0aed2c15daded5a60300e4abb9d8020bca"
|
||||
dependencies = [
|
||||
"der",
|
||||
"digest",
|
||||
"elliptic-curve",
|
||||
"rfc6979",
|
||||
"signature",
|
||||
"spki",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "elliptic-curve"
|
||||
version = "0.13.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b5e6043086bf7973472e0c7dff2142ea0b680d30e18d9cc40f267efbf222bd47"
|
||||
dependencies = [
|
||||
"base16ct",
|
||||
"crypto-bigint",
|
||||
"digest",
|
||||
"ff",
|
||||
"generic-array",
|
||||
"group",
|
||||
"pem-rfc7468",
|
||||
"pkcs8",
|
||||
"rand_core",
|
||||
"sec1",
|
||||
"subtle",
|
||||
"zeroize",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ff"
|
||||
version = "0.13.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c0b50bfb653653f9ca9095b427bed08ab8d75a137839d9ad64eb11810d5b6393"
|
||||
dependencies = [
|
||||
"rand_core",
|
||||
"subtle",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "fiat-crypto"
|
||||
version = "0.2.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "28dea519a9695b9977216879a3ebfddf92f1c08c05d984f8996aecd6ecdc811d"
|
||||
|
||||
[[package]]
|
||||
name = "generic-array"
|
||||
version = "0.14.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4bb6743198531e02858aeaea5398fcc883e71851fcbcb5a2f773e2fb6cb1edf2"
|
||||
dependencies = [
|
||||
"typenum",
|
||||
"version_check",
|
||||
"zeroize",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "getrandom"
|
||||
version = "0.2.17"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"libc",
|
||||
"wasi",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "group"
|
||||
version = "0.13.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f0f9ef7462f7c099f518d754361858f86d8a07af53ba9af0fe635bbccb151a63"
|
||||
dependencies = [
|
||||
"ff",
|
||||
"rand_core",
|
||||
"subtle",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "hmac"
|
||||
version = "0.12.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e"
|
||||
dependencies = [
|
||||
"digest",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "lazy_static"
|
||||
version = "1.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe"
|
||||
dependencies = [
|
||||
"spin",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "libc"
|
||||
version = "0.2.180"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bcc35a38544a891a5f7c865aca548a982ccb3b8650a5b06d0fd33a10283c56fc"
|
||||
|
||||
[[package]]
|
||||
name = "libm"
|
||||
version = "0.2.16"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b6d2cec3eae94f9f509c767b45932f1ada8350c4bdb85af2fcab4a3c14807981"
|
||||
|
||||
[[package]]
|
||||
name = "num-bigint-dig"
|
||||
version = "0.8.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e661dda6640fad38e827a6d4a310ff4763082116fe217f279885c97f511bb0b7"
|
||||
dependencies = [
|
||||
"lazy_static",
|
||||
"libm",
|
||||
"num-integer",
|
||||
"num-iter",
|
||||
"num-traits",
|
||||
"rand",
|
||||
"smallvec",
|
||||
"zeroize",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "num-integer"
|
||||
version = "0.1.46"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f"
|
||||
dependencies = [
|
||||
"num-traits",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "num-iter"
|
||||
version = "0.1.45"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1429034a0490724d0075ebb2bc9e875d6503c3cf69e235a8941aa757d83ef5bf"
|
||||
dependencies = [
|
||||
"autocfg",
|
||||
"num-integer",
|
||||
"num-traits",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "num-traits"
|
||||
version = "0.2.19"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841"
|
||||
dependencies = [
|
||||
"autocfg",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "once_cell"
|
||||
version = "1.21.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d"
|
||||
|
||||
[[package]]
|
||||
name = "p256"
|
||||
version = "0.13.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c9863ad85fa8f4460f9c48cb909d38a0d689dba1f6f6988a5e3e0d31071bcd4b"
|
||||
dependencies = [
|
||||
"ecdsa",
|
||||
"elliptic-curve",
|
||||
"primeorder",
|
||||
"sha2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pem-rfc7468"
|
||||
version = "0.7.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "88b39c9bfcfc231068454382784bb460aae594343fb030d46e9f50a645418412"
|
||||
dependencies = [
|
||||
"base64ct",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pkcs8"
|
||||
version = "0.10.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f950b2377845cebe5cf8b5165cb3cc1a5e0fa5cfa3e1f7f55707d8fd82e0a7b7"
|
||||
dependencies = [
|
||||
"der",
|
||||
"spki",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ppv-lite86"
|
||||
version = "0.2.21"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9"
|
||||
dependencies = [
|
||||
"zerocopy",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "primeorder"
|
||||
version = "0.13.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "353e1ca18966c16d9deb1c69278edbc5f194139612772bd9537af60ac231e1e6"
|
||||
dependencies = [
|
||||
"elliptic-curve",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "proc-macro2"
|
||||
version = "1.0.106"
|
||||
@@ -120,6 +409,45 @@ dependencies = [
|
||||
"proc-macro2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rand"
|
||||
version = "0.8.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404"
|
||||
dependencies = [
|
||||
"rand_chacha",
|
||||
"rand_core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rand_chacha"
|
||||
version = "0.3.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88"
|
||||
dependencies = [
|
||||
"ppv-lite86",
|
||||
"rand_core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rand_core"
|
||||
version = "0.6.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c"
|
||||
dependencies = [
|
||||
"getrandom",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rfc6979"
|
||||
version = "0.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f8dd2a808d456c4a54e300a23e9f5a67e122c3024119acbfd73e3bf664491cb2"
|
||||
dependencies = [
|
||||
"hmac",
|
||||
"subtle",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rustc_version"
|
||||
version = "0.4.1"
|
||||
@@ -141,16 +469,79 @@ version = "0.1.0"
|
||||
dependencies = [
|
||||
"blake2b_simd",
|
||||
"curve25519-dalek",
|
||||
"der",
|
||||
"dsa",
|
||||
"p256",
|
||||
"sha2",
|
||||
"signature",
|
||||
"spki",
|
||||
"tiny-keccak",
|
||||
"wasm-bindgen",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "sec1"
|
||||
version = "0.7.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d3e97a565f76233a6003f9f5c54be1d9c5bdfa3eccfb189469f11ec4901c47dc"
|
||||
dependencies = [
|
||||
"base16ct",
|
||||
"der",
|
||||
"generic-array",
|
||||
"pkcs8",
|
||||
"subtle",
|
||||
"zeroize",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "semver"
|
||||
version = "1.0.27"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d767eb0aabc880b29956c35734170f26ed551a859dbd361d140cdbeca61ab1e2"
|
||||
|
||||
[[package]]
|
||||
name = "sha2"
|
||||
version = "0.10.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"cpufeatures",
|
||||
"digest",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "signature"
|
||||
version = "2.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "77549399552de45a898a580c1b41d445bf730df867cc44e6c0233bbc4b8329de"
|
||||
dependencies = [
|
||||
"digest",
|
||||
"rand_core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "smallvec"
|
||||
version = "1.15.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03"
|
||||
|
||||
[[package]]
|
||||
name = "spin"
|
||||
version = "0.9.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67"
|
||||
|
||||
[[package]]
|
||||
name = "spki"
|
||||
version = "0.7.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d91ed6c858b01f942cd56b37a94b3e0a1798290327d1236e4d9cf4eaca44d29d"
|
||||
dependencies = [
|
||||
"base64ct",
|
||||
"der",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "subtle"
|
||||
version = "2.6.1"
|
||||
@@ -177,12 +568,30 @@ dependencies = [
|
||||
"crunchy",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "typenum"
|
||||
version = "1.19.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "562d481066bde0658276a35467c4af00bdc6ee726305698a55b86e61d7ad82bb"
|
||||
|
||||
[[package]]
|
||||
name = "unicode-ident"
|
||||
version = "1.0.22"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9312f7c4f6ff9069b165498234ce8be658059c6728633667c526e27dc2cf1df5"
|
||||
|
||||
[[package]]
|
||||
name = "version_check"
|
||||
version = "0.9.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a"
|
||||
|
||||
[[package]]
|
||||
name = "wasi"
|
||||
version = "0.11.1+wasi-snapshot-preview1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b"
|
||||
|
||||
[[package]]
|
||||
name = "wasm-bindgen"
|
||||
version = "0.2.108"
|
||||
@@ -228,6 +637,26 @@ dependencies = [
|
||||
"unicode-ident",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zerocopy"
|
||||
version = "0.8.39"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "db6d35d663eadb6c932438e763b262fe1a70987f9ae936e60158176d710cae4a"
|
||||
dependencies = [
|
||||
"zerocopy-derive",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zerocopy-derive"
|
||||
version = "0.8.39"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4122cd3169e94605190e77839c9a40d40ed048d305bfdc146e7df40ab0f3e517"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zeroize"
|
||||
version = "1.8.2"
|
||||
|
||||
@@ -13,6 +13,17 @@ blake2b_simd = "1.0"
|
||||
wasm-bindgen = "0.2"
|
||||
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"
|
||||
|
||||
[target.'cfg(not(target_arch = "wasm32"))'.dependencies]
|
||||
p256 = { version = "0.13", features = ["ecdsa"] }
|
||||
dsa = "0.6"
|
||||
spki = "0.7"
|
||||
signature = "2"
|
||||
der = "0.7"
|
||||
|
||||
[features]
|
||||
default = []
|
||||
ffi = []
|
||||
|
||||
@@ -135,6 +135,24 @@ int32_t salvium_zero_commit(
|
||||
int32_t salvium_gen_commitment_mask(
|
||||
const uint8_t *secret /* 32 */, uint8_t *out /* 32 */);
|
||||
|
||||
/* ─── Oracle Signature Verification ───────────────────────────────────────── */
|
||||
|
||||
/** SHA-256 hash */
|
||||
int32_t salvium_sha256(
|
||||
const uint8_t *data, size_t data_len,
|
||||
uint8_t *out /* 32 */);
|
||||
|
||||
/**
|
||||
* Verify signature against DER-encoded SPKI public key.
|
||||
* Supports ECDSA P-256 (testnet) and DSA (mainnet).
|
||||
* Message is hashed with SHA-256 internally.
|
||||
* Returns 1 for valid, 0 for invalid/error.
|
||||
*/
|
||||
int32_t salvium_verify_signature(
|
||||
const uint8_t *message, size_t msg_len,
|
||||
const uint8_t *signature, size_t sig_len,
|
||||
const uint8_t *pubkey_der, size_t key_len);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
@@ -354,6 +354,37 @@ pub unsafe extern "C" fn salvium_gen_commitment_mask(
|
||||
0
|
||||
}
|
||||
|
||||
// ─── Oracle Signature Verification ──────────────────────────────────────────
|
||||
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn salvium_sha256(
|
||||
data: *const u8,
|
||||
data_len: usize,
|
||||
out: *mut u8,
|
||||
) -> i32 {
|
||||
let data = slice::from_raw_parts(data, data_len);
|
||||
let result = crate::sha256(data);
|
||||
ptr::copy_nonoverlapping(result.as_ptr(), out, 32);
|
||||
0
|
||||
}
|
||||
|
||||
/// Verify a signature against a DER-encoded SPKI public key.
|
||||
/// Returns 1 for valid, 0 for invalid/error.
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn salvium_verify_signature(
|
||||
message: *const u8,
|
||||
msg_len: usize,
|
||||
signature: *const u8,
|
||||
sig_len: usize,
|
||||
pubkey_der: *const u8,
|
||||
key_len: usize,
|
||||
) -> i32 {
|
||||
let message = slice::from_raw_parts(message, msg_len);
|
||||
let signature = slice::from_raw_parts(signature, sig_len);
|
||||
let pubkey_der = slice::from_raw_parts(pubkey_der, key_len);
|
||||
crate::verify_signature(message, signature, pubkey_der)
|
||||
}
|
||||
|
||||
// ─── Tests ──────────────────────────────────────────────────────────────────
|
||||
|
||||
#[cfg(test)]
|
||||
@@ -726,4 +757,71 @@ mod tests {
|
||||
assert_eq!(rc, 0);
|
||||
assert_ffi_matches(&out, &crate::gen_commitment_mask(&secret));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_sha256() {
|
||||
let data = b"test data";
|
||||
let mut out = [0u8; 32];
|
||||
let rc = unsafe { salvium_sha256(data.as_ptr(), data.len(), out.as_mut_ptr()) };
|
||||
assert_eq!(rc, 0);
|
||||
// Known SHA-256 of "test data"
|
||||
let expected = crate::sha256(data);
|
||||
assert_eq!(&out[..], &expected[..]);
|
||||
// Verify against known hash
|
||||
assert_ne!(out, [0u8; 32]); // not all zeros
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_sha256_empty() {
|
||||
let mut out = [0u8; 32];
|
||||
let rc = unsafe { salvium_sha256([].as_ptr(), 0, out.as_mut_ptr()) };
|
||||
assert_eq!(rc, 0);
|
||||
// SHA-256("") = e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855
|
||||
assert_eq!(out[0], 0xe3);
|
||||
assert_eq!(out[1], 0xb0);
|
||||
assert_eq!(out[31], 0x55);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_verify_signature_invalid() {
|
||||
// Invalid inputs should return 0, not crash
|
||||
let message = b"test";
|
||||
let bad_sig = [0u8; 64];
|
||||
let bad_key = [0u8; 32];
|
||||
let rc = unsafe {
|
||||
salvium_verify_signature(
|
||||
message.as_ptr(), message.len(),
|
||||
bad_sig.as_ptr(), bad_sig.len(),
|
||||
bad_key.as_ptr(), bad_key.len(),
|
||||
)
|
||||
};
|
||||
assert_eq!(rc, 0); // invalid → 0
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_verify_signature_ecdsa_p256() {
|
||||
// Testnet oracle public key (ECDSA P-256, DER-encoded SPKI)
|
||||
let key_der: [u8; 91] = [
|
||||
0x30, 0x59, 0x30, 0x13, 0x06, 0x07, 0x2a, 0x86, 0x48, 0xce, 0x3d, 0x02,
|
||||
0x01, 0x06, 0x08, 0x2a, 0x86, 0x48, 0xce, 0x3d, 0x03, 0x01, 0x07, 0x03,
|
||||
0x42, 0x00, 0x04, 0xe5, 0x80, 0x71, 0x5b, 0x1d, 0x40, 0x64, 0x20, 0x3d,
|
||||
0x8d, 0x35, 0x24, 0xf0, 0xfa, 0xf6, 0xb9, 0x9f, 0x63, 0xa5, 0xf4, 0x6d,
|
||||
0x29, 0x6b, 0xf7, 0x56, 0x8d, 0x7f, 0x1a, 0x7c, 0xbe, 0xd6, 0xf7, 0xda,
|
||||
0xc6, 0xc5, 0xe1, 0x05, 0x08, 0x86, 0xd4, 0xa9, 0x47, 0x91, 0xa7, 0xcd,
|
||||
0x19, 0xaa, 0xf3, 0xa0, 0xbd, 0x16, 0x1d, 0x6e, 0x28, 0x72, 0xa6, 0x9a,
|
||||
0xa8, 0x5e, 0x62, 0xbf, 0xc8, 0xb7, 0xe4,
|
||||
];
|
||||
// Verify that parsing the key doesn't crash with an invalid signature
|
||||
let message = b"test message";
|
||||
let bad_sig = [0u8; 70];
|
||||
let rc = unsafe {
|
||||
salvium_verify_signature(
|
||||
message.as_ptr(), message.len(),
|
||||
bad_sig.as_ptr(), bad_sig.len(),
|
||||
key_der.as_ptr(), key_der.len(),
|
||||
)
|
||||
};
|
||||
// Should return 0 (invalid sig), not crash
|
||||
assert_eq!(rc, 0);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ use curve25519_dalek::scalar::Scalar;
|
||||
use curve25519_dalek::edwards::{CompressedEdwardsY, EdwardsPoint};
|
||||
use curve25519_dalek::constants::ED25519_BASEPOINT_TABLE;
|
||||
use curve25519_dalek::traits::VartimeMultiscalarMul;
|
||||
use sha2::{Sha256, Digest};
|
||||
|
||||
mod elligator2;
|
||||
|
||||
@@ -314,3 +315,111 @@ pub fn gen_commitment_mask(shared_secret: &[u8]) -> Vec<u8> {
|
||||
let hash = keccak256_internal(&data);
|
||||
Scalar::from_bytes_mod_order(hash).to_bytes().to_vec()
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Oracle Signature Verification (SHA-256, ECDSA P-256, DSA)
|
||||
// ============================================================================
|
||||
|
||||
/// SHA-256 hash
|
||||
#[wasm_bindgen]
|
||||
pub fn sha256(data: &[u8]) -> Vec<u8> {
|
||||
let mut hasher = Sha256::new();
|
||||
hasher.update(data);
|
||||
hasher.finalize().to_vec()
|
||||
}
|
||||
|
||||
/// Verify a signature against a DER-encoded SubjectPublicKeyInfo (SPKI) key.
|
||||
///
|
||||
/// Supports:
|
||||
/// - ECDSA P-256 (testnet oracle) — OID 1.2.840.10045.2.1
|
||||
/// - DSA (mainnet oracle) — OID 1.2.840.10040.4.1
|
||||
///
|
||||
/// The message is hashed with SHA-256 internally (matching Node.js createVerify('SHA256')).
|
||||
/// Signature must be DER-encoded (ASN.1 SEQUENCE of two INTEGERs r, s).
|
||||
/// Public key is the raw DER bytes of the SPKI structure (base64-decoded PEM body).
|
||||
///
|
||||
/// Returns 1 for valid, 0 for invalid/error.
|
||||
///
|
||||
/// Only available on native targets (not WASM). On WASM, the JS crypto shim is used.
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
#[wasm_bindgen]
|
||||
pub fn verify_signature(message: &[u8], signature: &[u8], pubkey_der: &[u8]) -> i32 {
|
||||
verify_signature_internal(message, signature, pubkey_der)
|
||||
.unwrap_or(0)
|
||||
}
|
||||
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
fn verify_signature_internal(message: &[u8], signature: &[u8], pubkey_der: &[u8]) -> Option<i32> {
|
||||
use spki::SubjectPublicKeyInfoRef;
|
||||
use der::Decode;
|
||||
|
||||
// Parse the SPKI structure to determine algorithm
|
||||
let spki = SubjectPublicKeyInfoRef::from_der(pubkey_der).ok()?;
|
||||
let algorithm_oid = spki.algorithm.oid;
|
||||
|
||||
// OID 1.2.840.10045.2.1 = id-ecPublicKey (ECDSA)
|
||||
const EC_PUBLIC_KEY_OID: der::oid::ObjectIdentifier =
|
||||
der::oid::ObjectIdentifier::new_unwrap("1.2.840.10045.2.1");
|
||||
// OID 1.2.840.10040.4.1 = id-dsa (DSA)
|
||||
const DSA_OID: der::oid::ObjectIdentifier =
|
||||
der::oid::ObjectIdentifier::new_unwrap("1.2.840.10040.4.1");
|
||||
|
||||
if algorithm_oid == EC_PUBLIC_KEY_OID {
|
||||
verify_ecdsa_p256(message, signature, pubkey_der)
|
||||
} else if algorithm_oid == DSA_OID {
|
||||
verify_dsa(message, signature, pubkey_der)
|
||||
} else {
|
||||
None // Unknown algorithm
|
||||
}
|
||||
}
|
||||
|
||||
/// Verify ECDSA P-256 signature (SHA-256 digest)
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
fn verify_ecdsa_p256(message: &[u8], signature: &[u8], pubkey_der: &[u8]) -> Option<i32> {
|
||||
use p256::ecdsa::{VerifyingKey, Signature};
|
||||
use spki::DecodePublicKey;
|
||||
|
||||
let verifying_key = VerifyingKey::from_public_key_der(pubkey_der).ok()?;
|
||||
let sig = Signature::from_der(signature).ok()?;
|
||||
|
||||
// SHA-256 hash the message first (matching createVerify('SHA256'))
|
||||
let digest = {
|
||||
let mut hasher = Sha256::new();
|
||||
hasher.update(message);
|
||||
hasher.finalize()
|
||||
};
|
||||
|
||||
// p256 ecdsa Verifier::verify expects pre-hashed when using verify_prehash
|
||||
// But the standard Verifier trait hashes internally. Since createVerify('SHA256')
|
||||
// hashes with SHA-256 then verifies, we use verify_prehash.
|
||||
use p256::ecdsa::signature::hazmat::PrehashVerifier;
|
||||
match verifying_key.verify_prehash(&digest, &sig) {
|
||||
Ok(()) => Some(1),
|
||||
Err(_) => Some(0),
|
||||
}
|
||||
}
|
||||
|
||||
/// Verify DSA signature (SHA-256 digest)
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
fn verify_dsa(message: &[u8], signature: &[u8], pubkey_der: &[u8]) -> Option<i32> {
|
||||
use dsa::{VerifyingKey, Signature, signature::hazmat::PrehashVerifier};
|
||||
use spki::DecodePublicKey;
|
||||
use der::Decode as _;
|
||||
|
||||
let verifying_key = VerifyingKey::from_public_key_der(pubkey_der).ok()?;
|
||||
|
||||
// Parse DER signature
|
||||
let sig = Signature::from_der(signature).ok()?;
|
||||
|
||||
// SHA-256 hash the message
|
||||
let digest = {
|
||||
let mut hasher = Sha256::new();
|
||||
hasher.update(message);
|
||||
hasher.finalize()
|
||||
};
|
||||
|
||||
match verifying_key.verify_prehash(&digest, &sig) {
|
||||
Ok(()) => Some(1),
|
||||
Err(_) => Some(0),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -265,16 +265,8 @@ export function parseProof(proofBytes) {
|
||||
|
||||
let offset = 0;
|
||||
|
||||
// Salvium binary format: varint(V.len), V[], A, A1, B, r1, s1, d1, varint(L.len), L[], varint(R.len), R[]
|
||||
|
||||
// V (commitments)
|
||||
const { value: vCount, bytesRead: vBytes } = _decodeVarint(proofBytes, offset);
|
||||
offset += vBytes;
|
||||
const V = [];
|
||||
for (let i = 0; i < vCount; i++) {
|
||||
V.push(bytesToPoint(proofBytes.slice(offset, offset + 32)));
|
||||
offset += 32;
|
||||
}
|
||||
// Salvium binary format: A, A1, B, r1, s1, d1, varint(L.len), L[], varint(R.len), R[]
|
||||
// Note: V (commitments) is NOT in the wire format — restored from outPk.
|
||||
|
||||
// A, A1, B (points)
|
||||
const A = bytesToPoint(proofBytes.slice(offset, offset + 32));
|
||||
@@ -310,7 +302,7 @@ export function parseProof(proofBytes) {
|
||||
offset += 32;
|
||||
}
|
||||
|
||||
return { V, A, A1, B, r1, s1, d1, L, R };
|
||||
return { A, A1, B, r1, s1, d1, L, R };
|
||||
}
|
||||
|
||||
function _decodeVarint(bytes, offset) {
|
||||
@@ -1079,22 +1071,18 @@ export function bulletproofPlusProve(amounts, masks) {
|
||||
* Serialize a Bulletproof+ proof to bytes
|
||||
*/
|
||||
export function serializeProof(proof) {
|
||||
const { V, A, A1, B, r1, s1, d1, L, R } = proof;
|
||||
const { A, A1, B, r1, s1, d1, L, R } = proof;
|
||||
|
||||
// Monero/Salvium binary format for BulletproofPlus:
|
||||
// varint(V.length), V[0..n] (32 bytes each)
|
||||
// A (32), A1 (32), B (32)
|
||||
// r1 (32), s1 (32), d1 (32)
|
||||
// varint(L.length), L[0..n] (32 bytes each)
|
||||
// varint(R.length), R[0..n] (32 bytes each)
|
||||
// Note: V (commitments) is NOT serialized — restored from outPk.
|
||||
// Must match parseProof format for roundtrip compatibility.
|
||||
|
||||
const chunks = [];
|
||||
|
||||
// V (commitments)
|
||||
chunks.push(_encodeVarint(V.length));
|
||||
for (const v of V) chunks.push(v.toBytes());
|
||||
|
||||
// A, A1, B (points)
|
||||
chunks.push(A.toBytes());
|
||||
chunks.push(A1.toBytes());
|
||||
|
||||
+4
-2
@@ -7,6 +7,8 @@
|
||||
* Reference: salvium/src/cryptonote_config.h, cryptonote_basic_impl.cpp, difficulty.cpp
|
||||
*/
|
||||
|
||||
import { bytesToHex } from './address.js';
|
||||
|
||||
// =============================================================================
|
||||
// CORE CONSTANTS
|
||||
// =============================================================================
|
||||
@@ -891,11 +893,11 @@ export function validateBlockLinkage(currentHeader, previousHeader) {
|
||||
// Check previous hash matches
|
||||
const prevHashHex = typeof currentHeader.prevId === 'string'
|
||||
? currentHeader.prevId
|
||||
: Buffer.from(currentHeader.prevId).toString('hex');
|
||||
: bytesToHex(new Uint8Array(currentHeader.prevId));
|
||||
|
||||
const expectedPrevHash = typeof previousHeader.hash === 'string'
|
||||
? previousHeader.hash
|
||||
: Buffer.from(previousHeader.hash).toString('hex');
|
||||
: bytesToHex(new Uint8Array(previousHeader.hash));
|
||||
|
||||
if (prevHashHex !== expectedPrevHash) {
|
||||
return {
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
|
||||
import { keccak256 as jsKeccak } from '../keccak.js';
|
||||
import { blake2b as jsBlake2b } from '../blake2b.js';
|
||||
import { sha256 as nobleSha256 } from '@noble/hashes/sha2.js';
|
||||
import {
|
||||
scAdd, scSub, scMul, scMulAdd, scMulSub,
|
||||
scReduce32, scReduce64, scInvert, scCheck, scIsZero
|
||||
@@ -75,4 +76,117 @@ export class JsCryptoBackend {
|
||||
commit(amount, mask) { return commit(amount, mask); }
|
||||
zeroCommit(amount) { return zeroCommit(amount); }
|
||||
genCommitmentMask(sharedSecret) { return genCommitmentMask(sharedSecret); }
|
||||
|
||||
// Oracle signature verification
|
||||
sha256(data) { return nobleSha256(data); }
|
||||
|
||||
/**
|
||||
* Verify signature using WebCrypto (browser/Node.js 15+) or Node.js crypto.
|
||||
* @param {Uint8Array} message - Message bytes (will be SHA-256 hashed)
|
||||
* @param {Uint8Array} signature - DER-encoded signature
|
||||
* @param {Uint8Array} pubkeyDer - DER-encoded SPKI public key
|
||||
* @returns {Promise<boolean>} True if valid
|
||||
*/
|
||||
async verifySignature(message, signature, pubkeyDer) {
|
||||
// Try WebCrypto first (works in browsers and modern Node.js)
|
||||
if (typeof globalThis.crypto?.subtle?.verify === 'function') {
|
||||
return verifyWithWebCrypto(message, signature, pubkeyDer);
|
||||
}
|
||||
// Fall back to Node.js crypto module
|
||||
return verifyWithNodeCrypto(message, signature, pubkeyDer);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Verify using WebCrypto API.
|
||||
* Detects ECDSA P-256 vs DSA from the SPKI OID.
|
||||
*/
|
||||
async function verifyWithWebCrypto(message, signature, pubkeyDer) {
|
||||
try {
|
||||
// Detect algorithm from SPKI OID bytes
|
||||
// ECDSA P-256 OID: 06 07 2a 86 48 ce 3d 02 01 (1.2.840.10045.2.1)
|
||||
// DSA OID: 06 07 2a 86 48 ce 38 04 01 (1.2.840.10040.4.1)
|
||||
const isEcdsa = containsBytes(pubkeyDer, [0x06, 0x07, 0x2a, 0x86, 0x48, 0xce, 0x3d, 0x02, 0x01]);
|
||||
|
||||
if (isEcdsa) {
|
||||
// Convert DER signature to raw r||s format for WebCrypto
|
||||
const rawSig = derSignatureToRaw(signature, 32);
|
||||
const key = await globalThis.crypto.subtle.importKey(
|
||||
'spki', pubkeyDer,
|
||||
{ name: 'ECDSA', namedCurve: 'P-256' },
|
||||
false, ['verify']
|
||||
);
|
||||
return await globalThis.crypto.subtle.verify(
|
||||
{ name: 'ECDSA', hash: 'SHA-256' },
|
||||
key, rawSig, message
|
||||
);
|
||||
}
|
||||
// WebCrypto doesn't support DSA — fall back to Node.js crypto
|
||||
return verifyWithNodeCrypto(message, signature, pubkeyDer);
|
||||
} catch (_e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Verify using Node.js crypto module (createVerify).
|
||||
* Works for both ECDSA and DSA.
|
||||
*/
|
||||
async function verifyWithNodeCrypto(message, signature, pubkeyDer) {
|
||||
try {
|
||||
const { createVerify } = await import('crypto');
|
||||
// Wrap DER key in PEM
|
||||
const b64 = typeof Buffer !== 'undefined'
|
||||
? Buffer.from(pubkeyDer).toString('base64')
|
||||
: btoa(String.fromCharCode(...pubkeyDer));
|
||||
const pem = `-----BEGIN PUBLIC KEY-----\n${b64}\n-----END PUBLIC KEY-----`;
|
||||
const verifier = createVerify('SHA256');
|
||||
verifier.update(message);
|
||||
return verifier.verify(pem, Buffer.from(signature));
|
||||
} catch (_e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert DER-encoded ECDSA signature to raw r||s format.
|
||||
* WebCrypto expects raw format, not DER.
|
||||
*/
|
||||
function derSignatureToRaw(der, componentLen) {
|
||||
// DER: 30 <len> 02 <rlen> <r> 02 <slen> <s>
|
||||
let offset = 2; // skip SEQUENCE tag + length
|
||||
if (der[0] !== 0x30) return der; // not DER, return as-is
|
||||
|
||||
// Parse r
|
||||
if (der[offset] !== 0x02) return der;
|
||||
const rLen = der[offset + 1];
|
||||
const rStart = offset + 2;
|
||||
offset = rStart + rLen;
|
||||
|
||||
// Parse s
|
||||
if (der[offset] !== 0x02) return der;
|
||||
const sLen = der[offset + 1];
|
||||
const sStart = offset + 2;
|
||||
|
||||
const raw = new Uint8Array(componentLen * 2);
|
||||
// Copy r (right-aligned, strip leading zeros)
|
||||
const rBytes = der.slice(rStart, rStart + rLen);
|
||||
const rTrim = rBytes[0] === 0 ? rBytes.slice(1) : rBytes;
|
||||
raw.set(rTrim, componentLen - rTrim.length);
|
||||
// Copy s (right-aligned, strip leading zeros)
|
||||
const sBytes = der.slice(sStart, sStart + sLen);
|
||||
const sTrim = sBytes[0] === 0 ? sBytes.slice(1) : sBytes;
|
||||
raw.set(sTrim, componentLen * 2 - sTrim.length);
|
||||
return raw;
|
||||
}
|
||||
|
||||
/** Check if haystack contains needle bytes at any offset */
|
||||
function containsBytes(haystack, needle) {
|
||||
outer: for (let i = 0; i <= haystack.length - needle.length; i++) {
|
||||
for (let j = 0; j < needle.length; j++) {
|
||||
if (haystack[i + j] !== needle[j]) continue outer;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -124,4 +124,15 @@ export class JsiCryptoBackend {
|
||||
sharedSecret = ensureBytes(sharedSecret);
|
||||
return this.native.genCommitmentMask(sharedSecret);
|
||||
}
|
||||
|
||||
// ─── Oracle Signature Verification ──────────────────────────────────────
|
||||
|
||||
sha256(data) {
|
||||
return this.native.sha256(data);
|
||||
}
|
||||
|
||||
async verifySignature(message, signature, pubkeyDer) {
|
||||
// Native module returns 1 for valid, 0 for invalid
|
||||
return this.native.verifySignature(message, signature, pubkeyDer) === 1;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,6 +7,8 @@
|
||||
* @module crypto/backend-wasm
|
||||
*/
|
||||
|
||||
import { sha256 as nobleSha256 } from '@noble/hashes/sha2.js';
|
||||
|
||||
let wasmExports = null;
|
||||
|
||||
/**
|
||||
@@ -162,4 +164,84 @@ export class WasmCryptoBackend {
|
||||
}
|
||||
return this.wasm.gen_commitment_mask(sharedSecret);
|
||||
}
|
||||
|
||||
// Oracle signature verification
|
||||
sha256(data) { return nobleSha256(data); }
|
||||
|
||||
async verifySignature(message, signature, pubkeyDer) {
|
||||
// WASM can't do ECDSA/DSA verification (native-only crates).
|
||||
// Use WebCrypto (browser/Node 15+) or Node.js crypto as fallback.
|
||||
if (typeof globalThis.crypto?.subtle?.verify === 'function') {
|
||||
return wasmVerifyWebCrypto(message, signature, pubkeyDer);
|
||||
}
|
||||
return wasmVerifyNodeCrypto(message, signature, pubkeyDer);
|
||||
}
|
||||
}
|
||||
|
||||
// ─── WASM backend verify helpers (same logic as JS backend) ───────────────
|
||||
|
||||
async function wasmVerifyWebCrypto(message, signature, pubkeyDer) {
|
||||
try {
|
||||
const isEcdsa = containsBytesWasm(pubkeyDer, [0x06, 0x07, 0x2a, 0x86, 0x48, 0xce, 0x3d, 0x02, 0x01]);
|
||||
if (isEcdsa) {
|
||||
const rawSig = derToRawWasm(signature, 32);
|
||||
const key = await globalThis.crypto.subtle.importKey(
|
||||
'spki', pubkeyDer,
|
||||
{ name: 'ECDSA', namedCurve: 'P-256' },
|
||||
false, ['verify']
|
||||
);
|
||||
return await globalThis.crypto.subtle.verify(
|
||||
{ name: 'ECDSA', hash: 'SHA-256' },
|
||||
key, rawSig, message
|
||||
);
|
||||
}
|
||||
return wasmVerifyNodeCrypto(message, signature, pubkeyDer);
|
||||
} catch (_e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
async function wasmVerifyNodeCrypto(message, signature, pubkeyDer) {
|
||||
try {
|
||||
const { createVerify } = await import('crypto');
|
||||
const b64 = typeof Buffer !== 'undefined'
|
||||
? Buffer.from(pubkeyDer).toString('base64')
|
||||
: btoa(String.fromCharCode(...pubkeyDer));
|
||||
const pem = `-----BEGIN PUBLIC KEY-----\n${b64}\n-----END PUBLIC KEY-----`;
|
||||
const verifier = createVerify('SHA256');
|
||||
verifier.update(message);
|
||||
return verifier.verify(pem, Buffer.from(signature));
|
||||
} catch (_e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
function derToRawWasm(der, componentLen) {
|
||||
let offset = 2;
|
||||
if (der[0] !== 0x30) return der;
|
||||
if (der[offset] !== 0x02) return der;
|
||||
const rLen = der[offset + 1];
|
||||
const rStart = offset + 2;
|
||||
offset = rStart + rLen;
|
||||
if (der[offset] !== 0x02) return der;
|
||||
const sLen = der[offset + 1];
|
||||
const sStart = offset + 2;
|
||||
const raw = new Uint8Array(componentLen * 2);
|
||||
const rBytes = der.slice(rStart, rStart + rLen);
|
||||
const rTrim = rBytes[0] === 0 ? rBytes.slice(1) : rBytes;
|
||||
raw.set(rTrim, componentLen - rTrim.length);
|
||||
const sBytes = der.slice(sStart, sStart + sLen);
|
||||
const sTrim = sBytes[0] === 0 ? sBytes.slice(1) : sBytes;
|
||||
raw.set(sTrim, componentLen * 2 - sTrim.length);
|
||||
return raw;
|
||||
}
|
||||
|
||||
function containsBytesWasm(haystack, needle) {
|
||||
outer: for (let i = 0; i <= haystack.length - needle.length; i++) {
|
||||
for (let j = 0; j < needle.length; j++) {
|
||||
if (haystack[i + j] !== needle[j]) continue outer;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -36,6 +36,8 @@ export {
|
||||
ecdhDecode, ecdhDecodeFull, ecdhEncode,
|
||||
// Pedersen commitments
|
||||
commit, zeroCommit, genCommitmentMask,
|
||||
// Oracle signature verification
|
||||
sha256, verifySignature,
|
||||
// CARROT key derivation
|
||||
computeCarrotSpendPubkey, computeCarrotAccountViewPubkey,
|
||||
computeCarrotMainAddressViewPubkey,
|
||||
|
||||
+18
-9
@@ -32,15 +32,18 @@ export async function setCryptoBackend(type) {
|
||||
currentBackend = new JsiCryptoBackend();
|
||||
await currentBackend.init();
|
||||
} else if (type === 'wasm') {
|
||||
// Dynamic import() is not supported in React Native/Hermes.
|
||||
// In Node/Bun, import backend-wasm.js directly:
|
||||
// import { WasmCryptoBackend } from './backend-wasm.js';
|
||||
// const backend = new WasmCryptoBackend(); await backend.init();
|
||||
throw new Error(
|
||||
"WASM backend cannot be loaded via setCryptoBackend() — dynamic import() " +
|
||||
"is not available in all runtimes (e.g. Hermes). In Node/Bun, import " +
|
||||
"WasmCryptoBackend from './crypto/backend-wasm.js' directly."
|
||||
);
|
||||
// Dynamic import() works in Node/Bun but not React Native/Hermes.
|
||||
// On Hermes, use 'jsi' backend instead.
|
||||
try {
|
||||
const { WasmCryptoBackend } = await import('./backend-wasm.js');
|
||||
currentBackend = new WasmCryptoBackend();
|
||||
await currentBackend.init();
|
||||
} catch (e) {
|
||||
throw new Error(
|
||||
"WASM backend failed to load: " + e.message + ". " +
|
||||
"On React Native/Hermes, use setCryptoBackend('jsi') instead."
|
||||
);
|
||||
}
|
||||
} else {
|
||||
throw new Error(`Unknown crypto backend: ${type}. Use 'js', 'wasm', or 'jsi'.`);
|
||||
}
|
||||
@@ -111,6 +114,12 @@ export function commit(amount, mask) { return getCryptoBackend().commit(amount,
|
||||
export function zeroCommit(amount) { return getCryptoBackend().zeroCommit(amount); }
|
||||
export function genCommitmentMask(sharedSecret) { return getCryptoBackend().genCommitmentMask(sharedSecret); }
|
||||
|
||||
// Oracle signature verification
|
||||
export function sha256(data) { return getCryptoBackend().sha256(data); }
|
||||
export async function verifySignature(message, signature, pubkeyDer) {
|
||||
return getCryptoBackend().verifySignature(message, signature, pubkeyDer);
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Composite functions — built on top of backend primitives
|
||||
// =============================================================================
|
||||
|
||||
+77
-30
@@ -11,7 +11,7 @@
|
||||
* @module oracle
|
||||
*/
|
||||
|
||||
import { createHash, createVerify } from 'crypto';
|
||||
import { verifySignature } from './crypto/provider.js';
|
||||
|
||||
// ============================================================================
|
||||
// CONSTANTS
|
||||
@@ -204,6 +204,55 @@ export function getAssetMaPrice(pr, assetType) {
|
||||
// SIGNATURE VERIFICATION
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* Convert PEM-encoded public key to DER bytes.
|
||||
* Strips the -----BEGIN/END PUBLIC KEY----- headers and base64-decodes.
|
||||
*
|
||||
* @param {string} pem - PEM-encoded public key
|
||||
* @returns {Uint8Array} DER-encoded SPKI public key
|
||||
*/
|
||||
function pemToDer(pem) {
|
||||
const b64 = pem
|
||||
.replace(/-----BEGIN PUBLIC KEY-----/, '')
|
||||
.replace(/-----END PUBLIC KEY-----/, '')
|
||||
.replace(/\s+/g, '');
|
||||
return _base64Decode(b64);
|
||||
}
|
||||
|
||||
/** Decode base64 string to Uint8Array (works on QuickJS, browsers, Node.js) */
|
||||
function _base64Decode(b64) {
|
||||
if (typeof Buffer !== 'undefined') {
|
||||
return new Uint8Array(Buffer.from(b64, 'base64'));
|
||||
}
|
||||
if (typeof atob === 'function') {
|
||||
const binary = atob(b64);
|
||||
const bytes = new Uint8Array(binary.length);
|
||||
for (let i = 0; i < binary.length; i++) bytes[i] = binary.charCodeAt(i);
|
||||
return bytes;
|
||||
}
|
||||
// Manual base64 decoder for QuickJS and other minimal runtimes
|
||||
const lookup = new Uint8Array(128);
|
||||
const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/';
|
||||
for (let i = 0; i < chars.length; i++) lookup[chars.charCodeAt(i)] = i;
|
||||
const len = b64.length;
|
||||
let pad = 0;
|
||||
if (b64[len - 1] === '=') pad++;
|
||||
if (b64[len - 2] === '=') pad++;
|
||||
const outLen = (len * 3 / 4) - pad;
|
||||
const bytes = new Uint8Array(outLen);
|
||||
let j = 0;
|
||||
for (let i = 0; i < len; i += 4) {
|
||||
const a = lookup[b64.charCodeAt(i)];
|
||||
const b = lookup[b64.charCodeAt(i + 1)];
|
||||
const c = lookup[b64.charCodeAt(i + 2)];
|
||||
const d = lookup[b64.charCodeAt(i + 3)];
|
||||
bytes[j++] = (a << 2) | (b >> 4);
|
||||
if (j < outLen) bytes[j++] = ((b & 15) << 4) | (c >> 2);
|
||||
if (j < outLen) bytes[j++] = ((c & 3) << 6) | d;
|
||||
}
|
||||
return bytes;
|
||||
}
|
||||
|
||||
/**
|
||||
* Build the JSON message that was signed
|
||||
*
|
||||
@@ -241,30 +290,32 @@ export function buildSignatureMessage(pr) {
|
||||
/**
|
||||
* Verify pricing record signature
|
||||
*
|
||||
* Uses the crypto provider's verifySignature which dispatches to the active
|
||||
* backend (Rust FFI, WASM, WebCrypto, or Node.js crypto). No direct
|
||||
* dependency on Node.js crypto module.
|
||||
*
|
||||
* Reference: ~/github/salvium/src/oracle/pricing_record.cpp (verifySignature)
|
||||
*
|
||||
* @param {PricingRecord} pr - Pricing record to verify
|
||||
* @param {string} publicKeyPem - Oracle public key in PEM format
|
||||
* @returns {boolean} True if signature is valid
|
||||
* @returns {Promise<boolean>} True if signature is valid
|
||||
*/
|
||||
export function verifyPricingRecordSignature(pr, publicKeyPem) {
|
||||
export async function verifyPricingRecordSignature(pr, publicKeyPem) {
|
||||
if (!pr.signature || pr.signature.length === 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
// Build the message that was signed
|
||||
// Build the message that was signed and encode to UTF-8 bytes
|
||||
const message = buildSignatureMessage(pr);
|
||||
const messageBytes = new TextEncoder().encode(message);
|
||||
|
||||
// Create verifier
|
||||
// Note: The mainnet key is DSA, testnet is ECDSA
|
||||
// Node.js crypto handles both with the 'dsa' or 'ecdsa' algorithm
|
||||
const verifier = createVerify('SHA256');
|
||||
verifier.update(message);
|
||||
// Convert PEM public key to DER bytes for the crypto provider
|
||||
const pubkeyDer = pemToDer(publicKeyPem);
|
||||
|
||||
// Verify the signature
|
||||
return verifier.verify(publicKeyPem, Buffer.from(pr.signature));
|
||||
} catch (error) {
|
||||
// Verify via crypto provider (backend handles SHA-256 hashing internally)
|
||||
return await verifySignature(messageBytes, pr.signature, pubkeyDer);
|
||||
} catch (_e) {
|
||||
// Signature verification failed
|
||||
return false;
|
||||
}
|
||||
@@ -300,7 +351,7 @@ export function getOraclePublicKey(network = 'mainnet') {
|
||||
* @param {number} options.lastBlockTimestamp - Previous block timestamp
|
||||
* @returns {{valid: boolean, error?: string}} Validation result
|
||||
*/
|
||||
export function validatePricingRecord(pr, options = {}) {
|
||||
export async function validatePricingRecord(pr, options = {}) {
|
||||
const {
|
||||
network = 'mainnet',
|
||||
hfVersion = 0,
|
||||
@@ -322,7 +373,7 @@ export function validatePricingRecord(pr, options = {}) {
|
||||
|
||||
// Rule 3: Signature must be valid
|
||||
const publicKey = getOraclePublicKey(network);
|
||||
if (!verifyPricingRecordSignature(pr, publicKey)) {
|
||||
if (!await verifyPricingRecordSignature(pr, publicKey)) {
|
||||
return { valid: false, error: 'Invalid pricing record signature' };
|
||||
}
|
||||
|
||||
@@ -600,25 +651,21 @@ export async function fetchPricingRecord(options = {}) {
|
||||
} = options;
|
||||
|
||||
// Build request URL
|
||||
const queryParams = new URLSearchParams({
|
||||
height: height.toString(),
|
||||
sal: salSupply.toString(),
|
||||
vsd: vsdSupply.toString()
|
||||
});
|
||||
const queryString = [
|
||||
`height=${encodeURIComponent(height.toString())}`,
|
||||
`sal=${encodeURIComponent(salSupply.toString())}`,
|
||||
`vsd=${encodeURIComponent(vsdSupply.toString())}`,
|
||||
].join('&');
|
||||
|
||||
const fullUrl = `${url}/price?${queryParams}`;
|
||||
const fullUrl = `${url}/price?${queryString}`;
|
||||
|
||||
try {
|
||||
// Create abort controller for timeout
|
||||
const controller = new AbortController();
|
||||
const timeoutId = setTimeout(() => controller.abort(), timeout);
|
||||
|
||||
const response = await fetch(fullUrl, {
|
||||
method: 'GET',
|
||||
signal: controller.signal
|
||||
});
|
||||
|
||||
clearTimeout(timeoutId);
|
||||
const response = await Promise.race([
|
||||
fetch(fullUrl, { method: 'GET' }),
|
||||
new Promise((_, reject) =>
|
||||
setTimeout(() => reject(new Error('Oracle request timed out')), timeout)
|
||||
),
|
||||
]);
|
||||
|
||||
if (!response.ok) {
|
||||
return {
|
||||
|
||||
@@ -24,7 +24,7 @@ export const DATASET_SIZE = RANDOMX_DATASET_ITEM_COUNT * DATASET_ITEM_SIZE;
|
||||
export function getCpuCount() {
|
||||
try {
|
||||
return cpus().length;
|
||||
} catch {
|
||||
} catch (_e) {
|
||||
if (typeof navigator !== 'undefined' && navigator.hardwareConcurrency) {
|
||||
return navigator.hardwareConcurrency;
|
||||
}
|
||||
|
||||
@@ -64,7 +64,7 @@ export function getAvailableCores() {
|
||||
try {
|
||||
const os = require('os');
|
||||
return os.cpus().length;
|
||||
} catch {
|
||||
} catch (_e) {
|
||||
return 4; // Default fallback
|
||||
}
|
||||
}
|
||||
|
||||
+27
-21
@@ -12,6 +12,21 @@
|
||||
|
||||
import { ConnectionManager, SEED_NODES } from './connection-manager.js';
|
||||
|
||||
/** Base64 encode a string (works in QuickJS, browsers, Node.js) */
|
||||
function _toBase64(str) {
|
||||
if (typeof btoa === 'function') return btoa(str);
|
||||
const bytes = new TextEncoder().encode(str);
|
||||
const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/';
|
||||
let result = '';
|
||||
for (let i = 0; i < bytes.length; i += 3) {
|
||||
const a = bytes[i], b = bytes[i + 1] || 0, c = bytes[i + 2] || 0;
|
||||
result += chars[a >> 2] + chars[((a & 3) << 4) | (b >> 4)]
|
||||
+ (i + 1 < bytes.length ? chars[((b & 15) << 2) | (c >> 6)] : '=')
|
||||
+ (i + 2 < bytes.length ? chars[c & 63] : '=');
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* @typedef {Object} RPCClientOptions
|
||||
* @property {string} url - RPC server URL (e.g., 'http://localhost:19081')
|
||||
@@ -151,7 +166,7 @@ export class RPCClient {
|
||||
|
||||
// Add basic auth if credentials provided
|
||||
if (this.username && this.password) {
|
||||
const credentials = btoa(`${this.username}:${this.password}`);
|
||||
const credentials = _toBase64(`${this.username}:${this.password}`);
|
||||
headers['Authorization'] = `Basic ${credentials}`;
|
||||
}
|
||||
|
||||
@@ -176,18 +191,12 @@ export class RPCClient {
|
||||
* @private
|
||||
*/
|
||||
async _fetchWithTimeout(url, options) {
|
||||
const controller = new AbortController();
|
||||
const timeoutId = setTimeout(() => controller.abort(), this.timeout);
|
||||
|
||||
try {
|
||||
const response = await fetch(url, {
|
||||
...options,
|
||||
signal: controller.signal
|
||||
});
|
||||
return response;
|
||||
} finally {
|
||||
clearTimeout(timeoutId);
|
||||
}
|
||||
return Promise.race([
|
||||
fetch(url, options),
|
||||
new Promise((_, reject) =>
|
||||
setTimeout(() => reject(new Error('Request timed out')), this.timeout)
|
||||
),
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -519,13 +528,10 @@ export class RPCClient {
|
||||
: `${this.url}/${endpoint}`;
|
||||
|
||||
// Add query parameters
|
||||
const queryParams = new URLSearchParams();
|
||||
for (const [key, value] of Object.entries(params)) {
|
||||
if (value !== undefined && value !== null) {
|
||||
queryParams.append(key, String(value));
|
||||
}
|
||||
}
|
||||
const queryString = queryParams.toString();
|
||||
const queryString = Object.entries(params)
|
||||
.filter(([, v]) => v !== undefined && v !== null)
|
||||
.map(([k, v]) => `${encodeURIComponent(k)}=${encodeURIComponent(String(v))}`)
|
||||
.join('&');
|
||||
if (queryString) {
|
||||
url += `?${queryString}`;
|
||||
}
|
||||
@@ -597,7 +603,7 @@ export class RPCClient {
|
||||
headers: this._buildHeaders()
|
||||
});
|
||||
return response.ok || response.status === 404; // 404 is OK, server is reachable
|
||||
} catch {
|
||||
} catch (_e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -83,31 +83,31 @@ export class ConnectionManager {
|
||||
this._racing = true;
|
||||
|
||||
try {
|
||||
const results = await Promise.allSettled(
|
||||
const results = await Promise.all(
|
||||
this.urls.map(async (url) => {
|
||||
const start = Date.now();
|
||||
const controller = new AbortController();
|
||||
const timer = setTimeout(() => controller.abort(), this.raceTimeout);
|
||||
|
||||
try {
|
||||
const resp = await fetch(`${url.replace(/\/+$/, '')}/get_info`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: '{}',
|
||||
signal: controller.signal,
|
||||
});
|
||||
clearTimeout(timer);
|
||||
const resp = await Promise.race([
|
||||
fetch(`${url.replace(/\/+$/, '')}/get_info`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: '{}',
|
||||
}),
|
||||
new Promise((_, reject) =>
|
||||
setTimeout(() => reject(new Error('Race timeout')), this.raceTimeout)
|
||||
),
|
||||
]);
|
||||
|
||||
if (!resp.ok) throw new Error(`HTTP ${resp.status}`);
|
||||
await resp.json(); // consume body
|
||||
|
||||
const latency = Date.now() - start;
|
||||
this._latencies.set(url, latency);
|
||||
return { url, latency };
|
||||
return { status: 'fulfilled', value: { url, latency } };
|
||||
} catch (e) {
|
||||
clearTimeout(timer);
|
||||
this._latencies.set(url, Infinity);
|
||||
throw e;
|
||||
return { status: 'rejected', reason: e };
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
+21
-14
@@ -1198,9 +1198,11 @@ export class GammaPicker {
|
||||
this.shape = options.shape || GAMMA_SHAPE;
|
||||
this.scale = options.scale || GAMMA_SCALE;
|
||||
|
||||
// Use coinbase maturity window (60) as the exclusion zone to ensure
|
||||
// all selected decoys are unlocked (coinbase outputs need 60 blocks)
|
||||
const unlockExclusion = CRYPTONOTE_MINED_MONEY_UNLOCK_WINDOW;
|
||||
// Match C++ gamma_picker: use DEFAULT_TX_SPENDABLE_AGE (10) as the
|
||||
// exclusion zone, not MINED_MONEY_UNLOCK_WINDOW (60). Locked coinbase
|
||||
// outputs still have valid public keys and are fine as ring decoys.
|
||||
// The C++ wallet over-requests outputs to compensate for locked picks.
|
||||
const unlockExclusion = CRYPTONOTE_DEFAULT_TX_SPENDABLE_AGE;
|
||||
|
||||
if (rctOffsets.length <= unlockExclusion) {
|
||||
throw new Error('Not enough blocks for decoy selection');
|
||||
@@ -1276,7 +1278,7 @@ export class GammaPicker {
|
||||
findBlockIndex(outputIndex) {
|
||||
// Binary search
|
||||
let low = 0;
|
||||
let high = this.rctOffsets.length - CRYPTONOTE_MINED_MONEY_UNLOCK_WINDOW;
|
||||
let high = this.rctOffsets.length - CRYPTONOTE_DEFAULT_TX_SPENDABLE_AGE;
|
||||
|
||||
while (low < high) {
|
||||
const mid = Math.floor((low + high) / 2);
|
||||
@@ -2151,16 +2153,21 @@ export function buildTransaction(params, options = {}) {
|
||||
|
||||
// Generate Bulletproofs+ range proofs BEFORE pre-MLSAG hash
|
||||
// (C++ genRctSimple generates BP+ first, then hashes components for CLSAG message)
|
||||
const bpAmounts = outputs.map(o => o.amount);
|
||||
const bpMasks = outputMasks.map(m => {
|
||||
const bytes = typeof m === 'string' ? hexToBytes(m) : m;
|
||||
return _bytesToBigInt(bytes);
|
||||
});
|
||||
const bpProof = bulletproofPlusProve(bpAmounts, bpMasks);
|
||||
const bulletproofPlus = {
|
||||
...bpProof,
|
||||
serialized: serializeBpPlus(bpProof)
|
||||
};
|
||||
// Skip for transactions with no outputs (e.g. AUDIT — all coins locked, no change)
|
||||
let bpProof = null;
|
||||
let bulletproofPlus = null;
|
||||
if (outputs.length > 0) {
|
||||
const bpAmounts = outputs.map(o => o.amount);
|
||||
const bpMasks = outputMasks.map(m => {
|
||||
const bytes = typeof m === 'string' ? hexToBytes(m) : m;
|
||||
return _bytesToBigInt(bytes);
|
||||
});
|
||||
bpProof = bulletproofPlusProve(bpAmounts, bpMasks);
|
||||
bulletproofPlus = {
|
||||
...bpProof,
|
||||
serialized: serializeBpPlus(bpProof)
|
||||
};
|
||||
}
|
||||
|
||||
// Build RingCT base (needed for pre-MLSAG hash)
|
||||
// Must include ecdhInfo, outPk, p_r, and salvium_data — matches C++ serialize_rctsig_base
|
||||
|
||||
+4
-3
@@ -34,6 +34,7 @@ import {
|
||||
} from './consensus.js';
|
||||
|
||||
import { TX_TYPE, RCT_TYPE } from './transaction.js';
|
||||
import { hexToBytes } from './address.js';
|
||||
|
||||
// =============================================================================
|
||||
// CONSTANTS
|
||||
@@ -372,7 +373,7 @@ export function validateOutputPubkeySorting(tx, hfVersion) {
|
||||
if (!key) continue;
|
||||
|
||||
const keyBytes = typeof key === 'string'
|
||||
? Buffer.from(key, 'hex')
|
||||
? hexToBytes(key)
|
||||
: key;
|
||||
|
||||
if (prevKey !== null) {
|
||||
@@ -533,10 +534,10 @@ export function validateInputs(tx, hfVersion) {
|
||||
const keyImage = input.k_image || input.keyImage;
|
||||
if (keyImage && prevKeyImage) {
|
||||
const kiBytes = typeof keyImage === 'string'
|
||||
? Buffer.from(keyImage, 'hex')
|
||||
? hexToBytes(keyImage)
|
||||
: keyImage;
|
||||
const prevKiBytes = typeof prevKeyImage === 'string'
|
||||
? Buffer.from(prevKeyImage, 'hex')
|
||||
? hexToBytes(prevKeyImage)
|
||||
: prevKeyImage;
|
||||
|
||||
// Compare lexicographically (must be strictly increasing)
|
||||
|
||||
@@ -166,7 +166,7 @@ export function decryptWalletJSON(envelope, password) {
|
||||
let plaintext;
|
||||
try {
|
||||
plaintext = gcm(encryptionKey, hexToBytes(enc.iv)).decrypt(hexToBytes(enc.ciphertext));
|
||||
} catch {
|
||||
} catch (_e) {
|
||||
throw new Error('Decryption failed: incorrect password or corrupted data');
|
||||
}
|
||||
|
||||
@@ -254,7 +254,7 @@ export function encryptData(key, plaintext) {
|
||||
export function decryptData(key, envelope) {
|
||||
try {
|
||||
return gcm(key, hexToBytes(envelope.iv)).decrypt(hexToBytes(envelope.ciphertext));
|
||||
} catch {
|
||||
} catch (_e) {
|
||||
throw new Error('Data decryption failed: wrong key or corrupted data');
|
||||
}
|
||||
}
|
||||
|
||||
+6
-3
@@ -462,9 +462,12 @@ export class WalletSync {
|
||||
: new Uint8Array(binBlock.block);
|
||||
const parsed = parseBlock(blockBlob);
|
||||
|
||||
// Compute miner_tx and protocol_tx hashes from the raw blob
|
||||
const minerTxHash = this._computeTxHashFromBlob(blockBlob, parsed.minerTx);
|
||||
const protocolTxHash = this._computeTxHashFromBlob(blockBlob, parsed.protocolTx);
|
||||
// Use tx hashes from block header (reliable) instead of computing from blob
|
||||
// (Salvium v3 tx hashing differs from Monero v2 — blob-based hash is wrong)
|
||||
const minerTxHash = header.miner_tx_hash
|
||||
|| this._computeTxHashFromBlob(blockBlob, parsed.minerTx);
|
||||
const protocolTxHash = header.protocol_tx_hash
|
||||
|| this._computeTxHashFromBlob(blockBlob, parsed.protocolTx);
|
||||
|
||||
// Process miner_tx (coinbase)
|
||||
if (parsed.minerTx && minerTxHash) {
|
||||
|
||||
@@ -170,7 +170,7 @@ function extractRejectionReason(respData) {
|
||||
try {
|
||||
const detail = JSON.stringify(respData, null, 0);
|
||||
return detail.length > 200 ? `${status} (response too large)` : `${status}: ${detail}`;
|
||||
} catch {
|
||||
} catch (_e) {
|
||||
return status;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -245,15 +245,11 @@ test('multiScalarMul with empty arrays', () => {
|
||||
console.log('\n--- Proof Parsing Tests ---');
|
||||
|
||||
test('parseProof extracts correct structure', () => {
|
||||
// Build a proof in Salvium binary format:
|
||||
// varint(V.len), V[], A, A1, B, r1, s1, d1, varint(L.len), L[], varint(R.len), R[]
|
||||
// Build a proof in Salvium binary format (V is NOT serialized — restored from outPk):
|
||||
// A, A1, B, r1, s1, d1, varint(L.len), L[], varint(R.len), R[]
|
||||
const baseBytes = Point.BASE.toBytes();
|
||||
const chunks = [];
|
||||
|
||||
// V: 1 commitment
|
||||
chunks.push(new Uint8Array([1])); // varint(1)
|
||||
chunks.push(baseBytes);
|
||||
|
||||
// A, A1, B
|
||||
chunks.push(baseBytes);
|
||||
chunks.push(baseBytes);
|
||||
@@ -286,7 +282,6 @@ test('parseProof extracts correct structure', () => {
|
||||
assertExists(proof.A);
|
||||
assertExists(proof.A1);
|
||||
assertExists(proof.B);
|
||||
assertEqual(proof.V.length, 1);
|
||||
assertEqual(proof.r1, 1n);
|
||||
assertEqual(proof.s1, 2n);
|
||||
assertEqual(proof.d1, 3n);
|
||||
@@ -467,9 +462,9 @@ test('serializeProof produces correct size', () => {
|
||||
const proof = proveRange(amount, mask);
|
||||
const bytes = serializeProof(proof);
|
||||
|
||||
// Salvium binary format: varint(V.len) + V + A + A1 + B + r1 + s1 + d1 + varint(L.len) + L + varint(R.len) + R
|
||||
// For single amount: 1 + 32 + 3*32 + 3*32 + 1 + 6*32 + 1 + 6*32 = 611 bytes
|
||||
assertEqual(bytes.length, 611, 'Serialized proof should be 611 bytes');
|
||||
// Salvium binary format (no V): A + A1 + B + r1 + s1 + d1 + varint(L.len) + L + varint(R.len) + R
|
||||
// For single amount: 3*32 + 3*32 + 1 + 6*32 + 1 + 6*32 = 578 bytes
|
||||
assertEqual(bytes.length, 578, 'Serialized proof should be 578 bytes');
|
||||
});
|
||||
|
||||
test('serialized proof can be parsed and verified', () => {
|
||||
|
||||
@@ -37,12 +37,14 @@ const WALLET_DIR = join(homedir(), 'testnet-wallet');
|
||||
const LOG_PATH = join(WALLET_DIR, 'full-testnet-log.json');
|
||||
const DEFAULT_DAEMON = 'http://web.whiskymine.io:29081';
|
||||
|
||||
// Target heights (HF activation + coinbase maturity window)
|
||||
// Target heights (HF activation + coinbase maturity + ring buffer)
|
||||
// Coinbase outputs lock for 60 blocks. After a fork, new-era outputs need
|
||||
// to unlock before they can be spent. Need: 60 (lock) + 16 (ring size) + 4 buffer = 80.
|
||||
const TARGET = {
|
||||
HF2_MATURE: 270, // HF2 at 250 + 20 extra
|
||||
HF6_MATURE: 835, // HF6 at 815 + 20 extra
|
||||
CARROT_MATURE: 1120, // HF10 at 1100 + 20 extra
|
||||
FINAL: 1200,
|
||||
HF2_MATURE: 270, // HF2 at 250 + 20 (SAL outputs already plentiful)
|
||||
HF6_MATURE: 895, // HF6 at 815 + 80 (SAL1 coinbase needs 60-block unlock)
|
||||
CARROT_MATURE: 1180, // HF10 at 1100 + 80 (same coinbase maturity requirement)
|
||||
FINAL: 1260,
|
||||
};
|
||||
|
||||
const MATURITY_BLOCKS = 10; // blocks to mine after TX for spendable age
|
||||
@@ -344,7 +346,7 @@ async function phase2_cnTxTests(daemon, daemonUrl, walletA, walletB) {
|
||||
}
|
||||
|
||||
async function phase3_mineToHF6(daemon, daemonUrl, walletA) {
|
||||
console.log('\n═══ Phase 3: Mine to HF6 / SAL1 (target height 835) ═══');
|
||||
console.log(`\n═══ Phase 3: Mine to HF6 / SAL1 (target height ${TARGET.HF6_MATURE}) ═══`);
|
||||
const t0 = performance.now();
|
||||
const address = walletA.getLegacyAddress();
|
||||
await mineTo(daemon, TARGET.HF6_MATURE, address, daemonUrl, 'rust');
|
||||
@@ -391,7 +393,7 @@ 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 1120) ═══');
|
||||
console.log(`\n═══ Phase 5: Mine to CARROT (target height ${TARGET.CARROT_MATURE}) ═══`);
|
||||
const t0 = performance.now();
|
||||
const address = walletA.getLegacyAddress();
|
||||
|
||||
|
||||
+8
-8
@@ -306,17 +306,17 @@ describe('Pricing Record Validation', () => {
|
||||
|
||||
describe('validatePricingRecord', () => {
|
||||
|
||||
test('empty record is always valid', () => {
|
||||
test('empty record is always valid', async () => {
|
||||
const pr = createEmptyPricingRecord();
|
||||
const result = validatePricingRecord(pr);
|
||||
const result = await validatePricingRecord(pr);
|
||||
expect(result.valid).toBe(true);
|
||||
});
|
||||
|
||||
test('non-empty record requires HF >= HF_VERSION_SLIPPAGE_YIELD', () => {
|
||||
test('non-empty record requires HF >= HF_VERSION_SLIPPAGE_YIELD', async () => {
|
||||
const pr = createMockPricingRecord();
|
||||
|
||||
// Before HF
|
||||
const result1 = validatePricingRecord(pr, { hfVersion: 5 });
|
||||
const result1 = await validatePricingRecord(pr, { hfVersion: 5 });
|
||||
expect(result1.valid).toBe(false);
|
||||
expect(result1.error).toContain('not allowed before oracle HF');
|
||||
|
||||
@@ -324,14 +324,14 @@ describe('Pricing Record Validation', () => {
|
||||
// Just testing the HF gate here
|
||||
});
|
||||
|
||||
test('rejects timestamp too far in future', () => {
|
||||
test('rejects timestamp too far in future', async () => {
|
||||
const pr = createMockPricingRecord();
|
||||
const currentTime = Math.floor(Date.now() / 1000);
|
||||
|
||||
// Set timestamp 200 seconds in future (> 120 seconds allowed)
|
||||
pr.timestamp = currentTime + 200;
|
||||
|
||||
const result = validatePricingRecord(pr, {
|
||||
const result = await validatePricingRecord(pr, {
|
||||
hfVersion: 255,
|
||||
blockTimestamp: currentTime
|
||||
});
|
||||
@@ -340,14 +340,14 @@ describe('Pricing Record Validation', () => {
|
||||
// but if we had a valid signature, timestamp would be checked
|
||||
});
|
||||
|
||||
test('rejects timestamp older than previous block', () => {
|
||||
test('rejects timestamp older than previous block', async () => {
|
||||
const pr = createMockPricingRecord();
|
||||
const currentTime = Math.floor(Date.now() / 1000);
|
||||
|
||||
// Set timestamp older than last block
|
||||
pr.timestamp = currentTime - 100;
|
||||
|
||||
const result = validatePricingRecord(pr, {
|
||||
const result = await validatePricingRecord(pr, {
|
||||
hfVersion: 255,
|
||||
blockTimestamp: currentTime,
|
||||
lastBlockTimestamp: currentTime - 50 // Previous block was 50 seconds ago
|
||||
|
||||
Reference in New Issue
Block a user