Skip to content

Commit e3a51ce

Browse files
authored
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
1 parent cf2941f commit e3a51ce

File tree

4 files changed

+279
-1
lines changed

4 files changed

+279
-1
lines changed

ironfish-rust-nodejs/index.d.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -251,6 +251,7 @@ export class UnsignedTransaction {
251251
publicKeyRandomness(): string
252252
hash(): Buffer
253253
signingPackage(nativeIdentiferCommitments: Array<string>): string
254+
signingPackageFromRaw(identities: Array<string>, rawCommitments: Array<string>): string
254255
sign(spenderHexKey: string): Buffer
255256
addSignature(signature: Buffer): Buffer
256257
}
@@ -333,6 +334,13 @@ export namespace multisig {
333334
proofAuthorizingKey: string
334335
}
335336
export function aggregateSignatureShares(publicKeyPackageStr: string, signingPackageStr: string, signatureSharesArr: Array<string>): Buffer
337+
export type NativeSignatureShare = SignatureShare
338+
export class SignatureShare {
339+
constructor(jsBytes: Buffer)
340+
static fromFrost(frostSignatureShare: Buffer, identity: Buffer): NativeSignatureShare
341+
identity(): Buffer
342+
frostSignatureShare(): Buffer
343+
}
336344
export class ParticipantSecret {
337345
constructor(jsBytes: Buffer)
338346
serialize(): Buffer
@@ -356,13 +364,15 @@ export namespace multisig {
356364
export class SigningCommitment {
357365
constructor(jsBytes: Buffer)
358366
identity(): Buffer
367+
rawCommitments(): Buffer
359368
verifyChecksum(transactionHash: Buffer, signerIdentities: Array<string>): boolean
360369
}
361370
export type NativeSigningPackage = SigningPackage
362371
export class SigningPackage {
363372
constructor(jsBytes: Buffer)
364373
unsignedTransaction(): NativeUnsignedTransaction
365374
signers(): Array<Buffer>
375+
frostSigningPackage(): Buffer
366376
}
367377
}
368378
export namespace xchacha20poly1305 {

ironfish-rust-nodejs/src/multisig.rs

Lines changed: 74 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,9 @@
44

55
use crate::{structs::NativeUnsignedTransaction, to_napi_err};
66
use ironfish::{
7-
frost::{keys::KeyPackage, round2, Randomizer},
7+
frost::{
8+
frost::round2::SignatureShare as FrostSignatureShare, keys::KeyPackage, round2, Randomizer,
9+
},
810
frost_utils::{
911
account_keys::derive_account_keys, signing_package::SigningPackage,
1012
split_spender_key::split_spender_key,
@@ -136,6 +138,55 @@ pub fn create_signature_share(
136138
Ok(bytes_to_hex(&bytes[..]))
137139
}
138140

141+
#[napi(js_name = "SignatureShare", namespace = "multisig")]
142+
pub struct NativeSignatureShare {
143+
signature_share: SignatureShare,
144+
}
145+
146+
#[napi(namespace = "multisig")]
147+
impl NativeSignatureShare {
148+
#[napi(constructor)]
149+
pub fn new(js_bytes: JsBuffer) -> Result<NativeSignatureShare> {
150+
let bytes = js_bytes.into_value()?;
151+
SignatureShare::deserialize_from(bytes.as_ref())
152+
.map(|signature_share| NativeSignatureShare { signature_share })
153+
.map_err(to_napi_err)
154+
}
155+
156+
#[napi(factory)]
157+
pub fn from_frost(
158+
frost_signature_share: JsBuffer,
159+
identity: JsBuffer,
160+
) -> Result<NativeSignatureShare> {
161+
let frost_signature_share = frost_signature_share.into_value()?;
162+
let frost_signature_share =
163+
FrostSignatureShare::deserialize(frost_signature_share.as_ref())
164+
.map_err(to_napi_err)?;
165+
166+
let identity = identity.into_value()?;
167+
let identity = Identity::deserialize_from(&identity[..]).map_err(to_napi_err)?;
168+
169+
let signature_share = SignatureShare::from_frost(frost_signature_share, identity);
170+
171+
Ok(NativeSignatureShare { signature_share })
172+
}
173+
174+
#[napi]
175+
pub fn identity(&self) -> Buffer {
176+
Buffer::from(self.signature_share.identity().serialize().as_slice())
177+
}
178+
179+
#[napi]
180+
pub fn frost_signature_share(&self) -> Buffer {
181+
Buffer::from(
182+
self.signature_share
183+
.frost_signature_share()
184+
.serialize()
185+
.as_slice(),
186+
)
187+
}
188+
}
189+
139190
#[napi(namespace = "multisig")]
140191
pub struct ParticipantSecret {
141192
secret: Secret,
@@ -333,6 +384,17 @@ impl NativeSigningCommitment {
333384
Buffer::from(self.signing_commitment.identity().serialize().as_slice())
334385
}
335386

387+
#[napi]
388+
pub fn raw_commitments(&self) -> Result<Buffer> {
389+
Ok(Buffer::from(
390+
self.signing_commitment
391+
.raw_commitments()
392+
.serialize()
393+
.map_err(to_napi_err)?
394+
.as_slice(),
395+
))
396+
}
397+
336398
#[napi]
337399
pub fn verify_checksum(
338400
&self,
@@ -378,6 +440,17 @@ impl NativeSigningPackage {
378440
.map(|signer| Buffer::from(&signer.serialize()[..]))
379441
.collect()
380442
}
443+
444+
#[napi]
445+
pub fn frost_signing_package(&self) -> Result<Buffer> {
446+
Ok(Buffer::from(
447+
&self
448+
.signing_package
449+
.frost_signing_package
450+
.serialize()
451+
.map_err(to_napi_err)?[..],
452+
))
453+
}
381454
}
382455

383456
#[napi(namespace = "multisig")]

ironfish-rust-nodejs/src/structs/transaction.rs

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ use ironfish::frost::round1::SigningCommitments;
1212
use ironfish::frost::round2::SignatureShare as FrostSignatureShare;
1313
use ironfish::frost::Identifier;
1414
use ironfish::frost_utils::signing_package::SigningPackage;
15+
use ironfish::participant::Identity;
1516
use ironfish::serializing::bytes_to_hex;
1617
use ironfish::serializing::fr::FrSerializable;
1718
use ironfish::serializing::hex_to_vec_bytes;
@@ -455,6 +456,37 @@ impl NativeUnsignedTransaction {
455456
Ok(bytes_to_hex(&vec))
456457
}
457458

459+
#[napi]
460+
pub fn signing_package_from_raw(
461+
&self,
462+
identities: Vec<String>,
463+
raw_commitments: Vec<String>,
464+
) -> Result<String> {
465+
let mut commitments = Vec::new();
466+
467+
for (index, identity) in identities.iter().enumerate() {
468+
let identity_bytes = hex_to_vec_bytes(identity).map_err(to_napi_err)?;
469+
let identity = Identity::deserialize_from(&identity_bytes[..]).map_err(to_napi_err)?;
470+
471+
let raw_commitment = &raw_commitments[index];
472+
let commitment_bytes = hex_to_vec_bytes(raw_commitment).map_err(to_napi_err)?;
473+
let commitment =
474+
SigningCommitments::deserialize(&commitment_bytes[..]).map_err(to_napi_err)?;
475+
476+
commitments.push((identity, commitment));
477+
}
478+
479+
let signing_package = self
480+
.transaction
481+
.signing_package(commitments)
482+
.map_err(to_napi_err)?;
483+
484+
let mut vec: Vec<u8> = vec![];
485+
signing_package.write(&mut vec).map_err(to_napi_err)?;
486+
487+
Ok(bytes_to_hex(&vec))
488+
}
489+
458490
#[napi]
459491
pub fn sign(&mut self, spender_hex_key: String) -> Result<Buffer> {
460492
let spender_key = SaplingKey::from_hex(&spender_hex_key).map_err(to_napi_err)?;

ironfish/src/multisig.test.slow.ts

Lines changed: 163 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,163 @@
1+
/* This Source Code Form is subject to the terms of the Mozilla Public
2+
* License, v. 2.0. If a copy of the MPL was not distributed with this
3+
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
4+
5+
import { Asset, multisig, Note as NativeNote, verifyTransactions } from '@ironfish/rust-nodejs'
6+
import { Note, RawTransaction } from './primitives'
7+
import { Transaction, TransactionVersion } from './primitives/transaction'
8+
import { makeFakeWitness } from './testUtilities'
9+
10+
describe('multisig', () => {
11+
describe('dkg', () => {
12+
it('should create multisig accounts and sign transactions', () => {
13+
const participantSecrets = [
14+
multisig.ParticipantSecret.random(),
15+
multisig.ParticipantSecret.random(),
16+
multisig.ParticipantSecret.random(),
17+
]
18+
19+
const secrets = participantSecrets.map((secret) => secret.serialize().toString('hex'))
20+
const identities = participantSecrets.map((secret) =>
21+
secret.toIdentity().serialize().toString('hex'),
22+
)
23+
24+
const minSigners = 2
25+
26+
const round1Packages = secrets.map((_, index) =>
27+
multisig.dkgRound1(identities[index], minSigners, identities),
28+
)
29+
30+
const round1PublicPackages = round1Packages.map(
31+
(packages) => packages.round1PublicPackage,
32+
)
33+
34+
const round2Packages = secrets.map((secret, index) =>
35+
multisig.dkgRound2(
36+
secret,
37+
round1Packages[index].round1SecretPackage,
38+
round1PublicPackages,
39+
),
40+
)
41+
42+
const round2PublicPackages = round2Packages.map(
43+
(packages) => packages.round2PublicPackage,
44+
)
45+
46+
const round3Packages = participantSecrets.map((participantSecret, index) =>
47+
multisig.dkgRound3(
48+
participantSecret,
49+
round2Packages[index].round2SecretPackage,
50+
round1PublicPackages,
51+
round2PublicPackages,
52+
),
53+
)
54+
55+
const publicAddress = round3Packages[0].publicAddress
56+
57+
const raw = new RawTransaction(TransactionVersion.V1)
58+
59+
const inNote = new NativeNote(
60+
publicAddress,
61+
42n,
62+
Buffer.from(''),
63+
Asset.nativeId(),
64+
publicAddress,
65+
)
66+
const outNote = new NativeNote(
67+
publicAddress,
68+
40n,
69+
Buffer.from(''),
70+
Asset.nativeId(),
71+
publicAddress,
72+
)
73+
const asset = new Asset(publicAddress, 'Testcoin', 'A really cool coin')
74+
const mintOutNote = new NativeNote(
75+
publicAddress,
76+
5n,
77+
Buffer.from(''),
78+
asset.id(),
79+
publicAddress,
80+
)
81+
82+
const witness = makeFakeWitness(new Note(inNote.serialize()))
83+
84+
raw.spends.push({ note: new Note(inNote.serialize()), witness })
85+
raw.outputs.push({ note: new Note(outNote.serialize()) })
86+
raw.outputs.push({ note: new Note(mintOutNote.serialize()) })
87+
raw.mints.push({
88+
creator: asset.creator().toString('hex'),
89+
name: asset.name().toString(),
90+
metadata: asset.metadata().toString(),
91+
value: mintOutNote.value(),
92+
})
93+
raw.fee = 1n
94+
95+
const proofAuthorizingKey = round3Packages[0].proofAuthorizingKey
96+
const viewKey = round3Packages[0].viewKey
97+
const outgoingViewKey = round3Packages[0].outgoingViewKey
98+
99+
const unsignedTransaction = raw.build(proofAuthorizingKey, viewKey, outgoingViewKey)
100+
const transactionHash = unsignedTransaction.hash()
101+
102+
const commitments = secrets.map((secret, index) =>
103+
multisig.createSigningCommitment(
104+
secret,
105+
round3Packages[index].keyPackage,
106+
transactionHash,
107+
identities,
108+
),
109+
)
110+
111+
// Simulates receiving raw commitments from Ledger
112+
// Ledger app generates raw commitments, not wrapped SigningCommitment
113+
const commitmentIdentities: string[] = []
114+
const rawCommitments: string[] = []
115+
for (const commitment of commitments) {
116+
const signingCommitment = new multisig.SigningCommitment(Buffer.from(commitment, 'hex'))
117+
commitmentIdentities.push(signingCommitment.identity().toString('hex'))
118+
rawCommitments.push(signingCommitment.rawCommitments().toString('hex'))
119+
}
120+
121+
const signingPackage = unsignedTransaction.signingPackageFromRaw(
122+
commitmentIdentities,
123+
rawCommitments,
124+
)
125+
126+
// Ensure that we can extract deserialize and extract frost signing package
127+
// Ledger app needs frost signing package to generate signature shares
128+
const frostSigningPackage = new multisig.SigningPackage(
129+
Buffer.from(signingPackage, 'hex'),
130+
).frostSigningPackage()
131+
expect(frostSigningPackage).not.toBeUndefined()
132+
133+
const signatureShares = secrets.map((secret, index) =>
134+
multisig.createSignatureShare(secret, round3Packages[index].keyPackage, signingPackage),
135+
)
136+
137+
// Ensure we can construct SignatureShare from parts
138+
// Ledger app returns raw frost signature shares
139+
for (const share of signatureShares) {
140+
const signatureShare = new multisig.SignatureShare(Buffer.from(share, 'hex'))
141+
const reconstructed = multisig.SignatureShare.fromFrost(
142+
signatureShare.frostSignatureShare(),
143+
signatureShare.identity(),
144+
)
145+
expect(reconstructed.frostSignatureShare()).toEqual(
146+
signatureShare.frostSignatureShare(),
147+
)
148+
expect(reconstructed.identity()).toEqual(signatureShare.identity())
149+
}
150+
151+
const serializedTransaction = multisig.aggregateSignatureShares(
152+
round3Packages[0].publicKeyPackage,
153+
signingPackage,
154+
signatureShares,
155+
)
156+
const transaction = new Transaction(serializedTransaction)
157+
158+
expect(verifyTransactions([serializedTransaction])).toBeTruthy()
159+
160+
expect(transaction.unsignedHash().equals(transactionHash)).toBeTruthy()
161+
})
162+
})
163+
})

0 commit comments

Comments
 (0)