From 26d36fbe2f640bf875b829896af6cc9bfe4d5711 Mon Sep 17 00:00:00 2001 From: Borislav Itskov Date: Tue, 15 Oct 2024 11:57:57 +0300 Subject: [PATCH 1/7] 3.0.0 --- package-lock.json | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index bd11fdd..43d22ae 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@borislav.itskov/schnorrkel.js", - "version": "2.0.85", + "version": "3.0.0", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "@borislav.itskov/schnorrkel.js", - "version": "2.0.85", + "version": "3.0.0", "license": "ISC", "dependencies": { "bigi": "^1.4.2", diff --git a/package.json b/package.json index e46577c..ffeb571 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@borislav.itskov/schnorrkel.js", - "version": "2.0.85", + "version": "3.0.0", "description": "", "main": "dist/index.js", "types": "dist/index.d.ts", From 52468f6dbd74251d62b3fe4be657af7cc9780294 Mon Sep 17 00:00:00 2001 From: Borislav Itskov Date: Thu, 17 Oct 2024 17:02:16 +0300 Subject: [PATCH 2/7] add: a schnorr signer class to simplify signing and formating --- src/schnorrSigner.ts | 83 ++++++++++++++++++++++ tests/schnorrkel/onchainSingleSign.test.ts | 19 ++--- 2 files changed, 87 insertions(+), 15 deletions(-) create mode 100644 src/schnorrSigner.ts diff --git a/src/schnorrSigner.ts b/src/schnorrSigner.ts new file mode 100644 index 0000000..890d60f --- /dev/null +++ b/src/schnorrSigner.ts @@ -0,0 +1,83 @@ +import { + arrayify, + computePublicKey, + defaultAbiCoder, + isHexString, +} from "ethers/lib/utils"; +import { _generateSchnorrAddr } from "./core"; +import Schnorrkel from "./schnorrkel"; +import { Key, PublicNonces, SignatureOutput } from "./types"; + +type Hex = string; + +class SchnorrSigner { + _privateKey: Key; + _publicKey: Key; + _schnorrkel: Schnorrkel; + + constructor(privateKey: Hex) { + if (!isHexString(privateKey)) throw new Error("invalid hex for privateKey"); + + this._privateKey = new Key(Buffer.from(privateKey.substring(2), "hex")); + this._publicKey = new Key( + Buffer.from(arrayify(computePublicKey(privateKey, true))) + ); + this._schnorrkel = new Schnorrkel(); + } + + getPublicKey(): Key { + return this._publicKey; + } + + /** + * This yields the schnorr address for a 1/1 setup. + * If multisig, combine the public keys and use _generateSchnorrAddr manually + * + * @returns address + */ + getSchnorrAddress() { + return _generateSchnorrAddr(this._publicKey.buffer); + } + + getPublicNonces(): PublicNonces { + return this._schnorrkel.generatePublicNonces(this._privateKey); + } + + sign(commitment: string): SignatureOutput { + return Schnorrkel.sign(this._privateKey, commitment); + } + + mutliSignatureSign( + msg: string, + publicKeys: Key[], + publicNonces: PublicNonces[] + ) { + return this._schnorrkel.multiSigSign( + this._privateKey, + msg, + publicKeys, + publicNonces + ); + } + + /** + * The on chain structure + * + * @param sigOutput + * @param combinedPublicKey - pass is along if a multisig + * @returns hex forOnchainValidation + */ + getEcrecoverStructure(sigOutput: SignatureOutput, combinedPublicKey?: Key) { + const publicKey = combinedPublicKey + ? arrayify(combinedPublicKey.buffer) + : arrayify(this._publicKey.buffer); + const px = publicKey.slice(1, 33); + const parity = publicKey[0] - 2 + 27; + return defaultAbiCoder.encode( + ["bytes32", "bytes32", "bytes32", "uint8"], + [px, sigOutput.challenge.buffer, sigOutput.signature.buffer, parity] + ); + } +} + +export default SchnorrSigner; diff --git a/tests/schnorrkel/onchainSingleSign.test.ts b/tests/schnorrkel/onchainSingleSign.test.ts index fa50808..b216723 100644 --- a/tests/schnorrkel/onchainSingleSign.test.ts +++ b/tests/schnorrkel/onchainSingleSign.test.ts @@ -2,6 +2,7 @@ import { describe, expect, it } from 'vitest' import secp256k1 from 'secp256k1' import { ethers } from 'ethers' import Schnorrkel, { Key } from '../../src/index' +import SchnorrSigner from '../../src/schnorrSigner.js' import { compile } from '../../utils/compile.js' import { pk1, wallet } from '../config.js' @@ -32,21 +33,9 @@ describe('Single Sign Tests', function () { // sign const msg = 'just a test message' const msgHash = ethers.utils.hashMessage(msg) - const privateKey = new Key(Buffer.from(ethers.utils.arrayify(pk1))) - const sig = Schnorrkel.sign(privateKey, msgHash) - - // wrap the result - const publicKey = secp256k1.publicKeyCreate(ethers.utils.arrayify(pk1)) - const px = publicKey.slice(1, 33) - const parity = publicKey[0] - 2 + 27 - const abiCoder = new ethers.utils.AbiCoder() - const sigData = abiCoder.encode([ 'bytes32', 'bytes32', 'bytes32', 'uint8' ], [ - px, - sig.challenge.buffer, - sig.signature.buffer, - parity - ]) - const result = await contract.isValidSignature(msgHash, sigData) + const signer = new SchnorrSigner(pk1) + const sig = signer.sign(msgHash) + const result = await contract.isValidSignature(msgHash, signer.getEcrecoverStructure(sig)) expect(result).to.equal(ERC1271_MAGICVALUE_BYTES32) }) }) \ No newline at end of file From 1789875913c417527d68f685e9352e48784ff684 Mon Sep 17 00:00:00 2001 From: Borislav Itskov Date: Mon, 21 Oct 2024 14:45:42 +0300 Subject: [PATCH 3/7] refactor k nonce generation to be held in a random32 bytes instead of the private key; alpha version of a SchnorrSigner and MultiSignerHelper --- src/core/index.ts | 20 +- src/core/types.ts | 2 +- src/schnorrMultisigHelper.ts | 36 ++ src/schnorrSigner.ts | 11 +- src/schnorrkel.ts | 88 ++- src/types/nonce.ts | 2 +- tests/schnorrkel/getPublicNonces.test.ts | 6 +- tests/schnorrkel/hasNonces.test.ts | 6 +- tests/schnorrkel/multiSigSign.test.ts | 12 +- tests/schnorrkel/onchainMultiSign.test.ts | 504 ++++++++++-------- tests/schnorrkel/onchainSingleSign.test.ts | 65 +-- tests/schnorrkel/sumSigs.test.ts | 4 +- tests/schnorrkel/verify.test.ts | 16 +- tests/unsafe-schnorrkel/fromJson.test.ts | 4 +- .../generatePublicNonces.test.ts | 13 +- utils/DefaultSigner.ts | 9 +- 16 files changed, 444 insertions(+), 354 deletions(-) create mode 100644 src/schnorrMultisigHelper.ts diff --git a/src/core/index.ts b/src/core/index.ts index 61e042f..a222619 100644 --- a/src/core/index.ts +++ b/src/core/index.ts @@ -153,21 +153,16 @@ export const _hashPrivateKey = (privateKey: Buffer): string => { /** * Generate the nonces for the next signature. - * Use the hash of the private key for a unique identifier * - * @param privateKey * @returns */ -export const _generateNonces = (privateKey: Buffer): { +export const _generateNonces = (): { privateNonceData: Pick, - publicNonceData: InternalPublicNonces, - hash: string, + publicNonceData: InternalPublicNonces } => { - const hash = _hashPrivateKey(privateKey) const nonce = generateNonce() return { - hash, privateNonceData: { k: nonce.k, kTwo: nonce.kTwo, @@ -179,14 +174,13 @@ export const _generateNonces = (privateKey: Buffer): { } } -export const _multiSigSign = (nonces: InternalNonces, combinedPublicKey: Buffer, privateKey: Buffer, hash: string, publicKeys: Buffer[], publicNonces: InternalPublicNonces[]): InternalSignature => { +export const _multiSigSign = (nonceId: string, nonces: InternalNonces, combinedPublicKey: Buffer, privateKey: Buffer, hash: string, publicKeys: Buffer[], publicNonces: InternalPublicNonces[]): InternalSignature => { if (publicKeys.length < 2) { throw Error('At least 2 public keys should be provided') } const localPk = Buffer.from(privateKey) - const xHashed = _hashPrivateKey(localPk) - if (!(xHashed in nonces) || Object.keys(nonces[xHashed]).length === 0) { + if (!(nonceId in nonces) || Object.keys(nonces[nonceId]).length === 0) { throw Error('Nonces should be exchanged before signing') } @@ -199,8 +193,8 @@ export const _multiSigSign = (nonces: InternalNonces, combinedPublicKey: Buffer, return Buffer.from(secp256k1.publicKeyCombine([batch.kPublic, secp256k1.publicKeyTweakMul(batch.kTwoPublic, b)])) }) const signerEffectiveNonce = Buffer.from(secp256k1.publicKeyCombine([ - nonces[xHashed].kPublic, - secp256k1.publicKeyTweakMul(nonces[xHashed].kTwoPublic, b) + nonces[nonceId].kPublic, + secp256k1.publicKeyTweakMul(nonces[nonceId].kTwoPublic, b) ])) const inArray = effectiveNonces.filter(nonce => areBuffersSame(nonce, signerEffectiveNonce)).length != 0 if (!inArray) { @@ -210,7 +204,7 @@ export const _multiSigSign = (nonces: InternalNonces, combinedPublicKey: Buffer, const R = Buffer.from(secp256k1.publicKeyCombine(effectiveNonces)) const e = challenge(R, hash, combinedPublicKey) - const { k, kTwo } = nonces[xHashed] + const { k, kTwo } = nonces[nonceId] // xe = x * e const xe = secp256k1.privateKeyTweakMul(localPk, e) diff --git a/src/core/types.ts b/src/core/types.ts index 75a7bd1..ac2fb93 100644 --- a/src/core/types.ts +++ b/src/core/types.ts @@ -24,5 +24,5 @@ export interface InternalNoncePairs { } export type InternalNonces = { - [privateKey: string]: InternalNoncePairs + [nonceId: string]: InternalNoncePairs } \ No newline at end of file diff --git a/src/schnorrMultisigHelper.ts b/src/schnorrMultisigHelper.ts new file mode 100644 index 0000000..9bcf28e --- /dev/null +++ b/src/schnorrMultisigHelper.ts @@ -0,0 +1,36 @@ +import { arrayify, defaultAbiCoder } from "ethers/lib/utils"; +import { _generateSchnorrAddr } from "./core"; +import Schnorrkel from "./schnorrkel"; +import { Key, SignatureOutput } from "./types"; + +class SchnorrMultisigHelper { + _publicKeys: Key[]; + + constructor(publicKeys: Key[]) { + this._publicKeys = publicKeys; + } + + getSchnorrAddress() { + return _generateSchnorrAddr( + Schnorrkel.getCombinedPublicKey(this._publicKeys).buffer + ); + } + + getEcrecoverSignature(sigOutputs: SignatureOutput[]) { + const publicKey = arrayify( + Schnorrkel.getCombinedPublicKey(this._publicKeys).buffer + ); + const sSummed = Schnorrkel.sumSigs( + sigOutputs.map((output) => output.signature) + ); + const challenge = sigOutputs[0].challenge; + const px = publicKey.slice(1, 33); + const parity = publicKey[0] - 2 + 27; + return defaultAbiCoder.encode( + ["bytes32", "bytes32", "bytes32", "uint8"], + [px, challenge.buffer, sSummed.buffer, parity] + ); + } +} + +export default SchnorrMultisigHelper; diff --git a/src/schnorrSigner.ts b/src/schnorrSigner.ts index 890d60f..771ace6 100644 --- a/src/schnorrSigner.ts +++ b/src/schnorrSigner.ts @@ -40,7 +40,7 @@ class SchnorrSigner { } getPublicNonces(): PublicNonces { - return this._schnorrkel.generatePublicNonces(this._privateKey); + return this._schnorrkel.generateOrGetPublicNonces(); } sign(commitment: string): SignatureOutput { @@ -61,16 +61,13 @@ class SchnorrSigner { } /** - * The on chain structure + * The onchain structure * * @param sigOutput - * @param combinedPublicKey - pass is along if a multisig * @returns hex forOnchainValidation */ - getEcrecoverStructure(sigOutput: SignatureOutput, combinedPublicKey?: Key) { - const publicKey = combinedPublicKey - ? arrayify(combinedPublicKey.buffer) - : arrayify(this._publicKey.buffer); + getEcrecoverSignature(sigOutput: SignatureOutput) { + const publicKey = arrayify(this._publicKey.buffer); const px = publicKey.slice(1, 33); const parity = publicKey[0] - 2 + 27; return defaultAbiCoder.encode( diff --git a/src/schnorrkel.ts b/src/schnorrkel.ts index e01750d..d6d382d 100644 --- a/src/schnorrkel.ts +++ b/src/schnorrkel.ts @@ -3,23 +3,21 @@ import secp256k1 from 'secp256k1' import { Key, Nonces, PublicNonces, Signature, NoncePairs } from './types' import { _generateL, _aCoefficient, _generateNonces, _multiSigSign, _hashPrivateKey, _sumSigs, _verify, _generateSchnorrAddr, _sign } from './core' -import { InternalNonces, InternalPublicNonces, InternalSignature } from './core/types' +import { InternalNonces, InternalPublicNonces } from './core/types' import { Challenge, PublicNonce, SignatureOutput } from './types/signature' +import { hexlify, randomBytes } from 'ethers/lib/utils' class Schnorrkel { protected nonces: Nonces = {} + private readonly nonceId: string = hexlify(randomBytes(32)) /** * Set the nonces for the next multisignature. * Nonces should not be manipulated outside the library. Also, * they should be completely random. - * - * @param privateKey - we use the private key to create - * an unique identifier hash. See _generateNonces - * @returns string identifier */ - private setNonce(privateKey: Buffer): string { - const { publicNonceData, privateNonceData, hash } = _generateNonces(privateKey) + private setNonce(): void { + const { publicNonceData, privateNonceData } = _generateNonces() const mappedPublicNonce: PublicNonces = { kPublic: new Key(Buffer.from(publicNonceData.kPublic)), @@ -31,31 +29,25 @@ class Schnorrkel { kTwo: new Key(Buffer.from(privateNonceData.kTwo)) } - this.nonces[hash] = { ...mappedPrivateNonce, ...mappedPublicNonce } - return hash + this.nonces[this.nonceId] = { ...mappedPrivateNonce, ...mappedPublicNonce } } /** * Clear the nonces used in the last signature * This is a very important step as otherwise, we go into nonce * reuse scenario - * - * @param privateKey */ - private clearNonces(privateKey: Key): void { - const x = privateKey.buffer - const hash = _hashPrivateKey(x) - + private clearNonces(): void { // this shouldn't happen, just extra safety // clearNonces should be called after a signature has been crafted. // If the hash is not found in the nonces by any chance after // a signature, then the process should be stopped as we don't // know nonces have been used for the signature - if (! this.nonces[hash]) { + if (! this.nonces[this.nonceId]) { throw new Error('Multisignature nonces not found') } - delete this.nonces[hash] + delete this.nonces[this.nonceId] } private getMappedPublicNonces(publicNonces: PublicNonces[]): InternalPublicNonces[] { @@ -68,17 +60,18 @@ class Schnorrkel { } private getMappedNonces(): InternalNonces { - return Object.fromEntries(Object.entries(this.nonces).map(([hash, nonce]) => { - return [ - hash, - { - k: nonce.k.buffer, - kTwo: nonce.kTwo.buffer, - kPublic: nonce.kPublic.buffer, - kTwoPublic: nonce.kTwoPublic.buffer, - } - ] - })) + if (!this.nonces[this.nonceId]) { + return {} + } + + return { + [this.nonceId]: { + k: this.nonces[this.nonceId].k.buffer, + kTwo: this.nonces[this.nonceId].kTwo.buffer, + kPublic: this.nonces[this.nonceId].kPublic.buffer, + kTwoPublic: this.nonces[this.nonceId].kTwoPublic.buffer, + } + } } /** @@ -124,15 +117,14 @@ class Schnorrkel { * This is a method you should use if you don't want to manage * the nonces yourself * - * @param privateKey * @returns PublicNonces */ - generateOrGetPublicNonces(privateKey: Key): PublicNonces { - if (this.hasNonces(privateKey)) { - return this.getPublicNonces(privateKey) + generateOrGetPublicNonces(): PublicNonces { + if (this.hasNonces()) { + return this.getPublicNonces() } - return this.generatePublicNonces(privateKey) + return this.generatePublicNonces() } /** @@ -142,12 +134,11 @@ class Schnorrkel { * You need to maintain the state for the nonce exchanging phase and * the signing phase * - * @param privateKey * @returns PublicNonces */ - generatePublicNonces(privateKey: Key): PublicNonces { - const hash = this.setNonce(privateKey.buffer) - const nonce = this.nonces[hash] + generatePublicNonces(): PublicNonces { + this.setNonce() + const nonce = this.nonces[this.nonceId] return { kPublic: nonce.kPublic, @@ -158,13 +149,11 @@ class Schnorrkel { /** * Get the public nonces. * If none are set, an error is returned - * - * @param privateKey + * * @returns PublicNonces */ - getPublicNonces(privateKey: Key): PublicNonces { - const hash = _hashPrivateKey(privateKey.buffer) - const nonce = this.nonces[hash] + getPublicNonces(): PublicNonces { + const nonce = this.nonces[this.nonceId] if (!nonce) { throw new Error('Nonces not set') @@ -176,15 +165,8 @@ class Schnorrkel { } } - /** - * Check if there are nonces generated in the state - * - * @param privateKey - * @returns Key - */ - hasNonces(privateKey: Key): boolean { - const hash = _hashPrivateKey(privateKey.buffer) - return hash in this.nonces + hasNonces(): boolean { + return this.nonceId in this.nonces } /** @@ -202,11 +184,11 @@ class Schnorrkel { const mappedPublicNonce = this.getMappedPublicNonces(publicNonces) const mappedNonces = this.getMappedNonces() - const musigData = _multiSigSign(mappedNonces, combinedPublicKey.buffer, privateKey.buffer, hash, publicKeys.map(key => key.buffer), mappedPublicNonce) + const musigData = _multiSigSign(this.nonceId, mappedNonces, combinedPublicKey.buffer, privateKey.buffer, hash, publicKeys.map(key => key.buffer), mappedPublicNonce) // absolutely crucial to delete the nonces once a signature has been crafted. // nonce reuse will lead to private key leakage! - this.clearNonces(privateKey) + this.clearNonces() return { signature: new Signature(Buffer.from(musigData.signature)), diff --git a/src/types/nonce.ts b/src/types/nonce.ts index ecbbfe6..ee6f5e2 100644 --- a/src/types/nonce.ts +++ b/src/types/nonce.ts @@ -14,5 +14,5 @@ export interface PublicNonces { export type Nonces = { - [privateKey: string]: NoncePairs + [nonceId: string]: NoncePairs } \ No newline at end of file diff --git a/tests/schnorrkel/getPublicNonces.test.ts b/tests/schnorrkel/getPublicNonces.test.ts index 42b248d..5bbb307 100644 --- a/tests/schnorrkel/getPublicNonces.test.ts +++ b/tests/schnorrkel/getPublicNonces.test.ts @@ -9,7 +9,7 @@ describe('testing getPublicNonces', () => { const schnorrkel = new Schnorrkel() const keyPair = generateRandomKeys() - const publicNonces = schnorrkel.generatePublicNonces(keyPair.privateKey) + const publicNonces = schnorrkel.generatePublicNonces() expect(publicNonces).toBeDefined() expect(publicNonces.kPublic).toBeDefined() @@ -17,7 +17,7 @@ describe('testing getPublicNonces', () => { expect(publicNonces.kPublic.buffer).toHaveLength(33) expect(publicNonces.kTwoPublic.buffer).toHaveLength(33) - const retrievedPublicNonces = schnorrkel.getPublicNonces(keyPair.privateKey) + const retrievedPublicNonces = schnorrkel.getPublicNonces() expect(retrievedPublicNonces.kPublic.buffer).to.equal(publicNonces.kPublic.buffer) expect(retrievedPublicNonces.kTwoPublic.buffer).to.equal(publicNonces.kTwoPublic.buffer) }) @@ -25,6 +25,6 @@ describe('testing getPublicNonces', () => { const schnorrkel = new Schnorrkel() const keyPair = generateRandomKeys() - expect(() => schnorrkel.getPublicNonces(keyPair.privateKey)).toThrowError('Nonces not set') + expect(() => schnorrkel.getPublicNonces()).toThrowError('Nonces not set') }) }) \ No newline at end of file diff --git a/tests/schnorrkel/hasNonces.test.ts b/tests/schnorrkel/hasNonces.test.ts index 3f7df9e..4453744 100644 --- a/tests/schnorrkel/hasNonces.test.ts +++ b/tests/schnorrkel/hasNonces.test.ts @@ -9,8 +9,8 @@ describe('testing hasNonces', () => { const schnorrkel = new Schnorrkel() const keyPair = generateRandomKeys() - expect(schnorrkel.hasNonces(keyPair.privateKey)).to.equal(false) - schnorrkel.generatePublicNonces(keyPair.privateKey) - expect(schnorrkel.hasNonces(keyPair.privateKey)).to.equal(true) + expect(schnorrkel.hasNonces()).to.equal(false) + schnorrkel.generatePublicNonces() + expect(schnorrkel.hasNonces()).to.equal(true) }) }) \ No newline at end of file diff --git a/tests/schnorrkel/multiSigSign.test.ts b/tests/schnorrkel/multiSigSign.test.ts index 2fd0925..9e67b2c 100644 --- a/tests/schnorrkel/multiSigSign.test.ts +++ b/tests/schnorrkel/multiSigSign.test.ts @@ -12,8 +12,8 @@ describe('testing multiSigSign', () => { const keyPairOne = generateRandomKeys() const keyPairTwo = generateRandomKeys() - const publicNoncesOne = schnorrkelOne.generatePublicNonces(keyPairOne.privateKey) - const publicNoncesTwo = schnorrkelTwo.generatePublicNonces(keyPairTwo.privateKey) + const publicNoncesOne = schnorrkelOne.generatePublicNonces() + const publicNoncesTwo = schnorrkelTwo.generatePublicNonces() const publicNonces = [publicNoncesOne, publicNoncesTwo] const publicKeys = [keyPairOne.publicKey, keyPairTwo.publicKey] @@ -30,7 +30,7 @@ describe('testing multiSigSign', () => { it('should requires two public keys or more', () => { const schnorrkel = new Schnorrkel() const keyPair = generateRandomKeys() - const publicNonces = schnorrkel.generatePublicNonces(keyPair.privateKey) + const publicNonces = schnorrkel.generatePublicNonces() const msg = 'test message' const msgHash = ethers.utils.hashMessage(msg) @@ -39,7 +39,7 @@ describe('testing multiSigSign', () => { expect(() => schnorrkel.multiSigSign(keyPair.privateKey, msgHash, publicKeys, [publicNonces])).toThrowError('At least 2 public keys should be provided') }) - it('should requires nonces', () => { + it('should require nonces', () => { const schnorrkel = new Schnorrkel() const keyPairOne = generateRandomKeys() const keyPairTwo = generateRandomKeys() @@ -57,8 +57,8 @@ describe('testing multiSigSign', () => { const keyPairOne = generateRandomKeys() const keyPairTwo = generateRandomKeys() - const publicNoncesOne = schnorrkelOne.generatePublicNonces(keyPairOne.privateKey) - const publicNoncesTwo = schnorrkelTwo.generatePublicNonces(keyPairTwo.privateKey) + const publicNoncesOne = schnorrkelOne.generatePublicNonces() + const publicNoncesTwo = schnorrkelTwo.generatePublicNonces() const publicNonces = [publicNoncesOne, publicNoncesTwo] const publicKeys = [keyPairOne.publicKey, keyPairTwo.publicKey] diff --git a/tests/schnorrkel/onchainMultiSign.test.ts b/tests/schnorrkel/onchainMultiSign.test.ts index c9659f3..24af043 100644 --- a/tests/schnorrkel/onchainMultiSign.test.ts +++ b/tests/schnorrkel/onchainMultiSign.test.ts @@ -1,260 +1,338 @@ -import { describe, expect, it } from 'vitest' -import { ethers } from 'ethers' -import Schnorrkel from '../../src/index' -import { compile } from '../../utils/compile.js' -import { wallet2 } from '../config.js' -import DefaultSigner from '../../utils/DefaultSigner' -const ERC1271_MAGICVALUE_BYTES32 = '0x1626ba7e' - -describe('Multi Sign Tests', function () { - +import { describe, expect, it } from "vitest"; +import { ethers } from "ethers"; +import Schnorrkel from "../../src/index"; +import { compile } from "../../utils/compile"; +import { pk1, pk2, wallet2 } from "../config"; +import DefaultSigner from "../../utils/DefaultSigner"; +import SchnorrSigner from "../../src/schnorrSigner"; +import SchnorrMultisigHelper from "../../src/schnorrMultisigHelper"; +const ERC1271_MAGICVALUE_BYTES32 = "0x1626ba7e"; + +describe("Multi Sign Tests", function () { async function deployContract(signerOne: any, signerTwo: any) { - const SchnorrAccountAbstraction = compile('SchnorrAccountAbstraction') - const factory = new ethers.ContractFactory(SchnorrAccountAbstraction.abi, SchnorrAccountAbstraction.bytecode, wallet2) + const SchnorrAccountAbstraction = compile("SchnorrAccountAbstraction"); + const factory = new ethers.ContractFactory( + SchnorrAccountAbstraction.abi, + SchnorrAccountAbstraction.bytecode, + wallet2 + ); // get the public key const combinedPublicKey = Schnorrkel.getCombinedPublicKey([ signerOne.getPublicKey(), - signerTwo.getPublicKey() - ]) - const px = ethers.utils.hexlify(combinedPublicKey.buffer.slice(1, 33)) - const combinedPublicAddress = '0x' + px.slice(px.length - 40, px.length) - const contract: any = await factory.deploy([combinedPublicAddress]) - const isSigner = await contract.canSign(combinedPublicAddress) - expect(isSigner).to.equal('0x0000000000000000000000000000000000000000000000000000000000000001') - - return { contract } + signerTwo.getPublicKey(), + ]); + const px = ethers.utils.hexlify(combinedPublicKey.buffer.slice(1, 33)); + const combinedPublicAddress = "0x" + px.slice(px.length - 40, px.length); + const contract: any = await factory.deploy([combinedPublicAddress]); + const isSigner = await contract.canSign(combinedPublicAddress); + expect(isSigner).to.equal( + "0x0000000000000000000000000000000000000000000000000000000000000001" + ); + + return { contract }; } - it('should generate a schnorr musig2 and validate it on the blockchain', async function () { - // deploy the contract - const signerOne = new DefaultSigner(0) - const signerTwo = new DefaultSigner(1) - const { contract } = await deployContract(signerOne, signerTwo) - - const msg = 'just a test message' - const msgHash = ethers.utils.solidityKeccak256(['string'], [msg]) - const publicKeys = [signerOne.getPublicKey(), signerTwo.getPublicKey()] - const publicNonces = [signerOne.getPublicNonces(), signerTwo.getPublicNonces()] - const combinedPublicKey = Schnorrkel.getCombinedPublicKey(publicKeys) - const {signature: sigOne, challenge: e, publicNonce} = signerOne.multiSignMessage(msgHash, publicKeys, publicNonces) - const {signature: sigTwo} = signerTwo.multiSignMessage(msgHash, publicKeys, publicNonces) - const sSummed = Schnorrkel.sumSigs([sigOne, sigTwo]) - - // the multisig px and parity - const px = ethers.utils.hexlify(combinedPublicKey.buffer.slice(1, 33)) - const parity = combinedPublicKey.buffer[0] - 2 + 27 + async function deployContractTwo(multisigHelper: SchnorrMultisigHelper) { + const SchnorrAccountAbstraction = compile("SchnorrAccountAbstraction"); + const factory = new ethers.ContractFactory( + SchnorrAccountAbstraction.abi, + SchnorrAccountAbstraction.bytecode, + wallet2 + ); + const schnorrAddr = multisigHelper.getSchnorrAddress(); + const contract: any = await factory.deploy([schnorrAddr]); + const isSigner = await contract.canSign(schnorrAddr); + expect(isSigner).to.equal( + "0x0000000000000000000000000000000000000000000000000000000000000001" + ); + + return { contract }; + } - // wrap the result - const abiCoder = new ethers.utils.AbiCoder() - const sigData = abiCoder.encode([ 'bytes32', 'bytes32', 'bytes32', 'uint8' ], [ - px, - e.buffer, - sSummed.buffer, - parity - ]) - const result = await contract.isValidSignature(msgHash, sigData) - expect(result).to.equal(ERC1271_MAGICVALUE_BYTES32) - }) - - it('should generate the same sig to be sure caching does not affect validation', async function () { + it("should generate a schnorr musig2 and validate it on the blockchain", async function () { // deploy the contract - const signerOne = new DefaultSigner(0) - const signerTwo = new DefaultSigner(1) - const { contract } = await deployContract(signerOne, signerTwo) - - const msg = 'just a test message' - const msgHash = ethers.utils.solidityKeccak256(['string'], [msg]) - const publicKeys = [signerOne.getPublicKey(), signerTwo.getPublicKey()] - const publicNonces = [signerOne.getPublicNonces(), signerTwo.getPublicNonces()] - const combinedPublicKey = Schnorrkel.getCombinedPublicKey(publicKeys) - const {signature: sigOne, challenge: e, publicNonce} = signerOne.multiSignMessage(msgHash, publicKeys, publicNonces) - const {signature: sigTwo} = signerTwo.multiSignMessage(msgHash, publicKeys, publicNonces) - const sSummed = Schnorrkel.sumSigs([sigOne, sigTwo]) + const signerOne = new SchnorrSigner(pk1); + const signerTwo = new SchnorrSigner(pk2); + const multisigHelper = new SchnorrMultisigHelper([ + signerOne.getPublicKey(), + signerTwo.getPublicKey(), + ]); + const { contract } = await deployContractTwo(multisigHelper); + + const msg = "just a test message"; + const msgHash = ethers.utils.solidityKeccak256(["string"], [msg]); + const publicKeys = [signerOne.getPublicKey(), signerTwo.getPublicKey()]; + const publicNonces = [ + signerOne.getPublicNonces(), + signerTwo.getPublicNonces(), + ]; + const signature = signerOne.mutliSignatureSign( + msgHash, + publicKeys, + publicNonces + ); + const signatureTwo = signerTwo.mutliSignatureSign( + msgHash, + publicKeys, + publicNonces + ); + const result = await contract.isValidSignature( + msgHash, + multisigHelper.getEcrecoverSignature([signature, signatureTwo]) + ); + expect(result).to.equal(ERC1271_MAGICVALUE_BYTES32); + }); + + it("should generate the same sig to be sure caching does not affect validation", async function () { + // deploy the contract + const signerOne = new DefaultSigner(); + const signerTwo = new DefaultSigner(); + const { contract } = await deployContract(signerOne, signerTwo); + + const msg = "just a test message"; + const msgHash = ethers.utils.solidityKeccak256(["string"], [msg]); + const publicKeys = [signerOne.getPublicKey(), signerTwo.getPublicKey()]; + const publicNonces = [ + signerOne.getPublicNonces(), + signerTwo.getPublicNonces(), + ]; + const combinedPublicKey = Schnorrkel.getCombinedPublicKey(publicKeys); + const { + signature: sigOne, + challenge: e, + publicNonce, + } = signerOne.multiSignMessage(msgHash, publicKeys, publicNonces); + const { signature: sigTwo } = signerTwo.multiSignMessage( + msgHash, + publicKeys, + publicNonces + ); + const sSummed = Schnorrkel.sumSigs([sigOne, sigTwo]); // the multisig px and parity - const px = ethers.utils.hexlify(combinedPublicKey.buffer.slice(1, 33)) + const px = ethers.utils.hexlify(combinedPublicKey.buffer.slice(1, 33)); - const parity = combinedPublicKey.buffer[0] - 2 + 27 + const parity = combinedPublicKey.buffer[0] - 2 + 27; // wrap the result - const abiCoder = new ethers.utils.AbiCoder() - const sigData = abiCoder.encode([ 'bytes32', 'bytes32', 'bytes32', 'uint8' ], [ - px, - e.buffer, - sSummed.buffer, - parity - ]) - const result = await contract.isValidSignature(msgHash, sigData) - expect(result).to.equal(ERC1271_MAGICVALUE_BYTES32) - }) - - it('should fail if the signer is totally different', async function () { + const abiCoder = new ethers.utils.AbiCoder(); + const sigData = abiCoder.encode( + ["bytes32", "bytes32", "bytes32", "uint8"], + [px, e.buffer, sSummed.buffer, parity] + ); + const result = await contract.isValidSignature(msgHash, sigData); + expect(result).to.equal(ERC1271_MAGICVALUE_BYTES32); + }); + + it("should fail if the signer is totally different", async function () { // deploy the contract - const signerOne = new DefaultSigner(0) - const signerTwo = new DefaultSigner(1) - const { contract } = await deployContract(signerOne, signerTwo) - - const signerThree = new DefaultSigner(2) - const msg = 'just a test message' - const msgHash = ethers.utils.solidityKeccak256(['string'], [msg]) - const publicKeys = [signerOne.getPublicKey(), signerThree.getPublicKey()] - const publicNonces = [signerOne.getPublicNonces(), signerThree.getPublicNonces()] - const combinedPublicKey = Schnorrkel.getCombinedPublicKey(publicKeys) - -// publicNonce: publicNonce, // the final public nonce -// challenge: Challenge, // the schnorr challenge -// signature: Signature, // the signature - const {signature: sigOne, challenge: e} = signerOne.multiSignMessage(msgHash, publicKeys, publicNonces) - const {signature: sigTwo} = signerThree.multiSignMessage(msgHash, publicKeys, publicNonces) - const sSummed = Schnorrkel.sumSigs([sigOne, sigTwo]) + const signerOne = new DefaultSigner(); + const signerTwo = new DefaultSigner(); + const { contract } = await deployContract(signerOne, signerTwo); + + const signerThree = new DefaultSigner(); + const msg = "just a test message"; + const msgHash = ethers.utils.solidityKeccak256(["string"], [msg]); + const publicKeys = [signerOne.getPublicKey(), signerThree.getPublicKey()]; + const publicNonces = [ + signerOne.getPublicNonces(), + signerThree.getPublicNonces(), + ]; + const combinedPublicKey = Schnorrkel.getCombinedPublicKey(publicKeys); + + // publicNonce: publicNonce, // the final public nonce + // challenge: Challenge, // the schnorr challenge + // signature: Signature, // the signature + const { signature: sigOne, challenge: e } = signerOne.multiSignMessage( + msgHash, + publicKeys, + publicNonces + ); + const { signature: sigTwo } = signerThree.multiSignMessage( + msgHash, + publicKeys, + publicNonces + ); + const sSummed = Schnorrkel.sumSigs([sigOne, sigTwo]); // the multisig px and parity - const px = combinedPublicKey.buffer.slice(1,33) - const parity = combinedPublicKey.buffer[0] - 2 + 27 + const px = combinedPublicKey.buffer.slice(1, 33); + const parity = combinedPublicKey.buffer[0] - 2 + 27; // wrap the result - const abiCoder = new ethers.utils.AbiCoder() - const sigData = abiCoder.encode([ 'bytes32', 'bytes32', 'bytes32', 'uint8' ], [ - px, - e.buffer, - sSummed.buffer, - parity - ]) - const result = await contract.isValidSignature(msgHash, sigData) - expect(result).to.equal('0xffffffff') - }) - - it('should fail if only one signature is provided', async function () { + const abiCoder = new ethers.utils.AbiCoder(); + const sigData = abiCoder.encode( + ["bytes32", "bytes32", "bytes32", "uint8"], + [px, e.buffer, sSummed.buffer, parity] + ); + const result = await contract.isValidSignature(msgHash, sigData); + expect(result).to.equal("0xffffffff"); + }); + + it("should fail if only one signature is provided", async function () { // deploy the contract - const signerOne = new DefaultSigner(0) - const signerTwo = new DefaultSigner(1) - const { contract } = await deployContract(signerOne, signerTwo) - - const msg = 'just a test message' - const msgHash = ethers.utils.solidityKeccak256(['string'], [msg]) - const publicKeys = [signerOne.getPublicKey(), signerTwo.getPublicKey()] - const publicNonces = [signerOne.getPublicNonces(), signerTwo.getPublicNonces()] - const combinedPublicKey = Schnorrkel.getCombinedPublicKey(publicKeys) - const {signature: sigOne, challenge: e} = signerOne.multiSignMessage(msgHash, publicKeys, publicNonces) + const signerOne = new DefaultSigner(); + const signerTwo = new DefaultSigner(); + const { contract } = await deployContract(signerOne, signerTwo); + + const msg = "just a test message"; + const msgHash = ethers.utils.solidityKeccak256(["string"], [msg]); + const publicKeys = [signerOne.getPublicKey(), signerTwo.getPublicKey()]; + const publicNonces = [ + signerOne.getPublicNonces(), + signerTwo.getPublicNonces(), + ]; + const combinedPublicKey = Schnorrkel.getCombinedPublicKey(publicKeys); + const { signature: sigOne, challenge: e } = signerOne.multiSignMessage( + msgHash, + publicKeys, + publicNonces + ); // the multisig px and parity - const px = combinedPublicKey.buffer.slice(1,33) - const parity = combinedPublicKey.buffer[0] - 2 + 27 + const px = combinedPublicKey.buffer.slice(1, 33); + const parity = combinedPublicKey.buffer[0] - 2 + 27; // wrap the result - const abiCoder = new ethers.utils.AbiCoder() - const sigData = abiCoder.encode([ 'bytes32', 'bytes32', 'bytes32', 'uint8' ], [ - px, - e.buffer, - sigOne.buffer, - parity - ]) - const result = await contract.isValidSignature(msgHash, sigData) - expect(result).to.equal('0xffffffff') - }) - - it('should fail if a signer tries to sign twice with the same nonce', async function () { + const abiCoder = new ethers.utils.AbiCoder(); + const sigData = abiCoder.encode( + ["bytes32", "bytes32", "bytes32", "uint8"], + [px, e.buffer, sigOne.buffer, parity] + ); + const result = await contract.isValidSignature(msgHash, sigData); + expect(result).to.equal("0xffffffff"); + }); + + it("should fail if a signer tries to sign twice with the same nonce", async function () { // deploy the contract - const signerOne = new DefaultSigner(0) - const signerTwo = new DefaultSigner(1) - await deployContract(signerOne, signerTwo) - - const msg = 'just a test message' - const msgHash = ethers.utils.solidityKeccak256(['string'], [msg]) - const publicKeys = [signerOne.getPublicKey(), signerTwo.getPublicKey()] - const publicNonces = [signerOne.getPublicNonces(), signerTwo.getPublicNonces()] - signerOne.multiSignMessage(msgHash, publicKeys, publicNonces) - expect(signerOne.multiSignMessage.bind(signerOne, msgHash, publicKeys, publicNonces)).to.throw('Nonces should be exchanged before signing') - }) - - it('should fail if only one signer tries to sign the transaction providing 2 messages', async function () { + const signerOne = new DefaultSigner(); + const signerTwo = new DefaultSigner(); + await deployContract(signerOne, signerTwo); + + const msg = "just a test message"; + const msgHash = ethers.utils.solidityKeccak256(["string"], [msg]); + const publicKeys = [signerOne.getPublicKey(), signerTwo.getPublicKey()]; + const publicNonces = [ + signerOne.getPublicNonces(), + signerTwo.getPublicNonces(), + ]; + signerOne.multiSignMessage(msgHash, publicKeys, publicNonces); + expect( + signerOne.multiSignMessage.bind( + signerOne, + msgHash, + publicKeys, + publicNonces + ) + ).to.throw("Nonces should be exchanged before signing"); + }); + + it("should fail if only one signer tries to sign the transaction providing 2 messages", async function () { // deploy the contract - const signerOne = new DefaultSigner(0) - const signerTwo = new DefaultSigner(1) - const { contract } = await deployContract(signerOne, signerTwo) - - const msg = 'just a test message' - const msgHash = ethers.utils.solidityKeccak256(['string'], [msg]) - const publicKeys = [signerOne.getPublicKey(), signerTwo.getPublicKey()] - const signerTwoNonces = signerTwo.getPublicNonces() - const publicNonces = [signerOne.getPublicNonces(), signerTwoNonces] - const combinedPublicKey = Schnorrkel.getCombinedPublicKey(publicKeys) - const {signature: sigOne, challenge: e} = signerOne.multiSignMessage(msgHash, publicKeys, publicNonces) - const publicNoncesTwo = [signerOne.getPublicNonces(), signerTwoNonces] - const {signature: sigTwo} = signerOne.multiSignMessage(msgHash, publicKeys, publicNoncesTwo) - const sSummed = Schnorrkel.sumSigs([sigOne, sigTwo]) + const signerOne = new DefaultSigner(); + const signerTwo = new DefaultSigner(); + const { contract } = await deployContract(signerOne, signerTwo); + + const msg = "just a test message"; + const msgHash = ethers.utils.solidityKeccak256(["string"], [msg]); + const publicKeys = [signerOne.getPublicKey(), signerTwo.getPublicKey()]; + const signerTwoNonces = signerTwo.getPublicNonces(); + const publicNonces = [signerOne.getPublicNonces(), signerTwoNonces]; + const combinedPublicKey = Schnorrkel.getCombinedPublicKey(publicKeys); + const { signature: sigOne, challenge: e } = signerOne.multiSignMessage( + msgHash, + publicKeys, + publicNonces + ); + const publicNoncesTwo = [signerOne.getPublicNonces(), signerTwoNonces]; + const { signature: sigTwo } = signerOne.multiSignMessage( + msgHash, + publicKeys, + publicNoncesTwo + ); + const sSummed = Schnorrkel.sumSigs([sigOne, sigTwo]); // the multisig px and parity - const px = combinedPublicKey.buffer.slice(1,33) - const parity = combinedPublicKey.buffer[0] - 2 + 27 + const px = combinedPublicKey.buffer.slice(1, 33); + const parity = combinedPublicKey.buffer[0] - 2 + 27; // wrap the result - const abiCoder = new ethers.utils.AbiCoder() - const sigData = abiCoder.encode([ 'bytes32', 'bytes32', 'bytes32', 'uint8' ], [ - px, - e.buffer, - sSummed.buffer, - parity - ]) - const result = await contract.isValidSignature(msgHash, sigData) - expect(result).to.equal('0xffffffff') - }) - - it('should successfully pass even if the order of the public keys is different', async function () { + const abiCoder = new ethers.utils.AbiCoder(); + const sigData = abiCoder.encode( + ["bytes32", "bytes32", "bytes32", "uint8"], + [px, e.buffer, sSummed.buffer, parity] + ); + const result = await contract.isValidSignature(msgHash, sigData); + expect(result).to.equal("0xffffffff"); + }); + + it("should successfully pass even if the order of the public keys is different", async function () { // deploy the contract - const signerOne = new DefaultSigner(0) - const signerTwo = new DefaultSigner(1) - const { contract } = await deployContract(signerOne, signerTwo) - - const msg = 'just a test message' - const msgHash = ethers.utils.solidityKeccak256(['string'], [msg]) - const publicKeys = [signerTwo.getPublicKey(), signerOne.getPublicKey()] - const publicNonces = [signerOne.getPublicNonces(), signerTwo.getPublicNonces()] - const combinedPublicKey = Schnorrkel.getCombinedPublicKey(publicKeys) - const {signature: sigOne, challenge: e} = signerOne.multiSignMessage(msgHash, publicKeys, publicNonces) - const {signature: sigTwo} = signerTwo.multiSignMessage(msgHash, publicKeys, publicNonces) - const sSummed = Schnorrkel.sumSigs([sigOne, sigTwo]) + const signerOne = new DefaultSigner(); + const signerTwo = new DefaultSigner(); + const { contract } = await deployContract(signerOne, signerTwo); + + const msg = "just a test message"; + const msgHash = ethers.utils.solidityKeccak256(["string"], [msg]); + const publicKeys = [signerTwo.getPublicKey(), signerOne.getPublicKey()]; + const publicNonces = [ + signerOne.getPublicNonces(), + signerTwo.getPublicNonces(), + ]; + const combinedPublicKey = Schnorrkel.getCombinedPublicKey(publicKeys); + const { signature: sigOne, challenge: e } = signerOne.multiSignMessage( + msgHash, + publicKeys, + publicNonces + ); + const { signature: sigTwo } = signerTwo.multiSignMessage( + msgHash, + publicKeys, + publicNonces + ); + const sSummed = Schnorrkel.sumSigs([sigOne, sigTwo]); // the multisig px and parity - const px = combinedPublicKey.buffer.slice(1,33) - const parity = combinedPublicKey.buffer[0] - 2 + 27 + const px = combinedPublicKey.buffer.slice(1, 33); + const parity = combinedPublicKey.buffer[0] - 2 + 27; // wrap the result - const abiCoder = new ethers.utils.AbiCoder() - const sigData = abiCoder.encode([ 'bytes32', 'bytes32', 'bytes32', 'uint8' ], [ - px, - e.buffer, - sSummed.buffer, - parity - ]) - const result = await contract.isValidSignature(msgHash, sigData) - expect(result).to.equal(ERC1271_MAGICVALUE_BYTES32) - }) - - it('should throw error requirements for public keys when generating nonces and multi singatures', async function () { + const abiCoder = new ethers.utils.AbiCoder(); + const sigData = abiCoder.encode( + ["bytes32", "bytes32", "bytes32", "uint8"], + [px, e.buffer, sSummed.buffer, parity] + ); + const result = await contract.isValidSignature(msgHash, sigData); + expect(result).to.equal(ERC1271_MAGICVALUE_BYTES32); + }); + + it("should throw error requirements for public keys when generating nonces and multi singatures", async function () { // chai.Assertion.expectExpects(3) - const signerOne = new DefaultSigner(0) - const signerTwo = new DefaultSigner(1) + const signerOne = new DefaultSigner(); + const signerTwo = new DefaultSigner(); try { - Schnorrkel.getCombinedPublicKey([signerTwo.getPublicKey()]) + Schnorrkel.getCombinedPublicKey([signerTwo.getPublicKey()]); } catch (e: any) { - expect(e.message).to.equal('At least 2 public keys should be provided') + expect(e.message).to.equal("At least 2 public keys should be provided"); } try { - Schnorrkel.getCombinedAddress([signerOne.getPublicKey()]) + Schnorrkel.getCombinedAddress([signerOne.getPublicKey()]); } catch (e: any) { - expect(e.message).to.equal('At least 2 public keys should be provided') + expect(e.message).to.equal("At least 2 public keys should be provided"); } - const msgHash = ethers.utils.hashMessage('just a test message') - const publicKeys = [signerOne.getPublicKey()] - const publicNonces = [signerOne.getPublicNonces(), signerTwo.getPublicNonces()] + const msgHash = ethers.utils.hashMessage("just a test message"); + const publicKeys = [signerOne.getPublicKey()]; + const publicNonces = [ + signerOne.getPublicNonces(), + signerTwo.getPublicNonces(), + ]; try { - signerOne.multiSignMessage(msgHash, publicKeys, publicNonces) + signerOne.multiSignMessage(msgHash, publicKeys, publicNonces); } catch (e: any) { - expect(e.message).to.equal('At least 2 public keys should be provided') + expect(e.message).to.equal("At least 2 public keys should be provided"); } - }) -}) \ No newline at end of file + }); +}); diff --git a/tests/schnorrkel/onchainSingleSign.test.ts b/tests/schnorrkel/onchainSingleSign.test.ts index b216723..81bdee5 100644 --- a/tests/schnorrkel/onchainSingleSign.test.ts +++ b/tests/schnorrkel/onchainSingleSign.test.ts @@ -1,41 +1,46 @@ -import { describe, expect, it } from 'vitest' -import secp256k1 from 'secp256k1' -import { ethers } from 'ethers' -import Schnorrkel, { Key } from '../../src/index' -import SchnorrSigner from '../../src/schnorrSigner.js' -import { compile } from '../../utils/compile.js' -import { pk1, wallet } from '../config.js' +import { describe, expect, it } from "vitest"; +import { ethers } from "ethers"; +import SchnorrSigner from "../../src/schnorrSigner.js"; +import { compile } from "../../utils/compile.js"; +import { pk1, wallet } from "../config.js"; -const ERC1271_MAGICVALUE_BYTES32 = '0x1626ba7e' +const ERC1271_MAGICVALUE_BYTES32 = "0x1626ba7e"; -describe('Single Sign Tests', function () { +describe("Single Sign Tests", function () { async function deployContract() { - const SchnorrAccountAbstraction = compile('SchnorrAccountAbstraction') + const SchnorrAccountAbstraction = compile("SchnorrAccountAbstraction"); - // the eth address - const publicKey = secp256k1.publicKeyCreate(ethers.utils.arrayify(pk1)) - const px = publicKey.slice(1, 33) - const pxGeneratedAddress = ethers.utils.hexlify(px) - const address = '0x' + pxGeneratedAddress.slice(pxGeneratedAddress.length - 40, pxGeneratedAddress.length) + // get the schnorr addr + const signer = new SchnorrSigner(pk1); + const schnorrAddr = signer.getSchnorrAddress(); // deploying the contract - const factory = new ethers.ContractFactory(SchnorrAccountAbstraction.abi, SchnorrAccountAbstraction.bytecode, wallet) - const contract: any = await factory.deploy([address]) - const isSigner = await contract.canSign(address) - expect(isSigner).to.equal('0x0000000000000000000000000000000000000000000000000000000000000001') + const factory = new ethers.ContractFactory( + SchnorrAccountAbstraction.abi, + SchnorrAccountAbstraction.bytecode, + wallet + ); + const contract: any = await factory.deploy([schnorrAddr]); + const isSigner = await contract.canSign(schnorrAddr); + expect(isSigner).to.equal( + "0x0000000000000000000000000000000000000000000000000000000000000001" + ); - return { contract } + return { contract }; } - it('should generate a schnorr signature and verify onchain', async function () { - const { contract } = await deployContract() + it("should generate a schnorr signature and verify onchain", async function () { + const { contract } = await deployContract(); // sign - const msg = 'just a test message' - const msgHash = ethers.utils.hashMessage(msg) - const signer = new SchnorrSigner(pk1) - const sig = signer.sign(msgHash) - const result = await contract.isValidSignature(msgHash, signer.getEcrecoverStructure(sig)) - expect(result).to.equal(ERC1271_MAGICVALUE_BYTES32) - }) -}) \ No newline at end of file + const msg = "just a test message"; + const msgHash = ethers.utils.hashMessage(msg); + const signer = new SchnorrSigner(pk1); + const sig = signer.sign(msgHash); + const result = await contract.isValidSignature( + msgHash, + signer.getEcrecoverSignature(sig) + ); + expect(result).to.equal(ERC1271_MAGICVALUE_BYTES32); + }); +}); diff --git a/tests/schnorrkel/sumSigs.test.ts b/tests/schnorrkel/sumSigs.test.ts index 4688968..1f3aa1f 100644 --- a/tests/schnorrkel/sumSigs.test.ts +++ b/tests/schnorrkel/sumSigs.test.ts @@ -12,8 +12,8 @@ describe('testing sumSigs', () => { const keyPairOne = generateRandomKeys() const keyPairTwo = generateRandomKeys() - const publicNoncesOne = schnorrkelOne.generatePublicNonces(keyPairOne.privateKey) - const publicNoncesTwo = schnorrkelTwo.generatePublicNonces(keyPairTwo.privateKey) + const publicNoncesOne = schnorrkelOne.generatePublicNonces() + const publicNoncesTwo = schnorrkelTwo.generatePublicNonces() const publicNonces = [publicNoncesOne, publicNoncesTwo] const publicKeys = [keyPairOne.publicKey, keyPairTwo.publicKey] diff --git a/tests/schnorrkel/verify.test.ts b/tests/schnorrkel/verify.test.ts index 1007cef..7eae9f8 100644 --- a/tests/schnorrkel/verify.test.ts +++ b/tests/schnorrkel/verify.test.ts @@ -35,8 +35,8 @@ describe('testing verify', () => { const keyPairOne = generateRandomKeys() const keyPairTwo = generateRandomKeys() - const publicNoncesOne = schnorrkelOne.generatePublicNonces(keyPairOne.privateKey) - const publicNoncesTwo = schnorrkelTwo.generatePublicNonces(keyPairTwo.privateKey) + const publicNoncesOne = schnorrkelOne.generatePublicNonces() + const publicNoncesTwo = schnorrkelTwo.generatePublicNonces() const publicNonces = [publicNoncesOne, publicNoncesTwo] const publicKeys = [keyPairOne.publicKey, keyPairTwo.publicKey] @@ -61,8 +61,8 @@ describe('testing verify', () => { const keyPairOne = generateRandomKeys() const keyPairTwo = generateRandomKeys() - const publicNoncesOne = schnorrkelOne.generatePublicNonces(keyPairOne.privateKey) - const publicNoncesTwo = schnorrkelTwo.generatePublicNonces(keyPairTwo.privateKey) + const publicNoncesOne = schnorrkelOne.generatePublicNonces() + const publicNoncesTwo = schnorrkelTwo.generatePublicNonces() const publicNonces = [publicNoncesOne, publicNoncesTwo] const publicKeys = [keyPairOne.publicKey, keyPairTwo.publicKey] @@ -117,8 +117,8 @@ describe('testing verify', () => { const keyPairOne = generateRandomKeys() const keyPairTwo = generateRandomKeys() - const publicNoncesOne = schnorrkelOne.generatePublicNonces(keyPairOne.privateKey) - const publicNoncesTwo = schnorrkelTwo.generatePublicNonces(keyPairTwo.privateKey) + const publicNoncesOne = schnorrkelOne.generatePublicNonces() + const publicNoncesTwo = schnorrkelTwo.generatePublicNonces() const publicNonces = [publicNoncesOne, publicNoncesTwo] const publicKeys = [keyPairOne.publicKey, keyPairTwo.publicKey] @@ -162,8 +162,8 @@ describe('testing verify', () => { const keyPairOne = generateRandomKeys() const keyPairTwo = generateRandomKeys() - const publicNoncesOne = schnorrkelOne.generatePublicNonces(keyPairOne.privateKey) - const publicNoncesTwo = schnorrkelTwo.generatePublicNonces(keyPairTwo.privateKey) + const publicNoncesOne = schnorrkelOne.generatePublicNonces() + const publicNoncesTwo = schnorrkelTwo.generatePublicNonces() const publicNonces = [publicNoncesOne, publicNoncesTwo] const publicKeys = [keyPairOne.publicKey, keyPairTwo.publicKey] diff --git a/tests/unsafe-schnorrkel/fromJson.test.ts b/tests/unsafe-schnorrkel/fromJson.test.ts index 4a17731..edfe291 100644 --- a/tests/unsafe-schnorrkel/fromJson.test.ts +++ b/tests/unsafe-schnorrkel/fromJson.test.ts @@ -9,7 +9,7 @@ describe('testing fromJson', () => { const schnorrkel = new UnsafeSchnorrkel() const keyPair = generateRandomKeys() - schnorrkel.generatePublicNonces(keyPair.privateKey) + schnorrkel.generatePublicNonces() const jsonData = schnorrkel.toJson() const schnorrkelFromJson = UnsafeSchnorrkel.fromJson(jsonData) @@ -22,7 +22,7 @@ describe('testing fromJson', () => { const schnorrkel = new UnsafeSchnorrkel() const keyPair = generateRandomKeys() - schnorrkel.generatePublicNonces(keyPair.privateKey) + schnorrkel.generatePublicNonces() const jsonData = schnorrkel.toJson() const invalidJsonData = jsonData.slice(0, -1) diff --git a/tests/unsafe-schnorrkel/generatePublicNonces.test.ts b/tests/unsafe-schnorrkel/generatePublicNonces.test.ts index 027bda9..b3ca90f 100644 --- a/tests/unsafe-schnorrkel/generatePublicNonces.test.ts +++ b/tests/unsafe-schnorrkel/generatePublicNonces.test.ts @@ -1,17 +1,16 @@ import { describe, expect, it } from 'vitest' import {UnsafeSchnorrkel} from '../../src/index' -import { _hashPrivateKey, generateRandomKeys } from '../../src/core' +import { _hashPrivateKey } from '../../src/core' describe('testing generatePublicNonces', () => { it('should overwrite public nonces with same private key', () => { const schnorrkel = new UnsafeSchnorrkel() - const keyPair = generateRandomKeys() - const publicNoncesOne = schnorrkel.generatePublicNonces(keyPair.privateKey) + const publicNoncesOne = schnorrkel.generatePublicNonces() const jsonDataOne = schnorrkel.toJson() - const publicNoncesTwo = schnorrkel.generatePublicNonces(keyPair.privateKey) + const publicNoncesTwo = schnorrkel.generatePublicNonces() const jsonDataTwo = schnorrkel.toJson() expect(publicNoncesOne.kPublic).not.toEqual(publicNoncesTwo.kPublic) @@ -20,8 +19,8 @@ describe('testing generatePublicNonces', () => { const dataOne = JSON.parse(jsonDataOne) const dataTwo = JSON.parse(jsonDataTwo) - const hash = _hashPrivateKey(keyPair.privateKey.buffer) - - expect(dataOne.nonces[hash]).not.toEqual(dataTwo.nonces[hash]) + const firstNonceId = Object.keys(dataOne.nonces)[0] + const secondNonceId = Object.keys(dataTwo.nonces)[0] + expect(dataOne.nonces[firstNonceId]).not.toEqual(dataTwo.nonces[secondNonceId]) }) }) \ No newline at end of file diff --git a/utils/DefaultSigner.ts b/utils/DefaultSigner.ts index 21650b7..c9818dd 100644 --- a/utils/DefaultSigner.ts +++ b/utils/DefaultSigner.ts @@ -1,13 +1,12 @@ import Schnorrkel, { Key, PublicNonces } from '../src/index' import { generateRandomKeys } from "../src/core"; -const schnorrkel = new Schnorrkel(); export default class DefaultSigner { - + #schnorrkel = new Schnorrkel(); #privateKey: Key; #publicKey: Key; - constructor(index: number) { + constructor() { const keys = generateRandomKeys() this.#privateKey = keys.privateKey this.#publicKey = keys.publicKey @@ -18,10 +17,10 @@ export default class DefaultSigner { } getPublicNonces(): PublicNonces { - return schnorrkel.generatePublicNonces(this.#privateKey); + return this.#schnorrkel.generatePublicNonces(); } multiSignMessage(msg: string, publicKeys: Key[], publicNonces: PublicNonces[]) { - return schnorrkel.multiSigSign(this.#privateKey, msg, publicKeys, publicNonces); + return this.#schnorrkel.multiSigSign(this.#privateKey, msg, publicKeys, publicNonces); } } \ No newline at end of file From 404f5d12530fbdc80f6cb1eba07993e1f2681a0d Mon Sep 17 00:00:00 2001 From: Borislav Itskov Date: Mon, 21 Oct 2024 18:21:58 +0300 Subject: [PATCH 4/7] add: schnorr providers for single and multisigs --- src/core/index.ts | 4 + src/providers/schnorrMultisigProvider.ts | 54 ++++ src/providers/schnorrProvider.ts | 40 +++ src/schnorrMultisigHelper.ts | 36 --- src/schnorrSigner.ts | 80 ----- src/signers/schnorrSigner.ts | 47 +++ tests/schnorrkel/generatePublicNonces.test.ts | 3 +- tests/schnorrkel/getPublicNonces.test.ts | 6 +- tests/schnorrkel/hasNonces.test.ts | 1 - tests/schnorrkel/onchainMultiSign.test.ts | 274 ++++++------------ tests/schnorrkel/onchainSingleSign.test.ts | 2 +- 11 files changed, 229 insertions(+), 318 deletions(-) create mode 100644 src/providers/schnorrMultisigProvider.ts create mode 100644 src/providers/schnorrProvider.ts delete mode 100644 src/schnorrMultisigHelper.ts delete mode 100644 src/schnorrSigner.ts create mode 100644 src/signers/schnorrSigner.ts diff --git a/src/core/index.ts b/src/core/index.ts index a222619..6b0d0c9 100644 --- a/src/core/index.ts +++ b/src/core/index.ts @@ -236,6 +236,10 @@ export const _multiSigSign = (nonceId: string, nonces: InternalNonces, combinedP * @returns Buffer summed signature */ export const _sumSigs = (signatures: Buffer[]): Buffer => { + if (signatures.length < 2) { + throw Error('Expected at least 2 signatures for aggregation') + } + let combined = new Uint8Array() for (let i = 0; i < signatures.length - 1; i++) { diff --git a/src/providers/schnorrMultisigProvider.ts b/src/providers/schnorrMultisigProvider.ts new file mode 100644 index 0000000..0c9c328 --- /dev/null +++ b/src/providers/schnorrMultisigProvider.ts @@ -0,0 +1,54 @@ +import { arrayify, defaultAbiCoder } from "ethers/lib/utils"; +import { _generateSchnorrAddr } from "../core"; +import Schnorrkel from "../schnorrkel"; +import { PublicNonces, SignatureOutput } from "../types"; +import SchnorrProvider from "./schnorrProvider"; + +class SchnorrMultisigProvider { + private _schnorrProviders: SchnorrProvider[]; + + constructor(schnorrProviders: SchnorrProvider[]) { + this._schnorrProviders = schnorrProviders; + } + + addProvider(schnorrProvider: SchnorrProvider) { + this._schnorrProviders.push(schnorrProvider); + } + + getPublicKeys() { + return this._schnorrProviders.map((provider) => provider.publicKey); + } + + getSchnorrAddress(): string { + return _generateSchnorrAddr( + Schnorrkel.getCombinedPublicKey(this.getPublicKeys()).buffer + ); + } + + /** + * Call this method only once to retrieve the nonces. + * Do not call it again until all signatures have concluded + * @returns + */ + getPublicNonces(): PublicNonces[] { + return this._schnorrProviders.map((provider) => provider.getPublicNonces()); + } + + getEcrecoverSignature(sigOutputs: SignatureOutput[]): string { + const publicKey = arrayify( + Schnorrkel.getCombinedPublicKey(this.getPublicKeys()).buffer + ); + const sSummed = Schnorrkel.sumSigs( + sigOutputs.map((output) => output.signature) + ); + const challenge = sigOutputs[0].challenge; + const px = publicKey.slice(1, 33); + const parity = publicKey[0] - 2 + 27; + return defaultAbiCoder.encode( + ["bytes32", "bytes32", "bytes32", "uint8"], + [px, challenge.buffer, sSummed.buffer, parity] + ); + } +} + +export default SchnorrMultisigProvider; diff --git a/src/providers/schnorrProvider.ts b/src/providers/schnorrProvider.ts new file mode 100644 index 0000000..4e9288e --- /dev/null +++ b/src/providers/schnorrProvider.ts @@ -0,0 +1,40 @@ +import { arrayify, defaultAbiCoder } from "ethers/lib/utils"; +import { _generateSchnorrAddr } from "../core"; +import Schnorrkel from "../schnorrkel"; +import { Key, PublicNonces, SignatureOutput } from "../types"; + +class SchnorrProvider { + public readonly publicKey: Key; + protected readonly _schnorrkel: Schnorrkel; + + constructor(publicKey: Key) { + this.publicKey = publicKey; + this._schnorrkel = new Schnorrkel(); + } + + getSchnorrAddress(): string { + return _generateSchnorrAddr(this.publicKey.buffer); + } + + getPublicNonces(): PublicNonces { + return this._schnorrkel.generateOrGetPublicNonces(); + } + + /** + * The onchain structure + * + * @param sigOutput + * @returns hex forOnchainValidation + */ + getEcrecoverSignature(sigOutput: SignatureOutput): string { + const buffer = arrayify(this.publicKey.buffer); + const px = buffer.slice(1, 33); + const parity = buffer[0] - 2 + 27; + return defaultAbiCoder.encode( + ["bytes32", "bytes32", "bytes32", "uint8"], + [px, sigOutput.challenge.buffer, sigOutput.signature.buffer, parity] + ); + } +} + +export default SchnorrProvider; diff --git a/src/schnorrMultisigHelper.ts b/src/schnorrMultisigHelper.ts deleted file mode 100644 index 9bcf28e..0000000 --- a/src/schnorrMultisigHelper.ts +++ /dev/null @@ -1,36 +0,0 @@ -import { arrayify, defaultAbiCoder } from "ethers/lib/utils"; -import { _generateSchnorrAddr } from "./core"; -import Schnorrkel from "./schnorrkel"; -import { Key, SignatureOutput } from "./types"; - -class SchnorrMultisigHelper { - _publicKeys: Key[]; - - constructor(publicKeys: Key[]) { - this._publicKeys = publicKeys; - } - - getSchnorrAddress() { - return _generateSchnorrAddr( - Schnorrkel.getCombinedPublicKey(this._publicKeys).buffer - ); - } - - getEcrecoverSignature(sigOutputs: SignatureOutput[]) { - const publicKey = arrayify( - Schnorrkel.getCombinedPublicKey(this._publicKeys).buffer - ); - const sSummed = Schnorrkel.sumSigs( - sigOutputs.map((output) => output.signature) - ); - const challenge = sigOutputs[0].challenge; - const px = publicKey.slice(1, 33); - const parity = publicKey[0] - 2 + 27; - return defaultAbiCoder.encode( - ["bytes32", "bytes32", "bytes32", "uint8"], - [px, challenge.buffer, sSummed.buffer, parity] - ); - } -} - -export default SchnorrMultisigHelper; diff --git a/src/schnorrSigner.ts b/src/schnorrSigner.ts deleted file mode 100644 index 771ace6..0000000 --- a/src/schnorrSigner.ts +++ /dev/null @@ -1,80 +0,0 @@ -import { - arrayify, - computePublicKey, - defaultAbiCoder, - isHexString, -} from "ethers/lib/utils"; -import { _generateSchnorrAddr } from "./core"; -import Schnorrkel from "./schnorrkel"; -import { Key, PublicNonces, SignatureOutput } from "./types"; - -type Hex = string; - -class SchnorrSigner { - _privateKey: Key; - _publicKey: Key; - _schnorrkel: Schnorrkel; - - constructor(privateKey: Hex) { - if (!isHexString(privateKey)) throw new Error("invalid hex for privateKey"); - - this._privateKey = new Key(Buffer.from(privateKey.substring(2), "hex")); - this._publicKey = new Key( - Buffer.from(arrayify(computePublicKey(privateKey, true))) - ); - this._schnorrkel = new Schnorrkel(); - } - - getPublicKey(): Key { - return this._publicKey; - } - - /** - * This yields the schnorr address for a 1/1 setup. - * If multisig, combine the public keys and use _generateSchnorrAddr manually - * - * @returns address - */ - getSchnorrAddress() { - return _generateSchnorrAddr(this._publicKey.buffer); - } - - getPublicNonces(): PublicNonces { - return this._schnorrkel.generateOrGetPublicNonces(); - } - - sign(commitment: string): SignatureOutput { - return Schnorrkel.sign(this._privateKey, commitment); - } - - mutliSignatureSign( - msg: string, - publicKeys: Key[], - publicNonces: PublicNonces[] - ) { - return this._schnorrkel.multiSigSign( - this._privateKey, - msg, - publicKeys, - publicNonces - ); - } - - /** - * The onchain structure - * - * @param sigOutput - * @returns hex forOnchainValidation - */ - getEcrecoverSignature(sigOutput: SignatureOutput) { - const publicKey = arrayify(this._publicKey.buffer); - const px = publicKey.slice(1, 33); - const parity = publicKey[0] - 2 + 27; - return defaultAbiCoder.encode( - ["bytes32", "bytes32", "bytes32", "uint8"], - [px, sigOutput.challenge.buffer, sigOutput.signature.buffer, parity] - ); - } -} - -export default SchnorrSigner; diff --git a/src/signers/schnorrSigner.ts b/src/signers/schnorrSigner.ts new file mode 100644 index 0000000..f6b392a --- /dev/null +++ b/src/signers/schnorrSigner.ts @@ -0,0 +1,47 @@ +import { arrayify, computePublicKey, isHexString } from "ethers/lib/utils"; +import { _generateSchnorrAddr } from "../core"; +import Schnorrkel from "../schnorrkel"; +import { Key, PublicNonces, SignatureOutput } from "../types"; +import SchnorrProvider from "../providers/schnorrProvider"; + +type Hex = string; + +class SchnorrSigner extends SchnorrProvider { + private _privateKey: Key; + + constructor(privateKeyHex: Hex) { + if (!isHexString(privateKeyHex)) + throw new Error("invalid hex for privateKey"); + + super( + new Key(Buffer.from(arrayify(computePublicKey(privateKeyHex, true)))) + ); + + this._privateKey = new Key(Buffer.from(privateKeyHex.substring(2), "hex")); + } + + sign(commitment: string): SignatureOutput; + sign( + commitment: string, + publicKeys: Key[], + publicNonces: PublicNonces[] + ): SignatureOutput; + sign( + commitment: string, + publicKeys?: Key[], + publicNonces?: PublicNonces[] + ): SignatureOutput { + if (publicKeys && publicNonces) { + return this._schnorrkel.multiSigSign( + this._privateKey, + commitment, + publicKeys, + publicNonces + ); + } + + return Schnorrkel.sign(this._privateKey, commitment); + } +} + +export default SchnorrSigner; diff --git a/tests/schnorrkel/generatePublicNonces.test.ts b/tests/schnorrkel/generatePublicNonces.test.ts index c99d933..5c57d27 100644 --- a/tests/schnorrkel/generatePublicNonces.test.ts +++ b/tests/schnorrkel/generatePublicNonces.test.ts @@ -8,8 +8,7 @@ describe('testing generatePublicNonces', () => { it('should generate public nonces', () => { const schnorrkel = new Schnorrkel() - const keyPair = generateRandomKeys() - const publicNonces = schnorrkel.generatePublicNonces(keyPair.privateKey) + const publicNonces = schnorrkel.generatePublicNonces() expect(publicNonces).toBeDefined() expect(publicNonces.kPublic).toBeDefined() diff --git a/tests/schnorrkel/getPublicNonces.test.ts b/tests/schnorrkel/getPublicNonces.test.ts index 5bbb307..a9b5d3d 100644 --- a/tests/schnorrkel/getPublicNonces.test.ts +++ b/tests/schnorrkel/getPublicNonces.test.ts @@ -7,8 +7,6 @@ import { _hashPrivateKey, generateRandomKeys } from '../../src/core' describe('testing getPublicNonces', () => { it('should generate the public nonces and afterwards get them successfully', () => { const schnorrkel = new Schnorrkel() - - const keyPair = generateRandomKeys() const publicNonces = schnorrkel.generatePublicNonces() expect(publicNonces).toBeDefined() @@ -22,9 +20,7 @@ describe('testing getPublicNonces', () => { expect(retrievedPublicNonces.kTwoPublic.buffer).to.equal(publicNonces.kTwoPublic.buffer) }) it('should throw an error when calling getPublicNonces if they are not set', () => { - const schnorrkel = new Schnorrkel() - - const keyPair = generateRandomKeys() + const schnorrkel = new Schnorrkel() expect(() => schnorrkel.getPublicNonces()).toThrowError('Nonces not set') }) }) \ No newline at end of file diff --git a/tests/schnorrkel/hasNonces.test.ts b/tests/schnorrkel/hasNonces.test.ts index 4453744..318aed7 100644 --- a/tests/schnorrkel/hasNonces.test.ts +++ b/tests/schnorrkel/hasNonces.test.ts @@ -8,7 +8,6 @@ describe('testing hasNonces', () => { it('should check if there are nonces set before manipulating them', () => { const schnorrkel = new Schnorrkel() - const keyPair = generateRandomKeys() expect(schnorrkel.hasNonces()).to.equal(false) schnorrkel.generatePublicNonces() expect(schnorrkel.hasNonces()).to.equal(true) diff --git a/tests/schnorrkel/onchainMultiSign.test.ts b/tests/schnorrkel/onchainMultiSign.test.ts index 24af043..5662b51 100644 --- a/tests/schnorrkel/onchainMultiSign.test.ts +++ b/tests/schnorrkel/onchainMultiSign.test.ts @@ -2,38 +2,14 @@ import { describe, expect, it } from "vitest"; import { ethers } from "ethers"; import Schnorrkel from "../../src/index"; import { compile } from "../../utils/compile"; -import { pk1, pk2, wallet2 } from "../config"; +import { pk1, pk2, pk3, wallet2 } from "../config"; import DefaultSigner from "../../utils/DefaultSigner"; -import SchnorrSigner from "../../src/schnorrSigner"; -import SchnorrMultisigHelper from "../../src/schnorrMultisigHelper"; +import SchnorrSigner from "../../src/signers/schnorrSigner"; +import SchnorrMultisigProvider from "../../src/providers/schnorrMultisigProvider"; const ERC1271_MAGICVALUE_BYTES32 = "0x1626ba7e"; describe("Multi Sign Tests", function () { - async function deployContract(signerOne: any, signerTwo: any) { - const SchnorrAccountAbstraction = compile("SchnorrAccountAbstraction"); - const factory = new ethers.ContractFactory( - SchnorrAccountAbstraction.abi, - SchnorrAccountAbstraction.bytecode, - wallet2 - ); - - // get the public key - const combinedPublicKey = Schnorrkel.getCombinedPublicKey([ - signerOne.getPublicKey(), - signerTwo.getPublicKey(), - ]); - const px = ethers.utils.hexlify(combinedPublicKey.buffer.slice(1, 33)); - const combinedPublicAddress = "0x" + px.slice(px.length - 40, px.length); - const contract: any = await factory.deploy([combinedPublicAddress]); - const isSigner = await contract.canSign(combinedPublicAddress); - expect(isSigner).to.equal( - "0x0000000000000000000000000000000000000000000000000000000000000001" - ); - - return { contract }; - } - - async function deployContractTwo(multisigHelper: SchnorrMultisigHelper) { + async function deployContract(multisigHelper: SchnorrMultisigProvider) { const SchnorrAccountAbstraction = compile("SchnorrAccountAbstraction"); const factory = new ethers.ContractFactory( SchnorrAccountAbstraction.abi, @@ -51,30 +27,22 @@ describe("Multi Sign Tests", function () { } it("should generate a schnorr musig2 and validate it on the blockchain", async function () { - // deploy the contract const signerOne = new SchnorrSigner(pk1); const signerTwo = new SchnorrSigner(pk2); - const multisigHelper = new SchnorrMultisigHelper([ - signerOne.getPublicKey(), - signerTwo.getPublicKey(), - ]); - const { contract } = await deployContractTwo(multisigHelper); + const multisigHelper = new SchnorrMultisigProvider([signerOne, signerTwo]); + const { contract } = await deployContract(multisigHelper); const msg = "just a test message"; const msgHash = ethers.utils.solidityKeccak256(["string"], [msg]); - const publicKeys = [signerOne.getPublicKey(), signerTwo.getPublicKey()]; - const publicNonces = [ - signerOne.getPublicNonces(), - signerTwo.getPublicNonces(), - ]; - const signature = signerOne.mutliSignatureSign( + const publicNonces = multisigHelper.getPublicNonces(); + const signature = signerOne.sign( msgHash, - publicKeys, + multisigHelper.getPublicKeys(), publicNonces ); - const signatureTwo = signerTwo.mutliSignatureSign( + const signatureTwo = signerTwo.sign( msgHash, - publicKeys, + multisigHelper.getPublicKeys(), publicNonces ); const result = await contract.isValidSignature( @@ -85,208 +53,129 @@ describe("Multi Sign Tests", function () { }); it("should generate the same sig to be sure caching does not affect validation", async function () { - // deploy the contract - const signerOne = new DefaultSigner(); - const signerTwo = new DefaultSigner(); - const { contract } = await deployContract(signerOne, signerTwo); + const signerOne = new SchnorrSigner(pk1); + const signerTwo = new SchnorrSigner(pk2); + const multisigHelper = new SchnorrMultisigProvider([signerOne, signerTwo]); + const { contract } = await deployContract(multisigHelper); const msg = "just a test message"; const msgHash = ethers.utils.solidityKeccak256(["string"], [msg]); - const publicKeys = [signerOne.getPublicKey(), signerTwo.getPublicKey()]; - const publicNonces = [ - signerOne.getPublicNonces(), - signerTwo.getPublicNonces(), - ]; - const combinedPublicKey = Schnorrkel.getCombinedPublicKey(publicKeys); - const { - signature: sigOne, - challenge: e, - publicNonce, - } = signerOne.multiSignMessage(msgHash, publicKeys, publicNonces); - const { signature: sigTwo } = signerTwo.multiSignMessage( + const publicKeys = multisigHelper.getPublicKeys(); + const publicNonces = multisigHelper.getPublicNonces(); + const signature = signerOne.sign(msgHash, publicKeys, publicNonces); + const signatureTwo = signerTwo.sign(msgHash, publicKeys, publicNonces); + const result = await contract.isValidSignature( msgHash, - publicKeys, - publicNonces - ); - const sSummed = Schnorrkel.sumSigs([sigOne, sigTwo]); - - // the multisig px and parity - const px = ethers.utils.hexlify(combinedPublicKey.buffer.slice(1, 33)); - - const parity = combinedPublicKey.buffer[0] - 2 + 27; - - // wrap the result - const abiCoder = new ethers.utils.AbiCoder(); - const sigData = abiCoder.encode( - ["bytes32", "bytes32", "bytes32", "uint8"], - [px, e.buffer, sSummed.buffer, parity] + multisigHelper.getEcrecoverSignature([signature, signatureTwo]) ); - const result = await contract.isValidSignature(msgHash, sigData); expect(result).to.equal(ERC1271_MAGICVALUE_BYTES32); }); it("should fail if the signer is totally different", async function () { - // deploy the contract - const signerOne = new DefaultSigner(); - const signerTwo = new DefaultSigner(); - const { contract } = await deployContract(signerOne, signerTwo); + const signerOne = new SchnorrSigner(pk1); + const signerTwo = new SchnorrSigner(pk2); + const multisigHelper = new SchnorrMultisigProvider([signerOne, signerTwo]); + const { contract } = await deployContract(multisigHelper); - const signerThree = new DefaultSigner(); + const signerThree = new SchnorrSigner(pk3); + const multisigHelperAttacker = new SchnorrMultisigProvider([ + signerOne, + signerThree, + ]); const msg = "just a test message"; const msgHash = ethers.utils.solidityKeccak256(["string"], [msg]); - const publicKeys = [signerOne.getPublicKey(), signerThree.getPublicKey()]; - const publicNonces = [ - signerOne.getPublicNonces(), - signerThree.getPublicNonces(), - ]; - const combinedPublicKey = Schnorrkel.getCombinedPublicKey(publicKeys); + const publicKeys = multisigHelperAttacker.getPublicKeys(); + const publicNonces = multisigHelperAttacker.getPublicNonces(); - // publicNonce: publicNonce, // the final public nonce - // challenge: Challenge, // the schnorr challenge - // signature: Signature, // the signature - const { signature: sigOne, challenge: e } = signerOne.multiSignMessage( - msgHash, - publicKeys, - publicNonces - ); - const { signature: sigTwo } = signerThree.multiSignMessage( + const signature = signerOne.sign(msgHash, publicKeys, publicNonces); + const signatureTwo = signerThree.sign(msgHash, publicKeys, publicNonces); + const result = await contract.isValidSignature( msgHash, - publicKeys, - publicNonces + multisigHelperAttacker.getEcrecoverSignature([signature, signatureTwo]) ); - const sSummed = Schnorrkel.sumSigs([sigOne, sigTwo]); - - // the multisig px and parity - const px = combinedPublicKey.buffer.slice(1, 33); - const parity = combinedPublicKey.buffer[0] - 2 + 27; - - // wrap the result - const abiCoder = new ethers.utils.AbiCoder(); - const sigData = abiCoder.encode( - ["bytes32", "bytes32", "bytes32", "uint8"], - [px, e.buffer, sSummed.buffer, parity] - ); - const result = await contract.isValidSignature(msgHash, sigData); expect(result).to.equal("0xffffffff"); }); it("should fail if only one signature is provided", async function () { - // deploy the contract - const signerOne = new DefaultSigner(); - const signerTwo = new DefaultSigner(); - const { contract } = await deployContract(signerOne, signerTwo); + const signerOne = new SchnorrSigner(pk1); + const signerTwo = new SchnorrSigner(pk2); + const multisigHelper = new SchnorrMultisigProvider([signerOne, signerTwo]); + const { contract } = await deployContract(multisigHelper); const msg = "just a test message"; const msgHash = ethers.utils.solidityKeccak256(["string"], [msg]); - const publicKeys = [signerOne.getPublicKey(), signerTwo.getPublicKey()]; - const publicNonces = [ - signerOne.getPublicNonces(), - signerTwo.getPublicNonces(), - ]; - const combinedPublicKey = Schnorrkel.getCombinedPublicKey(publicKeys); - const { signature: sigOne, challenge: e } = signerOne.multiSignMessage( - msgHash, - publicKeys, - publicNonces - ); + const publicKeys = multisigHelper.getPublicKeys(); + const publicNonces = multisigHelper.getPublicNonces(); + const signature = signerOne.sign(msgHash, publicKeys, publicNonces); - // the multisig px and parity - const px = combinedPublicKey.buffer.slice(1, 33); - const parity = combinedPublicKey.buffer[0] - 2 + 27; + try { + multisigHelper.getEcrecoverSignature([signature]) + } catch (e: any) { + expect(e.message).to.equal("Expected at least 2 signatures for aggregation"); + } - // wrap the result - const abiCoder = new ethers.utils.AbiCoder(); - const sigData = abiCoder.encode( - ["bytes32", "bytes32", "bytes32", "uint8"], - [px, e.buffer, sigOne.buffer, parity] + const result = await contract.isValidSignature( + msgHash, + signerOne.getEcrecoverSignature(signature) ); - const result = await contract.isValidSignature(msgHash, sigData); expect(result).to.equal("0xffffffff"); }); it("should fail if a signer tries to sign twice with the same nonce", async function () { - // deploy the contract - const signerOne = new DefaultSigner(); - const signerTwo = new DefaultSigner(); - await deployContract(signerOne, signerTwo); + const signerOne = new SchnorrSigner(pk1); + const signerTwo = new SchnorrSigner(pk2); + const multisigHelper = new SchnorrMultisigProvider([signerOne, signerTwo]); const msg = "just a test message"; const msgHash = ethers.utils.solidityKeccak256(["string"], [msg]); - const publicKeys = [signerOne.getPublicKey(), signerTwo.getPublicKey()]; - const publicNonces = [ - signerOne.getPublicNonces(), - signerTwo.getPublicNonces(), - ]; - signerOne.multiSignMessage(msgHash, publicKeys, publicNonces); + const publicKeys = multisigHelper.getPublicKeys(); expect( - signerOne.multiSignMessage.bind( - signerOne, - msgHash, - publicKeys, - publicNonces - ) + signerOne.sign.bind(signerOne, msgHash, publicKeys, []) ).to.throw("Nonces should be exchanged before signing"); }); - it("should fail if only one signer tries to sign the transaction providing 2 messages", async function () { + it("should fail if only one signer tries to sign the transaction providing 2 different public nonces", async function () { // deploy the contract - const signerOne = new DefaultSigner(); - const signerTwo = new DefaultSigner(); - const { contract } = await deployContract(signerOne, signerTwo); + const signerOne = new SchnorrSigner(pk1); + const signerTwo = new SchnorrSigner(pk2); + const multisigHelper = new SchnorrMultisigProvider([signerOne, signerTwo]); + const { contract } = await deployContract(multisigHelper); const msg = "just a test message"; const msgHash = ethers.utils.solidityKeccak256(["string"], [msg]); - const publicKeys = [signerOne.getPublicKey(), signerTwo.getPublicKey()]; - const signerTwoNonces = signerTwo.getPublicNonces(); - const publicNonces = [signerOne.getPublicNonces(), signerTwoNonces]; - const combinedPublicKey = Schnorrkel.getCombinedPublicKey(publicKeys); - const { signature: sigOne, challenge: e } = signerOne.multiSignMessage( + const publicKeys = multisigHelper.getPublicKeys() + const signature = signerOne.sign( msgHash, publicKeys, - publicNonces + multisigHelper.getPublicNonces() ); - const publicNoncesTwo = [signerOne.getPublicNonces(), signerTwoNonces]; - const { signature: sigTwo } = signerOne.multiSignMessage( + const signatreTwo = signerOne.sign( msgHash, publicKeys, - publicNoncesTwo + multisigHelper.getPublicNonces() ); - const sSummed = Schnorrkel.sumSigs([sigOne, sigTwo]); - - // the multisig px and parity - const px = combinedPublicKey.buffer.slice(1, 33); - const parity = combinedPublicKey.buffer[0] - 2 + 27; - - // wrap the result - const abiCoder = new ethers.utils.AbiCoder(); - const sigData = abiCoder.encode( - ["bytes32", "bytes32", "bytes32", "uint8"], - [px, e.buffer, sSummed.buffer, parity] - ); - const result = await contract.isValidSignature(msgHash, sigData); + const result = await contract.isValidSignature(msgHash, multisigHelper.getEcrecoverSignature([signature, signatreTwo])); expect(result).to.equal("0xffffffff"); }); it("should successfully pass even if the order of the public keys is different", async function () { // deploy the contract - const signerOne = new DefaultSigner(); - const signerTwo = new DefaultSigner(); - const { contract } = await deployContract(signerOne, signerTwo); + const signerOne = new SchnorrSigner(pk1); + const signerTwo = new SchnorrSigner(pk2); + const multisigHelper = new SchnorrMultisigProvider([signerOne, signerTwo]); + const { contract } = await deployContract(multisigHelper); const msg = "just a test message"; const msgHash = ethers.utils.solidityKeccak256(["string"], [msg]); - const publicKeys = [signerTwo.getPublicKey(), signerOne.getPublicKey()]; - const publicNonces = [ - signerOne.getPublicNonces(), - signerTwo.getPublicNonces(), - ]; + const publicKeys = [signerTwo.publicKey, signerOne.publicKey]; + const publicNonces = multisigHelper.getPublicNonces() const combinedPublicKey = Schnorrkel.getCombinedPublicKey(publicKeys); - const { signature: sigOne, challenge: e } = signerOne.multiSignMessage( + const { signature: sigOne, challenge: e } = signerOne.sign( msgHash, publicKeys, publicNonces ); - const { signature: sigTwo } = signerTwo.multiSignMessage( + const { signature: sigTwo } = signerTwo.sign( msgHash, publicKeys, publicNonces @@ -308,29 +197,28 @@ describe("Multi Sign Tests", function () { }); it("should throw error requirements for public keys when generating nonces and multi singatures", async function () { - // chai.Assertion.expectExpects(3) - const signerOne = new DefaultSigner(); - const signerTwo = new DefaultSigner(); + const signerOne = new SchnorrSigner(pk1); + const signerTwo = new SchnorrSigner(pk2); try { - Schnorrkel.getCombinedPublicKey([signerTwo.getPublicKey()]); + Schnorrkel.getCombinedPublicKey([signerTwo.publicKey]); } catch (e: any) { expect(e.message).to.equal("At least 2 public keys should be provided"); } try { - Schnorrkel.getCombinedAddress([signerOne.getPublicKey()]); + Schnorrkel.getCombinedAddress([signerOne.publicKey]); } catch (e: any) { expect(e.message).to.equal("At least 2 public keys should be provided"); } const msgHash = ethers.utils.hashMessage("just a test message"); - const publicKeys = [signerOne.getPublicKey()]; + const publicKeys = [signerOne.publicKey]; const publicNonces = [ signerOne.getPublicNonces(), signerTwo.getPublicNonces(), ]; try { - signerOne.multiSignMessage(msgHash, publicKeys, publicNonces); + signerOne.sign(msgHash, publicKeys, publicNonces); } catch (e: any) { expect(e.message).to.equal("At least 2 public keys should be provided"); } diff --git a/tests/schnorrkel/onchainSingleSign.test.ts b/tests/schnorrkel/onchainSingleSign.test.ts index 81bdee5..f4b214a 100644 --- a/tests/schnorrkel/onchainSingleSign.test.ts +++ b/tests/schnorrkel/onchainSingleSign.test.ts @@ -1,6 +1,6 @@ import { describe, expect, it } from "vitest"; import { ethers } from "ethers"; -import SchnorrSigner from "../../src/schnorrSigner.js"; +import SchnorrSigner from "../../src/signers/schnorrSigner.js"; import { compile } from "../../utils/compile.js"; import { pk1, wallet } from "../config.js"; From 9d3c38cd5d7133c785d254ac9545d26a8f5f869e Mon Sep 17 00:00:00 2001 From: Borislav Itskov Date: Mon, 21 Oct 2024 19:02:32 +0300 Subject: [PATCH 5/7] add: verify() to signer, update readme, add two signer tests, expose signer and providers in index --- README.md | 158 ++++++++++++--------- src/index.ts | 20 ++- src/providers/schnorrMultisigProvider.ts | 22 ++- src/signers/schnorrSigner.ts | 18 ++- tests/schnorrkel/onchainSingleSign.test.ts | 2 +- tests/schnorrkel/verify.test.ts | 33 ++++- 6 files changed, 178 insertions(+), 75 deletions(-) diff --git a/README.md b/README.md index 47fefb2..38c2b40 100644 --- a/README.md +++ b/README.md @@ -1,36 +1,40 @@ # Schnorr Signatures + A javaScript library for signing and verifying Schnorr Signatures. It can be used for single and multi signatures. -Blockchain validation via ecrecover is also supported. +Blockchain validation via ecrecover is also supported. # Typescript support + Since version 2.0.0, we're moving entirely to Typescript. ## Version 2.0 Breaking changes -* `sign()` and `multiSigSign()` return an instance of `SignatureOutput`. Each element in it has a buffer property - * instead of `e` we return `challenge` for the Schnorr Challenge. To accces its value, use `challenge.buffer` - * instead of `s` we return `signature` for the Schnorr Signature. To accces its value, use `signature.buffer` - * instead of `R` we return `publicNonce` for the nonce. To accces its value, use `publicNonce.buffer` -* `getCombinedPublicKey()` returns a `Key` class. To get the actual key, use `key.buffer` -* a lot of method become static as they don't keep any state: - * `verify` - * `sign` - * `sumSigs` - * `getCombinedPublicKey` - * `getCombinedAddress` + +- `sign()` and `multiSigSign()` return an instance of `SignatureOutput`. Each element in it has a buffer property + - instead of `e` we return `challenge` for the Schnorr Challenge. To accces its value, use `challenge.buffer` + - instead of `s` we return `signature` for the Schnorr Signature. To accces its value, use `signature.buffer` + - instead of `R` we return `publicNonce` for the nonce. To accces its value, use `publicNonce.buffer` +- `getCombinedPublicKey()` returns a `Key` class. To get the actual key, use `key.buffer` +- a lot of method become static as they don't keep any state: + - `verify` + - `sign` + - `sumSigs` + - `getCombinedPublicKey` + - `getCombinedAddress` ## Version 3.0 Breaking changes -* `finalPublicNonce`, `FinalPublicNonce` is replaced everywhere with `publicNonce`, `PublicNonce`. The old name just didn't make sense. -* `sign()` is the former `signHash()`. A sign function that accepts a plain-text message as an argument no longer exists. -* `multiSigSign()` is the former `multiSigSignHash()`. A sign function that accepts a plain-text message as an argument no longer exists. -* `verify()` is the former `verifyHash()`. A verification function that accepts a plain-text message as an argument no longer exists. + +- `finalPublicNonce`, `FinalPublicNonce` is replaced everywhere with `publicNonce`, `PublicNonce`. The old name just didn't make sense. +- `sign()` is the former `signHash()`. A sign function that accepts a plain-text message as an argument no longer exists. +- `multiSigSign()` is the former `multiSigSignHash()`. A sign function that accepts a plain-text message as an argument no longer exists. +- `verify()` is the former `verifyHash()`. A verification function that accepts a plain-text message as an argument no longer exists. In version 2, we had plenty of ways to sign a message. This broad a lot of confusion as to what function was the correct one to use in various situations. This lead us to believe that making things simpler and forcing a hash to be passed to the methods is the way forward. ## Requirements: -* Node: >=16.0.0, <20.0.0 -* npm (Node.js package manager) v9.x.x +- Node: >=16.0.0, <20.0.0 +- npm (Node.js package manager) v9.x.x ## Installation @@ -41,6 +45,7 @@ npm i ``` ## Testing + ``` npm run test ``` @@ -48,30 +53,33 @@ npm run test ## Usage ### Single Signatures + We refer to Single Signatures as ones that have a single signer. Sign: + ```js -import Schnorrkel from '@borislav.itskov/schnorrkel.js' +import { SchnorrSigner } from "@borislav.itskov/schnorrkel.js"; -const privateKey = new Key(Buffer.from(ethers.utils.randomBytes(32))) -const msg = 'test message' -const hash = ethers.utils.hashMessage(msg) -const {signature, publicNonce, challenge} = Schnorrkel.sign(privateKey, hash) +const privateKey = hexlify(ethers.utils.randomBytes(32)); +const signer = new SchnorrSigner(pk1); +const msg = "test message"; +const hash = ethers.utils.hashMessage(msg); +const { signature, publicNonce, challenge } = signer.sign(hash); ``` Offchain verification: -We take the `signature`, `hash` and `publicNonce` from the example above and do: +We take the `signature` and `hash` from the example above and do: + ```js -const publicKey = Buffer.from(secp256k1.publicKeyCreate(privateKey.buffer)) -// signature and publicNonce come from Schnorrkel.sign -const result = Schnorrkel.verify(signature, hash, publicNonce, publicKey) +const result = signer.verify(hash, signature); ``` Onchain verification: First, you will need a contract that verifies schnorr. We have it in the repository and it is called `SchnorrAccountAbstraction`. But all in all, you need this onchain: + ```js function verifySchnorr(bytes32 hash, bytes memory sig) internal pure returns (bool) { // px := public key x-coord @@ -97,34 +105,42 @@ We explain how ecrecover works and why it is needed later [in this document](#ec Let's send a request to the local hardhat node. First run in the terminal: npx hardhat node Afterwards, here is part of the code: + ```js -import { ethers } from 'ethers' -import secp256k1 from 'secp256k1' - -const privateKey = new Key(Buffer.from(ethers.utils.randomBytes(32))) -const publicKey = secp256k1.publicKeyCreate(ethers.utils.arrayify(privateKey)) -const px = publicKey.slice(1, 33) -const pxGeneratedAddress = ethers.utils.hexlify(px) -const schnorrAddr = '0x' + pxGeneratedAddress.slice(pxGeneratedAddress.length - 40, pxGeneratedAddress.length) -const factory = new ethers.ContractFactory(SchnorrAccountAbstraction.abi, SchnorrAccountAbstraction.bytecode, wallet) -const contract: any = await factory.deploy([schnorrAddr]) - -const pkBuffer = new Key(Buffer.from(ethers.utils.arrayify(privateKey))) -const msg = 'just a test message'; -const msgHash = ethers.utils.hashMessage(msg) -const sig = Schnorrkel.sign(pkBuffer, msgHash) +import { ethers } from "ethers"; +import secp256k1 from "secp256k1"; + +const privateKey = new Key(Buffer.from(ethers.utils.randomBytes(32))); +const publicKey = secp256k1.publicKeyCreate(ethers.utils.arrayify(privateKey)); +const px = publicKey.slice(1, 33); +const pxGeneratedAddress = ethers.utils.hexlify(px); +const schnorrAddr = + "0x" + + pxGeneratedAddress.slice( + pxGeneratedAddress.length - 40, + pxGeneratedAddress.length + ); +const factory = new ethers.ContractFactory( + SchnorrAccountAbstraction.abi, + SchnorrAccountAbstraction.bytecode, + wallet +); +const contract: any = await factory.deploy([schnorrAddr]); + +const pkBuffer = new Key(Buffer.from(ethers.utils.arrayify(privateKey))); +const msg = "just a test message"; +const msgHash = ethers.utils.hashMessage(msg); +const sig = Schnorrkel.sign(pkBuffer, msgHash); // wrap the result -const publicKey = secp256k1.publicKeyCreate(ethers.utils.arrayify(privateKey)) +const publicKey = secp256k1.publicKeyCreate(ethers.utils.arrayify(privateKey)); const px = publicKey.slice(1, 33); const parity = publicKey[0] - 2 + 27; const abiCoder = new ethers.utils.AbiCoder(); -const sigData = abiCoder.encode([ "bytes32", "bytes32", "bytes32", "uint8" ], [ - px, - sig.challenge.buffer, - sig.signature.buffer, - parity -]); +const sigData = abiCoder.encode( + ["bytes32", "bytes32", "bytes32", "uint8"], + [px, sig.challenge.buffer, sig.signature.buffer, parity] +); const result = await contract.isValidSignature(msgHash, sigData); ``` @@ -140,8 +156,8 @@ Below are all the steps needed to craft a successful multisig. Public nonces need to be exchanged between signers before they sign. Normally, the Signer should implement this library as define a `getPublicNonces` method that will call the library and return the nonces. For our test example, we're going to call the schnorrkel library directly: ```js -const privateKey1: Buffer = '...' -const privateKey2: Buffer = '...' +const privateKey1: Buffer = "..."; +const privateKey2: Buffer = "..."; const publicNonces1 = schnorrkel.generatePublicNonces(privateKey1); const publicNonces2 = schnorrkel.generatePublicNonces(privateKey2); ``` @@ -153,28 +169,34 @@ Again, this isn't how the flow is supposed to work. A signer needs to implement After we have them, here is how to sign: ```js -const publicKey1: Buffer = '...' -const publicKey2: Buffer = '...' +const publicKey1: Buffer = "..."; +const publicKey2: Buffer = "..."; const publicKeys = [publicKey1, publicKey2]; -const combinedPublicKey = schnorrkel.getCombinedPublicKey(publicKeys) -const {signature: sigOne, challenge: e, publicNonce} = signerOne.multiSignMessage(msg, publicKeys, publicNonces) -const {signature: sigTwo} = signerTwo.multiSignMessage(msg, publicKeys, publicNonces) -const sSummed = Schnorrkel.sumSigs([sigOne, sigTwo]) +const combinedPublicKey = schnorrkel.getCombinedPublicKey(publicKeys); +const { + signature: sigOne, + challenge: e, + publicNonce, +} = signerOne.multiSignMessage(msg, publicKeys, publicNonces); +const { signature: sigTwo } = signerTwo.multiSignMessage( + msg, + publicKeys, + publicNonces +); +const sSummed = Schnorrkel.sumSigs([sigOne, sigTwo]); ``` #### verify onchain ```js -const px = combinedPublicKey.buffer.slice(1,33); +const px = combinedPublicKey.buffer.slice(1, 33); const parity = combinedPublicKey.buffer[0] - 2 + 27; const abiCoder = new ethers.utils.AbiCoder(); -const sigData = abiCoder.encode([ "bytes32", "bytes32", "bytes32", "uint8" ], [ - px, - challenge.buffer, - sSummed.buffer, - parity -]); -const msgHash = ethers.utils.solidityKeccak256(['string'], [msg]); +const sigData = abiCoder.encode( + ["bytes32", "bytes32", "bytes32", "uint8"], + [px, challenge.buffer, sSummed.buffer, parity] +); +const msgHash = ethers.utils.solidityKeccak256(["string"], [msg]); const result = await contract.isValidSignature(msgHash, sigData); ``` @@ -187,6 +209,7 @@ const result = schnorrkel.verify(sSummed, msg, publicNonce, combinedPublicKey); You can find reference to this in `tests/schnorrkel/onchainMultiSign.test.ts` in this repository. ## ecrecover + For the schnorr on-chain verification, we were inspired by the work of [noot](https://github.com/noot). Without his work, it would've required a lot more time for RnD to reach this point. You can take a look at his repository [here](https://github.com/noot/schnorr-verify) We utilize Ethereum ecrecover to verify the signature. This is how it works: @@ -211,6 +234,7 @@ calculate e = H(address(R) || m) and P_x = x-coordinate of P ``` pass: + ``` m = -s*P_x v = parity of P @@ -232,5 +256,5 @@ Q = G*s - P*e // same as schnorr verify above the returned value is address(Q). -* calculate e' = h(address(Q) || m) -* check e' == e to verify the signature. +- calculate e' = h(address(Q) || m) +- check e' == e to verify the signature. diff --git a/src/index.ts b/src/index.ts index bf4d2ae..298a791 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,6 +1,18 @@ -import Schnorrkel from './schnorrkel' -export { default as UnsafeSchnorrkel } from './unsafe-schnorrkel' +import Schnorrkel from "./schnorrkel"; +export { default as UnsafeSchnorrkel } from "./unsafe-schnorrkel"; -export { Key, KeyPair, Signature, PublicNonces, Challenge, SignatureOutput, PublicNonce } from './types' +export { + Key, + KeyPair, + Signature, + PublicNonces, + Challenge, + SignatureOutput, + PublicNonce, +} from "./types"; -export default Schnorrkel \ No newline at end of file +export { default as SchnorrSigner } from "./signers/schnorrSigner"; +export { default as SchnorrProvider } from "./providers/schnorrProvider"; +export { default as SchnorrMultisigProvider } from "./providers/schnorrMultisigProvider"; + +export default Schnorrkel; diff --git a/src/providers/schnorrMultisigProvider.ts b/src/providers/schnorrMultisigProvider.ts index 0c9c328..ce7d2fe 100644 --- a/src/providers/schnorrMultisigProvider.ts +++ b/src/providers/schnorrMultisigProvider.ts @@ -1,5 +1,5 @@ import { arrayify, defaultAbiCoder } from "ethers/lib/utils"; -import { _generateSchnorrAddr } from "../core"; +import { _generateSchnorrAddr, _verify } from "../core"; import Schnorrkel from "../schnorrkel"; import { PublicNonces, SignatureOutput } from "../types"; import SchnorrProvider from "./schnorrProvider"; @@ -49,6 +49,26 @@ class SchnorrMultisigProvider { [px, challenge.buffer, sSummed.buffer, parity] ); } + + /** + * Off-chain signature verification + * + * @param signature - the computed output after sign + * @param commitment - the commitment that should have been signed + * @returns boolean + */ + verify(commitment: string, sigOutputs: SignatureOutput[]): boolean { + const sSummed = Schnorrkel.sumSigs( + sigOutputs.map((output) => output.signature) + ); + + return _verify( + sSummed.buffer, + commitment, + sigOutputs[0].publicNonce.buffer, + Schnorrkel.getCombinedPublicKey(this.getPublicKeys()).buffer + ); + } } export default SchnorrMultisigProvider; diff --git a/src/signers/schnorrSigner.ts b/src/signers/schnorrSigner.ts index f6b392a..91a8512 100644 --- a/src/signers/schnorrSigner.ts +++ b/src/signers/schnorrSigner.ts @@ -1,5 +1,5 @@ import { arrayify, computePublicKey, isHexString } from "ethers/lib/utils"; -import { _generateSchnorrAddr } from "../core"; +import { _generateSchnorrAddr, _verify } from "../core"; import Schnorrkel from "../schnorrkel"; import { Key, PublicNonces, SignatureOutput } from "../types"; import SchnorrProvider from "../providers/schnorrProvider"; @@ -42,6 +42,22 @@ class SchnorrSigner extends SchnorrProvider { return Schnorrkel.sign(this._privateKey, commitment); } + + /** + * Off-chain signature verification + * + * @param signature - the computed output after sign + * @param commitment - the commitment that should have been signed + * @returns boolean + */ + verify(commitment: string, signature: SignatureOutput): boolean { + return _verify( + signature.signature.buffer, + commitment, + signature.publicNonce.buffer, + this.publicKey.buffer + ); + } } export default SchnorrSigner; diff --git a/tests/schnorrkel/onchainSingleSign.test.ts b/tests/schnorrkel/onchainSingleSign.test.ts index f4b214a..e18d67e 100644 --- a/tests/schnorrkel/onchainSingleSign.test.ts +++ b/tests/schnorrkel/onchainSingleSign.test.ts @@ -1,6 +1,6 @@ import { describe, expect, it } from "vitest"; import { ethers } from "ethers"; -import SchnorrSigner from "../../src/signers/schnorrSigner.js"; +import { SchnorrSigner } from "../../src"; import { compile } from "../../utils/compile.js"; import { pk1, wallet } from "../config.js"; diff --git a/tests/schnorrkel/verify.test.ts b/tests/schnorrkel/verify.test.ts index 7eae9f8..837f385 100644 --- a/tests/schnorrkel/verify.test.ts +++ b/tests/schnorrkel/verify.test.ts @@ -1,8 +1,9 @@ import { describe, expect, it } from 'vitest' -import Schnorrkel, { Key } from '../../src/index' +import Schnorrkel, { Key, SchnorrMultisigProvider, SchnorrSigner } from '../../src/index' import { _hashPrivateKey, generateRandomKeys } from '../../src/core' import { ethers } from 'ethers' +import { hexlify } from 'ethers/lib/utils' describe('testing verify', () => { it('should verify a normal schnorr signature and make sure sign does not overwrite the private key', () => { @@ -29,6 +30,21 @@ describe('testing verify', () => { const secondRes = Schnorrkel.verify(secondSig.signature, ethers.utils.solidityKeccak256(['string'], [secondMsg]), secondSig.publicNonce, new Key(Buffer.from(publicKey))) expect(secondRes).toEqual(true) }) + it('should verify a normal schnorr signature using the schnorr signer', () => { + const privateKey = hexlify(ethers.utils.randomBytes(32)) + const signer = new SchnorrSigner(privateKey) + + const msg = 'test message' + const msgHash = ethers.utils.solidityKeccak256(['string'], [msg]) + const signature = signer.sign(msgHash) + + expect(signature).toBeDefined() + expect(signature.publicNonce.buffer).toHaveLength(33) + expect(signature.signature.buffer).toHaveLength(32) + expect(signature.challenge.buffer).toHaveLength(32) + const result = signer.verify(msgHash, signature) + expect(result).toEqual(true) + }) it('should sum signatures and verify them', () => { const schnorrkelOne = new Schnorrkel() const schnorrkelTwo = new Schnorrkel() @@ -54,6 +70,21 @@ describe('testing verify', () => { expect(result).toEqual(true) }) + it('should sum signatures and verify them using the schnorr signer', () => { + const signerOne = new SchnorrSigner(`0x${generateRandomKeys().privateKey.toHex()}`) + const signerTwo = new SchnorrSigner(`0x${generateRandomKeys().privateKey.toHex()}`) + const multisigProvider = new SchnorrMultisigProvider([signerOne, signerTwo]) + const publicNonces = multisigProvider.getPublicNonces() + const publicKeys = multisigProvider.getPublicKeys() + + const msg = 'test message' + const msgHash = ethers.utils.solidityKeccak256(['string'], [msg]) + const signatureOne = signerOne.sign(msgHash, publicKeys, publicNonces) + const signatureTwo = signerTwo.sign(msgHash, publicKeys, publicNonces) + + const result = multisigProvider.verify(msgHash, [signatureOne, signatureTwo]) + expect(result).toEqual(true) + }) it('should make sure private keys are not overwritten during signing', () => { const schnorrkelOne = new Schnorrkel() const schnorrkelTwo = new Schnorrkel() From 1f328b81d46a760e142dcf1e2a07edefb8294313 Mon Sep 17 00:00:00 2001 From: Borislav Itskov Date: Wed, 23 Oct 2024 18:13:48 +0300 Subject: [PATCH 6/7] add: better readme and readability in code --- README.md | 159 +++++++++++++-------- tests/schnorrkel/onchainMultiSign.test.ts | 133 ++++++++++------- tests/schnorrkel/onchainSingleSign.test.ts | 7 +- utils/DefaultSigner.ts | 26 ---- 4 files changed, 184 insertions(+), 141 deletions(-) delete mode 100644 utils/DefaultSigner.ts diff --git a/README.md b/README.md index 38c2b40..8346cfb 100644 --- a/README.md +++ b/README.md @@ -60,12 +60,13 @@ Sign: ```js import { SchnorrSigner } from "@borislav.itskov/schnorrkel.js"; +import { hexlify, randomBytes, hashMessage } from "ethers/lib/utils"; -const privateKey = hexlify(ethers.utils.randomBytes(32)); +const privateKey = hexlify(randomBytes(32)); const signer = new SchnorrSigner(pk1); const msg = "test message"; -const hash = ethers.utils.hashMessage(msg); -const { signature, publicNonce, challenge } = signer.sign(hash); +const commitment = hashMessage(msg); +const signature = signer.sign(commitment); ``` Offchain verification: @@ -107,41 +108,25 @@ npx hardhat node Afterwards, here is part of the code: ```js -import { ethers } from "ethers"; -import secp256k1 from "secp256k1"; +import { SchnorrSigner } from "@borislav.itskov/schnorrkel.js"; +import { hexlify, randomBytes, hashMessage } from "ethers/lib/utils"; +import { ContractFactory } from "ethers"; -const privateKey = new Key(Buffer.from(ethers.utils.randomBytes(32))); -const publicKey = secp256k1.publicKeyCreate(ethers.utils.arrayify(privateKey)); -const px = publicKey.slice(1, 33); -const pxGeneratedAddress = ethers.utils.hexlify(px); -const schnorrAddr = - "0x" + - pxGeneratedAddress.slice( - pxGeneratedAddress.length - 40, - pxGeneratedAddress.length - ); -const factory = new ethers.ContractFactory( +const privateKey = hexlify(randomBytes(32)); +const signer = new SchnorrSigner(privateKey); +const factory = new ContractFactory( SchnorrAccountAbstraction.abi, SchnorrAccountAbstraction.bytecode, wallet ); -const contract: any = await factory.deploy([schnorrAddr]); - -const pkBuffer = new Key(Buffer.from(ethers.utils.arrayify(privateKey))); +const contract: any = await factory.deploy([signer.getSchnorrAddress()]); const msg = "just a test message"; -const msgHash = ethers.utils.hashMessage(msg); -const sig = Schnorrkel.sign(pkBuffer, msgHash); - -// wrap the result -const publicKey = secp256k1.publicKeyCreate(ethers.utils.arrayify(privateKey)); -const px = publicKey.slice(1, 33); -const parity = publicKey[0] - 2 + 27; -const abiCoder = new ethers.utils.AbiCoder(); -const sigData = abiCoder.encode( - ["bytes32", "bytes32", "bytes32", "uint8"], - [px, sig.challenge.buffer, sig.signature.buffer, parity] +const commitment = hashMessage(msg); +const sig = signer.sign(commitment); +const result = await contract.isValidSignature( + commitment, + signer.getEcrecoverSignature(sig) ); -const result = await contract.isValidSignature(msgHash, sigData); ``` You can see the full implementation in `tests/schnorrkel/onchainSingleSign.test.ts` in this repository. @@ -151,59 +136,113 @@ You can see the full implementation in `tests/schnorrkel/onchainSingleSign.test. Schnorr multisignatures work on the basis n/n - all of the signers need to sign in order for the signature to be valid. Below are all the steps needed to craft a successful multisig. +### MultisigProvider + +To make multisig easier, a `SchnorrMultisigProvider` class was created. It expects all `SchnorrProvider` objects that participate in the multisig. The `SchnorrProvider` can be passed by itself or one could use the `SchnorrSigner`. The meaninful point is that you don't need possession of the private keys to use the `SchnorrMultisigProvider`. It's goal is to provider helper functions for fetching the correct on-chain schnorr address, the combined public key of all the signers and provide a easy way to fetch the expected on-chain structure for validation + +```js +import { + SchnorrSigner, + SchnorrMultisigProvider, +} from "@borislav.itskov/schnorrkel.js"; + +const signerOne = new SchnorrSigner(pk1); +const signerTwo = new SchnorrSigner(pk2); +const multisigProvider = new SchnorrMultisigProvider([signerOne, signerTwo]); +``` + #### Public nonces -Public nonces need to be exchanged between signers before they sign. Normally, the Signer should implement this library as define a `getPublicNonces` method that will call the library and return the nonces. For our test example, we're going to call the schnorrkel library directly: +Public nonces need to be exchanged between signers before they sign. You can do this in two ways. +Using the multisigProvider: ```js -const privateKey1: Buffer = "..."; -const privateKey2: Buffer = "..."; -const publicNonces1 = schnorrkel.generatePublicNonces(privateKey1); -const publicNonces2 = schnorrkel.generatePublicNonces(privateKey2); +const publicNonces = multisigProvider.getPublicNonces(); ``` -Again, this isn't how the flow is supposed to work. A signer needs to implement the library and when `getPublicNonces` is called, the user should be ask whether he is okay to generate and give his public nonces. +Or manually by calling each signer individially: + +```js +const publicNonces = [signerOne.getPublicNonces(), signerOne.getPublicNonces()]; +``` + +Nonces need to be exchanged before signing can begin. Also, `getPublicNonces` should not be called again before all signers complete their signing process. Or at least one should be careful not to mixes the public nonces with newly generated ones. In the case of mixed nonces, signing will not work. #### sign -After we have them, here is how to sign: +Here is an example of a signing process. Public keys and public nonce can be retriever either manually by calling the signer or directly by calling the multisigProvider. ```js -const publicKey1: Buffer = "..."; -const publicKey2: Buffer = "..."; -const publicKeys = [publicKey1, publicKey2]; -const combinedPublicKey = schnorrkel.getCombinedPublicKey(publicKeys); -const { - signature: sigOne, - challenge: e, - publicNonce, -} = signerOne.multiSignMessage(msg, publicKeys, publicNonces); -const { signature: sigTwo } = signerTwo.multiSignMessage( - msg, - publicKeys, - publicNonces -); -const sSummed = Schnorrkel.sumSigs([sigOne, sigTwo]); +import { solidityKeccak256 } from "ethers/lib/utils"; + +const msg = "just a test message"; +const msgHash = solidityKeccak256(["string"], [msg]); +const publicKeys = multisigProvider.getPublicKeys(); +const publicNonces = multisigProvider.getPublicNonces(); +const signature = signerOne.sign(msgHash, publicKeys, publicNonces); +const signatureTwo = signerTwo.sign(msgHash, publicKeys, publicNonces); ``` #### verify onchain +Generation of the encoded data for the on-chain verification is somewhat complex and therefore, it's hidden away in the `multisigProvider`. +Here's an example of how to perform an on-chain verification using the provider: + ```js -const px = combinedPublicKey.buffer.slice(1, 33); -const parity = combinedPublicKey.buffer[0] - 2 + 27; -const abiCoder = new ethers.utils.AbiCoder(); -const sigData = abiCoder.encode( +const ecRecoverSchnorr = multisigProvider.getEcrecoverSignature([ + signature, + signatureTwo, +]); +const result = await contract.isValidSignature(msgHash, ecRecoverSchnorr); +``` + +Here's also an example of how you can do it without the multisigProvider: + +```js +import Schnorrkel from "@borislav.itskov/schnorrkel.js"; +import { defaultAbiCoder } from "ethers/lib/utils"; + +const publicKeys = [signerOne.publicKey, signerTwo.publicKey]; +const publicKey = arrayify(Schnorrkel.getCombinedPublicKey(publicKeys).buffer); +const sigOutputs = [signature, signatureTwo]; +const sSummed = Schnorrkel.sumSigs( + sigOutputs.map((output) => output.signature) +); +const challenge = sigOutputs[0].challenge; +const px = publicKey.slice(1, 33); +const parity = publicKey[0] - 2 + 27; +const ecRecoverSchnorr = defaultAbiCoder.encode( ["bytes32", "bytes32", "bytes32", "uint8"], [px, challenge.buffer, sSummed.buffer, parity] ); -const msgHash = ethers.utils.solidityKeccak256(["string"], [msg]); -const result = await contract.isValidSignature(msgHash, sigData); +const result = await contract.isValidSignature(msgHash, ecRecoverSchnorr); ``` #### verify offchain +With the multisig provider: + +```js +const result = multisigProvider.verify(msgHash, [signature, signatureTwo]); +``` + +Without it: + ```js -const result = schnorrkel.verify(sSummed, msg, publicNonce, combinedPublicKey); +import Schnorrkel from "@borislav.itskov/schnorrkel.js"; + +const publicKeys = [signerOne.publicKey, signerTwo.publicKey]; +const sigOutputs = [signature, signatureTwo]; +const sSummed = Schnorrkel.sumSigs( + sigOutputs.map((output) => output.signature) +); + +return _verify( + sSummed.buffer, + msgHash, + sigOutputs[0].publicNonce.buffer, + Schnorrkel.getCombinedPublicKey(publicKeys).buffer +); ``` You can find reference to this in `tests/schnorrkel/onchainMultiSign.test.ts` in this repository. diff --git a/tests/schnorrkel/onchainMultiSign.test.ts b/tests/schnorrkel/onchainMultiSign.test.ts index 5662b51..b5f329f 100644 --- a/tests/schnorrkel/onchainMultiSign.test.ts +++ b/tests/schnorrkel/onchainMultiSign.test.ts @@ -1,22 +1,26 @@ import { describe, expect, it } from "vitest"; -import { ethers } from "ethers"; +import { ContractFactory } from "ethers"; import Schnorrkel from "../../src/index"; import { compile } from "../../utils/compile"; import { pk1, pk2, pk3, wallet2 } from "../config"; -import DefaultSigner from "../../utils/DefaultSigner"; import SchnorrSigner from "../../src/signers/schnorrSigner"; import SchnorrMultisigProvider from "../../src/providers/schnorrMultisigProvider"; +import { + defaultAbiCoder, + hashMessage, + solidityKeccak256, +} from "ethers/lib/utils"; const ERC1271_MAGICVALUE_BYTES32 = "0x1626ba7e"; describe("Multi Sign Tests", function () { - async function deployContract(multisigHelper: SchnorrMultisigProvider) { + async function deployContract(multisigProvider: SchnorrMultisigProvider) { const SchnorrAccountAbstraction = compile("SchnorrAccountAbstraction"); - const factory = new ethers.ContractFactory( + const factory = new ContractFactory( SchnorrAccountAbstraction.abi, SchnorrAccountAbstraction.bytecode, wallet2 ); - const schnorrAddr = multisigHelper.getSchnorrAddress(); + const schnorrAddr = multisigProvider.getSchnorrAddress(); const contract: any = await factory.deploy([schnorrAddr]); const isSigner = await contract.canSign(schnorrAddr); expect(isSigner).to.equal( @@ -29,25 +33,28 @@ describe("Multi Sign Tests", function () { it("should generate a schnorr musig2 and validate it on the blockchain", async function () { const signerOne = new SchnorrSigner(pk1); const signerTwo = new SchnorrSigner(pk2); - const multisigHelper = new SchnorrMultisigProvider([signerOne, signerTwo]); - const { contract } = await deployContract(multisigHelper); + const multisigProvider = new SchnorrMultisigProvider([ + signerOne, + signerTwo, + ]); + const { contract } = await deployContract(multisigProvider); const msg = "just a test message"; - const msgHash = ethers.utils.solidityKeccak256(["string"], [msg]); - const publicNonces = multisigHelper.getPublicNonces(); + const msgHash = solidityKeccak256(["string"], [msg]); + const publicNonces = multisigProvider.getPublicNonces(); const signature = signerOne.sign( msgHash, - multisigHelper.getPublicKeys(), + multisigProvider.getPublicKeys(), publicNonces ); const signatureTwo = signerTwo.sign( msgHash, - multisigHelper.getPublicKeys(), + multisigProvider.getPublicKeys(), publicNonces ); const result = await contract.isValidSignature( msgHash, - multisigHelper.getEcrecoverSignature([signature, signatureTwo]) + multisigProvider.getEcrecoverSignature([signature, signatureTwo]) ); expect(result).to.equal(ERC1271_MAGICVALUE_BYTES32); }); @@ -55,18 +62,21 @@ describe("Multi Sign Tests", function () { it("should generate the same sig to be sure caching does not affect validation", async function () { const signerOne = new SchnorrSigner(pk1); const signerTwo = new SchnorrSigner(pk2); - const multisigHelper = new SchnorrMultisigProvider([signerOne, signerTwo]); - const { contract } = await deployContract(multisigHelper); + const multisigProvider = new SchnorrMultisigProvider([ + signerOne, + signerTwo, + ]); + const { contract } = await deployContract(multisigProvider); const msg = "just a test message"; - const msgHash = ethers.utils.solidityKeccak256(["string"], [msg]); - const publicKeys = multisigHelper.getPublicKeys(); - const publicNonces = multisigHelper.getPublicNonces(); + const msgHash = solidityKeccak256(["string"], [msg]); + const publicKeys = multisigProvider.getPublicKeys(); + const publicNonces = multisigProvider.getPublicNonces(); const signature = signerOne.sign(msgHash, publicKeys, publicNonces); const signatureTwo = signerTwo.sign(msgHash, publicKeys, publicNonces); const result = await contract.isValidSignature( msgHash, - multisigHelper.getEcrecoverSignature([signature, signatureTwo]) + multisigProvider.getEcrecoverSignature([signature, signatureTwo]) ); expect(result).to.equal(ERC1271_MAGICVALUE_BYTES32); }); @@ -74,24 +84,27 @@ describe("Multi Sign Tests", function () { it("should fail if the signer is totally different", async function () { const signerOne = new SchnorrSigner(pk1); const signerTwo = new SchnorrSigner(pk2); - const multisigHelper = new SchnorrMultisigProvider([signerOne, signerTwo]); - const { contract } = await deployContract(multisigHelper); + const multisigProvider = new SchnorrMultisigProvider([ + signerOne, + signerTwo, + ]); + const { contract } = await deployContract(multisigProvider); const signerThree = new SchnorrSigner(pk3); - const multisigHelperAttacker = new SchnorrMultisigProvider([ + const multisigProviderAttacker = new SchnorrMultisigProvider([ signerOne, signerThree, ]); const msg = "just a test message"; - const msgHash = ethers.utils.solidityKeccak256(["string"], [msg]); - const publicKeys = multisigHelperAttacker.getPublicKeys(); - const publicNonces = multisigHelperAttacker.getPublicNonces(); + const msgHash = solidityKeccak256(["string"], [msg]); + const publicKeys = multisigProviderAttacker.getPublicKeys(); + const publicNonces = multisigProviderAttacker.getPublicNonces(); const signature = signerOne.sign(msgHash, publicKeys, publicNonces); const signatureTwo = signerThree.sign(msgHash, publicKeys, publicNonces); const result = await contract.isValidSignature( msgHash, - multisigHelperAttacker.getEcrecoverSignature([signature, signatureTwo]) + multisigProviderAttacker.getEcrecoverSignature([signature, signatureTwo]) ); expect(result).to.equal("0xffffffff"); }); @@ -99,19 +112,24 @@ describe("Multi Sign Tests", function () { it("should fail if only one signature is provided", async function () { const signerOne = new SchnorrSigner(pk1); const signerTwo = new SchnorrSigner(pk2); - const multisigHelper = new SchnorrMultisigProvider([signerOne, signerTwo]); - const { contract } = await deployContract(multisigHelper); + const multisigProvider = new SchnorrMultisigProvider([ + signerOne, + signerTwo, + ]); + const { contract } = await deployContract(multisigProvider); const msg = "just a test message"; - const msgHash = ethers.utils.solidityKeccak256(["string"], [msg]); - const publicKeys = multisigHelper.getPublicKeys(); - const publicNonces = multisigHelper.getPublicNonces(); + const msgHash = solidityKeccak256(["string"], [msg]); + const publicKeys = multisigProvider.getPublicKeys(); + const publicNonces = multisigProvider.getPublicNonces(); const signature = signerOne.sign(msgHash, publicKeys, publicNonces); try { - multisigHelper.getEcrecoverSignature([signature]) + multisigProvider.getEcrecoverSignature([signature]); } catch (e: any) { - expect(e.message).to.equal("Expected at least 2 signatures for aggregation"); + expect(e.message).to.equal( + "Expected at least 2 signatures for aggregation" + ); } const result = await contract.isValidSignature( @@ -124,37 +142,46 @@ describe("Multi Sign Tests", function () { it("should fail if a signer tries to sign twice with the same nonce", async function () { const signerOne = new SchnorrSigner(pk1); const signerTwo = new SchnorrSigner(pk2); - const multisigHelper = new SchnorrMultisigProvider([signerOne, signerTwo]); + const multisigProvider = new SchnorrMultisigProvider([ + signerOne, + signerTwo, + ]); const msg = "just a test message"; - const msgHash = ethers.utils.solidityKeccak256(["string"], [msg]); - const publicKeys = multisigHelper.getPublicKeys(); - expect( - signerOne.sign.bind(signerOne, msgHash, publicKeys, []) - ).to.throw("Nonces should be exchanged before signing"); + const msgHash = solidityKeccak256(["string"], [msg]); + const publicKeys = multisigProvider.getPublicKeys(); + expect(signerOne.sign.bind(signerOne, msgHash, publicKeys, [])).to.throw( + "Nonces should be exchanged before signing" + ); }); it("should fail if only one signer tries to sign the transaction providing 2 different public nonces", async function () { // deploy the contract const signerOne = new SchnorrSigner(pk1); const signerTwo = new SchnorrSigner(pk2); - const multisigHelper = new SchnorrMultisigProvider([signerOne, signerTwo]); - const { contract } = await deployContract(multisigHelper); + const multisigProvider = new SchnorrMultisigProvider([ + signerOne, + signerTwo, + ]); + const { contract } = await deployContract(multisigProvider); const msg = "just a test message"; - const msgHash = ethers.utils.solidityKeccak256(["string"], [msg]); - const publicKeys = multisigHelper.getPublicKeys() + const msgHash = solidityKeccak256(["string"], [msg]); + const publicKeys = multisigProvider.getPublicKeys(); const signature = signerOne.sign( msgHash, publicKeys, - multisigHelper.getPublicNonces() + multisigProvider.getPublicNonces() ); const signatreTwo = signerOne.sign( msgHash, publicKeys, - multisigHelper.getPublicNonces() + multisigProvider.getPublicNonces() + ); + const result = await contract.isValidSignature( + msgHash, + multisigProvider.getEcrecoverSignature([signature, signatreTwo]) ); - const result = await contract.isValidSignature(msgHash, multisigHelper.getEcrecoverSignature([signature, signatreTwo])); expect(result).to.equal("0xffffffff"); }); @@ -162,13 +189,16 @@ describe("Multi Sign Tests", function () { // deploy the contract const signerOne = new SchnorrSigner(pk1); const signerTwo = new SchnorrSigner(pk2); - const multisigHelper = new SchnorrMultisigProvider([signerOne, signerTwo]); - const { contract } = await deployContract(multisigHelper); + const multisigProvider = new SchnorrMultisigProvider([ + signerOne, + signerTwo, + ]); + const { contract } = await deployContract(multisigProvider); const msg = "just a test message"; - const msgHash = ethers.utils.solidityKeccak256(["string"], [msg]); + const msgHash = solidityKeccak256(["string"], [msg]); const publicKeys = [signerTwo.publicKey, signerOne.publicKey]; - const publicNonces = multisigHelper.getPublicNonces() + const publicNonces = multisigProvider.getPublicNonces(); const combinedPublicKey = Schnorrkel.getCombinedPublicKey(publicKeys); const { signature: sigOne, challenge: e } = signerOne.sign( msgHash, @@ -187,8 +217,7 @@ describe("Multi Sign Tests", function () { const parity = combinedPublicKey.buffer[0] - 2 + 27; // wrap the result - const abiCoder = new ethers.utils.AbiCoder(); - const sigData = abiCoder.encode( + const sigData = defaultAbiCoder.encode( ["bytes32", "bytes32", "bytes32", "uint8"], [px, e.buffer, sSummed.buffer, parity] ); @@ -211,7 +240,7 @@ describe("Multi Sign Tests", function () { expect(e.message).to.equal("At least 2 public keys should be provided"); } - const msgHash = ethers.utils.hashMessage("just a test message"); + const msgHash = hashMessage("just a test message"); const publicKeys = [signerOne.publicKey]; const publicNonces = [ signerOne.getPublicNonces(), diff --git a/tests/schnorrkel/onchainSingleSign.test.ts b/tests/schnorrkel/onchainSingleSign.test.ts index e18d67e..819f2f8 100644 --- a/tests/schnorrkel/onchainSingleSign.test.ts +++ b/tests/schnorrkel/onchainSingleSign.test.ts @@ -1,8 +1,9 @@ import { describe, expect, it } from "vitest"; -import { ethers } from "ethers"; +import { ContractFactory } from "ethers"; import { SchnorrSigner } from "../../src"; import { compile } from "../../utils/compile.js"; import { pk1, wallet } from "../config.js"; +import { hashMessage } from "ethers/lib/utils.js"; const ERC1271_MAGICVALUE_BYTES32 = "0x1626ba7e"; @@ -15,7 +16,7 @@ describe("Single Sign Tests", function () { const schnorrAddr = signer.getSchnorrAddress(); // deploying the contract - const factory = new ethers.ContractFactory( + const factory = new ContractFactory( SchnorrAccountAbstraction.abi, SchnorrAccountAbstraction.bytecode, wallet @@ -34,7 +35,7 @@ describe("Single Sign Tests", function () { // sign const msg = "just a test message"; - const msgHash = ethers.utils.hashMessage(msg); + const msgHash = hashMessage(msg); const signer = new SchnorrSigner(pk1); const sig = signer.sign(msgHash); const result = await contract.isValidSignature( diff --git a/utils/DefaultSigner.ts b/utils/DefaultSigner.ts deleted file mode 100644 index c9818dd..0000000 --- a/utils/DefaultSigner.ts +++ /dev/null @@ -1,26 +0,0 @@ -import Schnorrkel, { Key, PublicNonces } from '../src/index' -import { generateRandomKeys } from "../src/core"; - -export default class DefaultSigner { - #schnorrkel = new Schnorrkel(); - #privateKey: Key; - #publicKey: Key; - - constructor() { - const keys = generateRandomKeys() - this.#privateKey = keys.privateKey - this.#publicKey = keys.publicKey - } - - getPublicKey(): Key { - return this.#publicKey; - } - - getPublicNonces(): PublicNonces { - return this.#schnorrkel.generatePublicNonces(); - } - - multiSignMessage(msg: string, publicKeys: Key[], publicNonces: PublicNonces[]) { - return this.#schnorrkel.multiSigSign(this.#privateKey, msg, publicKeys, publicNonces); - } -} \ No newline at end of file From 733e00b31507efb4d2be8f463f8879d94c2c9293 Mon Sep 17 00:00:00 2001 From: Borislav Itskov Date: Wed, 23 Oct 2024 18:43:57 +0300 Subject: [PATCH 7/7] fix: use artifacts v4 for github --- .github/workflows/tests.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index c25945b..fafd6d8 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -36,7 +36,7 @@ jobs: run: npx hardhat node & npm run test:coverage - name: Archive code coverage results - uses: actions/upload-artifact@v2 + uses: actions/upload-artifact@v4 with: name: code-coverage-report path: reports/coverage/lcov.info