From e3a51ce07928a1201e26daee4adb3714e512362e Mon Sep 17 00:00:00 2001 From: Hugh Cunningham <57735705+hughy@users.noreply.github.com> Date: Tue, 17 Sep 2024 10:01:12 -0700 Subject: [PATCH] adds napi methods to support Ledger multisig (#5376) adds typescript version of test_dkg_signing example adds multisig.test.slow.ts that replicates the logic of test_dkg_signing from ironfish-rust - adds method to retrieve frost signing package from deserialized signing package - adds signingPackageFromRaw method - allows construction of signing package from identities and raw commitments (from frost, not ironfish) - adds method to NativeSigningCommitment to get raw_commitments - defines NativeSignatureShare to support deserializing ironfish SignatureShares and accessing the underlying identity and frost signature share - adds from_frost factor method to reconstruct SignatureShare from parts --- ironfish-rust-nodejs/index.d.ts | 10 ++ ironfish-rust-nodejs/src/multisig.rs | 75 +++++++- .../src/structs/transaction.rs | 32 ++++ ironfish/src/multisig.test.slow.ts | 163 ++++++++++++++++++ 4 files changed, 279 insertions(+), 1 deletion(-) create mode 100644 ironfish/src/multisig.test.slow.ts diff --git a/ironfish-rust-nodejs/index.d.ts b/ironfish-rust-nodejs/index.d.ts index e7eb933dc8..5eac35fc49 100644 --- a/ironfish-rust-nodejs/index.d.ts +++ b/ironfish-rust-nodejs/index.d.ts @@ -251,6 +251,7 @@ export class UnsignedTransaction { publicKeyRandomness(): string hash(): Buffer signingPackage(nativeIdentiferCommitments: Array): string + signingPackageFromRaw(identities: Array, rawCommitments: Array): string sign(spenderHexKey: string): Buffer addSignature(signature: Buffer): Buffer } @@ -333,6 +334,13 @@ export namespace multisig { proofAuthorizingKey: string } export function aggregateSignatureShares(publicKeyPackageStr: string, signingPackageStr: string, signatureSharesArr: Array): Buffer + export type NativeSignatureShare = SignatureShare + export class SignatureShare { + constructor(jsBytes: Buffer) + static fromFrost(frostSignatureShare: Buffer, identity: Buffer): NativeSignatureShare + identity(): Buffer + frostSignatureShare(): Buffer + } export class ParticipantSecret { constructor(jsBytes: Buffer) serialize(): Buffer @@ -356,6 +364,7 @@ export namespace multisig { export class SigningCommitment { constructor(jsBytes: Buffer) identity(): Buffer + rawCommitments(): Buffer verifyChecksum(transactionHash: Buffer, signerIdentities: Array): boolean } export type NativeSigningPackage = SigningPackage @@ -363,6 +372,7 @@ export namespace multisig { constructor(jsBytes: Buffer) unsignedTransaction(): NativeUnsignedTransaction signers(): Array + frostSigningPackage(): Buffer } } export namespace xchacha20poly1305 { diff --git a/ironfish-rust-nodejs/src/multisig.rs b/ironfish-rust-nodejs/src/multisig.rs index 8fae0761b1..0291d9bfde 100644 --- a/ironfish-rust-nodejs/src/multisig.rs +++ b/ironfish-rust-nodejs/src/multisig.rs @@ -4,7 +4,9 @@ use crate::{structs::NativeUnsignedTransaction, to_napi_err}; use ironfish::{ - frost::{keys::KeyPackage, round2, Randomizer}, + frost::{ + frost::round2::SignatureShare as FrostSignatureShare, keys::KeyPackage, round2, Randomizer, + }, frost_utils::{ account_keys::derive_account_keys, signing_package::SigningPackage, split_spender_key::split_spender_key, @@ -136,6 +138,55 @@ pub fn create_signature_share( Ok(bytes_to_hex(&bytes[..])) } +#[napi(js_name = "SignatureShare", namespace = "multisig")] +pub struct NativeSignatureShare { + signature_share: SignatureShare, +} + +#[napi(namespace = "multisig")] +impl NativeSignatureShare { + #[napi(constructor)] + pub fn new(js_bytes: JsBuffer) -> Result { + let bytes = js_bytes.into_value()?; + SignatureShare::deserialize_from(bytes.as_ref()) + .map(|signature_share| NativeSignatureShare { signature_share }) + .map_err(to_napi_err) + } + + #[napi(factory)] + pub fn from_frost( + frost_signature_share: JsBuffer, + identity: JsBuffer, + ) -> Result { + let frost_signature_share = frost_signature_share.into_value()?; + let frost_signature_share = + FrostSignatureShare::deserialize(frost_signature_share.as_ref()) + .map_err(to_napi_err)?; + + let identity = identity.into_value()?; + let identity = Identity::deserialize_from(&identity[..]).map_err(to_napi_err)?; + + let signature_share = SignatureShare::from_frost(frost_signature_share, identity); + + Ok(NativeSignatureShare { signature_share }) + } + + #[napi] + pub fn identity(&self) -> Buffer { + Buffer::from(self.signature_share.identity().serialize().as_slice()) + } + + #[napi] + pub fn frost_signature_share(&self) -> Buffer { + Buffer::from( + self.signature_share + .frost_signature_share() + .serialize() + .as_slice(), + ) + } +} + #[napi(namespace = "multisig")] pub struct ParticipantSecret { secret: Secret, @@ -333,6 +384,17 @@ impl NativeSigningCommitment { Buffer::from(self.signing_commitment.identity().serialize().as_slice()) } + #[napi] + pub fn raw_commitments(&self) -> Result { + Ok(Buffer::from( + self.signing_commitment + .raw_commitments() + .serialize() + .map_err(to_napi_err)? + .as_slice(), + )) + } + #[napi] pub fn verify_checksum( &self, @@ -378,6 +440,17 @@ impl NativeSigningPackage { .map(|signer| Buffer::from(&signer.serialize()[..])) .collect() } + + #[napi] + pub fn frost_signing_package(&self) -> Result { + Ok(Buffer::from( + &self + .signing_package + .frost_signing_package + .serialize() + .map_err(to_napi_err)?[..], + )) + } } #[napi(namespace = "multisig")] diff --git a/ironfish-rust-nodejs/src/structs/transaction.rs b/ironfish-rust-nodejs/src/structs/transaction.rs index 17070949cd..3b597bb932 100644 --- a/ironfish-rust-nodejs/src/structs/transaction.rs +++ b/ironfish-rust-nodejs/src/structs/transaction.rs @@ -12,6 +12,7 @@ use ironfish::frost::round1::SigningCommitments; use ironfish::frost::round2::SignatureShare as FrostSignatureShare; use ironfish::frost::Identifier; use ironfish::frost_utils::signing_package::SigningPackage; +use ironfish::participant::Identity; use ironfish::serializing::bytes_to_hex; use ironfish::serializing::fr::FrSerializable; use ironfish::serializing::hex_to_vec_bytes; @@ -455,6 +456,37 @@ impl NativeUnsignedTransaction { Ok(bytes_to_hex(&vec)) } + #[napi] + pub fn signing_package_from_raw( + &self, + identities: Vec, + raw_commitments: Vec, + ) -> Result { + let mut commitments = Vec::new(); + + for (index, identity) in identities.iter().enumerate() { + let identity_bytes = hex_to_vec_bytes(identity).map_err(to_napi_err)?; + let identity = Identity::deserialize_from(&identity_bytes[..]).map_err(to_napi_err)?; + + let raw_commitment = &raw_commitments[index]; + let commitment_bytes = hex_to_vec_bytes(raw_commitment).map_err(to_napi_err)?; + let commitment = + SigningCommitments::deserialize(&commitment_bytes[..]).map_err(to_napi_err)?; + + commitments.push((identity, commitment)); + } + + let signing_package = self + .transaction + .signing_package(commitments) + .map_err(to_napi_err)?; + + let mut vec: Vec = vec![]; + signing_package.write(&mut vec).map_err(to_napi_err)?; + + Ok(bytes_to_hex(&vec)) + } + #[napi] pub fn sign(&mut self, spender_hex_key: String) -> Result { let spender_key = SaplingKey::from_hex(&spender_hex_key).map_err(to_napi_err)?; diff --git a/ironfish/src/multisig.test.slow.ts b/ironfish/src/multisig.test.slow.ts new file mode 100644 index 0000000000..8af50cad91 --- /dev/null +++ b/ironfish/src/multisig.test.slow.ts @@ -0,0 +1,163 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +import { Asset, multisig, Note as NativeNote, verifyTransactions } from '@ironfish/rust-nodejs' +import { Note, RawTransaction } from './primitives' +import { Transaction, TransactionVersion } from './primitives/transaction' +import { makeFakeWitness } from './testUtilities' + +describe('multisig', () => { + describe('dkg', () => { + it('should create multisig accounts and sign transactions', () => { + const participantSecrets = [ + multisig.ParticipantSecret.random(), + multisig.ParticipantSecret.random(), + multisig.ParticipantSecret.random(), + ] + + const secrets = participantSecrets.map((secret) => secret.serialize().toString('hex')) + const identities = participantSecrets.map((secret) => + secret.toIdentity().serialize().toString('hex'), + ) + + const minSigners = 2 + + const round1Packages = secrets.map((_, index) => + multisig.dkgRound1(identities[index], minSigners, identities), + ) + + const round1PublicPackages = round1Packages.map( + (packages) => packages.round1PublicPackage, + ) + + const round2Packages = secrets.map((secret, index) => + multisig.dkgRound2( + secret, + round1Packages[index].round1SecretPackage, + round1PublicPackages, + ), + ) + + const round2PublicPackages = round2Packages.map( + (packages) => packages.round2PublicPackage, + ) + + const round3Packages = participantSecrets.map((participantSecret, index) => + multisig.dkgRound3( + participantSecret, + round2Packages[index].round2SecretPackage, + round1PublicPackages, + round2PublicPackages, + ), + ) + + const publicAddress = round3Packages[0].publicAddress + + const raw = new RawTransaction(TransactionVersion.V1) + + const inNote = new NativeNote( + publicAddress, + 42n, + Buffer.from(''), + Asset.nativeId(), + publicAddress, + ) + const outNote = new NativeNote( + publicAddress, + 40n, + Buffer.from(''), + Asset.nativeId(), + publicAddress, + ) + const asset = new Asset(publicAddress, 'Testcoin', 'A really cool coin') + const mintOutNote = new NativeNote( + publicAddress, + 5n, + Buffer.from(''), + asset.id(), + publicAddress, + ) + + const witness = makeFakeWitness(new Note(inNote.serialize())) + + raw.spends.push({ note: new Note(inNote.serialize()), witness }) + raw.outputs.push({ note: new Note(outNote.serialize()) }) + raw.outputs.push({ note: new Note(mintOutNote.serialize()) }) + raw.mints.push({ + creator: asset.creator().toString('hex'), + name: asset.name().toString(), + metadata: asset.metadata().toString(), + value: mintOutNote.value(), + }) + raw.fee = 1n + + const proofAuthorizingKey = round3Packages[0].proofAuthorizingKey + const viewKey = round3Packages[0].viewKey + const outgoingViewKey = round3Packages[0].outgoingViewKey + + const unsignedTransaction = raw.build(proofAuthorizingKey, viewKey, outgoingViewKey) + const transactionHash = unsignedTransaction.hash() + + const commitments = secrets.map((secret, index) => + multisig.createSigningCommitment( + secret, + round3Packages[index].keyPackage, + transactionHash, + identities, + ), + ) + + // Simulates receiving raw commitments from Ledger + // Ledger app generates raw commitments, not wrapped SigningCommitment + const commitmentIdentities: string[] = [] + const rawCommitments: string[] = [] + for (const commitment of commitments) { + const signingCommitment = new multisig.SigningCommitment(Buffer.from(commitment, 'hex')) + commitmentIdentities.push(signingCommitment.identity().toString('hex')) + rawCommitments.push(signingCommitment.rawCommitments().toString('hex')) + } + + const signingPackage = unsignedTransaction.signingPackageFromRaw( + commitmentIdentities, + rawCommitments, + ) + + // Ensure that we can extract deserialize and extract frost signing package + // Ledger app needs frost signing package to generate signature shares + const frostSigningPackage = new multisig.SigningPackage( + Buffer.from(signingPackage, 'hex'), + ).frostSigningPackage() + expect(frostSigningPackage).not.toBeUndefined() + + const signatureShares = secrets.map((secret, index) => + multisig.createSignatureShare(secret, round3Packages[index].keyPackage, signingPackage), + ) + + // Ensure we can construct SignatureShare from parts + // Ledger app returns raw frost signature shares + for (const share of signatureShares) { + const signatureShare = new multisig.SignatureShare(Buffer.from(share, 'hex')) + const reconstructed = multisig.SignatureShare.fromFrost( + signatureShare.frostSignatureShare(), + signatureShare.identity(), + ) + expect(reconstructed.frostSignatureShare()).toEqual( + signatureShare.frostSignatureShare(), + ) + expect(reconstructed.identity()).toEqual(signatureShare.identity()) + } + + const serializedTransaction = multisig.aggregateSignatureShares( + round3Packages[0].publicKeyPackage, + signingPackage, + signatureShares, + ) + const transaction = new Transaction(serializedTransaction) + + expect(verifyTransactions([serializedTransaction])).toBeTruthy() + + expect(transaction.unsignedHash().equals(transactionHash)).toBeTruthy() + }) + }) +})