A Cryptographic Library for Smooth Blockchain uses.
This repository is a private fork of the audited SCL. In addition to SCL generic ECC solidity, it contains the actual experiments around Musig2 and FROST.
The aim of the Smooth-LibMPC is to provide an open source implementation of
- Musig2: is a MPC scriptless algorithm, specified in BIP327, it is part of Taproot and enables n out of n signature,
- Atomic Swaps: use adaptator signatures to enable trustless bridges, requires Musig2,
- FROST is a threshold signature scheme (k out of n), which enables governance, without requiring to deploy on chain contract, providing more privacy on the governance.
The SmoothMPCLib consists in two parts:
- An onchain solidity verifier, implemented in libSCL_BIP327.sol as part of SCL (Smoo.th Crypto Lib)
- A javascript implementation of signer side to be integrated into any webApp leveraging the targeted protocol.
- Compatibility with BIP340 : When curve is set to 'secp256k1', the result of the MPC procedure passes BIP340 verification for the BIP340 X-only version of the group public key and a message.
- Compatibility with RFC8032 : When curve is set to 'ed25519', the result of the MPC procedure passes RFC8032 verification for the compressed signature version.
| Protocol | status | branch | Comment | File |
|---|---|---|---|---|
| Onchain Verifier | OK | main | libSCL_BIP327.sol (secp256k1), libSCL_RIP6565.sol (ed25519) | |
| Musig2-secp256k1 | OK | main | bip327.mjs or SCL_Musig2.mjs | |
| Musig2-ed25519 | OK | main | SCL_Musig2.mjs | |
| Atomic Swaps | OK | main | SCL_atomic_swaps.mjs | |
| Frost | OK | main | SCL_frost.mjs |
|
It is necessary to install noble-curves, which the library is based on for the elliptic primitives function.
npm install @noble/curves
Test of BIP327 can be run typing node test_bip327.mjs.
The test includes the BIP327 test vectors, enforcing compatibility of the signer with BTC, and any 4337/7702 integrating the libSCL_BIP327.sol verifier.
Clone the repository, then type forge test. (Some troubles are solved running foundryup and forge init --force).
Musig2 is a MPC signature protocol in which
A 2 of 2 session is described here. It generalizes identically with larger user set. We use BIP327 with no tweak.
Prior to any use, an object of type SCL_Musig2 must be initialized with one of the supported curve.
const curve = 'secp256k1';
const signer = new SCL_Musig2(curve);
curve can also be configured to 'ed25519'.
First, user1 and user2 generates their private key, or import them from seed.
const sk1=secp256k1.utils.randomPrivateKey();//this provides a 32 bytes array
const sk2=secp256k1.utils.randomPrivateKey();
Corresponding aggregated key is derived from public keys:
const pubK1=signer.IndividualPubKey_array(sk1);
const pubK2=signer.IndividualPubKey_array(sk2);
let aggpk = signer.Key_agg(pubkeys)[0];//here aggpk is a 33 bytes compressed public key
let x_aggpk=aggpk.slice(1,33);//x-only version for noncegen
(of course in practice derivation occurs separately in each signer secure domain)
Assuming user generated their public key according to previous section, they now want to jointly sign a message msg. An example session is provided here.
In first round, user1 and user2 generates public and secret nonces. Public are shared, secret keep in respective secure domain.
const nonce1=signer.Nonce_gen_internal(rand1, sk1, pubK1, x_aggpk, msg, Buffer.from("00000000", 'hex'));
const nonce2=signer.Nonce_gen_internal(rand2, sk2, pubK2, x_aggpk, msg, Buffer.from("00000000", 'hex'));
let aggnonce = signer.Nonce_agg([nonce1[1].toString('hex'), nonce2[1].toString('hex')]);
In second round, each user computes its partial signature, which are then aggregated and broadcast on chain:
const tweaks=[];
const session_ctx=[aggnonce, pubkeys, [], [], msg];
let p1=signer.Psign(nonce1[0], sk1, session_ctx);
let p2=signer.Psign(nonce2[0], sk2, session_ctx);
let psigs=[p1,p2];
let res=signer.Partial_sig_agg(psigs, session_ctx);
res is the final results to push onchain. One can check the correctness in front before pushing the results:
let check=signer.Schnorr_verify(msg, x_aggpk, res);
console.log("check=", check);
An atomic swap is a process allowing two users to exchange information/token from distinct chains. While it is possible to use a basic 'hash locked mechanism', such a process reveals information about the swap, it also requires a script. Using Musig2, it is possible to provide the functionnality but disabling the possibility to link the transactions on each chain. With liquidity, atomic swaps provide a building block to provide a permissionless bridge.
The description doesn't include the timelock on both chains, which cancel the deposits if Alice and Bob didn't succeed in their withdrawal.
Abortion of one of the participant is the only way the protocol shall fail, which is resolved by the timelock condition of withdrawal.
By convention chain1 is the chain with the lowest chainID.
The sequencing of a Musig2 based atomic swap session is as follow:
- Alice and Bob owns respective key pairs
$(sk_a, Q_A)$ and$(sk_b, Q_B)$ -
$P$ is the multisig public key computed withkey_agg, - A sends 1 token to AB on chain 2, B sends 1 token to AB on chain 1, both are timelocked (refund if protocol fails),
- A and B agrees offchain on R1 and R2 using
nonce_gen_internalandnonce_agg. First nonce R1 is used for chain1, second R2 for chain2. Corresponding Alice and Bob secnonces are denoted as rA1, rA2, rB1, rB2. - A chooses a random tweak t, and computes the tweaked partial signature with
psignthen using t as tweak adaptator:- Alice signs and broadcast off chain
$T=tG$ ,$s'_A1=(t+ra_1)G+H(R, P, m_1)sk_A$ , where$m_1$ is the transaction sending coins from AB to A usingpsign_adapt. t is kept secret by Alice. - Using the same
$t, T$ , alice signs and broadcast off chain$s'A2=(t+ra_2)G+H(R, P, m_2)sk_A$ where$m_2$ is the transaction sending coins from AB to B.
- Alice signs and broadcast off chain
- B checks the compliance of
$T, s'_A1, s'_A2$ usingatomic_check - B signs and broadcast off chain partial signature
$s_B1=rb.G+H(P,R,m_1)P_B$ withpsign - knowing
$t, S_A1, S_B1$ A computes$S_{AB}$ the Musig2 signatures of$m_1$ usingsign_untweak, and broadcast it on chain 1. - B reads the value
$S_{AB}$ on chain 1, learns t, then broadcast on chain 2$S_{AB}(m_2)$ usingsign_untweakon chain 2 to unlock its token.
To reduce the complexity for developpers, the library provides state machine for the initiator and responder of the swap. Each of the previous exchange between a message from Alice to Bob.
//generating keypairs
let Initiator=new SCL_Atomic_Initiator(curve, signer.curve.Get_Random_privateKey());
let Responder=new SCL_Atomic_Responder(curve, signer.curve.Get_Random_privateKey());
//the transaction unlocking tokens for Alice and Bob, must be multisigned with Musig2
//Alice want to compute msg1 signed by AB
//Bob wants to compute msg2 signed by AB
const tx1=Buffer.from("Unlock 1 BTC on bitcoin to Alice",'utf-8');
const tx2=Buffer.from("Unlock 1WBTC on Ethereum to Bob",'utf-8');
console.log("Initiator Start session");
let Message_I1=Initiator.InitSession(tx1, tx2); //Initiator sends I1 to responder offchain
console.log("Responder Start session");
let Message_R1=Responder.RespondInit(Message_I1);//Respondeur sends R1 to Initiator offchain
console.log("Initiator Partial Sign and tweak");
let Message_I2=Initiator.PartialSign_Tweaked(Message_R1);//Initiator sends I2 to responder offchain
//At this Point Alice and Bob locks the funds to multisig address on chain 1 and chain 2
console.log("Responder Check and Partial Sign");
let Message_R2=Responder.PartialSign(Message_I2);//Respondeur sends R2 to Initiator offchain
console.log("Initiator Signature Aggregation and Unlock");
let UnlockSigAlice=Initiator.FinalUnlock(Message_R2);//final signature to Unlock chain1 token by Initiator
console.log("Responder Signature Aggregation and Unlock");
let UnlockSigBob=Initiator.FinalUnlock(UnlockSigAlice);//final signature to Unlock chain2 token by Responder
Note: the protocol requires to broadcast onchain 4 values (2 locked tokens, then two unlocking signatures).
-
$P_A, P_B$ and$P_{AB}$ might differ on chain 1 and 2. - Using a zk-proof of Discrete Log, it is possible to use a different curve on chain1 and chain2.
The element
FROST is a TSS (threshold signature scheme), enabling a 'm out of n' authentication. n users owns a private key tied to a secret polynomial generated during an initial phase (which can be centralized or not). The group key, indistinguishable from a classic Schnorr (Taproot) key is the evaluation of this polynomial at origin. Later on, when m users colludes, they are able to create a signature for a given message without revealing their share.
In simpler terms, FROST provides a 'm out of n' authentication, without revealing the interaction between signers, keeping governance private. It can be used as a privacy enhancement for contracts like Safe. It can also be used to provide a private policy management for recovery.
The following describes a full session.
Related code is available in test_random_fullsession() which generates random input and select a random subset of size k+1 from the n users to perform a session.
For now the generation use a 'secret sharing' like key generation. It is implemented in the SCL_trustedKeyGen class.
let curve=new SCL_ecc(Curvename);
let sk=curve.Get_Random_privateKey();
let dealer=new SCL_trustedKeyGen(Curvename,sk, n,k);
//provide shares to users:
let b8_pubshares=elements.map(index => curve.PointCompress(dealer.pubshares[index]))
let secshares=elements.map(index => dealer.secshares[index]);
Curvename can be set to either 'secp256k1' or 'ed25519'. The input of the generation are the curvename ('secp256k1' or 'ed25519' for now), the private key of the dealer, the maximal number of users, and the degree of the polynomial (the number of minimal participants to authenticate minus 1). Once initialized, the structure contains the secret shares and public shares of users, to be distributed through a safe channel.
Each user generates its nonce, and shares to others (or an aggregator) its public part
for(let i=0;i<k+1;i++){
let nonce=frost.Nonce_gen( int_to_bytes(secshares[i][1],32), b8_pubshares[i], group_pk, msg, extra_in );
secnonces.push(nonce[0]);
pubnonces.push(nonce[1].toString('hex'));
}
The public nonces are then aggregated
let aggnonce=frost.Nonce_agg(pubnonces)
Once the aggregated nonce is known to everyone, each signer process its partial signature:
let psigs=[];
for(let i=0;i<k+1;i++){
let psig=frost.Psign(secnonces[i], int_to_bytes(secshares[i][1],32), Ids[i], session_context);
console.log("psig:", psig);
psigs.push(psig);
}
Once all partial signatures are computed, they can be aggregated to produce the final signature to broadcast on chain:
let final_sig=frost.Partial_sig_agg(psigs, Ids, session_context);
It can be verified that the resulting signature is compliant to BIP140(secp256k1 on BTC) or RFC8032 (Yubikey, Cosmos, Solana)
console.log("final verif:", frost.Schnorr_verify(msg, x_group_pk,final_sig ));
The generation and distribution of FROST's shares are out of scope of its specification. However FROST-RFC specifies a trusted key dealer generation which is the most obvious. The DKG is implemented in the class SCL_trustedkeygen. In the future the more decentralized chill-DKG shall be implemented.
Tests can be ran using the following command :
node test_Musig2.mjs
Tests are run against BIP327 reference vectors to unitary test each function. Then a full Musig2 session is ran using dynamically generated input for each supported curve.
Tests can be ran using the following command :
node test_atomic_swap.mjs
The file test_atomic_bitcoin.js aims to provide a full onchain demonstration of a bridging.
Tests can be ran using the following command :
node test_frost.mjs
- Musig2 can be used to provide protection against trappoored hardware as explained here. Using Musig2 with account abstraction, it is possible to have a companion app handling a first key, the hardware wallet a second.
- Atomic swaps enables trustless bridging between non EVM chains. We are gonna use this library to provide trustless bridge with EVM, Starknet and BTC, and later with Cosmos and Solana using the Ed25519 version.
- FROST can be used to make an invisible governance in a Vault application. While SCL passkey module (using 7212) makes traditional single owner invisible, using FROST, it is the Safe itself that can be concealed.
https://github.com/BlockstreamResearch/scriptless-scripts/blob/a8b6ff21fc7f4529eabbe639fbff49f047a3579d/md/musig2-adaptorsig.md https://github.com/BlockstreamResearch/scriptless-scripts/blob/master/md/atomic-swap.md https://eprint.iacr.org/2021/150.pdf
While ecrecover enables an efficient implementation of Schnorr signatures, there is no trivial way to implement atomic swaps with Solana and Cosmos ecosystems. RIP7696 genericity solves the need for ed25519, avoiding to use complex zk-proof to prove discrete log equivalence.
License: This software is licensed under MIT License (see LICENSE FILE at root directory of project).