Skip to content

Commit

Permalink
adds napi methods to support Ledger multisig (#5376)
Browse files Browse the repository at this point in the history
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
  • Loading branch information
hughy authored Sep 17, 2024
1 parent cf2941f commit e3a51ce
Show file tree
Hide file tree
Showing 4 changed files with 279 additions and 1 deletion.
10 changes: 10 additions & 0 deletions ironfish-rust-nodejs/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -251,6 +251,7 @@ export class UnsignedTransaction {
publicKeyRandomness(): string
hash(): Buffer
signingPackage(nativeIdentiferCommitments: Array<string>): string
signingPackageFromRaw(identities: Array<string>, rawCommitments: Array<string>): string
sign(spenderHexKey: string): Buffer
addSignature(signature: Buffer): Buffer
}
Expand Down Expand Up @@ -333,6 +334,13 @@ export namespace multisig {
proofAuthorizingKey: string
}
export function aggregateSignatureShares(publicKeyPackageStr: string, signingPackageStr: string, signatureSharesArr: Array<string>): 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
Expand All @@ -356,13 +364,15 @@ export namespace multisig {
export class SigningCommitment {
constructor(jsBytes: Buffer)
identity(): Buffer
rawCommitments(): Buffer
verifyChecksum(transactionHash: Buffer, signerIdentities: Array<string>): boolean
}
export type NativeSigningPackage = SigningPackage
export class SigningPackage {
constructor(jsBytes: Buffer)
unsignedTransaction(): NativeUnsignedTransaction
signers(): Array<Buffer>
frostSigningPackage(): Buffer
}
}
export namespace xchacha20poly1305 {
Expand Down
75 changes: 74 additions & 1 deletion ironfish-rust-nodejs/src/multisig.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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<NativeSignatureShare> {
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<NativeSignatureShare> {
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,
Expand Down Expand Up @@ -333,6 +384,17 @@ impl NativeSigningCommitment {
Buffer::from(self.signing_commitment.identity().serialize().as_slice())
}

#[napi]
pub fn raw_commitments(&self) -> Result<Buffer> {
Ok(Buffer::from(
self.signing_commitment
.raw_commitments()
.serialize()
.map_err(to_napi_err)?
.as_slice(),
))
}

#[napi]
pub fn verify_checksum(
&self,
Expand Down Expand Up @@ -378,6 +440,17 @@ impl NativeSigningPackage {
.map(|signer| Buffer::from(&signer.serialize()[..]))
.collect()
}

#[napi]
pub fn frost_signing_package(&self) -> Result<Buffer> {
Ok(Buffer::from(
&self
.signing_package
.frost_signing_package
.serialize()
.map_err(to_napi_err)?[..],
))
}
}

#[napi(namespace = "multisig")]
Expand Down
32 changes: 32 additions & 0 deletions ironfish-rust-nodejs/src/structs/transaction.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -455,6 +456,37 @@ impl NativeUnsignedTransaction {
Ok(bytes_to_hex(&vec))
}

#[napi]
pub fn signing_package_from_raw(
&self,
identities: Vec<String>,
raw_commitments: Vec<String>,
) -> Result<String> {
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<u8> = 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<Buffer> {
let spender_key = SaplingKey::from_hex(&spender_hex_key).map_err(to_napi_err)?;
Expand Down
163 changes: 163 additions & 0 deletions ironfish/src/multisig.test.slow.ts
Original file line number Diff line number Diff line change
@@ -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()
})
})
})

0 comments on commit e3a51ce

Please sign in to comment.