Add hash bundle fn & ability to get aggregate bundle to client module

This commit is contained in:
JohnGuilding
2023-04-28 22:26:58 +01:00
parent 29542e4c98
commit cd010324a5
7 changed files with 153 additions and 23 deletions

View File

@@ -30,8 +30,8 @@ export type AddBundleResponse = { hash: string } | {
};
type BundleWithoutSignature = {
publicKey: [BigNumberish, BigNumberish, BigNumberish, BigNumberish];
operation: Omit<Operation, "gas">
senderPublicKeys: [BigNumberish, BigNumberish, BigNumberish, BigNumberish];
operations: Omit<Operation, "gas">;
};
export default class BundleService {
@@ -253,8 +253,8 @@ export default class BundleService {
async #hashSubBundles(bundle: Bundle): Promise<Array<string>> {
const bundlesWithoutSignature: Array<BundleWithoutSignature> =
bundle.operations.map((operation, index) => ({
publicKey: bundle.senderPublicKeys[index],
operation: {
senderPublicKeys: bundle.senderPublicKeys[index],
operations: {
nonce: operation.nonce,
actions: operation.actions,
},
@@ -263,8 +263,8 @@ export default class BundleService {
const serializedBundlesWithoutSignature = bundlesWithoutSignature.map(
bundleWithoutSignature => {
return JSON.stringify({
publicKey: bundleWithoutSignature.publicKey,
operation: bundleWithoutSignature.operation,
senderPublicKeys: bundleWithoutSignature.senderPublicKeys,
operations: bundleWithoutSignature.operations,
});
}
);

View File

@@ -274,16 +274,16 @@ Fixture.test("hashes bundle with single operation", async (fx) => {
const expectedSubBundleHashes = await Promise.all(bundle.operations.map(async (operation, index) => {
const bundlesWithoutSignature = {
publicKey: bundle.senderPublicKeys[index],
operation: {
senderPublicKeys: bundle.senderPublicKeys[index],
operations: {
nonce: operation.nonce,
actions: operation.actions,
},
}
const serializedBundle = JSON.stringify({
publicKey: bundlesWithoutSignature.publicKey,
operation: bundlesWithoutSignature.operation,
senderPublicKeys: bundlesWithoutSignature.senderPublicKeys,
operations: bundlesWithoutSignature.operations,
});
const bundleHash = ethers.utils.keccak256(ethers.utils.toUtf8Bytes(serializedBundle))
@@ -341,16 +341,16 @@ Fixture.test("hashes bundle with multiple operations", async (fx) => {
const expectedSubBundleHashes = await Promise.all(bundle.operations.map(async (operation, index) => {
const bundlesWithoutSignature = {
publicKey: bundle.senderPublicKeys[index],
operation: {
senderPublicKeys: bundle.senderPublicKeys[index],
operations: {
nonce: operation.nonce,
actions: operation.actions,
},
}
const serializedBundle = JSON.stringify({
publicKey: bundlesWithoutSignature.publicKey,
operation: bundlesWithoutSignature.operation,
senderPublicKeys: bundlesWithoutSignature.senderPublicKeys,
operations: bundlesWithoutSignature.operations,
});
const bundleHash = ethers.utils.keccak256(ethers.utils.toUtf8Bytes(serializedBundle))
@@ -377,16 +377,16 @@ Fixture.test("hashes empty bundle", async (fx) => {
const expectedSubBundleHashes = bundle.operations.map(async (operation, index) => {
const bundlesWithoutSignature = {
publicKey: bundle.senderPublicKeys[index],
operation: {
senderPublicKeys: bundle.senderPublicKeys[index],
operations: {
nonce: operation.nonce,
actions: operation.actions,
},
}
const serializedBundle = JSON.stringify({
publicKey: bundlesWithoutSignature.publicKey,
operation: bundlesWithoutSignature.operation,
senderPublicKeys: bundlesWithoutSignature.senderPublicKeys,
operations: bundlesWithoutSignature.operations,
});
const bundleHash = ethers.utils.keccak256(ethers.utils.toUtf8Bytes(serializedBundle))

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}/aggrgateBundle/${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

@@ -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,50 @@
import { BigNumberish, ethers } from "ethers";
import { Bundle, Operation } from "../signer";
type BundleWithoutSignature = {
senderPublicKeys: [BigNumberish, BigNumberish, BigNumberish, BigNumberish];
operations: Omit<Operation, "gas">;
};
export 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 bundlesWithoutSignature: Array<BundleWithoutSignature> =
bundle.operations.map((operation, index) => ({
senderPublicKeys: bundle.senderPublicKeys[index],
operations: {
nonce: operation.nonce,
actions: operation.actions,
},
}));
const serializedBundlesWithoutSignature = bundlesWithoutSignature.map(
(bundleWithoutSignature) => {
return JSON.stringify({
senderPublicKeys: bundleWithoutSignature.senderPublicKeys,
operations: bundleWithoutSignature.operations,
});
},
);
const bundleSubHashes = serializedBundlesWithoutSignature.map(
async (serializedBundleWithoutSignature) => {
const bundleHash = ethers.utils.keccak256(
ethers.utils.toUtf8Bytes(serializedBundleWithoutSignature),
);
const encoding = ethers.utils.defaultAbiCoder.encode(
["bytes32", "uint256"],
[bundleHash, chainId],
);
return ethers.utils.keccak256(encoding);
},
);
const concatenatedHashes = bundleSubHashes.join("");
return ethers.utils.keccak256(ethers.utils.toUtf8Bytes(concatenatedHashes));
}

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); // A keccak256 hash is 32 bytes, or 64 characters, plus the "0x" prefix
});
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

@@ -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