From d6289f210cef8156620a16a4b257ae6a5103bb5b Mon Sep 17 00:00:00 2001 From: Borislav Itskov Date: Mon, 18 Sep 2023 18:15:48 +0300 Subject: [PATCH] use the same elliptic curve library for signature and verification --- README.md | 38 ++++++++++++++-------- package-lock.json | 19 ----------- package.json | 1 - src/core/index.ts | 24 +++++--------- tests/schnorrkel/onchainSingleSign.test.ts | 6 ++-- 5 files changed, 37 insertions(+), 51 deletions(-) diff --git a/README.md b/README.md index 182bef1..9e18aad 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@ Blockchain validation via ecrecover is also supported. # Typescript support Since version 2.0.0, we're moving entirely to Typescript. -## Breaking changes +## 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` @@ -19,6 +19,13 @@ Since version 2.0.0, we're moving entirely to Typescript. * `getCombinedPublicKey` * `getCombinedAddress` +## Version 3.0 Breaking changes +* `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 @@ -32,7 +39,7 @@ cd schnorrkel.js npm i ``` -### Testing +## Testing ``` npm run test ``` @@ -44,18 +51,20 @@ We refer to Single Signatures as ones that have a single signer. Sign: ```js -import Schnorrkel from 'schnorrkel' +import Schnorrkel from '@borislav.itskov/schnorrkel.js' -const privateKey = randomBytes(32) // Buffer +const privateKey = new Key(Buffer.from(ethers.utils.randomBytes(32))) const msg = 'test message' -const {signature, finalPublicNonce} = Schnorrkel.sign(privateKey, msg) +const hash = ethers.utils.hashMessage(msg) +const {signature, finalPublicNonce, challenge} = Schnorrkel.sign(privateKey, hash) ``` Offchain verification: +We take the `signature`, `hash` and `finalPublicNonce` from the example above and do: ```js -const publicKey: Buffer = ... (derived from the privateKey) -// signature and finalPublicNonce come from s -const result = Schnorrkel.verify(signature, msg, finalPublicNonce, publicKey) +const publicKey = Buffer.from(secp256k1.publicKeyCreate(privateKey.buffer)) +// signature and finalPublicNonce come from Schnorrkel.sign +const result = Schnorrkel.verify(signature, hash, finalPublicNonce, publicKey) ``` Onchain verification: @@ -91,14 +100,18 @@ Afterwards, here is part of the code: import { ethers } from 'ethers' import secp256k1 from 'secp256k1' -const address = 'input schnorr generated address here' +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([address]) +const contract: any = await factory.deploy([schnorrAddr]) -const privateKey: Buffer = '...' const pkBuffer = new Key(Buffer.from(ethers.utils.arrayify(privateKey))) const msg = 'just a test message'; -const sig = schnorrkel.sign(msg, privateKey); +const msgHash = ethers.utils.hashMessage(msg) +const sig = Schnorrkel.sign(pkBuffer, msgHash) // wrap the result const publicKey = secp256k1.publicKeyCreate(ethers.utils.arrayify(privateKey)) @@ -111,7 +124,6 @@ const sigData = abiCoder.encode([ "bytes32", "bytes32", "bytes32", "uint8" ], [ sig.signature.buffer, parity ]); -const msgHash = ethers.utils.solidityKeccak256(['string'], [msg]); const result = await contract.isValidSignature(msgHash, sigData); ``` diff --git a/package-lock.json b/package-lock.json index 680f613..1647fdc 100644 --- a/package-lock.json +++ b/package-lock.json @@ -16,7 +16,6 @@ }, "devDependencies": { "@types/ecurve": "^1.0.0", - "@types/elliptic": "^6.4.14", "@types/secp256k1": "^4.0.3", "@vitest/coverage-c8": "^0.29.2", "hardhat": "^2.14.0", @@ -2135,15 +2134,6 @@ "@types/node": "*" } }, - "node_modules/@types/elliptic": { - "version": "6.4.14", - "resolved": "https://registry.npmjs.org/@types/elliptic/-/elliptic-6.4.14.tgz", - "integrity": "sha512-z4OBcDAU0GVwDTuwJzQCiL6188QvZMkvoERgcVjq0/mPM8jCfdwZ3x5zQEVoL9WCAru3aG5wl3Z5Ww5wBWn7ZQ==", - "dev": true, - "dependencies": { - "@types/bn.js": "*" - } - }, "node_modules/@types/istanbul-lib-coverage": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.4.tgz", @@ -7519,15 +7509,6 @@ "@types/node": "*" } }, - "@types/elliptic": { - "version": "6.4.14", - "resolved": "https://registry.npmjs.org/@types/elliptic/-/elliptic-6.4.14.tgz", - "integrity": "sha512-z4OBcDAU0GVwDTuwJzQCiL6188QvZMkvoERgcVjq0/mPM8jCfdwZ3x5zQEVoL9WCAru3aG5wl3Z5Ww5wBWn7ZQ==", - "dev": true, - "requires": { - "@types/bn.js": "*" - } - }, "@types/istanbul-lib-coverage": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.4.tgz", diff --git a/package.json b/package.json index b17f68e..e46577c 100644 --- a/package.json +++ b/package.json @@ -30,7 +30,6 @@ "devDependencies": { "solc": "0.8.19", "@types/ecurve": "^1.0.0", - "@types/elliptic": "^6.4.14", "@types/secp256k1": "^4.0.3", "@vitest/coverage-c8": "^0.29.2", "typescript": "^4.9.5", diff --git a/src/core/index.ts b/src/core/index.ts index 4346349..0a0eae4 100644 --- a/src/core/index.ts +++ b/src/core/index.ts @@ -1,18 +1,12 @@ import { ethers } from 'ethers' import secp256k1 from 'secp256k1' -import ecurve from 'ecurve' -import elliptic from 'elliptic' +import ecurve, { Point } from 'ecurve' import bigi from 'bigi' -import { BN } from 'bn.js' - import { InternalNoncePairs, InternalNonces, InternalPublicNonces, InternalSignature } from './types' import { KeyPair } from '../types' const curve = ecurve.getCurveByName('secp256k1') -const n = curve?.n -const EC = elliptic.ec -const ec = new EC('secp256k1') -const generatorPoint = ec.g +const n = curve.n const _generateNonce = (): InternalNoncePairs => { const k = Buffer.from(ethers.utils.randomBytes(32)) @@ -193,13 +187,13 @@ export const _sumSigs = (signatures: Buffer[]): Buffer => { export const _verify = (s: Buffer, hash: string, R: Buffer, publicKey: Buffer): boolean => { const eC = challenge(R, hash, publicKey) - const sG = generatorPoint.mul(ethers.utils.arrayify(s)) - const P = ec.keyFromPublic(publicKey).getPublic() - const bnEC = new BN(Buffer.from(eC).toString('hex'), 'hex') - const Pe = P.mul(bnEC) - const toPublicR = ec.keyFromPublic(R).getPublic() - const RplusPe = toPublicR.add(Pe) - return sG.eq(RplusPe) + + const sG = curve.G.multiply(bigi.fromBuffer(s)) + const PasPoint = Point.decodeFrom(curve, publicKey) + const Pe = PasPoint.multiply(bigi.fromBuffer(eC)) + const RasPoint = Point.decodeFrom(curve, R) + const RplusPetest = RasPoint.add(Pe) + return sG.equals(RplusPetest) } export const _generatePk = (combinedPublicKey: Buffer): string => { diff --git a/tests/schnorrkel/onchainSingleSign.test.ts b/tests/schnorrkel/onchainSingleSign.test.ts index e69791d..fa50808 100644 --- a/tests/schnorrkel/onchainSingleSign.test.ts +++ b/tests/schnorrkel/onchainSingleSign.test.ts @@ -31,9 +31,9 @@ describe('Single Sign Tests', function () { // sign const msg = 'just a test message' - const msgHash = ethers.utils.solidityKeccak256(['string'], [msg]) - const pkBuffer = new Key(Buffer.from(ethers.utils.arrayify(pk1))) - const sig = Schnorrkel.sign(pkBuffer, msgHash) + 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))