Merge pull request #592 from web3well/192-generate-determinisitc-bundle-hashes

192 generate determinisitc bundle hashes
This commit is contained in:
John Guilding
2023-05-09 17:48:22 +01:00
committed by GitHub
20 changed files with 588 additions and 36 deletions

View File

@@ -21,7 +21,7 @@
"@types/koa__cors": "^3.3.0",
"@types/koa__router": "^8.0.11",
"@types/node-fetch": "^2.6.1",
"bls-wallet-clients": "0.9.0-1620721",
"bls-wallet-clients": "0.9.0-2a20bfe",
"fp-ts": "^2.12.1",
"io-ts": "^2.2.16",
"io-ts-reporters": "^2.0.1",

View File

@@ -887,10 +887,10 @@ bech32@1.1.4:
resolved "https://registry.yarnpkg.com/bech32/-/bech32-1.1.4.tgz#e38c9f37bf179b8eb16ae3a772b40c356d4832e9"
integrity sha512-s0IrSOzLlbvX7yp4WBfPITzpAU8sqQcpsmwXDiKwrG4r491vwCO/XpejasRNl0piBMe/DvP4Tz0mIS/X1DPJBQ==
bls-wallet-clients@0.9.0-1620721:
version "0.9.0-1620721"
resolved "https://registry.yarnpkg.com/bls-wallet-clients/-/bls-wallet-clients-0.9.0-1620721.tgz#572798d10fa6246ab44bbd0e11b97df11abd6d15"
integrity sha512-ekSrK2bCiWoTuhnqdKTp0kXDuIUmE3lc9pqtonOAZC/6ReU2cVbW+F6U1/echtagdWL6GBArJgB2JnVQVznRpg==
bls-wallet-clients@0.9.0-2a20bfe:
version "0.9.0-2a20bfe"
resolved "https://registry.yarnpkg.com/bls-wallet-clients/-/bls-wallet-clients-0.9.0-2a20bfe.tgz#2e39757a18df3ba78d816ae15f6b88000443a2a6"
integrity sha512-w4efcArPBEowrAkIdVYc2mOLlkN8E5O9eIqEhoo6IrRVrN21p/JVNdoot4N3o5MAKFbeaYfid/u9lL6p2DNdiw==
dependencies:
"@thehubbleproject/bls" "^0.5.1"
ethers "^5.7.2"

View File

@@ -54,7 +54,7 @@ export type {
PublicKey,
Signature,
VerificationGateway,
} from "https://esm.sh/bls-wallet-clients@0.9.0-1620721";
} from "https://esm.sh/bls-wallet-clients@0.9.0-2a20bfe";
export {
Aggregator as AggregatorClient,
@@ -70,10 +70,10 @@ export {
getConfig,
MockERC20Factory,
VerificationGatewayFactory,
} from "https://esm.sh/bls-wallet-clients@0.9.0-1620721";
} from "https://esm.sh/bls-wallet-clients@0.9.0-2a20bfe";
// Workaround for esbuild's export-star bug
import blsWalletClients from "https://esm.sh/bls-wallet-clients@0.9.0-1620721";
import blsWalletClients from "https://esm.sh/bls-wallet-clients@0.9.0-2a20bfe";
const { bundleFromDto, bundleToDto, initBlsWalletSigner } = blsWalletClients;
export { bundleFromDto, bundleToDto, initBlsWalletSigner };

View File

@@ -39,5 +39,19 @@ export default function BundleRouter(bundleService: BundleService) {
},
);
router.get(
"aggregateBundle/:subBundleHash",
(ctx) => {
const bundleRows = bundleService.lookupAggregateBundle(ctx.params.subBundleHash!);
if (bundleRows === nil || !bundleRows?.length) {
ctx.response.status = 404;
return;
}
ctx.response.body = bundleRows;
},
);
return router;
}

View File

@@ -6,6 +6,7 @@ import {
delay,
ethers,
Semaphore,
VerificationGatewayFactory,
} from "../../deps.ts";
import { IClock } from "../helpers/Clock.ts";
@@ -18,10 +19,11 @@ import * as env from "../env.ts";
import runQueryGroup from "./runQueryGroup.ts";
import EthereumService from "./EthereumService.ts";
import AppEvent from "./AppEvent.ts";
import BundleTable, { BundleRow, makeHash } from "./BundleTable.ts";
import BundleTable, { BundleRow } from "./BundleTable.ts";
import plus from "./helpers/plus.ts";
import AggregationStrategy from "./AggregationStrategy.ts";
import nil from "../helpers/nil.ts";
import ExplicitAny from "../helpers/ExplicitAny.ts";
export type AddBundleResponse = { hash: string } | {
failures: TransactionFailure[];
@@ -174,7 +176,7 @@ export default class BundleService {
}
return await this.runQueryGroup(async () => {
const hash = makeHash();
const hash = await this.hashBundle(bundle);
this.bundleTable.add({
status: "pending",
@@ -202,15 +204,21 @@ export default class BundleService {
return this.bundleTable.findBundle(hash);
}
lookupAggregateBundle(subBundleHash: string) {
const subBundle = this.bundleTable.findBundle(subBundleHash);
return this.bundleTable.findAggregateBundle(subBundle?.aggregateHash!)
}
receiptFromBundle(bundle: BundleRow) {
if (!bundle.receipt) {
return nil;
}
const { receipt, hash } = bundle;
const { receipt, hash, aggregateHash } = bundle;
return {
bundleHash: hash,
aggregateBundleHash: aggregateHash,
to: receipt.to,
from: receipt.from,
contractAddress: receipt.contractAddress,
@@ -231,6 +239,44 @@ export default class BundleService {
};
}
async hashBundle(bundle: Bundle): Promise<string> {
const operationsWithZeroGas = bundle.operations.map((operation) => {
return {
...operation,
gas: BigNumber.from(0),
};
});
const verifyMethodName = "verify";
const bundleType = VerificationGatewayFactory.abi.find(
(entry) => "name" in entry && entry.name === verifyMethodName,
)?.inputs[0];
const validatedBundle = {
...bundle,
operations: operationsWithZeroGas,
};
const encodedBundleWithZeroSignature = ethers.utils.defaultAbiCoder.encode(
[bundleType as ExplicitAny],
[
{
...validatedBundle,
signature: [BigNumber.from(0), BigNumber.from(0)],
},
],
);
const bundleHash = ethers.utils.keccak256(encodedBundleWithZeroSignature);
const chainId = (await this.ethereumService.provider.getNetwork()).chainId;
const bundleAndChainIdEncoding = ethers.utils.defaultAbiCoder.encode(
["bytes32", "uint256"],
[bundleHash, chainId],
);
return ethers.utils.keccak256(bundleAndChainIdEncoding);
}
async runSubmission() {
this.submissionsInProgress++;
@@ -276,6 +322,13 @@ export default class BundleService {
},
});
if (aggregateBundle) {
const aggregateBundleHash = await this.hashBundle(aggregateBundle);
for (const row of includedRows) {
row.aggregateHash = aggregateBundleHash;
}
}
for (const failedRow of failedRows) {
this.emit({
type: "failed-row",

View File

@@ -30,6 +30,7 @@ type RawRow = {
nextEligibilityDelay: string;
submitError: string | null;
receipt: string | null;
aggregateHash: string | null;
};
const BundleStatuses = ["pending", "confirmed", "failed"] as const;
@@ -44,17 +45,12 @@ type Row = {
nextEligibilityDelay: BigNumber;
submitError?: string;
receipt?: ethers.ContractReceipt;
aggregateHash?: string;
};
type InsertRow = Omit<Row, "id">;
type InsertRawRow = Omit<RawRow, "id">;
export function makeHash() {
const buf = new Uint8Array(32);
crypto.getRandomValues(buf);
return ethers.utils.hexlify(buf);
}
export type BundleRow = Row;
function fromRawRow(rawRow: RawRow | sqlite.Row): Row {
@@ -68,6 +64,7 @@ function fromRawRow(rawRow: RawRow | sqlite.Row): Row {
nextEligibilityDelay: rawRow[5] as string,
submitError: rawRow[6] as string | null,
receipt: rawRow[7] as string | null,
aggregateHash: rawRow[8] as string | null,
};
}
@@ -99,6 +96,7 @@ function fromRawRow(rawRow: RawRow | sqlite.Row): Row {
nextEligibilityDelay: BigNumber.from(rawRow.nextEligibilityDelay),
submitError: rawRow.submitError ?? nil,
receipt,
aggregateHash: rawRow.aggregateHash ?? nil,
};
}
@@ -109,6 +107,7 @@ function toInsertRawRow(row: InsertRow): InsertRawRow {
bundle: JSON.stringify(bundleToDto(row.bundle)),
eligibleAfter: toUint256Hex(row.eligibleAfter),
nextEligibilityDelay: toUint256Hex(row.nextEligibilityDelay),
aggregateHash: row.aggregateHash ?? null,
receipt: JSON.stringify(row.receipt),
};
}
@@ -123,6 +122,7 @@ function toRawRow(row: Row): RawRow {
nextEligibilityDelay: toUint256Hex(row.nextEligibilityDelay),
submitError: row.submitError ?? null,
receipt: JSON.stringify(row.receipt),
aggregateHash: row.aggregateHash ?? null,
};
}
@@ -140,10 +140,11 @@ export default class BundleTable {
eligibleAfter TEXT NOT NULL,
nextEligibilityDelay TEXT NOT NULL,
submitError TEXT,
receipt TEXT
receipt TEXT,
aggregateHash TEXT
)
`);
}
}
dbQuery(sql: string, params?: sqlite.QueryParameterSet) {
this.onQuery(sql, params);
@@ -164,7 +165,8 @@ export default class BundleTable {
eligibleAfter,
nextEligibilityDelay,
submitError,
receipt
receipt,
aggregateHash
) VALUES (
:id,
:status,
@@ -173,7 +175,8 @@ export default class BundleTable {
:eligibleAfter,
:nextEligibilityDelay,
:submitError,
:receipt
:receipt,
:aggregateHash
)
`,
{
@@ -184,6 +187,7 @@ export default class BundleTable {
":nextEligibilityDelay": rawRow.nextEligibilityDelay,
":submitError": rawRow.submitError,
":receipt": rawRow.receipt,
":aggregateHash": rawRow.aggregateHash,
},
);
}
@@ -202,7 +206,8 @@ export default class BundleTable {
eligibleAfter = :eligibleAfter,
nextEligibilityDelay = :nextEligibilityDelay,
submitError = :submitError,
receipt = :receipt
receipt = :receipt,
aggregateHash = :aggregateHash
WHERE
id = :id
`,
@@ -215,6 +220,7 @@ export default class BundleTable {
":nextEligibilityDelay": rawRow.nextEligibilityDelay,
":submitError": rawRow.submitError,
":receipt": rawRow.receipt,
":aggregateHash": rawRow.aggregateHash,
},
);
}
@@ -255,6 +261,21 @@ export default class BundleTable {
return rows.map(fromRawRow)[0];
}
findAggregateBundle(aggregateHash: string): Row[] | nil {
const rows = this.dbQuery(
`
SELECT * from bundles
WHERE
aggregateHash = :aggregateHash AND
status = 'confirmed'
ORDER BY id ASC
`,
{ ":aggregateHash": aggregateHash },
);
return rows.map(fromRawRow);
}
count(): number {
const result = this.dbQuery("SELECT COUNT(*) FROM bundles")[0][0];
assert(typeof result === "number");

View File

@@ -1,5 +1,5 @@
import { assertBundleSucceeds, assertEquals, Operation } from "./deps.ts";
import { BigNumber, Operation, VerificationGatewayFactory, assertBundleSucceeds, assertEquals, ethers } from "./deps.ts";
import ExplicitAny from "../src/helpers/ExplicitAny.ts";
import Fixture from "./helpers/Fixture.ts";
Fixture.test("adds valid bundle", async (fx) => {
@@ -211,6 +211,227 @@ Fixture.test("adds bundle with future nonce", async (fx) => {
assertEquals(bundleService.bundleTable.count(), 1);
});
Fixture.test("Same bundle produces same hash", async (fx) => {
const bundleService = fx.createBundleService();
const [wallet] = await fx.setupWallets(1);
const nonce = await wallet.Nonce();
const firstBundle = wallet.sign({
nonce,
gas: 100000,
actions: [
{
ethValue: 0,
contractAddress: fx.testErc20.address,
encodedFunction: fx.testErc20.interface.encodeFunctionData(
"mint",
[wallet.address, "3"],
),
},
],
});
const secondBundle = wallet.sign({
nonce,
gas: 999999,
actions: [
{
ethValue: 0,
contractAddress: fx.testErc20.address,
encodedFunction: fx.testErc20.interface.encodeFunctionData(
"mint",
[wallet.address, "3"],
),
},
],
});
const firstBundleHash = await bundleService.hashBundle(firstBundle);
const secondBundleHash = await bundleService.hashBundle(secondBundle);
assertEquals(firstBundleHash, secondBundleHash);
});
Fixture.test("hashes bundle with single operation", async (fx) => {
const bundleService = fx.createBundleService();
const [wallet] = await fx.setupWallets(1);
const nonce = await wallet.Nonce();
const bundle = wallet.sign({
nonce,
gas: 100000,
actions: [
{
ethValue: 0,
contractAddress: fx.testErc20.address,
encodedFunction: fx.testErc20.interface.encodeFunctionData(
"mint",
[wallet.address, "3"],
),
},
],
});
const operationsWithZeroGas = bundle.operations.map((operation) => {
return {
...operation,
gas: BigNumber.from(0),
};
});
const bundleType = VerificationGatewayFactory.abi.find(
(entry) => "name" in entry && entry.name === "verify",
)?.inputs[0];
const validatedBundle = {
...bundle,
operations: operationsWithZeroGas,
};
const encodedBundleWithZeroSignature = ethers.utils.defaultAbiCoder.encode(
[bundleType as ExplicitAny],
[
{
...validatedBundle,
signature: [BigNumber.from(0), BigNumber.from(0)],
},
],
);
const bundleHash = ethers.utils.keccak256(encodedBundleWithZeroSignature);
const chainId = (await bundleService.ethereumService.provider.getNetwork()).chainId;
const bundleAndChainIdEncoding = ethers.utils.defaultAbiCoder.encode(
["bytes32", "uint256"],
[bundleHash, chainId],
);
const expectedBundleHash = ethers.utils.keccak256(bundleAndChainIdEncoding);
const hash = await bundleService.hashBundle(bundle);
assertEquals(hash, expectedBundleHash);
});
Fixture.test("hashes bundle with multiple operations", async (fx) => {
const bundleService = fx.createBundleService();
const [wallet] = await fx.setupWallets(1);
const nonce = await wallet.Nonce();
const bundle = fx.blsWalletSigner.aggregate([
wallet.sign({
nonce,
gas: 1_000_000,
actions: [
{
ethValue: 0,
contractAddress: fx.testErc20.address,
encodedFunction: fx.testErc20.interface.encodeFunctionData(
"mint",
[wallet.address, 3],
),
},
],
}),
wallet.sign({
nonce: nonce.add(1),
gas: 1_000_000,
actions: [
{
ethValue: 0,
contractAddress: fx.testErc20.address,
encodedFunction: fx.testErc20.interface.encodeFunctionData(
"mint",
[wallet.address, 5],
),
},
],
}),
]);
const operationsWithZeroGas = bundle.operations.map((operation) => {
return {
...operation,
gas: BigNumber.from(0),
};
});
const bundleType = VerificationGatewayFactory.abi.find(
(entry) => "name" in entry && entry.name === "verify",
)?.inputs[0];
const validatedBundle = {
...bundle,
operations: operationsWithZeroGas,
};
const encodedBundleWithZeroSignature = ethers.utils.defaultAbiCoder.encode(
[bundleType as ExplicitAny],
[
{
...validatedBundle,
signature: [BigNumber.from(0), BigNumber.from(0)],
},
],
);
const bundleHash = ethers.utils.keccak256(encodedBundleWithZeroSignature);
const chainId = (await bundleService.ethereumService.provider.getNetwork()).chainId;
const bundleAndChainIdEncoding = ethers.utils.defaultAbiCoder.encode(
["bytes32", "uint256"],
[bundleHash, chainId],
);
const expectedBundleHash = ethers.utils.keccak256(bundleAndChainIdEncoding);
const hash = await bundleService.hashBundle(bundle);
assertEquals(hash, expectedBundleHash);
});
Fixture.test("hashes empty bundle", async (fx) => {
const bundleService = fx.createBundleService();
const bundle = fx.blsWalletSigner.aggregate([]);
const operationsWithZeroGas = bundle.operations.map((operation) => {
return {
...operation,
gas: BigNumber.from(0),
};
});
const bundleType = VerificationGatewayFactory.abi.find(
(entry) => "name" in entry && entry.name === "verify",
)?.inputs[0];
const validatedBundle = {
...bundle,
operations: operationsWithZeroGas,
};
const encodedBundleWithZeroSignature = ethers.utils.defaultAbiCoder.encode(
[bundleType as ExplicitAny],
[
{
...validatedBundle,
signature: [BigNumber.from(0), BigNumber.from(0)],
},
],
);
const bundleHash = ethers.utils.keccak256(encodedBundleWithZeroSignature);
const chainId = (await bundleService.ethereumService.provider.getNetwork()).chainId;
const bundleAndChainIdEncoding = ethers.utils.defaultAbiCoder.encode(
["bytes32", "uint256"],
[bundleHash, chainId],
);
const expectedBundleHash = ethers.utils.keccak256(bundleAndChainIdEncoding);
const hash = await bundleService.hashBundle(bundle);
assertEquals(hash, expectedBundleHash);
});
// TODO (merge-ok): Add a mechanism for limiting the number of stored
// transactions (and add a new test for it).
// Fixture.test(

View File

@@ -358,3 +358,57 @@ Fixture.test("updates status of failing bundle when its eligibility delay is lar
const failedBundleRow = await bundleService.bundleTable.findBundle(res.hash);
assertEquals(failedBundleRow?.status, "failed");
});
Fixture.test("Retrieves all sub bundles included in a submitted bundle from single a sub bundle", async (fx) => {
const bundleService = fx.createBundleService(
bundleServiceConfig,
aggregationStrategyConfig,
);
const wallets = await fx.setupWallets(3);
const firstWallet = wallets[0];
const nonce = await firstWallet.Nonce();
const bundles = await Promise.all(
wallets.map((wallet) =>
wallet.signWithGasEstimate({
nonce,
actions: [
{
ethValue: 0,
contractAddress: fx.testErc20.address,
encodedFunction: fx.testErc20.interface.encodeFunctionData(
"mint",
[firstWallet.address, 1],
),
},
],
})
),
);
const subBundleHashes = await Promise.all(bundles.map(async (bundle) => {
const res = await bundleService.add(bundle);
if ("failures" in res) {
throw new Error("Bundle failed to be created");
}
return res.hash;
}));
await bundleService.submissionTimer.trigger();
await bundleService.waitForConfirmations();
const firstSubBundle = bundleService.lookupBundle(subBundleHashes[0]);
const secondSubBundle = bundleService.lookupBundle(subBundleHashes[1]);
const thirdSubBundle = bundleService.lookupBundle(subBundleHashes[2]);
const orderedSubBundles = [firstSubBundle, secondSubBundle, thirdSubBundle].sort((a, b) => a!.id - b!.id);
for (const subBundleHash of subBundleHashes) {
const aggregateBundle = bundleService.lookupAggregateBundle(subBundleHash);
assertEquals(aggregateBundle?.[0], orderedSubBundles[0]);
assertEquals(aggregateBundle?.[1], orderedSubBundles[1]);
assertEquals(aggregateBundle?.[2], orderedSubBundles[2]);
}
});

View File

@@ -29,6 +29,7 @@ const sampleRows: BundleRow[] = [
nextEligibilityDelay: BigNumber.from(1),
submitError: nil,
receipt: nil,
aggregateHash: nil,
},
];

View File

@@ -1,6 +1,6 @@
{
"name": "bls-wallet-clients",
"version": "0.9.0-1620721",
"version": "0.9.0-2a20bfe",
"description": "Client libraries for interacting with BLS Wallet components",
"main": "dist/src/index.js",
"types": "dist/src/index.d.ts",

View File

@@ -44,6 +44,7 @@ export type BundleReceiptError = {
*/
export type BlsBundleReceipt = {
bundleHash: string;
aggregateBundleHash: string;
};
/**
@@ -99,7 +100,7 @@ export default class Aggregator {
}
/**
* Estimates the fee required for a bundle by the aggreagtor to submit it.
* Estimates the fee required for a bundle by the aggregator to submit it.
*
* @param bundle Bundle to estimates the fee for
* @returns Estimate of the fee needed to submit the bundle
@@ -125,6 +126,21 @@ export default class Aggregator {
);
}
/**
* Gets the aggregate bundle that a sub bundle was a part of.
* This will return undefined if the bundle does not exist or does not have an aggregate bundle.
*
* @param hash Hash of the bundle to find the aggregate bundle for.
* @returns The aggregate bundle, or undefined if either the sub bundle or aggregate bundle were not found.
*/
async getAggregateBundleFromSubBundle(
subBundleHash: string,
): Promise<Bundle | undefined> {
return this.jsonGet<Bundle>(
`${this.origin}/aggregateBundle/${subBundleHash}`,
);
}
// Note: This should be private instead of exposed. Leaving as is for compatibility.
async jsonPost(path: string, body: unknown): Promise<unknown> {
const resp = await this.fetchImpl(`${this.origin}${path}`, {

View File

@@ -343,7 +343,7 @@ export default class BlsProvider extends ethers.providers.JsonRpcProvider {
return [
...actions,
{
ethValue: fee,
ethValue: fee.toHexString(),
contractAddress: this.aggregatorUtilitiesAddress,
encodedFunction:
aggregatorUtilitiesContract.interface.encodeFunctionData(

View File

@@ -6,7 +6,7 @@ import { BigNumber } from "ethers";
* the chance that bundles get accepted during aggregation.
*
* @param feeEstimate fee required for bundle
* @param safetyDivisor optional safety divisor. Default is 5
* @param safetyDivisor optional safety divisor. Default is 5 (adds a 20% safety margin)
* @returns fee estimate with added safety premium
*/
export default function addSafetyPremiumToFee(

View File

@@ -0,0 +1,58 @@
import { BigNumber, ethers } from "ethers";
import { Bundle } from "../signer";
import { VerificationGatewayFactory } from "../index";
/**
* Generates a deterministic hash of a bundle. Because the signature of the bundle could change, along with the gas property on operations,
* those values are set to 0 before hashing. This leads to a more consistent hash for variations of the same bundle.
*
* @remarks the hash output is senstive to the internal types of the bundle. For example, an identical bundle with a
* BigNumber value for one of the properties, vs the same bundle with a hex string value for one of the properties, will
* generate different hashes, even though the underlying value may be the same.
*
* @param bundle the signed bundle to generate the hash for
* @param chainId the chain id of the network the bundle is being submitted to
* @returns a deterministic hash of the bundle
*/
export default function hashBundle(bundle: Bundle, chainId: number): string {
if (bundle.operations.length !== bundle.senderPublicKeys.length) {
throw new Error(
"number of operations does not match number of public keys",
);
}
const operationsWithZeroGas = bundle.operations.map((operation) => {
return {
...operation,
gas: BigNumber.from(0),
};
});
const verifyMethodName = "verify";
const bundleType = VerificationGatewayFactory.abi.find(
(entry) => "name" in entry && entry.name === verifyMethodName,
)?.inputs[0];
const validatedBundle = {
...bundle,
operations: operationsWithZeroGas,
};
const encodedBundleWithZeroSignature = ethers.utils.defaultAbiCoder.encode(
[bundleType as any],
[
{
...validatedBundle,
signature: [BigNumber.from(0), BigNumber.from(0)],
},
],
);
const bundleHash = ethers.utils.keccak256(encodedBundleWithZeroSignature);
const encoding = ethers.utils.defaultAbiCoder.encode(
["bytes32", "uint256"],
[bundleHash, chainId],
);
return ethers.utils.keccak256(encoding);
}

View File

@@ -18,6 +18,8 @@ export {
OperationResultError,
} from "./OperationResults";
export { default as hashBundle } from "./helpers/hashBundle";
export {
VerificationGateway__factory as VerificationGatewayFactory,
AggregatorUtilities__factory as AggregatorUtilitiesFactory,

View File

@@ -0,0 +1,65 @@
import { expect } from "chai";
import hashBundle from "../src/helpers/hashBundle";
import { Bundle } from "../src";
import { BigNumber } from "ethers";
describe("hashBundle", () => {
it("should return a valid hash when provided with a valid bundle and chainId", () => {
// Arrange
const operation = {
nonce: BigNumber.from(123),
gas: 30_000_000,
actions: [],
};
const bundle: Bundle = {
signature: ["0x1234", "0x1234"],
operations: [operation, operation],
senderPublicKeys: [
["0x4321", "0x4321", "0x4321", "0x4321"],
["0x4321", "0x4321", "0x4321", "0x4321"],
],
};
const chainId = 1;
// Act
const result = hashBundle(bundle, chainId);
// Assert
expect(result).to.be.a("string");
expect(result.length).to.equal(66);
});
it("should throw an error when the number of operations does not match the number of public keys", () => {
// Arrange
const operation = {
nonce: BigNumber.from(123),
gas: 30_000_000,
actions: [],
};
const bundle1: Bundle = {
signature: ["0x1234", "0x1234"],
operations: [operation, operation],
senderPublicKeys: [["0x4321", "0x4321", "0x4321", "0x4321"]],
};
const bundle2: Bundle = {
signature: ["0x1234", "0x1234"],
operations: [operation],
senderPublicKeys: [
["0x4321", "0x4321", "0x4321", "0x4321"],
["0x4321", "0x4321", "0x4321", "0x4321"],
],
};
const chainId = 1;
// Act & Assert
expect(() => hashBundle(bundle1, chainId)).to.throw(
"number of operations does not match number of public keys",
);
expect(() => hashBundle(bundle2, chainId)).to.throw(
"number of operations does not match number of public keys",
);
});
});

View File

@@ -1,6 +1,7 @@
import chai, { expect } from "chai";
import { BigNumber, ethers } from "ethers";
import { formatEther, parseEther } from "ethers/lib/utils";
import sinon from "sinon";
import {
BlsWalletWrapper,
@@ -9,6 +10,7 @@ import {
BlsSigner,
MockERC20Factory,
NetworkConfig,
hashBundle,
} from "../clients/src";
import getNetworkConfig from "../shared/helpers/getNetworkConfig";
@@ -65,6 +67,7 @@ describe("BlsProvider", () => {
afterEach(() => {
chai.spy.restore();
sinon.restore();
});
it("calls a getter method on a contract using call()", async () => {
@@ -704,4 +707,49 @@ describe("BlsProvider", () => {
);
expect(feeData.gasPrice).to.deep.equal(expectedFeeData.gasPrice);
});
it("should return a deterministic hash generated by the aggregator that can be replicated by the client module", async () => {
// Arrange
const transaction = {
to: ethers.Wallet.createRandom().address,
value: parseEther("1"),
from: await blsSigner.getAddress(),
};
const action = {
ethValue: transaction.value?.toHexString(),
contractAddress: transaction.to!.toString(),
encodedFunction: "0x",
};
// BlsWalletWrapper.getRandomBlsPrivateKey from "estimateGas" method results in slightly different
// fee estimates. This fake avoids this mismatch by stubbing a constant value.
sinon.replace(
BlsWalletWrapper,
"getRandomBlsPrivateKey",
sinon.fake.resolves(blsSigner.wallet.blsWalletSigner.privateKey),
);
const feeEstimate = await blsProvider.estimateGas(transaction);
const actionsWithSafeFee = blsProvider._addFeePaymentActionWithSafeFee(
[action],
feeEstimate,
);
const bundle = blsSigner.wallet.sign({
nonce: await blsSigner.wallet.Nonce(),
gas: 100000,
actions: [...actionsWithSafeFee],
});
// Act
const transactionResponse = await blsSigner.sendTransaction(transaction);
// Assert
const expectedTransactionHash = hashBundle(
bundle,
blsProvider.network.chainId,
);
expect(transactionResponse.hash).to.deep.equal(expectedTransactionHash);
});
});

View File

@@ -112,9 +112,8 @@ yarn run dev:chrome # or dev:firefox, dev:opera
- In general, the bundle or submission issues we've encountered have been us misconfiguring the data in the bundle or not configuring the aggregator properly.
- Be careful using Hardhat accounts 0 and 1 in your code when running a local aggregator. This is because the local aggregator config uses the same key pairs as Hardhat accounts 0 and 1 by default. You can get around this by not using accounts 0 and 1 elsewhere, or changing the default accounts that the aggregator uses locally.
- When packages are updated in the aggregator, you'll need to reload the deno cache as the setup script won't do this for you. You can do this with `deno cache -r deps.ts` in the `./aggregator` directory.
- If running Quill against a local node, and if you're using MetaMask to fund Quill, make sure the MetaMask
localhost network uses chainId `1337`.
- Sometimes there are issues related to the deno cache. You can clear it with `deno cache -r deps.ts test/deps.ts` in the `./aggregator` directory.
- If running Quill against a local node, and if you're using MetaMask to fund Quill, make sure the MetaMask localhost network uses chainId `1337`.
### Tests

View File

@@ -37,7 +37,7 @@
"assert-browserify": "^2.0.0",
"async-mutex": "^0.3.2",
"axios": "^0.27.2",
"bls-wallet-clients": "0.9.0-1620721",
"bls-wallet-clients": "0.9.0-2a20bfe",
"browser-passworder": "^2.0.3",
"bs58check": "^2.1.2",
"crypto-browserify": "^3.12.0",

View File

@@ -2898,10 +2898,10 @@ blakejs@^1.1.0:
resolved "https://registry.yarnpkg.com/blakejs/-/blakejs-1.2.1.tgz#5057e4206eadb4a97f7c0b6e197a505042fc3814"
integrity sha512-QXUSXI3QVc/gJME0dBpXrag1kbzOqCjCX8/b54ntNyW6sjtoqxqRk3LTmXzaJoh71zMsDCjM+47jS7XiwN/+fQ==
bls-wallet-clients@0.9.0-1620721:
version "0.9.0-1620721"
resolved "https://registry.yarnpkg.com/bls-wallet-clients/-/bls-wallet-clients-0.9.0-1620721.tgz#572798d10fa6246ab44bbd0e11b97df11abd6d15"
integrity sha512-ekSrK2bCiWoTuhnqdKTp0kXDuIUmE3lc9pqtonOAZC/6ReU2cVbW+F6U1/echtagdWL6GBArJgB2JnVQVznRpg==
bls-wallet-clients@0.9.0-2a20bfe:
version "0.9.0-2a20bfe"
resolved "https://registry.yarnpkg.com/bls-wallet-clients/-/bls-wallet-clients-0.9.0-2a20bfe.tgz#2e39757a18df3ba78d816ae15f6b88000443a2a6"
integrity sha512-w4efcArPBEowrAkIdVYc2mOLlkN8E5O9eIqEhoo6IrRVrN21p/JVNdoot4N3o5MAKFbeaYfid/u9lL6p2DNdiw==
dependencies:
"@thehubbleproject/bls" "^0.5.1"
ethers "^5.7.2"