diff --git a/konclave-wasm/Cargo.lock b/konclave-wasm/Cargo.lock index 57b7411..144a010 100644 --- a/konclave-wasm/Cargo.lock +++ b/konclave-wasm/Cargo.lock @@ -631,6 +631,7 @@ dependencies = [ "blake2b_simd", "chacha20poly1305", "ff", + "frost-rerandomized", "getrandom", "hex", "hkdf", diff --git a/konclave-wasm/Cargo.toml b/konclave-wasm/Cargo.toml index e51bc07..9eeecdc 100644 --- a/konclave-wasm/Cargo.toml +++ b/konclave-wasm/Cargo.toml @@ -11,6 +11,10 @@ crate-type = ["cdylib", "rlib"] [dependencies] # FROST-redpallas signing round (proven wasm-clean: wasm-signer-spike) reddsa = { version = "0.5.2", features = ["frost"] } +# The rerandomized-FROST `sign` that takes an explicit randomizer (the Orchard alpha), which reddsa's +# curated `rerandomized` module doesn't re-export. Pinned to reddsa 0.5.2's transitive version so the +# ciphersuite types unify. +frost-rerandomized = "=3.0.0" # Orchard action verification + digest surface (proven wasm-clean: wasm-orchard-probe) orchard = { version = "0.14", default-features = false, features = ["unstable-frost"] } # ZIP-244 sig_digest recompute (proven wasm-clean: wasm-sighash-probe) diff --git a/konclave-wasm/src/lib.rs b/konclave-wasm/src/lib.rs index 3280714..d522af5 100644 --- a/konclave-wasm/src/lib.rs +++ b/konclave-wasm/src/lib.rs @@ -236,6 +236,69 @@ pub mod ceremony { .is_ok()) } + // ---- signing with a SPECIFIC Orchard randomizer (alpha), for real transactions ---- + // An Orchard spend must be signed under the note's own re-randomization: the alpha carried in + // the PCZT (read via pczt_bridge::extract_randomizers), NOT a commitment-derived seed. These + // three functions take that alpha explicitly, so the FROST signature verifies under ak+alpha and + // can be injected into the transaction (pczt_bridge::inject_sigs) and broadcast. The seed-based + // functions above stay for the self-contained demo; these are the real-transaction path. + + /// Participant round 2, signing with the given Orchard randomizer (alpha), 32 canonical bytes. + // frost-rerandomized deprecates the explicit-randomizer `sign` in favour of a seed the + // coordinator generates — but an Orchard spend's alpha is FIXED by the transaction (read from the + // PCZT), not freely chosen, so the seed path cannot express it. Signing under this exact + // randomizer is required and correct here. + #[allow(deprecated)] + pub fn participant_round2_with_randomizer( + sp_bytes: &[u8], + nonces: &SigningNonces, + kp: &KeyPackage, + randomizer: &[u8], + ) -> Result, E> { + let sp = SigningPackage::deserialize(sp_bytes).map_err(e)?; + let r = rerandomized::Randomizer::deserialize(randomizer).map_err(e)?; + let share = frost_rerandomized::sign(&sp, nonces, kp, r).map_err(e)?; + Ok(share.serialize()) + } + + /// Coordinator: aggregate the shares under the given randomizer (alpha) into a group signature. + pub fn coordinator_aggregate_with_randomizer( + sp_bytes: &[u8], + group_vk: &VerifyingKey, + randomizer: &[u8], + shares: &[(Vec, Vec)], + pubkeys: &PublicKeyPackage, + ) -> Result, E> { + let sp = SigningPackage::deserialize(sp_bytes).map_err(e)?; + let r = rerandomized::Randomizer::deserialize(randomizer).map_err(e)?; + let params = RandomizedParams::from_randomizer(group_vk, r); + let mut map = std::collections::BTreeMap::new(); + for (id_b, s_b) in shares { + let id = Identifier::deserialize(id_b).map_err(e)?; + let s = SignatureShare::deserialize(s_b).map_err(e)?; + map.insert(id, s); + } + let sig = rerandomized::aggregate(&sp, &map, pubkeys, ¶ms).map_err(e)?; + sig.serialize().map_err(e) + } + + /// Verify the group signature under the key re-randomized by the given alpha (what an Orchard + /// spend commits to). This is the check `pczt_bridge::inject_sigs` will pass on-chain. + pub fn verify_with_randomizer( + group_vk: &VerifyingKey, + randomizer: &[u8], + message: &[u8], + sig_bytes: &[u8], + ) -> Result { + let r = rerandomized::Randomizer::deserialize(randomizer).map_err(e)?; + let params = RandomizedParams::from_randomizer(group_vk, r); + let sig = frost::Signature::deserialize(sig_bytes).map_err(e)?; + Ok(params + .randomized_verifying_key() + .verify(message, &sig) + .is_ok()) + } + #[cfg(test)] mod tests { use super::*; @@ -286,6 +349,69 @@ pub mod ceremony { "the aggregated signature must verify against the randomized group key" ); } + + #[test] + fn signs_with_a_real_orchard_randomizer() { + // The alpha of Konclave's real mainnet DKG-vault spend (aab00f90…) — a valid Orchard + // randomizer. This is the path a real transaction takes: sign under the PCZT's alpha, + // not a commitment-derived seed, so the signature can be injected and broadcast. + let alpha = + hex::decode("b2ad61e8bf0de877dd01c52356526adf39b036ffed2e0217ece19407e1717624") + .unwrap(); + let (shares, pubkeys) = frost::keys::generate_with_dealer( + 3, + 2, + frost::keys::IdentifierList::Default, + OsRng, + ) + .unwrap(); + let kps: std::collections::BTreeMap<_, _> = shares + .into_iter() + .map(|(id, s)| (id, KeyPackage::try_from(s).unwrap())) + .collect(); + let group_vk = *pubkeys.verifying_key(); + let message = b"konclave: a real Orchard shielded sighash"; + + let signers: Vec<_> = kps.iter().take(2).collect(); + let mut local_nonces = Vec::new(); + let mut wire_commitments = Vec::new(); + for (id, kp) in &signers { + let (nonces, commit_bytes) = participant_round1(kp); + local_nonces.push((**id, nonces)); + wire_commitments.push((id.serialize(), commit_bytes)); + } + let sp_bytes = coordinator_signing_package(&wire_commitments, message).unwrap(); + + // Round 2 with the EXPLICIT Orchard alpha (no seed). + let mut wire_shares = Vec::new(); + for ((id, kp), (_id2, nonces)) in signers.iter().zip(local_nonces.iter()) { + let share = + participant_round2_with_randomizer(&sp_bytes, nonces, kp, &alpha).unwrap(); + wire_shares.push((id.serialize(), share)); + } + let sig = coordinator_aggregate_with_randomizer( + &sp_bytes, + &group_vk, + &alpha, + &wire_shares, + &pubkeys, + ) + .unwrap(); + + // Verifies under ak+alpha (what an Orchard spend commits to)... + assert!( + verify_with_randomizer(&group_vk, &alpha, message, &sig).unwrap(), + "signature must verify under the key randomized by the Orchard alpha" + ); + // ...and a DIFFERENT randomizer must NOT verify — the alpha binds the signature. + let other = + hex::decode("557c4ff828ed56eb33e8ba7f508a43915338ccf3ad71d1ecedc98e6e861bfc0f") + .unwrap(); + assert!( + !verify_with_randomizer(&group_vk, &other, message, &sig).unwrap(), + "a mismatched randomizer must fail" + ); + } } } @@ -1240,6 +1366,21 @@ mod js { ceremony::participant_round2(sp, &nonces, &kp, seed).map_err(je) } + /// Participant device, round 2 (JS), REAL-TRANSACTION path: sign with the given Orchard + /// randomizer (the 32-byte alpha from pczt_bridge.extractRandomizers) instead of a seed, so the + /// signature can be injected into the PCZT and broadcast. + #[wasm_bindgen(js_name = participantRound2WithRandomizer)] + pub fn participant_round2_with_randomizer( + sp: &[u8], + nonces_bytes: &[u8], + kp_bytes: &[u8], + randomizer: &[u8], + ) -> Result, JsValue> { + let nonces = SigningNonces::deserialize(nonces_bytes).map_err(je)?; + let kp = KeyPackage::deserialize(kp_bytes).map_err(je)?; + ceremony::participant_round2_with_randomizer(sp, &nonces, &kp, randomizer).map_err(je) + } + /// Coordinator (JS): accumulates the public wire material and produces the signature. #[wasm_bindgen] pub struct Coordinator { @@ -1298,6 +1439,35 @@ mod js { let vk = frost::VerifyingKey::deserialize(&self.group_vk).map_err(je)?; ceremony::verify(&vk, &self.sp, &self.seed, &self.message, sig).map_err(je) } + + /// REAL-TRANSACTION path: aggregate the accumulated shares under the given Orchard randomizer + /// (alpha) instead of the seed. The message must be the shielded sighash and the shares must + /// have been produced by `participantRound2WithRandomizer` with the SAME alpha. + #[wasm_bindgen(js_name = aggregateWithRandomizer)] + pub fn aggregate_with_randomizer(&self, randomizer: &[u8]) -> Result, JsValue> { + let vk = frost::VerifyingKey::deserialize(&self.group_vk).map_err(je)?; + let pubkeys = frost::keys::PublicKeyPackage::deserialize(&self.pubkeys).map_err(je)?; + ceremony::coordinator_aggregate_with_randomizer( + &self.sp, + &vk, + randomizer, + &self.shares, + &pubkeys, + ) + .map_err(je) + } + + /// Verify a group signature under the key re-randomized by the given alpha — the exact check + /// an Orchard spend passes on-chain. + #[wasm_bindgen(js_name = verifyWithRandomizer)] + pub fn verify_with_randomizer( + &self, + randomizer: &[u8], + sig: &[u8], + ) -> Result { + let vk = frost::VerifyingKey::deserialize(&self.group_vk).map_err(je)?; + ceremony::verify_with_randomizer(&vk, randomizer, &self.message, sig).map_err(je) + } } } diff --git a/ui/src/net-flow.test.ts b/ui/src/net-flow.test.ts new file mode 100644 index 0000000..c2a5e90 --- /dev/null +++ b/ui/src/net-flow.test.ts @@ -0,0 +1,113 @@ +/// +// Integration test of the /net multi-device flow, in one process, driving the exact WASM API the +// NetVault screen calls over the relay: a real 3-party DKG (2-of-3), then a signing ceremony over +// the REAL Orchard sighash NetVault signs, with the on-device describeOutputs check — and the +// real-transaction path that signs under the PCZT's Orchard randomizer (alpha), the piece a real +// broadcast needs. Closes the automated-test gap for the live /net ceremony (only the relay +// transport + React rendering are not exercised here; the cryptography is end-to-end). +import { readFileSync } from 'node:fs' +import { beforeAll, describe, expect, it } from 'vitest' +import init, { + DkgSession, + Coordinator, + identifierBytes, + participantRound1, + participantRound2, + participantRound2WithRandomizer, + verifyRedpallas, + describeOutputs, +} from './wasm-pkg/konclave_wasm.js' +import { bytesEqual } from './net' +import { dkgProvenPczt, DKG_SIGHASH } from './demo-vector' + +const hexToBytes = (s: string) => new Uint8Array(s.match(/../g)!.map((b) => parseInt(b, 16))) + +// The alpha of Konclave's real mainnet DKG-vault spend (aab00f90…) — a valid Orchard randomizer. +const DKG_ALPHA = hexToBytes('b2ad61e8bf0de877dd01c52356526adf39b036ffed2e0217ece19407e1717624') + +beforeAll(async () => { + await init(readFileSync(new URL('./wasm-pkg/konclave_wasm_bg.wasm', import.meta.url))) +}) + +// Run a real 3-party DKG (2-of-3) in one process, exactly as /net does across devices over the +// relay. Returns the two quorum sessions, their ids, and the shared group material. +function dkg2of3() { + const N = 3 + const T = 2 + const ids = [1, 2, 3].map((i) => identifierBytes(i)) + const sessions = ids.map((id) => new DkgSession(id, N, T)) + + const r1 = sessions.map((s) => s.round1Package()) + sessions.forEach((s, i) => r1.forEach((pkg, j) => { if (i !== j) s.addRound1(ids[j]!, pkg) })) + + sessions.forEach((s) => s.part2()) + sessions.forEach((s, i) => { + for (let k = 0; k < s.round2Count(); k++) { + const j = ids.findIndex((id) => bytesEqual(id, s.round2Recipient(k))) + sessions[j]!.addRound2(ids[i]!, s.round2Package(k)) + } + }) + sessions.forEach((s) => s.part3()) + + const [s0, s1, s2] = sessions as [DkgSession, DkgSession, DkgSession] + const [id0, id1] = ids as [Uint8Array, Uint8Array] + return { s0, s1, s2, id0, id1, groupVk: s0.groupVk(), pubkeys: s0.pubkeys() } +} + +describe('/net multi-device flow (DKG → sign → verify)', () => { + it('a 2-of-3 DKG-born vault signs the real Orchard sighash and every device verifies', () => { + const { s0, s1, s2, id0, id1, groupVk, pubkeys } = dkg2of3() + + // Every device derived the SAME group verifying key. + expect(bytesEqual(s1.groupVk(), groupVk)).toBe(true) + expect(bytesEqual(s2.groupVk(), groupVk)).toBe(true) + + // Sign the REAL Orchard sighash (the message /net signs), with devices 1 and 2 (quorum = 2). + const msg = hexToBytes(DKG_SIGHASH) + const a = participantRound1(s0.keyPackage()) + const b = participantRound1(s1.keyPackage()) + const coord = new Coordinator(groupVk, pubkeys, msg) + coord.addCommitment(id0, a.commitment()) + coord.addCommitment(id1, b.commitment()) + coord.prepare() + const sp = coord.signingPackage() + const seed = coord.seed() + coord.addShare(id0, participantRound2(sp, a.nonces(), s0.keyPackage(), seed)) + coord.addShare(id1, participantRound2(sp, b.nonces(), s1.keyPackage(), seed)) + const sig = coord.aggregate() + + expect(coord.verify(sig)).toBe(true) + expect(verifyRedpallas(groupVk, sp, seed, msg, sig)).toBe(true) + }) + + it('signs under the PCZT Orchard randomizer (alpha) — the real-transaction path', () => { + // A DKG-born vault signs the sighash under a SPECIFIC Orchard alpha (from extractRandomizers), + // not a commitment-derived seed. This is what lets the signature be injected into the PCZT and + // broadcast. The signature must verify under ak+alpha — the exact check an Orchard spend passes. + const { s0, s1, id0, id1, groupVk, pubkeys } = dkg2of3() + const msg = hexToBytes(DKG_SIGHASH) + const a = participantRound1(s0.keyPackage()) + const b = participantRound1(s1.keyPackage()) + const coord = new Coordinator(groupVk, pubkeys, msg) + coord.addCommitment(id0, a.commitment()) + coord.addCommitment(id1, b.commitment()) + coord.prepare() + const sp = coord.signingPackage() + coord.addShare(id0, participantRound2WithRandomizer(sp, a.nonces(), s0.keyPackage(), DKG_ALPHA)) + coord.addShare(id1, participantRound2WithRandomizer(sp, b.nonces(), s1.keyPackage(), DKG_ALPHA)) + const sig = coord.aggregateWithRandomizer(DKG_ALPHA) + + // Verifies under the key randomized by this alpha... + expect(coord.verifyWithRandomizer(DKG_ALPHA, sig)).toBe(true) + // ...and a DIFFERENT alpha does not — the randomizer binds the signature to the spend. + const otherAlpha = hexToBytes('557c4ff828ed56eb33e8ba7f508a43915338ccf3ad71d1ecedc98e6e861bfc0f') + expect(coord.verifyWithRandomizer(otherAlpha, sig)).toBe(false) + }) + + it('describeOutputs surfaces what the device is signing (recipient + value), as /net shows', () => { + const outs = JSON.parse(describeOutputs(dkgProvenPczt())) as { address: string | null; value: number | null }[] + const recipient = outs.find((o) => o.address !== null) + expect(recipient?.value).toBe(100000) // 0.001 ZEC — what /net renders before signing + expect(recipient?.address).toMatch(/^u1/) // a real Orchard unified address + }) +}) diff --git a/ui/src/wasm-pkg/konclave_wasm.d.ts b/ui/src/wasm-pkg/konclave_wasm.d.ts index 46810cb..5ad1df5 100644 --- a/ui/src/wasm-pkg/konclave_wasm.d.ts +++ b/ui/src/wasm-pkg/konclave_wasm.d.ts @@ -10,6 +10,12 @@ export class Coordinator { addCommitment(id: Uint8Array, commitment: Uint8Array): void; addShare(id: Uint8Array, share: Uint8Array): void; aggregate(): Uint8Array; + /** + * REAL-TRANSACTION path: aggregate the accumulated shares under the given Orchard randomizer + * (alpha) instead of the seed. The message must be the shielded sighash and the shares must + * have been produced by `participantRound2WithRandomizer` with the SAME alpha. + */ + aggregateWithRandomizer(randomizer: Uint8Array): Uint8Array; constructor(group_vk: Uint8Array, pubkeys: Uint8Array, message: Uint8Array); /** * Build the signing package + randomizer seed (both public). Returns nothing; read via getters. @@ -18,6 +24,11 @@ export class Coordinator { seed(): Uint8Array; signingPackage(): Uint8Array; verify(sig: Uint8Array): boolean; + /** + * Verify a group signature under the key re-randomized by the given alpha — the exact check + * an Orchard spend passes on-chain. + */ + verifyWithRandomizer(randomizer: Uint8Array, sig: Uint8Array): boolean; } /** @@ -188,6 +199,13 @@ export function participantRound1(kp_bytes: Uint8Array): Round1; */ export function participantRound2(sp: Uint8Array, nonces_bytes: Uint8Array, kp_bytes: Uint8Array, seed: Uint8Array): Uint8Array; +/** + * Participant device, round 2 (JS), REAL-TRANSACTION path: sign with the given Orchard + * randomizer (the 32-byte alpha from pczt_bridge.extractRandomizers) instead of a seed, so the + * signature can be injected into the PCZT and broadcast. + */ +export function participantRound2WithRandomizer(sp: Uint8Array, nonces_bytes: Uint8Array, kp_bytes: Uint8Array, randomizer: Uint8Array): Uint8Array; + /** * Seal `plaintext` to a recipient's 32-byte public key (used on each round-2 package so the * relay only ever carries ciphertext). `aad` binds context (sender+recipient) into the tag. @@ -211,6 +229,10 @@ export type InitInput = RequestInfo | URL | Response | BufferSource | WebAssembl export interface InitOutput { readonly memory: WebAssembly.Memory; + readonly describeOutputs: (a: number, b: number) => [number, number, number, number]; + readonly extractRandomizers: (a: number, b: number) => [number, number, number, number]; + readonly injectSigs: (a: number, b: number, c: number, d: number, e: number, f: number) => [number, number, number, number]; + readonly selftest: () => [number, number]; readonly __wbg_devicekey_free: (a: number, b: number) => void; readonly __wbg_dkgsession_free: (a: number, b: number) => void; readonly devicekey_fromSecret: (a: number, b: number) => [number, number, number]; @@ -234,36 +256,22 @@ export interface InitOutput { readonly identifierBytes: (a: number) => [number, number, number, number]; readonly sealTo: (a: number, b: number, c: number, d: number, e: number, f: number) => [number, number, number, number]; readonly verifyRedpallas: (a: number, b: number, c: number, d: number, e: number, f: number, g: number, h: number, i: number, j: number) => [number, number, number]; - readonly __wbg_recoverycombiner_free: (a: number, b: number) => void; - readonly __wbg_recoveryhelper_free: (a: number, b: number) => void; - readonly recoverycombiner_addSigma: (a: number, b: number, c: number) => void; - readonly recoverycombiner_keyPackage: (a: number) => [number, number, number, number]; - readonly recoverycombiner_new: (a: number, b: number, c: number, d: number) => number; - readonly recoveryhelper_addHelper: (a: number, b: number, c: number) => void; - readonly recoveryhelper_addIncomingDelta: (a: number, b: number, c: number) => void; - readonly recoveryhelper_computeDeltas: (a: number) => [number, number]; - readonly recoveryhelper_delta: (a: number, b: number) => [number, number]; - readonly recoveryhelper_deltaCount: (a: number) => number; - readonly recoveryhelper_deltaRecipient: (a: number, b: number) => [number, number]; - readonly recoveryhelper_new: (a: number, b: number, c: number, d: number) => number; - readonly recoveryhelper_sigma: (a: number) => [number, number, number, number]; - readonly selftest: () => [number, number]; - readonly describeOutputs: (a: number, b: number) => [number, number, number, number]; - readonly extractRandomizers: (a: number, b: number) => [number, number, number, number]; - readonly injectSigs: (a: number, b: number, c: number, d: number, e: number, f: number) => [number, number, number, number]; readonly __wbg_coordinator_free: (a: number, b: number) => void; readonly __wbg_round1_free: (a: number, b: number) => void; readonly __wbg_testvault_free: (a: number, b: number) => void; readonly coordinator_addCommitment: (a: number, b: number, c: number, d: number, e: number) => void; readonly coordinator_addShare: (a: number, b: number, c: number, d: number, e: number) => void; readonly coordinator_aggregate: (a: number) => [number, number, number, number]; + readonly coordinator_aggregateWithRandomizer: (a: number, b: number, c: number) => [number, number, number, number]; readonly coordinator_new: (a: number, b: number, c: number, d: number, e: number, f: number) => number; readonly coordinator_prepare: (a: number) => [number, number]; readonly coordinator_seed: (a: number) => [number, number]; readonly coordinator_signingPackage: (a: number) => [number, number]; readonly coordinator_verify: (a: number, b: number, c: number) => [number, number, number]; + readonly coordinator_verifyWithRandomizer: (a: number, b: number, c: number, d: number, e: number) => [number, number, number]; readonly participantRound1: (a: number, b: number) => [number, number, number]; readonly participantRound2: (a: number, b: number, c: number, d: number, e: number, f: number, g: number, h: number) => [number, number, number, number]; + readonly participantRound2WithRandomizer: (a: number, b: number, c: number, d: number, e: number, f: number, g: number, h: number) => [number, number, number, number]; readonly round1_commitment: (a: number) => [number, number]; readonly round1_nonces: (a: number) => [number, number]; readonly testvault_groupVk: (a: number) => [number, number]; @@ -271,6 +279,19 @@ export interface InitOutput { readonly testvault_key_package: (a: number, b: number) => [number, number]; readonly testvault_new: () => [number, number, number]; readonly testvault_pubkeys: (a: number) => [number, number]; + readonly __wbg_recoverycombiner_free: (a: number, b: number) => void; + readonly __wbg_recoveryhelper_free: (a: number, b: number) => void; + readonly recoverycombiner_addSigma: (a: number, b: number, c: number) => void; + readonly recoverycombiner_keyPackage: (a: number) => [number, number, number, number]; + readonly recoverycombiner_new: (a: number, b: number, c: number, d: number) => number; + readonly recoveryhelper_addHelper: (a: number, b: number, c: number) => void; + readonly recoveryhelper_addIncomingDelta: (a: number, b: number, c: number) => void; + readonly recoveryhelper_computeDeltas: (a: number) => [number, number]; + readonly recoveryhelper_delta: (a: number, b: number) => [number, number]; + readonly recoveryhelper_deltaCount: (a: number) => number; + readonly recoveryhelper_deltaRecipient: (a: number, b: number) => [number, number]; + readonly recoveryhelper_new: (a: number, b: number, c: number, d: number) => number; + readonly recoveryhelper_sigma: (a: number) => [number, number, number, number]; readonly __wbindgen_exn_store: (a: number) => void; readonly __externref_table_alloc: () => number; readonly __wbindgen_externrefs: WebAssembly.Table; diff --git a/ui/src/wasm-pkg/konclave_wasm.js b/ui/src/wasm-pkg/konclave_wasm.js index 6547cd5..5e9eea4 100644 --- a/ui/src/wasm-pkg/konclave_wasm.js +++ b/ui/src/wasm-pkg/konclave_wasm.js @@ -48,6 +48,24 @@ export class Coordinator { wasm.__wbindgen_free(ret[0], ret[1] * 1, 1); return v1; } + /** + * REAL-TRANSACTION path: aggregate the accumulated shares under the given Orchard randomizer + * (alpha) instead of the seed. The message must be the shielded sighash and the shares must + * have been produced by `participantRound2WithRandomizer` with the SAME alpha. + * @param {Uint8Array} randomizer + * @returns {Uint8Array} + */ + aggregateWithRandomizer(randomizer) { + const ptr0 = passArray8ToWasm0(randomizer, wasm.__wbindgen_malloc); + const len0 = WASM_VECTOR_LEN; + const ret = wasm.coordinator_aggregateWithRandomizer(this.__wbg_ptr, ptr0, len0); + if (ret[3]) { + throw takeFromExternrefTable0(ret[2]); + } + var v2 = getArrayU8FromWasm0(ret[0], ret[1]).slice(); + wasm.__wbindgen_free(ret[0], ret[1] * 1, 1); + return v2; + } /** * @param {Uint8Array} group_vk * @param {Uint8Array} pubkeys @@ -105,6 +123,24 @@ export class Coordinator { } return ret[0] !== 0; } + /** + * Verify a group signature under the key re-randomized by the given alpha — the exact check + * an Orchard spend passes on-chain. + * @param {Uint8Array} randomizer + * @param {Uint8Array} sig + * @returns {boolean} + */ + verifyWithRandomizer(randomizer, sig) { + const ptr0 = passArray8ToWasm0(randomizer, wasm.__wbindgen_malloc); + const len0 = WASM_VECTOR_LEN; + const ptr1 = passArray8ToWasm0(sig, wasm.__wbindgen_malloc); + const len1 = WASM_VECTOR_LEN; + const ret = wasm.coordinator_verifyWithRandomizer(this.__wbg_ptr, ptr0, len0, ptr1, len1); + if (ret[2]) { + throw takeFromExternrefTable0(ret[1]); + } + return ret[0] !== 0; + } } if (Symbol.dispose) Coordinator.prototype[Symbol.dispose] = Coordinator.prototype.free; @@ -730,6 +766,34 @@ export function participantRound2(sp, nonces_bytes, kp_bytes, seed) { return v5; } +/** + * Participant device, round 2 (JS), REAL-TRANSACTION path: sign with the given Orchard + * randomizer (the 32-byte alpha from pczt_bridge.extractRandomizers) instead of a seed, so the + * signature can be injected into the PCZT and broadcast. + * @param {Uint8Array} sp + * @param {Uint8Array} nonces_bytes + * @param {Uint8Array} kp_bytes + * @param {Uint8Array} randomizer + * @returns {Uint8Array} + */ +export function participantRound2WithRandomizer(sp, nonces_bytes, kp_bytes, randomizer) { + const ptr0 = passArray8ToWasm0(sp, wasm.__wbindgen_malloc); + const len0 = WASM_VECTOR_LEN; + const ptr1 = passArray8ToWasm0(nonces_bytes, wasm.__wbindgen_malloc); + const len1 = WASM_VECTOR_LEN; + const ptr2 = passArray8ToWasm0(kp_bytes, wasm.__wbindgen_malloc); + const len2 = WASM_VECTOR_LEN; + const ptr3 = passArray8ToWasm0(randomizer, wasm.__wbindgen_malloc); + const len3 = WASM_VECTOR_LEN; + const ret = wasm.participantRound2WithRandomizer(ptr0, len0, ptr1, len1, ptr2, len2, ptr3, len3); + if (ret[3]) { + throw takeFromExternrefTable0(ret[2]); + } + var v5 = getArrayU8FromWasm0(ret[0], ret[1]).slice(); + wasm.__wbindgen_free(ret[0], ret[1] * 1, 1); + return v5; +} + /** * Seal `plaintext` to a recipient's 32-byte public key (used on each round-2 package so the * relay only ever carries ciphertext). `aad` binds context (sender+recipient) into the tag. diff --git a/ui/src/wasm-pkg/konclave_wasm_bg.wasm b/ui/src/wasm-pkg/konclave_wasm_bg.wasm index 764c241..cbe94dc 100644 Binary files a/ui/src/wasm-pkg/konclave_wasm_bg.wasm and b/ui/src/wasm-pkg/konclave_wasm_bg.wasm differ diff --git a/ui/src/wasm-pkg/konclave_wasm_bg.wasm.d.ts b/ui/src/wasm-pkg/konclave_wasm_bg.wasm.d.ts index 70e48ad..5da455d 100644 --- a/ui/src/wasm-pkg/konclave_wasm_bg.wasm.d.ts +++ b/ui/src/wasm-pkg/konclave_wasm_bg.wasm.d.ts @@ -1,6 +1,10 @@ /* tslint:disable */ /* eslint-disable */ export const memory: WebAssembly.Memory; +export const describeOutputs: (a: number, b: number) => [number, number, number, number]; +export const extractRandomizers: (a: number, b: number) => [number, number, number, number]; +export const injectSigs: (a: number, b: number, c: number, d: number, e: number, f: number) => [number, number, number, number]; +export const selftest: () => [number, number]; export const __wbg_devicekey_free: (a: number, b: number) => void; export const __wbg_dkgsession_free: (a: number, b: number) => void; export const devicekey_fromSecret: (a: number, b: number) => [number, number, number]; @@ -24,36 +28,22 @@ export const dkgsession_round2Recipient: (a: number, b: number) => [number, numb export const identifierBytes: (a: number) => [number, number, number, number]; export const sealTo: (a: number, b: number, c: number, d: number, e: number, f: number) => [number, number, number, number]; export const verifyRedpallas: (a: number, b: number, c: number, d: number, e: number, f: number, g: number, h: number, i: number, j: number) => [number, number, number]; -export const __wbg_recoverycombiner_free: (a: number, b: number) => void; -export const __wbg_recoveryhelper_free: (a: number, b: number) => void; -export const recoverycombiner_addSigma: (a: number, b: number, c: number) => void; -export const recoverycombiner_keyPackage: (a: number) => [number, number, number, number]; -export const recoverycombiner_new: (a: number, b: number, c: number, d: number) => number; -export const recoveryhelper_addHelper: (a: number, b: number, c: number) => void; -export const recoveryhelper_addIncomingDelta: (a: number, b: number, c: number) => void; -export const recoveryhelper_computeDeltas: (a: number) => [number, number]; -export const recoveryhelper_delta: (a: number, b: number) => [number, number]; -export const recoveryhelper_deltaCount: (a: number) => number; -export const recoveryhelper_deltaRecipient: (a: number, b: number) => [number, number]; -export const recoveryhelper_new: (a: number, b: number, c: number, d: number) => number; -export const recoveryhelper_sigma: (a: number) => [number, number, number, number]; -export const selftest: () => [number, number]; -export const describeOutputs: (a: number, b: number) => [number, number, number, number]; -export const extractRandomizers: (a: number, b: number) => [number, number, number, number]; -export const injectSigs: (a: number, b: number, c: number, d: number, e: number, f: number) => [number, number, number, number]; export const __wbg_coordinator_free: (a: number, b: number) => void; export const __wbg_round1_free: (a: number, b: number) => void; export const __wbg_testvault_free: (a: number, b: number) => void; export const coordinator_addCommitment: (a: number, b: number, c: number, d: number, e: number) => void; export const coordinator_addShare: (a: number, b: number, c: number, d: number, e: number) => void; export const coordinator_aggregate: (a: number) => [number, number, number, number]; +export const coordinator_aggregateWithRandomizer: (a: number, b: number, c: number) => [number, number, number, number]; export const coordinator_new: (a: number, b: number, c: number, d: number, e: number, f: number) => number; export const coordinator_prepare: (a: number) => [number, number]; export const coordinator_seed: (a: number) => [number, number]; export const coordinator_signingPackage: (a: number) => [number, number]; export const coordinator_verify: (a: number, b: number, c: number) => [number, number, number]; +export const coordinator_verifyWithRandomizer: (a: number, b: number, c: number, d: number, e: number) => [number, number, number]; export const participantRound1: (a: number, b: number) => [number, number, number]; export const participantRound2: (a: number, b: number, c: number, d: number, e: number, f: number, g: number, h: number) => [number, number, number, number]; +export const participantRound2WithRandomizer: (a: number, b: number, c: number, d: number, e: number, f: number, g: number, h: number) => [number, number, number, number]; export const round1_commitment: (a: number) => [number, number]; export const round1_nonces: (a: number) => [number, number]; export const testvault_groupVk: (a: number) => [number, number]; @@ -61,6 +51,19 @@ export const testvault_id: (a: number, b: number) => [number, number]; export const testvault_key_package: (a: number, b: number) => [number, number]; export const testvault_new: () => [number, number, number]; export const testvault_pubkeys: (a: number) => [number, number]; +export const __wbg_recoverycombiner_free: (a: number, b: number) => void; +export const __wbg_recoveryhelper_free: (a: number, b: number) => void; +export const recoverycombiner_addSigma: (a: number, b: number, c: number) => void; +export const recoverycombiner_keyPackage: (a: number) => [number, number, number, number]; +export const recoverycombiner_new: (a: number, b: number, c: number, d: number) => number; +export const recoveryhelper_addHelper: (a: number, b: number, c: number) => void; +export const recoveryhelper_addIncomingDelta: (a: number, b: number, c: number) => void; +export const recoveryhelper_computeDeltas: (a: number) => [number, number]; +export const recoveryhelper_delta: (a: number, b: number) => [number, number]; +export const recoveryhelper_deltaCount: (a: number) => number; +export const recoveryhelper_deltaRecipient: (a: number, b: number) => [number, number]; +export const recoveryhelper_new: (a: number, b: number, c: number, d: number) => number; +export const recoveryhelper_sigma: (a: number) => [number, number, number, number]; export const __wbindgen_exn_store: (a: number) => void; export const __externref_table_alloc: () => number; export const __wbindgen_externrefs: WebAssembly.Table;