diff --git a/signer/package.json b/signer/package.json index fc43333d..a22fa330 100644 --- a/signer/package.json +++ b/signer/package.json @@ -1,6 +1,6 @@ { "name": "bls-wallet-signer", - "version": "0.6.1", + "version": "0.7.0", "description": "Client-side tool for signing bls transaction data", "main": "dist/src/index.js", "types": "dist/src/index.d.ts", diff --git a/signer/src/aggregate.ts b/signer/src/aggregate.ts index b5f45e77..15857097 100644 --- a/signer/src/aggregate.ts +++ b/signer/src/aggregate.ts @@ -1,21 +1,13 @@ import * as hubbleBls from "../deps/hubble-bls"; -import { AggregateTransactionData, TransactionData } from "./types"; +import { Transaction } from "./types"; -export default (txs: TransactionData[]): AggregateTransactionData => { +export default (txs: Transaction[]): Transaction => { const sigsG1 = txs.map(tx => hubbleBls.mcl.loadG1(tx.signature)); const aggSigG1 = hubbleBls.signer.aggregate(sigsG1); - const aggregateSignature = hubbleBls.mcl.dumpG1(aggSigG1); - return { - transactions: txs.map(tx => ({ - publicKey: tx.publicKey, - nonce: tx.nonce, - ethValue: tx.ethValue, - contractAddress: tx.contractAddress, - encodedFunction: tx.encodedFunction, - })), - signature: aggregateSignature, + subTransactions: txs.map(txSet => txSet.subTransactions).flat(), + signature: hubbleBls.mcl.dumpG1(aggSigG1), }; } diff --git a/signer/src/encodeMessageForSigning.ts b/signer/src/encodeMessageForSigning.ts index cfbcd90a..b05c9755 100644 --- a/signer/src/encodeMessageForSigning.ts +++ b/signer/src/encodeMessageForSigning.ts @@ -1,20 +1,20 @@ import { keccak256 } from "@ethersproject/keccak256"; import { pack as solidityPack } from "@ethersproject/solidity"; -import { RawTransactionData } from "./types"; +import { TransactionTemplate } from "./types"; export default ( chainId: number, ) => ( - rawTxData: RawTransactionData, + txTemplate: TransactionTemplate, ): string => { return solidityPack( ["uint256", "uint256", "uint256", "address", "bytes32"], [ chainId, - rawTxData.nonce, - rawTxData.ethValue, - rawTxData.contractAddress, - keccak256(rawTxData.encodedFunction), + txTemplate.nonce, + txTemplate.ethValue, + txTemplate.contractAddress, + keccak256(txTemplate.encodedFunction), ] ); } diff --git a/signer/src/index.ts b/signer/src/index.ts index 2a01b353..b475794a 100644 --- a/signer/src/index.ts +++ b/signer/src/index.ts @@ -7,7 +7,6 @@ import getPublicKeyHash from "./getPublicKeyHash"; import AsyncReturnType from "./helpers/AsyncReturnType"; import sign from "./sign"; import verify from "./verify"; -import verifyAggregate from "./verifyAggregate"; export * from "./types"; @@ -34,6 +33,5 @@ export async function initBlsWalletSigner({ getPublicKeyHash: getPublicKeyHash(signerFactory, domain), sign: sign(signerFactory, domain, chainId), verify: verify(domain, chainId), - verifyAggregate: verifyAggregate(domain, chainId), }; } diff --git a/signer/src/sign.ts b/signer/src/sign.ts index 2f62d931..0d42012f 100644 --- a/signer/src/sign.ts +++ b/signer/src/sign.ts @@ -2,24 +2,28 @@ import * as hubbleBls from "../deps/hubble-bls"; import encodeMessageForSigning from "./encodeMessageForSigning"; import getPublicKey from "./getPublicKey"; -import { RawTransactionData, TransactionData } from "./types"; +import { Transaction, TransactionTemplate } from "./types"; export default ( signerFactory: hubbleBls.signer.BlsSignerFactory, domain: Uint8Array, chainId: number, ) => ( - rawTransactionData: RawTransactionData, + txTemplate: TransactionTemplate, privateKey: string, -): TransactionData => { - const message = encodeMessageForSigning(chainId)(rawTransactionData); +): Transaction => { + const message = encodeMessageForSigning(chainId)(txTemplate); const signer = signerFactory.getSigner(domain, privateKey); const signature = hubbleBls.mcl.dumpG1(signer.sign(message)); return { - ...rawTransactionData, - publicKey: getPublicKey(signerFactory, domain)(privateKey), + subTransactions: [ + { + ...txTemplate, + publicKey: getPublicKey(signerFactory, domain)(privateKey), + } + ], signature, } }; diff --git a/signer/src/types.ts b/signer/src/types.ts index 1dbd6574..7596eed6 100644 --- a/signer/src/types.ts +++ b/signer/src/types.ts @@ -1,24 +1,15 @@ import { BigNumber } from "@ethersproject/bignumber"; -export type RawTransactionData = { +export type TransactionTemplate = { nonce: BigNumber; ethValue: BigNumber; contractAddress: string; encodedFunction: string; }; -export type TransactionData = RawTransactionData & { - publicKey: string; - signature: string; -}; +export type SubTransaction = TransactionTemplate & { publicKey: string }; -export type AggregateTransactionData = { - transactions: { - publicKey: string; - nonce: BigNumber; - ethValue: BigNumber; - contractAddress: string; - encodedFunction: string; - }[], - signature: string, +export type Transaction = { + subTransactions: SubTransaction[]; + signature: string; }; diff --git a/signer/src/verify.ts b/signer/src/verify.ts index 896c378c..8bcbf414 100644 --- a/signer/src/verify.ts +++ b/signer/src/verify.ts @@ -1,19 +1,21 @@ import * as hubbleBls from "../deps/hubble-bls"; import encodeMessageForSigning from "./encodeMessageForSigning"; -import { TransactionData } from "./types"; +import { Transaction } from "./types"; export default ( domain: Uint8Array, chainId: number, -) => ( - txData: TransactionData, -): boolean => { +) => (tx: Transaction): boolean => { const verifier = new hubbleBls.signer.BlsVerifier(domain); - return verifier.verify( - hubbleBls.mcl.loadG1(txData.signature), - hubbleBls.mcl.loadG2(txData.publicKey), - encodeMessageForSigning(chainId)(txData), + return verifier.verifyMultiple( + hubbleBls.mcl.loadG1(tx.signature), + tx.subTransactions.map( + subTx => hubbleBls.mcl.loadG2(subTx.publicKey), + ), + tx.subTransactions.map( + subTx => encodeMessageForSigning(chainId)(subTx), + ), ); }; diff --git a/signer/src/verifyAggregate.ts b/signer/src/verifyAggregate.ts deleted file mode 100644 index af58dfc7..00000000 --- a/signer/src/verifyAggregate.ts +++ /dev/null @@ -1,23 +0,0 @@ -import * as hubbleBls from "../deps/hubble-bls"; - -import encodeMessageForSigning from "./encodeMessageForSigning"; -import { AggregateTransactionData } from "./types"; - -export default ( - domain: Uint8Array, - chainId: number, -) => ( - aggregateTxData: AggregateTransactionData, -): boolean => { - const verifier = new hubbleBls.signer.BlsVerifier(domain); - - return verifier.verifyMultiple( - hubbleBls.mcl.loadG1(aggregateTxData.signature), - aggregateTxData.transactions.map( - tx => hubbleBls.mcl.loadG2(tx.publicKey), - ), - aggregateTxData.transactions.map( - tx => encodeMessageForSigning(chainId)(tx), - ), - ); -}; diff --git a/signer/test/helpers/Range.ts b/signer/test/helpers/Range.ts new file mode 100644 index 00000000..34a41808 --- /dev/null +++ b/signer/test/helpers/Range.ts @@ -0,0 +1,9 @@ +export default function Range(limit: number): number[] { + const result: number[] = []; + + for (let i = 0; i < limit; i += 1) { + result.push(i); + } + + return result; +} diff --git a/signer/test/index.test.ts b/signer/test/index.test.ts index c6f2457c..2d64f777 100644 --- a/signer/test/index.test.ts +++ b/signer/test/index.test.ts @@ -5,7 +5,8 @@ import { arrayify } from "@ethersproject/bytes"; import { keccak256 } from "@ethersproject/keccak256"; import { expect } from "chai"; -import { initBlsWalletSigner, RawTransactionData } from "../src"; +import { initBlsWalletSigner, Transaction, TransactionTemplate } from "../src"; +import Range from "./helpers/Range"; const domain = arrayify(keccak256("0xfeedbee5")); const weiPerToken = BigNumber.from(10).pow(18); @@ -14,7 +15,7 @@ const samples = (() => { const dummy256HexString = "0x" + "0123456789".repeat(10).slice(0, 64); const contractAddress = dummy256HexString; - const rawTx: RawTransactionData = { + const txTemplate: TransactionTemplate = { nonce: BigNumber.from(123), ethValue: BigNumber.from(0), contractAddress, @@ -26,7 +27,7 @@ const samples = (() => { return { contractAddress, - rawTx, + txTemplate, privateKey, otherPrivateKey, }; @@ -39,9 +40,9 @@ describe("index", () => { domain, }); - const { rawTx, privateKey, otherPrivateKey } = samples; + const { txTemplate, privateKey, otherPrivateKey } = samples; - const tx = sign(rawTx, privateKey); + const tx = sign(txTemplate, privateKey); expect(tx.signature).to.equal([ "0x177500780b42f245e98229245126c9042e1cdaadc7ada72021ddd43492963a7b26f7a", @@ -51,18 +52,22 @@ describe("index", () => { expect(verify(tx)).to.equal(true); const txBadSig = { - ...sign(rawTx, otherPrivateKey), - publicKey: tx.publicKey, // Pretend this is the public key + ...tx, + signature: sign(txTemplate, otherPrivateKey).signature, }; expect(verify(txBadSig)).to.equal(false); - const txBadMessage = { - ...tx, - - // Pretend the client signed to pay a million tokens - ethValue: weiPerToken.mul(1000000), - } + const txBadMessage: Transaction = { + subTransactions: [ + { + ...tx.subTransactions[0], + // Pretend the client signed to pay a million tokens + ethValue: weiPerToken.mul(1000000), + }, + ], + signature: tx.signature, + }; expect(verify(txBadMessage)).to.equal(false); }); @@ -71,34 +76,59 @@ describe("index", () => { const { sign, aggregate, - verifyAggregate, + verify, } = await initBlsWalletSigner({ chainId: 123, domain }); - const { rawTx, privateKey } = samples; + const { txTemplate, privateKey } = samples; - const tx = sign(rawTx, privateKey); - const aggregateTx = aggregate([tx, tx]); + const tx1 = sign(txTemplate, privateKey); + const tx2 = aggregate([tx1, tx1]); - expect(aggregateTx.signature).to.equal([ + expect(tx2.signature).to.equal([ "0x2cc0b05e8200cf564042735d15e2cc98181e730203530300022aafdd1ceb905830430", "28617145dca56a00bf0693710e24683616ff4a42bc3cca7d587b36ff91f", ].join("")); - expect(verifyAggregate(aggregateTx)).to.equal(true); + expect(verify(tx2)).to.equal(true); - const aggregateTxBadMessage = { - ...aggregateTx, - transactions: [ - aggregateTx.transactions[0], + const tx2BadMessage: Transaction = { + ...tx2, + subTransactions: [ + tx2.subTransactions[0], { - ...aggregateTx.transactions[1], + ...tx2.subTransactions[1], // Pretend this client signed to pay a million tokens ethValue: weiPerToken.mul(1000000), - } + }, ], } - expect(verifyAggregate(aggregateTxBadMessage)).to.equal(false); + expect(verify(tx2BadMessage)).to.equal(false); + }); + + it("can aggregate transactions which already have multiple subTransactions", async () => { + const { + sign, + aggregate, + verify, + } = await initBlsWalletSigner({ chainId: 123, domain }); + + const { txTemplate, privateKey } = samples; + + const txs = Range(4).map(i => sign( + { + ...txTemplate, + ethValue: BigNumber.from(i), + }, + privateKey, + )); + + const aggTx1 = aggregate(txs.slice(0, 2)); + const aggTx2 = aggregate(txs.slice(2, 4)); + + let aggAggTx = aggregate([aggTx1, aggTx2]); + + expect(verify(aggAggTx)).to.equal(true); }); });