11 Commits

Author SHA1 Message Date
Andrew Morris
8ac01049d7 txDataStatsByMethodId 2023-06-02 16:00:32 +10:00
Andrew Morris
8dce0947b0 biggest methods 2023-06-02 16:00:02 +10:00
Andrew Morris
f87cc7e6cb ERC20TransferEncoder 2023-06-02 16:00:02 +10:00
Andrew Morris
77fb5f1263 FallbackEncoder 2023-06-02 16:00:01 +10:00
Andrew Morris
7aa4ffe10b compressionRatio 2023-06-02 16:00:01 +10:00
Andrew Morris
e409094050 Check encode and decode, measure baseline length 2023-06-02 16:00:01 +10:00
Andrew Morris
5a32f14ff6 Include naive fallback encoding 2023-06-02 16:00:01 +10:00
Andrew Morris
0f5a0656e2 Port RegIndex 2023-06-02 16:00:01 +10:00
Andrew Morris
04359f7518 encode and decode for MultiEncoder 2023-06-02 16:00:01 +10:00
Andrew Morris
9b4d1c5401 Port PseudoFloat, VLQ 2023-06-02 16:00:01 +10:00
Andrew Morris
1ce28b9c2c start analysis 2023-06-02 16:00:00 +10:00
56 changed files with 65028 additions and 496 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-405e23a",
"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

@@ -569,6 +569,16 @@
version "5.7.0"
resolved "https://registry.yarnpkg.com/@ethersproject/transactions/-/transactions-5.7.0.tgz#91318fc24063e057885a6af13fdb703e1f993d3b"
integrity sha512-kmcNicCp1lp8qanMTC3RIikGgoJ80ztTyvtsFvCYpSCfkjhD0jZ2LOrnbcuxuToLIUYYf+4XwD1rP+B/erDIhQ==
dependencies:
"@ethersproject/address" "^5.7.0"
"@ethersproject/bignumber" "^5.7.0"
"@ethersproject/bytes" "^5.7.0"
"@ethersproject/constants" "^5.7.0"
"@ethersproject/keccak256" "^5.7.0"
"@ethersproject/logger" "^5.7.0"
"@ethersproject/properties" "^5.7.0"
"@ethersproject/rlp" "^5.7.0"
"@ethersproject/signing-key" "^5.7.0"
"@ethersproject/units@5.6.0":
version "5.6.0"
@@ -877,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-405e23a:
version "0.9.0-405e23a"
resolved "https://registry.npmjs.org/bls-wallet-clients/-/bls-wallet-clients-0.9.0-405e23a.tgz#b66121f9ec0cb4e821965606ada203e6601b773d"
integrity sha512-cMm6pq35VU30veCAHt6ArSavlqzXu+olQg+dzUH28fvqSeQsfWz2qiuBekGxSWOCfn8gX1j/8jHEhrGxXS509Q==
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

@@ -40,7 +40,3 @@ PRIORITY_FEE_PER_GAS=0
PREVIOUS_BASE_FEE_PERCENT_INCREASE=2
BUNDLE_CHECKING_CONCURRENCY=8
IS_OPTIMISM=false
OPTIMISM_GAS_PRICE_ORACLE_ADDRESS=0x420000000000000000000000000000000000000F
OPTIMISM_L1_BASE_FEE_PERCENT_INCREASE=2

View File

@@ -38,5 +38,3 @@ PRIORITY_FEE_PER_GAS=500000000
PREVIOUS_BASE_FEE_PERCENT_INCREASE=13
BUNDLE_CHECKING_CONCURRENCY=8
IS_OPTIMISM=false

View File

@@ -35,5 +35,3 @@ PRIORITY_FEE_PER_GAS=500000000
PREVIOUS_BASE_FEE_PERCENT_INCREASE=13
BUNDLE_CHECKING_CONCURRENCY=8
IS_OPTIMISM=false

View File

@@ -89,35 +89,32 @@ commands.
#### Environment Variables
| Name | Example Value | Description |
| ------------------------------------- | ------------------------------------------------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| RPC_URL | https://localhost:8545 | The RPC endpoint for an EVM node that the BLS Wallet contracts are deployed on |
| RPC_POLLING_INTERVAL | 4000 | How long to wait between retries, when needed (used by ethers when waiting for blocks) |
| USE_TEST_NET | false | Whether to set all transaction's `gasPrice` to 0. Workaround for some networks |
| ORIGIN | http://localhost:3000 | The origin for the aggregator client. Used only in manual tests |
| PORT | 3000 | The port to bind the aggregator to |
| NETWORK_CONFIG_PATH | ../contracts/networks/local.json | Path to the network config file, which contains information on deployed BLS Wallet contracts |
| PRIVATE_KEY_AGG | 0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80 | Private key for the EOA account used to submit bundles on chain. Transactions are paid by the account linked to PRIVATE_KEY_AGG. By default, bundles must pay for themselves by sending funds to tx.origin or the aggregators onchain address |
| PRIVATE_KEY_ADMIN | 0x59c6995e998f97a5a0044966f0945389dc9e86dae88c7a8412f4603b6b78690d | Private key for the admin EOA account. Used only in tests |
| TEST_BLS_WALLETS_SECRET | test-bls-wallets-secret | Secret used to seed BLS Wallet private keys during tests |
| DB_PATH | aggregator.sqlite | File path of the sqlite db |
| BUNDLE_QUERY_LIMIT | 100 | Maximum number of bundles returned from sqlite |
| MAX_GAS_PER_BUNDLE | 2000000 | Limits the amount of user operations which can be bundled together by using this value as the approximate limit on the amount of gas in an aggregate bundle |
| MAX_AGGREGATION_DELAY_MILLIS | 5000 | Maximum amount of time in milliseconds aggregator will wait before submitting bundles on chain. A higher number will allow more time for bundles to fill, but may result in longer periods before submission. A lower number allows more frequent L2 submissions, but may result in smaller bundles |
| MAX_UNCONFIRMED_AGGREGATIONS | 3 | Maximum unconfirmed bundle aggregations that will be submitted on chain |
| LOG_QUERIES | false | Whether to print sqlite queries in event log. When running tests, `TEST_LOGGING` must also be enabled |
| TEST_LOGGING | false | Whether to print aggregator server events to stdout during tests. Useful for debugging & logging |
| REQUIRE_FEES | true | Whether to require that user bundles pay the aggregator a sufficient fee |
| BREAKEVEN_OPERATION_COUNT | 4.5 | The aggregator must pay an overhead to submit a bundle regardless of how many operations it contains. This parameter determines how much each operation must contribute to this overhead |
| ALLOW_LOSSES | true | Even if each user bundle pays the required fee, the aggregate bundle may not be profitable if it is too small. Setting this to true makes the aggregator submit these bundles anyway |
| FEE_TYPE | ether OR token:0xabcd...1234 | The fee type the aggregator will accept. Either `ether` for ETH/chains native currency or `token:0xabcd...1234` (token contract address) for an ERC20 token |
| AUTO_CREATE_INTERNAL_BLS_WALLET | false | An internal BLS wallet is used to calculate bundle overheads. Setting this to true allows creating this wallet on startup, but might be undesirable in production (see `programs/createInternalBlsWallet.ts` for manual creation) |
| PRIORITY_FEE_PER_GAS | 0 | The priority fee used when submitting bundles (and passed on as a requirement for user bundles) |
| PREVIOUS_BASE_FEE_PERCENT_INCREASE | 2 | Used to determine the max basefee attached to aggregator transaction (and passed on as a requirement for user bundles)s |
| BUNDLE_CHECKING_CONCURRENCY | 8 | The maximum number of bundles that are checked concurrently (getting gas usage, detecting fees, etc) |
| IS_OPTIMISM | false | Optimism's strategy for charging for L1 fees requires special logic in the aggregator. In addition to gasEstimate * gasPrice, we need to replicate Optimism's calculation and pass it on to the user |
| OPTIMISM_GAS_PRICE_ORACLE_ADDRESS | 0x420000000000000000000000000000000000000F | Address for the Optimism gas price oracle contract. Required when IS_OPTIMISM is true |
| OPTIMISM_L1_BASE_FEE_PERCENT_INCREASE | 2 | Similar to PREVIOUS_BASE_FEE_PERCENT_INCREASE, but for the L1 basefee for the optimism-specific calculation. This gets passed on to users. Required when IS_OPTIMISM is true |
| Name | Example Value | Description |
| ---------------------------------- | ------------------------------------------------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| RPC_URL | https://localhost:8545 | The RPC endpoint for an EVM node that the BLS Wallet contracts are deployed on |
| RPC_POLLING_INTERVAL | 4000 | How long to wait between retries, when needed (used by ethers when waiting for blocks) |
| USE_TEST_NET | false | Whether to set all transaction's `gasPrice` to 0. Workaround for some networks |
| ORIGIN | http://localhost:3000 | The origin for the aggregator client. Used only in manual tests |
| PORT | 3000 | The port to bind the aggregator to |
| NETWORK_CONFIG_PATH | ../contracts/networks/local.json | Path to the network config file, which contains information on deployed BLS Wallet contracts |
| PRIVATE_KEY_AGG | 0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80 | Private key for the EOA account used to submit bundles on chain. Transactions are paid by the account linked to PRIVATE_KEY_AGG. By default, bundles must pay for themselves by sending funds to tx.origin or the aggregators onchain address |
| PRIVATE_KEY_ADMIN | 0x59c6995e998f97a5a0044966f0945389dc9e86dae88c7a8412f4603b6b78690d | Private key for the admin EOA account. Used only in tests |
| TEST_BLS_WALLETS_SECRET | test-bls-wallets-secret | Secret used to seed BLS Wallet private keys during tests |
| DB_PATH | aggregator.sqlite | File path of the sqlite db |
| BUNDLE_QUERY_LIMIT | 100 | Maximum number of bundles returned from sqlite |
| MAX_GAS_PER_BUNDLE | 2000000 | Limits the amount of user operations which can be bundled together by using this value as the approximate limit on the amount of gas in an aggregate bundle |
| MAX_AGGREGATION_DELAY_MILLIS | 5000 | Maximum amount of time in milliseconds aggregator will wait before submitting bundles on chain. A higher number will allow more time for bundles to fill, but may result in longer periods before submission. A lower number allows more frequent L2 submissions, but may result in smaller bundles |
| MAX_UNCONFIRMED_AGGREGATIONS | 3 | Maximum unconfirmed bundle aggregations that will be submitted on chain |
| LOG_QUERIES | false | Whether to print sqlite queries in event log. When running tests, `TEST_LOGGING` must also be enabled |
| TEST_LOGGING | false | Whether to print aggregator server events to stdout during tests. Useful for debugging & logging |
| REQUIRE_FEES | true | Whether to require that user bundles pay the aggregator a sufficient fee |
| BREAKEVEN_OPERATION_COUNT | 4.5 | The aggregator must pay an overhead to submit a bundle regardless of how many operations it contains. This parameter determines how much each operation must contribute to this overhead |
| ALLOW_LOSSES | true | Even if each user bundle pays the required fee, the aggregate bundle may not be profitable if it is too small. Setting this to true makes the aggregator submit these bundles anyway |
| FEE_TYPE | ether OR token:0xabcd...1234 | The fee type the aggregator will accept. Either `ether` for ETH/chains native currency or `token:0xabcd...1234` (token contract address) for an ERC20 token |
| AUTO_CREATE_INTERNAL_BLS_WALLET | false | An internal BLS wallet is used to calculate bundle overheads. Setting this to true allows creating this wallet on startup, but might be undesirable in production (see `programs/createInternalBlsWallet.ts` for manual creation) |
| PRIORITY_FEE_PER_GAS | 0 | The priority fee used when submitting bundles (and passed on as a requirement for user bundles) |
| PREVIOUS_BASE_FEE_PERCENT_INCREASE | 2 | Used to determine the max basefee attached to aggregator transaction (and passed on as a requirement for user bundles)s |
| BUNDLE_CHECKING_CONCURRENCY | 8 | The maximum number of bundles that are checked concurrently (getting gas usage, detecting fees, etc) |
## Running

File diff suppressed because one or more lines are too long

View File

@@ -54,7 +54,7 @@ export type {
PublicKey,
Signature,
VerificationGateway,
} from "https://esm.sh/bls-wallet-clients@0.9.0-405e23a";
} from "https://esm.sh/bls-wallet-clients@0.9.0-2a20bfe";
export {
Aggregator as AggregatorClient,
@@ -70,14 +70,13 @@ export {
getConfig,
MockERC20Factory,
VerificationGatewayFactory,
} from "https://esm.sh/bls-wallet-clients@0.9.0-405e23a";
} 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-405e23a";
import blsWalletClients from "https://esm.sh/bls-wallet-clients@0.9.0-2a20bfe";
const { bundleFromDto, bundleToDto, initBlsWalletSigner } = blsWalletClients;
export { bundleFromDto, bundleToDto, initBlsWalletSigner };
export * as sqlite from "https://deno.land/x/sqlite@v3.7.0/mod.ts";
export { Semaphore } from "https://deno.land/x/semaphore@v1.1.2/mod.ts";
export { pick } from "npm:@s-libs/micro-dash@15.2.0";
export { mapValues, once } from "npm:@s-libs/micro-dash@15.2.0";

View File

@@ -0,0 +1,49 @@
import assert from "../../src/helpers/assert.ts";
export default class ByteStream {
pos = 2;
constructor(public data: string) {
assert(/^0x[0-9a-fA-F]*/.test(data));
assert(data.length % 2 === 0);
}
getN(len: number): string {
const newPos = this.pos + 2 * len;
assert(newPos <= this.data.length);
const res = `0x${this.data.slice(this.pos, newPos)}`;
this.pos = newPos;
return res;
}
peekN(len: number) {
const res = this.getN(len);
this.pos -= 2 * len;
return res;
}
get(): number {
return parseInt(this.getN(1).slice(2), 16);
}
peek(): number {
const res = this.get();
this.pos -= 2;
return res;
}
getTail(): string {
const res = `0x${this.data.slice(this.pos)}`;
this.pos = this.data.length;
return res;
}
bytesRemaining() {
return (this.data.length - this.pos) / 2;
}
}

View File

@@ -0,0 +1,125 @@
import { mapValues, once } from "../../deps.ts";
import blocks from "../../data/blocksSample.json" assert { type: "json" };
import { sum } from "./util.ts";
import MultiEncoder from "./MultiEncoder.ts";
import assert from "../../src/helpers/assert.ts";
import VLQ from "./VLQ.ts";
export default class Calculator {
constructor(
public multiEncoder: MultiEncoder,
) {}
transactions = once(() => blocks.map((b) => b.transactions).flat());
transactionData = once(() => this.transactions().map((tx) => tx.input));
encodedTransactionData = once(() =>
this.transactionData().map((data) => this.multiEncoder.encode(data))
);
decodedTransactionData = once(() =>
this.encodedTransactionData().map(
(input) => this.multiEncoder.decode(input),
)
);
checkDecodedTransactionData = once(() => {
const transactionData = this.transactions().map((tx) => tx.input);
const decodedTransactionData = this.decodedTransactionData();
const len = transactionData.length;
assert(decodedTransactionData.length === len);
for (let i = 0; i < len; i++) {
assert(
transactionData[i] === decodedTransactionData[i],
`tx ${i}: ${transactionData[i]} !== ${decodedTransactionData[i]}`,
);
}
});
txDataByMethodId = once(() => {
const txDataByMethodId: Record<string, string[]> = {};
for (const data of this.transactionData()) {
txDataByMethodId[data.slice(0, 10)] ??= [];
txDataByMethodId[data.slice(0, 10)].push("0x" + data.slice(10));
}
return txDataByMethodId;
});
txDataStatsByMethodId = once(() =>
mapValues(
this.txDataByMethodId(),
(dataArray, methodId) => {
const count = dataArray.length;
const baselineLen = dataArray
.map((data) => 1 + (methodId.length / 2 - 1) + (data.length / 2 - 1))
.reduce(sum);
const avgBaselineLen = baselineLen / count;
const encodedLen = dataArray
.map((data) =>
this.multiEncoder.encode(methodId + data.slice(2)).length / 2 - 1
)
.reduce(sum);
const avgEncodedLen = encodedLen / count;
return {
count,
baselineLen,
avgBaselineLen,
encodedLen,
avgEncodedLen,
};
},
)
);
popularMethods = once(() => {
return Object.entries(this.txDataStatsByMethodId()).sort((a, b) =>
b[1].count - a[1].count
);
});
biggestMethods = once(() => {
return Object.entries(this.txDataStatsByMethodId()).sort((a, b) =>
b[1].baselineLen -
a[1].baselineLen
);
});
biggestEncodedMethods = once(() => {
return Object.entries(this.txDataStatsByMethodId()).sort((a, b) =>
b[1].encodedLen -
a[1].encodedLen
);
});
totalLength = once(() =>
this.transactions().map((t) => t.input.length / 2 - 1).reduce(sum)
);
baselineEncodedLength = once(() => {
let len = this.totalLength();
for (const txData of this.transactionData()) {
len += VLQ.encode(txData.length / 2 - 1).length / 2 - 1;
}
return len;
});
totalEncodedLength = once(() =>
this.encodedTransactionData().map((data) => data.length / 2 - 1).reduce(sum)
);
compressionRatio = once(() =>
this.totalEncodedLength() / this.baselineEncodedLength()
);
}

View File

@@ -0,0 +1,61 @@
import assert from "../../src/helpers/assert.ts";
import nil from "../../src/helpers/nil.ts";
import ByteStream from "./ByteStream.ts";
import VLQ from "./VLQ.ts";
import { hexJoin } from "./util.ts";
export type Encoder = {
encode(data: string): string | nil;
decode(encodedData: string): string;
};
export default class MultiEncoder {
encoders: {
id: number;
encoder: Encoder;
}[] = [];
encodersById: Record<number, Encoder> = {};
register(id: number, encoder: Encoder) {
assert(id !== 0);
this.encoders.push({ id, encoder });
this.encodersById[id] = encoder;
}
encode(data: string): string {
for (const { id, encoder } of this.encoders) {
const encoded = encoder.encode(data);
if (encoded === nil) {
continue;
}
return hexJoin([
VLQ.encode(id),
encoded,
]);
}
return hexJoin([
VLQ.encode(0),
VLQ.encode(data.length / 2 - 1),
data,
]);
}
decode(data: string): string {
const stream = new ByteStream(data);
const id = VLQ.decode(stream);
if (id.eq(0)) {
const len = VLQ.decode(stream);
return stream.getN(len.toNumber());
}
const encoder = this.encodersById[id.toNumber()];
assert(encoder !== nil);
return encoder.decode(stream.getTail());
}
}

View File

@@ -0,0 +1,50 @@
import { BigNumber, BigNumberish } from "../../deps.ts";
import ByteStream from "./ByteStream.ts";
import VLQ from "./VLQ.ts";
import { hexJoin } from "./util.ts";
// deno-lint-ignore no-namespace
namespace PseudoFloat {
export function encode(x: BigNumberish) {
x = BigNumber.from(x);
if (x.eq(0)) {
return "0x00";
}
let exponent = 0;
while (x.mod(10).eq(0) && exponent < 30) {
x = x.div(10);
exponent++;
}
const exponentBits = (exponent + 1).toString(2).padStart(5, "0");
const lowest3Bits = x.mod(8).toNumber().toString(2).padStart(3, "0");
const firstByte = parseInt(`${exponentBits}${lowest3Bits}`, 2)
.toString(16)
.padStart(2, "0");
return hexJoin([`0x${firstByte}`, VLQ.encode(x.div(8))]);
}
export function decode(stream: ByteStream) {
const firstByte = stream.get();
if (firstByte == 0) {
return BigNumber.from(0);
}
const exponent = ((firstByte & 0xf8) >> 3) - 1;
let mantissa = VLQ.decode(stream);
mantissa = mantissa.shl(3);
mantissa = mantissa.add(firstByte & 0x07);
return mantissa.mul(BigNumber.from(10).pow(exponent));
}
}
export default PseudoFloat;

View File

@@ -0,0 +1,28 @@
import { BigNumber, BigNumberish } from "../../deps.ts";
import ByteStream from "./ByteStream.ts";
import VLQ from "./VLQ.ts";
import { hexJoin } from "./util.ts";
// deno-lint-ignore no-namespace
namespace RegIndex {
export function encode(x: BigNumberish) {
x = BigNumber.from(x);
const vlqValue = x.div(0x10000);
const remainder = x.mod(0x10000);
return hexJoin([
VLQ.encode(vlqValue),
remainder.toNumber().toString(16).padStart(4, "0"),
]);
}
export function decode(stream: ByteStream) {
const vlqValue = VLQ.decode(stream);
const remainder = parseInt(stream.getTail().slice(2), 16);
return vlqValue.mul(0x10000).add(remainder);
}
}
export default RegIndex;

View File

@@ -0,0 +1,57 @@
import { BigNumber, BigNumberish } from "../../deps.ts";
import ByteStream from "./ByteStream.ts";
// deno-lint-ignore no-namespace
namespace VLQ {
export function encode(x: BigNumberish) {
x = BigNumber.from(x);
const segments: number[] = [];
while (true) {
const segment = x.mod(128);
segments.unshift(segment.toNumber());
x = x.sub(segment);
x = x.div(128);
if (x.eq(0)) {
break;
}
}
let result = "0x";
for (let i = 0; i < segments.length; i++) {
const keepGoing = i !== segments.length - 1;
const byte = (keepGoing ? 128 : 0) + segments[i];
result += byte.toString(16).padStart(2, "0");
}
return result;
}
export function decode(stream: ByteStream) {
let value = BigNumber.from(0);
while (true) {
const currentByte = stream.get();
// Add the lowest 7 bits to the value
value = value.add(currentByte & 0x7f);
// If the highest bit is zero, stop
if ((currentByte & 0x80) === 0) {
break;
}
// We're continuing. Shift the value 7 bits to the left (higher) to
// make room.
value = value.shl(7);
}
return value;
}
}
export default VLQ;

View File

@@ -0,0 +1,30 @@
import nil from "../../../src/helpers/nil.ts";
import ByteStream from "../ByteStream.ts";
import { Encoder } from "../MultiEncoder.ts";
import PseudoFloat from "../PseudoFloat.ts";
import { bigNumberToWord, hexJoin } from "../util.ts";
export default class ERC20TransferEncoder implements Encoder {
encode(data: string): string | nil {
const stream = new ByteStream(data);
if (stream.bytesRemaining() !== 68 || stream.getN(4) !== "0xa9059cbb") {
return nil;
}
return hexJoin([
"0x" + stream.getN(32).slice(26),
PseudoFloat.encode(stream.getN(32)),
]);
}
decode(encodedData: string): string {
const stream = new ByteStream(encodedData);
return hexJoin([
"0xa9059cbb",
"0x000000000000000000000000" + stream.getN(20).slice(2),
bigNumberToWord(PseudoFloat.decode(stream)),
]);
}
}

View File

@@ -0,0 +1,104 @@
import assert from "../../../src/helpers/assert.ts";
import nil from "../../../src/helpers/nil.ts";
import ByteStream from "../ByteStream.ts";
import { Encoder } from "../MultiEncoder.ts";
import PseudoFloat from "../PseudoFloat.ts";
import VLQ from "../VLQ.ts";
import { bigNumberToWord, getDataWords, hexJoin } from "../util.ts";
export default class FallbackEncoder implements Encoder {
encode(data: string): string {
const len = data.length / 2 - 1;
if ((data.length / 2 - 1) % 32 !== 4) {
return hexJoin([
VLQ.encode(2 * len),
data,
]);
}
const res: string[] = [];
const words = getDataWords(`0x${data.slice(10)}`);
res.push(VLQ.encode(2 * words.length + 1));
res.push(data.slice(0, 10));
for (const word of words) {
let encoding = hexJoin(["0x00", word]);
const altEncodings = [
hexJoin(["0x01", VLQ.encode(word)]),
hexJoin(["0x02", PseudoFloat.encode(word)]),
word.startsWith("0x000000000000000000000000")
? hexJoin(["0x03", `0x${word.slice(26)}`])
: nil,
];
for (const altEncoding of altEncodings) {
if (altEncoding === nil) {
continue;
}
if (altEncoding.length < encoding.length) {
encoding = altEncoding;
}
}
res.push(encoding);
}
return hexJoin(res);
}
decode(encodedData: string): string {
const stream = new ByteStream(encodedData);
const leadingVlq = VLQ.decode(stream);
if (leadingVlq.mod(2).eq(0)) {
const len = leadingVlq.div(2);
return stream.getN(len.toNumber());
}
const wordLen = leadingVlq.div(2).toNumber();
const methodId = stream.getN(4);
const words: string[] = [];
for (let i = 0; i < wordLen; i++) {
const typeId = stream.get();
switch (typeId) {
case 0: {
words.push(stream.getN(32));
break;
}
case 1: {
words.push(bigNumberToWord(VLQ.decode(stream)));
break;
}
case 2: {
words.push(bigNumberToWord(PseudoFloat.decode(stream)));
break;
}
case 3: {
words.push(`0x000000000000000000000000${stream.getN(20).slice(2)}`);
break;
}
default:
assert(false, `Unrecognized typeId ${typeId}`);
}
}
return hexJoin([
methodId,
...words,
]);
}
}

View File

@@ -0,0 +1,18 @@
import Calculator from "./Calculator.ts";
import MultiEncoder from "./MultiEncoder.ts";
import ERC20TransferEncoder from "./encoders/ERC20TransferEncoder.ts";
import FallbackEncoder from "./encoders/FallbackEncoder.ts";
const multiEncoder = new MultiEncoder();
multiEncoder.register(2, new ERC20TransferEncoder());
multiEncoder.register(1, new FallbackEncoder());
const calc = new Calculator(multiEncoder);
// calc.checkDecodedTransactionData();
console.log(
"biggestEncodedMethods",
calc.biggestEncodedMethods().slice(0, 10),
);

View File

@@ -0,0 +1,31 @@
import { BigNumber } from "../../deps.ts";
export function sum(a: number, b: number) {
return a + b;
}
export function getDataWords(data: string) {
const res = [];
for (let i = 2; i < data.length; i += 64) {
res.push("0x" + data.slice(i, i + 64));
}
return res;
}
export function hexJoin(hexStrings: string[]) {
return "0x" + hexStrings.map(remove0x).join("");
}
export function remove0x(hexString: string) {
if (!hexString.startsWith("0x")) {
throw new Error("Expected 0x prefix");
}
return hexString.slice(2);
}
export function bigNumberToWord(x: BigNumber) {
return "0x" + x.toHexString().slice(2).padStart(64, "0");
}

View File

@@ -1,17 +0,0 @@
#!/usr/bin/env -S deno run --allow-net --allow-env --allow-read
import * as env from "../src/env.ts";
import { ethers } from "../deps.ts";
import OptimismGasPriceOracle from "../src/app/OptimismGasPriceOracle.ts";
const oracle = new OptimismGasPriceOracle(
new ethers.providers.JsonRpcProvider(env.RPC_URL),
);
const { l1BaseFee, overhead, scalar, decimals } = await oracle.getAllParams();
console.log({
l1BaseFee: `${(l1BaseFee.toNumber() / 1e9).toFixed(3)} gwei`,
overhead: `${overhead.toNumber()} L1 gas`,
scalar: scalar.toNumber() / (10 ** decimals.toNumber()),
});

View File

@@ -1,17 +0,0 @@
#!/usr/bin/env -S deno run --allow-net --allow-env --allow-read
import { ethers } from "../deps.ts";
import * as env from "../src/env.ts";
import getOptimismL1Fee from "../src/helpers/getOptimismL1Fee.ts";
const provider = new ethers.providers.JsonRpcProvider(env.RPC_URL);
const txHash = Deno.args[0];
if (!txHash.startsWith("0x")) {
throw new Error("First arg should be tx hash");
}
const l1Fee = await getOptimismL1Fee(provider, txHash);
console.log(`${ethers.utils.formatEther(l1Fee)} ETH`);

View File

@@ -1,15 +0,0 @@
#!/usr/bin/env -S deno run --allow-net --allow-env --allow-read
import { ethers } from "../deps.ts";
import * as env from "../src/env.ts";
import getRawTransaction from "../src/helpers/getRawTransaction.ts";
const provider = new ethers.providers.JsonRpcProvider(env.RPC_URL);
const txHash = Deno.args[0];
if (!txHash.startsWith("0x")) {
throw new Error("First arg should be tx hash");
}
console.log(await getRawTransaction(provider, txHash));

View File

@@ -60,7 +60,7 @@ const bundles: Bundle[] = [];
for (const [i, wallet] of wallets.entries()) {
const nonce = await wallet.Nonce();
console.log("Funding wallet", i, "(1 wei to make estimateFee work)");
console.log("Funding wallet", i);
await (await adminWallet.sendTransaction({
to: wallet.address,
@@ -92,7 +92,7 @@ for (const [i, wallet] of wallets.entries()) {
// Ensure wallet can pay the fee
if (balance.lt(fee)) {
console.log("Funding wallet", i, "(based on estimateFee)");
console.log("Funding wallet");
await (await adminWallet.sendTransaction({
to: wallet.address,

View File

@@ -544,10 +544,9 @@ export default class AggregationStrategy {
bundleOverheadGas ??=
(await this.measureBundleOverhead()).bundleOverheadGas;
const gasEstimate = await this.ethereumService
.estimateEffectiveCompressedGas(
bundle,
);
const gasEstimate = await this.ethereumService.estimateCompressedGas(
bundle,
);
const marginalGasEstimate = gasEstimate.sub(bundleOverheadGas);
@@ -631,18 +630,12 @@ export default class AggregationStrategy {
expectedFee: fee,
requiredFee: feeInfo.requiredFee,
expectedMaxCost: feeInfo.expectedMaxCost,
errorReason: {
message: [
"Insufficient fee",
`(provided: ${ethers.utils.formatEther(fee)},`,
`required: ${ethers.utils.formatEther(feeInfo.requiredFee)})`,
].join(" "),
},
errorReason: { message: "Insufficient fee" },
};
}
const gasEstimate = feeInfo?.gasEstimate ??
await this.ethereumService.estimateEffectiveCompressedGas(bundle);
await this.ethereumService.estimateCompressedGas(bundle);
return {
success,
@@ -680,8 +673,8 @@ export default class AggregationStrategy {
});
const [oneOpGasEstimate, twoOpGasEstimate] = await Promise.all([
es.estimateEffectiveCompressedGas(bundle1),
es.estimateEffectiveCompressedGas(
es.estimateCompressedGas(bundle1),
es.estimateCompressedGas(
this.blsWalletSigner.aggregate([bundle1, bundle2]),
),
]);

View File

@@ -1,7 +1,6 @@
import { HTTPMethods } from "../../deps.ts";
type AppEvent =
| { type: "starting" }
| { type: "listening"; data: { port: number } }
| { type: "db-query"; data: { sql: string; params: unknown } }
| { type: "waiting-unconfirmed-space" }

View File

@@ -23,7 +23,6 @@ import BundleTable, { BundleRow } from "./BundleTable.ts";
import plus from "./helpers/plus.ts";
import AggregationStrategy from "./AggregationStrategy.ts";
import nil from "../helpers/nil.ts";
import getOptimismL1Fee from "../helpers/getOptimismL1Fee.ts";
import ExplicitAny from "../helpers/ExplicitAny.ts";
export type AddBundleResponse = { hash: string } | {
@@ -37,7 +36,6 @@ export default class BundleService {
maxAggregationDelayMillis: env.MAX_AGGREGATION_DELAY_MILLIS,
maxUnconfirmedAggregations: env.MAX_UNCONFIRMED_AGGREGATIONS,
maxEligibilityDelay: env.MAX_ELIGIBILITY_DELAY,
isOptimism: env.IS_OPTIMISM,
};
unconfirmedBundles = new Set<Bundle>();
@@ -167,8 +165,7 @@ export default class BundleService {
if (!signedCorrectly) {
failures.push({
type: "invalid-signature",
description:
`invalid bundle signature for signature ${bundle.signature}`,
description: `invalid bundle signature for signature ${bundle.signature}`,
});
}
@@ -209,7 +206,7 @@ export default class BundleService {
lookupAggregateBundle(subBundleHash: string) {
const subBundle = this.bundleTable.findBundle(subBundleHash);
return this.bundleTable.findAggregateBundle(subBundle?.aggregateHash!);
return this.bundleTable.findAggregateBundle(subBundle?.aggregateHash!)
}
receiptFromBundle(bundle: BundleRow) {
@@ -249,17 +246,17 @@ export default class BundleService {
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],
[
@@ -269,7 +266,7 @@ export default class BundleService {
},
],
);
const bundleHash = ethers.utils.keccak256(encodedBundleWithZeroSignature);
const chainId = (await this.ethereumService.provider.getNetwork()).chainId;
@@ -420,16 +417,7 @@ export default class BundleService {
const profit = balanceAfter.sub(balanceBefore);
/** What we paid to process the bundle */
let cost = receipt.gasUsed.mul(receipt.effectiveGasPrice);
if (this.config.isOptimism) {
cost = cost.add(
await getOptimismL1Fee(
this.ethereumService.provider,
receipt.transactionHash,
),
);
}
const cost = receipt.gasUsed.mul(receipt.effectiveGasPrice);
/** Fees collected from users */
const actualFee = profit.add(cost);

View File

@@ -26,8 +26,6 @@ import toPublicKeyShort from "./helpers/toPublicKeyShort.ts";
import AsyncReturnType from "../helpers/AsyncReturnType.ts";
import ExplicitAny from "../helpers/ExplicitAny.ts";
import nil from "../helpers/nil.ts";
import hexToUint8Array from "../helpers/hexToUint8Array.ts";
import OptimismGasPriceOracle from "./OptimismGasPriceOracle.ts";
export type TxCheckResult = {
failures: TransactionFailure[];
@@ -78,7 +76,6 @@ export default class EthereumService {
public emit: (evt: AppEvent) => void,
public wallet: Wallet,
public provider: ethers.providers.Provider,
public chainId: number,
public blsWalletWrapper: BlsWalletWrapper,
public blsWalletSigner: BlsWalletSigner,
public verificationGateway: VerificationGateway,
@@ -172,7 +169,6 @@ export default class EthereumService {
emit,
wallet,
provider,
chainId,
blsWalletWrapper,
blsWalletSigner,
verificationGateway,
@@ -345,10 +341,10 @@ export default class EthereumService {
};
const attempt = async () => {
let response: ethers.providers.TransactionResponse;
let txResponse: ethers.providers.TransactionResponse;
try {
response = await this.wallet.sendTransaction(txRequest);
txResponse = await this.wallet.sendTransaction(txRequest);
} catch (error) {
if (/\binvalid transaction nonce\b/.test(error.message)) {
// This can occur when the nonce is in the future, which can
@@ -364,10 +360,7 @@ export default class EthereumService {
}
try {
return {
type: "complete" as const,
value: await response.wait(),
};
return { type: "receipt" as const, value: await txResponse.wait() };
} catch (error) {
return { type: "waitError" as const, value: error };
}
@@ -383,7 +376,7 @@ export default class EthereumService {
const attemptResult = await attempt();
if (attemptResult.type === "complete") {
if (attemptResult.type === "receipt") {
return attemptResult.value;
}
@@ -412,44 +405,17 @@ export default class EthereumService {
throw new Error("Expected return or throw from attempt loop");
}
/**
* Estimates the amount of effective gas needed to process the bundle using
* compression.
*
* Here 'effective' gas means the number you need to multiply by gasPrice in
* order to get the right fee. There are a few cases here:
*
* 1. L1 chains (used in testing, eg gethDev)
* - Effective gas is equal to regular gas
* 2. Arbitrum
* - The Arbitrum node already responds with effective gas when calling
* estimateGas
* 3. Optimism
* - We estimate Optimism's calculation for the amount of L1 gas it will
* charge for, and then convert that into an equivalend amount of L2 gas.
*/
async estimateEffectiveCompressedGas(bundle: Bundle): Promise<BigNumber> {
async estimateCompressedGas(bundle: Bundle): Promise<BigNumber> {
const compressedBundle = await this.bundleCompressor.compress(bundle);
let gasEstimate = await this.wallet.estimateGas({
return await this.wallet.estimateGas({
to: this.expanderEntryPoint.address,
data: compressedBundle,
});
if (env.IS_OPTIMISM) {
const extraGasEstimate = await this.estimateOptimismL2GasNeededForL1Gas(
compressedBundle,
gasEstimate,
);
gasEstimate = gasEstimate.add(extraGasEstimate);
}
return gasEstimate;
}
async GasConfig(block?: ethers.providers.Block) {
block ??= await this.provider.getBlock("latest");
async GasConfig() {
const block = await this.provider.getBlock("latest");
const previousBaseFee = block.baseFeePerGas;
assert(previousBaseFee !== null && previousBaseFee !== nil);
@@ -476,66 +442,6 @@ export default class EthereumService {
};
}
/**
* Estimates the L1 gas that Optimism will charge us for and expresses it as
* an amount of equivalent L2 gas.
*
* This is very similar to what Arbitrum does, but in Arbitrum it's built-in,
* and you actually sign for that additional L2 gas. On Optimism, you only
* sign for the actual L2 gas, and optimism just adds the L1 fee.
*
* For our purposes, this works as a way to normalize the behavior between
* the different chains.
*/
async estimateOptimismL2GasNeededForL1Gas(
compressedBundle: string,
gasLimit: BigNumber,
): Promise<BigNumber> {
const block = await this.provider.getBlock("latest");
const gasConfig = await this.GasConfig(block);
const txBytes = await this.wallet.signTransaction({
type: 2,
chainId: this.chainId,
nonce: this.nextNonce,
to: this.expanderEntryPoint.address,
data: compressedBundle,
...gasConfig,
gasLimit,
});
let l1Gas = 0;
for (const byte of hexToUint8Array(txBytes)) {
if (byte === 0) {
l1Gas += 4;
} else {
l1Gas += 16;
}
}
const gasOracle = new OptimismGasPriceOracle(this.provider);
const { l1BaseFee, overhead, scalar, decimals } = await gasOracle
.getAllParams();
const scalarNum = scalar.toNumber() / (10 ** decimals.toNumber());
l1Gas += overhead.toNumber();
assert(block.baseFeePerGas !== null && block.baseFeePerGas !== nil);
assert(env.OPTIMISM_L1_BASE_FEE_PERCENT_INCREASE !== nil);
const adjustedL1BaseFee = l1BaseFee.toNumber() * scalarNum *
(1 + env.OPTIMISM_L1_BASE_FEE_PERCENT_INCREASE / 100);
const feeRatio = adjustedL1BaseFee / block.baseFeePerGas.toNumber();
return BigNumber.from(
Math.ceil(feeRatio * l1Gas),
);
}
private static Wallet(
provider: ethers.providers.Provider,
privateKey: string,

View File

@@ -1,52 +0,0 @@
import { BigNumber, ethers } from "../../deps.ts";
import assert from "../helpers/assert.ts";
import { OPTIMISM_GAS_PRICE_ORACLE_ADDRESS } from "../env.ts";
export default class OptimismGasPriceOracle {
constructor(
public provider: ethers.providers.Provider,
) {}
private async callFn(method: string, blockTag?: ethers.providers.BlockTag) {
const outputBytes = await this.provider.call({
to: OPTIMISM_GAS_PRICE_ORACLE_ADDRESS,
data: ethers.utils.id(method),
}, blockTag);
const result = ethers.utils.defaultAbiCoder.decode(
["uint256"],
outputBytes,
)[0];
assert(result instanceof BigNumber);
return result;
}
async l1BaseFee(blockTag?: ethers.providers.BlockTag) {
return await this.callFn("l1BaseFee()", blockTag);
}
async overhead(blockTag?: ethers.providers.BlockTag) {
return await this.callFn("overhead()", blockTag);
}
async scalar(blockTag?: ethers.providers.BlockTag) {
return await this.callFn("scalar()", blockTag);
}
async decimals(blockTag?: ethers.providers.BlockTag) {
return await this.callFn("decimals()", blockTag);
}
async getAllParams(blockTag?: ethers.providers.BlockTag) {
const [l1BaseFee, overhead, scalar, decimals] = await Promise.all([
this.l1BaseFee(blockTag),
this.overhead(blockTag),
this.scalar(blockTag),
this.decimals(blockTag),
]);
return { l1BaseFee, overhead, scalar, decimals };
}
}

View File

@@ -18,8 +18,6 @@ import HealthService from "./HealthService.ts";
import HealthRouter from "./HealthRouter.ts";
export default async function app(emit: (evt: AppEvent) => void) {
emit({ type: "starting" });
const clock = Clock.create();
const bundleTableMutex = new Mutex();

View File

@@ -1,6 +1,4 @@
import assert from "./helpers/assert.ts";
import {
optionalEnv,
optionalNumberEnv,
requireBigNumberEnv,
requireBoolEnv,
@@ -97,38 +95,3 @@ export const PREVIOUS_BASE_FEE_PERCENT_INCREASE = requireNumberEnv(
export const BUNDLE_CHECKING_CONCURRENCY = requireIntEnv(
"BUNDLE_CHECKING_CONCURRENCY",
);
/**
* Optimism's strategy for charging for L1 fees requires special logic in the
* aggregator. In addition to gasEstimate * gasPrice, we need to replicate
* Optimism's calculation and pass it on to the user.
*/
export const IS_OPTIMISM = requireBoolEnv("IS_OPTIMISM");
/**
* Address for the Optimism gas price oracle contract. Required when
* IS_OPTIMISM is true.
*/
export const OPTIMISM_GAS_PRICE_ORACLE_ADDRESS = optionalEnv(
"OPTIMISM_GAS_PRICE_ORACLE_ADDRESS",
);
/**
* Similar to PREVIOUS_BASE_FEE_PERCENT_INCREASE, but for the L1 basefee for
* the optimism-specific calculation. This gets passed on to users.
* Required when IS_OPTIMISM is true.
*/
export const OPTIMISM_L1_BASE_FEE_PERCENT_INCREASE = optionalNumberEnv(
"OPTIMISM_L1_BASE_FEE_PERCENT_INCREASE",
);
if (IS_OPTIMISM) {
assert(
OPTIMISM_L1_BASE_FEE_PERCENT_INCREASE !== nil,
"OPTIMISM_L1_BASE_FEE_PERCENT_INCREASE is required when IS_OPTIMISM is true",
);
assert(
OPTIMISM_GAS_PRICE_ORACLE_ADDRESS !== nil,
"OPTIMISM_GAS_PRICE_ORACLE_ADDRESS is required when IS_OPTIMISM is true",
);
}

View File

@@ -1,50 +0,0 @@
import { BigNumber, ethers } from "../../deps.ts";
import OptimismGasPriceOracle from "../app/OptimismGasPriceOracle.ts";
import assert from "./assert.ts";
import getRawTransaction from "./getRawTransaction.ts";
import hexToUint8Array from "./hexToUint8Array.ts";
import nil from "./nil.ts";
export default async function getOptimismL1Fee(
provider: ethers.providers.Provider,
txResponseOrHash: string | ethers.providers.TransactionResponse,
) {
const tx = typeof txResponseOrHash === "string"
? await provider.getTransaction(txResponseOrHash)
: txResponseOrHash;
const rawTx = await getRawTransaction(provider, tx);
let l1Gas = 0;
for (const byte of hexToUint8Array(rawTx)) {
if (byte === 0) {
l1Gas += 4;
} else {
l1Gas += 16;
}
}
const gasOracle = new OptimismGasPriceOracle(provider);
assert(tx.blockNumber !== nil);
const {
l1BaseFee,
overhead,
scalar,
decimals,
} = await gasOracle.getAllParams(tx.blockNumber);
l1Gas = l1Gas += overhead.toNumber();
const l1Fee = BigNumber
.from(l1Gas)
.mul(l1BaseFee)
.mul(scalar)
.div(
BigNumber.from(10).pow(decimals),
);
return l1Fee;
}

View File

@@ -1,49 +0,0 @@
import { ethers, pick } from "../../deps.ts";
import assert from "./assert.ts";
import nil from "./nil.ts";
export default async function getRawTransaction(
provider: ethers.providers.Provider,
txResponseOrHash: string | ethers.providers.TransactionResponse,
) {
const tx = typeof txResponseOrHash === "string"
? await provider.getTransaction(txResponseOrHash)
: txResponseOrHash;
const txHash = typeof txResponseOrHash === "string"
? txResponseOrHash
: tx.hash;
assert(typeof txHash === "string");
const { v, r, s } = tx;
assert(r !== nil);
const txBytes = ethers.utils.serializeTransaction(
pick(
tx,
"to",
"nonce",
"gasLimit",
...(tx.type === 2 ? [] : ["gasPrice"] as const),
"data",
"value",
"chainId",
"type",
...(tx.type !== 2 ? [] : [
"accessList",
"maxPriorityFeePerGas",
"maxFeePerGas",
] as const),
),
{ v, r, s },
);
const reconstructedHash = ethers.utils.keccak256(txBytes);
if (reconstructedHash !== txHash) {
throw new Error("Reconstructed hash did not match original hash");
}
return txBytes;
}

View File

@@ -1,16 +0,0 @@
import assert from "./assert.ts";
export default function hexToUint8Array(hex: string) {
assert(hex.startsWith("0x"));
assert(hex.length % 2 === 0);
const len = (hex.length - 2) / 2;
const result = new Uint8Array(len);
for (let i = 0; i < len; i++) {
const hexPos = 2 * i + 2;
result[i] = parseInt(hex.slice(hexPos, hexPos + 2), 16);
}
return result;
}

View File

@@ -37,7 +37,6 @@ export const bundleServiceDefaultTestConfig:
maxAggregationDelayMillis: 5000,
maxUnconfirmedAggregations: 3,
maxEligibilityDelay: 300,
isOptimism: false,
};
export const aggregationStrategyDefaultTestConfig: AggregationStrategyConfig = {

View File

@@ -1,11 +0,0 @@
# Audits
## 2021
[Hubble contracts, including core BLS contracts](https://github.com/thehubbleproject/hubble-contracts/blob/master/audits/2021-03-17%20Igor%20Gulamov.md)
## 2022
[BLS Wallet](./Sigma_Prime_-_Ethereum_Foundation_-_BLS_Wallet_Smart_Contract_Security_Assessment_Report_-_v1.0.pdf)
All critical & high issues were addressed, but re-audit was not conducted.

View File

@@ -1,6 +1,6 @@
{
"name": "bls-wallet-clients",
"version": "0.9.0",
"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

@@ -1,4 +1,4 @@
//SPDX-License-Identifier: MIT
//SPDX-License-Identifier: Unlicense
pragma solidity >=0.7.0 <0.9.0;
pragma abicoder v2;

View File

@@ -1,4 +1,4 @@
//SPDX-License-Identifier: MIT
//SPDX-License-Identifier: Unlicense
pragma solidity >=0.8.0 <0.9.0;
pragma abicoder v2;

View File

@@ -1,4 +1,4 @@
//SPDX-License-Identifier: MIT
//SPDX-License-Identifier: Unlicense
pragma solidity >=0.7.0 <0.9.0;
pragma abicoder v2;

View File

@@ -1,4 +1,4 @@
//SPDX-License-Identifier: MIT
//SPDX-License-Identifier: Unlicense
pragma solidity >=0.7.0 <0.9.0;
pragma abicoder v2;

View File

@@ -1,4 +1,4 @@
//SPDX-License-Identifier: MIT
//SPDX-License-Identifier: Unlicense
pragma solidity >=0.7.0 <0.9.0;
pragma abicoder v2;

View File

@@ -1,4 +1,4 @@
//SPDX-License-Identifier: MIT
//SPDX-License-Identifier: Unlicense
pragma solidity >=0.8.4 <0.9.0;
pragma abicoder v2;

View File

@@ -1,4 +1,4 @@
//SPDX-License-Identifier: MIT
//SPDX-License-Identifier: Unlicense
pragma solidity >=0.7.0 <0.9.0;
pragma abicoder v2;

View File

@@ -1,4 +1,4 @@
//SPDX-License-Identifier: MIT
//SPDX-License-Identifier: Unlicense
pragma solidity >=0.7.0 <0.9.0;
pragma abicoder v2;

View File

@@ -1,4 +1,4 @@
//SPDX-License-Identifier: MIT
//SPDX-License-Identifier: Unlicense
pragma solidity >=0.7.0 <0.9.0;
pragma abicoder v2;

View File

@@ -1,4 +1,4 @@
//SPDX-License-Identifier: MIT
//SPDX-License-Identifier: Unlicense
pragma solidity >=0.8.4 <0.9.0;
pragma abicoder v2;
@@ -157,17 +157,21 @@ contract VerificationGateway
@dev overrides previous wallet address registered with the given public key
@param messageSenderSignature signature of message containing only the calling address
@param publicKey that signed the caller's address
@param signatureExpiryTimestamp that the signature is valid until
*/
function setBLSKeyForWallet(
uint256[2] memory messageSenderSignature,
uint256[BLS_KEY_LEN] memory publicKey
uint256[BLS_KEY_LEN] memory publicKey,
uint256 signatureExpiryTimestamp
) public {
require(blsLib.isZeroBLSKey(publicKey) == false, "VG: key is zero");
IWallet wallet = IWallet(msg.sender);
bytes32 existingHash = hashFromWallet[wallet];
// Can't register new wallet contracts, only what this gateway deployed.
if (existingHash != bytes32(0)) { // wallet already has a key registered, set after delay
if (existingHash == bytes32(0)) { // wallet does not yet have a bls key registered with this gateway
// set it instantly
safeSetWallet(messageSenderSignature, publicKey, wallet, signatureExpiryTimestamp);
}
else { // wallet already has a key registered, set after delay
pendingMessageSenderSignatureFromHash[existingHash] = messageSenderSignature;
pendingBLSPublicKeyFromHash[existingHash] = publicKey;
pendingBLSPublicKeyTimeFromHash[existingHash] = block.timestamp + 604800; // 1 week from now
@@ -231,11 +235,6 @@ contract VerificationGateway
}
}
require((selectorId != ProxyAdmin.upgrade.selector)
&& (selectorId != ProxyAdmin.upgradeAndCall.selector),
"VG: wallet not upgradable"
);
wallet.setAnyPending();
// ensure wallet has pre-approved encodedFunction

View File

@@ -1,4 +1,4 @@
//SPDX-License-Identifier: MIT
//SPDX-License-Identifier: Unlicense
pragma solidity >=0.7.0 <0.9.0;
pragma abicoder v2;

View File

@@ -1,4 +1,4 @@
//SPDX-License-Identifier: MIT
//SPDX-License-Identifier: Unlicense
pragma solidity >=0.8.4 <0.9.0;
pragma abicoder v2;

View File

@@ -1,4 +1,4 @@
//SPDX-License-Identifier: MIT
//SPDX-License-Identifier: Unlicense
pragma solidity >=0.7.0 <0.9.0;
pragma abicoder v2;

View File

@@ -1,4 +1,4 @@
//SPDX-License-Identifier: MIT
//SPDX-License-Identifier: Unlicense
pragma solidity >=0.8.15;
import "./VLQ.sol";

View File

@@ -1,4 +1,4 @@
//SPDX-License-Identifier: MIT
//SPDX-License-Identifier: Unlicense
pragma solidity >=0.7.0 <0.9.0;
pragma abicoder v2;

View File

@@ -1,4 +1,4 @@
//SPDX-License-Identifier: MIT
//SPDX-License-Identifier: Unlicense
pragma solidity >=0.8.4 <0.9.0;
pragma abicoder v2;

View File

@@ -95,7 +95,7 @@ describe("Recovery", async function () {
wallet1,
vg,
"setBLSKeyForWallet",
[addressSignature, wallet2.PublicKey()],
[addressSignature, wallet2.PublicKey(), signatureExpiryTimestamp],
1,
30_000_000,
);
@@ -372,7 +372,7 @@ describe("Recovery", async function () {
wallet1,
vg,
"setBLSKeyForWallet",
[attackSignature, walletAttacker.PublicKey()],
[attackSignature, walletAttacker.PublicKey(), signatureExpiryTimestamp],
recoveredWalletNonce++,
30_000_000,
);
@@ -660,7 +660,7 @@ describe("Recovery", async function () {
wallet1,
vg,
"setBLSKeyForWallet",
[addressSignature, wallet2.PublicKey()],
[addressSignature, wallet2.PublicKey(), invalidSignatureExpiryTimestamp],
1,
30_000_000,
);
@@ -724,7 +724,7 @@ describe("Recovery", async function () {
wallet1,
vg,
"setBLSKeyForWallet",
[addressSignature, wallet2.PublicKey()],
[addressSignature, wallet2.PublicKey(), signatureExpiryTimestamp],
1,
30_000_000,
);

View File

@@ -1,13 +1,21 @@
import { expect } from "chai";
import { BigNumber, ContractReceipt } from "ethers";
import { solidityPack } from "ethers/lib/utils";
import { ethers } from "hardhat";
import { ethers, network } from "hardhat";
import { expectPubkeysEql } from "./expect";
import { ActionData, getOperationResults } from "../clients/src";
import {
ActionData,
BlsWalletWrapper,
getOperationResults,
} from "../clients/src";
import Fixture from "../shared/helpers/Fixture";
import { proxyAdminCall } from "../shared/helpers/callProxyAdmin";
import {
proxyAdminBundle,
proxyAdminCall,
} from "../shared/helpers/callProxyAdmin";
import getPublicKeyFromHash from "../shared/helpers/getPublicKeyFromHash";
import deploy from "../shared/deploy";
const expectOperationsToSucceed = (txnReceipt: ContractReceipt) => {
const opResults = getOperationResults(txnReceipt);
@@ -36,7 +44,7 @@ describe("Upgrade", async function () {
fx = await Fixture.getSingleton();
});
it("should NOT upgrade wallet contract", async () => {
it("should upgrade wallet contract", async () => {
const MockWalletUpgraded = await ethers.getContractFactory(
"MockWalletUpgraded",
);
@@ -49,19 +57,243 @@ describe("Upgrade", async function () {
wallet.address,
mockWalletUpgraded.address,
]);
expectOperationFailure(txnReceipt1, "VG: wallet not upgradable");
expectOperationsToSucceed(txnReceipt1);
// Advance time one week
const latestTimestamp = (await ethers.provider.getBlock("latest"))
.timestamp;
await network.provider.send("evm_setNextBlockTimestamp", [
BigNumber.from(latestTimestamp)
.add(safetyDelaySeconds + 1)
.toHexString(),
]);
// make call
const txnReceipt2 = await proxyAdminCall(fx, wallet, "upgradeAndCall", [
const txnReceipt2 = await proxyAdminCall(fx, wallet, "upgrade", [
wallet.address,
mockWalletUpgraded.address,
[],
]);
expectOperationFailure(txnReceipt2, "VG: wallet not upgradable");
expectOperationsToSucceed(txnReceipt2);
const newBLSWallet = MockWalletUpgraded.attach(wallet.address);
await (await newBLSWallet.setNewData(wallet.address)).wait();
await expect(newBLSWallet.newData()).to.eventually.equal(wallet.address);
});
it("should register with new verification gateway", async () => {
// Still possible to point wallets to a new gateway if desired, just not with v1 deployment
// Deploy new verification gateway
const [signer] = await ethers.getSigners();
const deployment2 = await deploy(
signer,
ethers.utils.solidityPack(["uint256"], [2]),
);
const vg2 = deployment2.verificationGateway;
// Recreate hubble bls signer
const walletOldVg = await fx.createBLSWallet();
const walletAddress = walletOldVg.address;
const blsSecret = walletOldVg.blsWalletSigner.privateKey;
// Sign simple address message
const walletNewVg = await BlsWalletWrapper.connect(
blsSecret,
vg2.address,
vg2.provider,
);
const signatureExpiryTimestamp =
(await fx.provider.getBlock("latest")).timestamp +
safetyDelaySeconds +
signatureExpiryOffsetSeconds;
const addressMessage = solidityPack(
["address", "uint256"],
[walletAddress, signatureExpiryTimestamp],
);
const addressSignature = walletNewVg.signMessage(addressMessage);
const proxyAdmin2Address = await vg2.walletProxyAdmin();
// Get admin action to change proxy
const bundle = await proxyAdminBundle(fx, walletOldVg, "changeProxyAdmin", [
walletAddress,
proxyAdmin2Address,
]);
const changeProxyAction = bundle.operations[0].actions[0];
// prepare call
const txnReceipt = await proxyAdminCall(
fx,
walletOldVg,
"changeProxyAdmin",
[walletAddress, proxyAdmin2Address],
);
expectOperationsToSucceed(txnReceipt);
// Advance time one week
await fx.advanceTimeBy(safetyDelaySeconds + 1);
const hash = walletOldVg.blsWalletSigner.getPublicKeyHash();
const setExternalWalletAction: ActionData = {
ethValue: BigNumber.from(0),
contractAddress: vg2.address,
encodedFunction: vg2.interface.encodeFunctionData("setBLSKeyForWallet", [
addressSignature,
walletOldVg.PublicKey(),
signatureExpiryTimestamp,
]),
};
const setTrustedBLSGatewayAction: ActionData = {
ethValue: BigNumber.from(0),
contractAddress: fx.verificationGateway.address,
encodedFunction: fx.verificationGateway.interface.encodeFunctionData(
"setTrustedBLSGateway",
[hash, vg2.address],
),
};
// Upgrading the gateway requires these three steps:
// 1. register external wallet in vg2
// 2. change proxy admin to that in vg2
// 3. lastly, set wallet's new trusted gateway
//
// If (1) or (2) are skipped, then (3) should fail, and therefore the whole
// operation should fail.
{
// Fail if setExternalWalletAction is skipped
const { successes } =
await fx.verificationGateway.callStatic.processBundle(
walletOldVg.sign({
nonce: BigNumber.from(1),
gas: BigNumber.from(30_000_000),
actions: [
// skip: setExternalWalletAction,
changeProxyAction,
setTrustedBLSGatewayAction,
],
}),
);
expect(successes).to.deep.equal([false]);
}
{
// Fail if changeProxyAction is skipped
const { successes } =
await fx.verificationGateway.callStatic.processBundle(
walletOldVg.sign({
nonce: BigNumber.from(1),
gas: BigNumber.from(30_000_000),
actions: [
setExternalWalletAction,
// skip: changeProxyAction,
setTrustedBLSGatewayAction,
],
}),
);
expect(successes).to.deep.equal([false]);
}
{
// Succeed if nothing is skipped
const { successes } =
await fx.verificationGateway.callStatic.processBundle(
walletOldVg.sign({
nonce: BigNumber.from(1),
gas: BigNumber.from(30_000_000),
actions: [
setExternalWalletAction,
changeProxyAction,
setTrustedBLSGatewayAction,
],
}),
);
expect(successes).to.deep.equal([true]);
}
await expect(vg2.walletFromHash(hash)).to.eventually.not.equal(
walletAddress,
);
// Now actually perform the upgrade so we can perform some more detailed
// checks.
await fx.processBundleWithExtraGas(
walletOldVg.sign({
nonce: BigNumber.from(1),
gas: BigNumber.from(30_000_000),
actions: [
setExternalWalletAction,
changeProxyAction,
setTrustedBLSGatewayAction,
],
}),
);
// Create required objects for data/contracts for checks
const proxyAdmin = await ethers.getContractAt(
"ProxyAdmin",
await vg2.walletProxyAdmin(),
);
// Direct checks corresponding to each action
await expect(vg2.walletFromHash(hash)).to.eventually.equal(walletAddress);
await expect(vg2.hashFromWallet(walletAddress)).to.eventually.equal(hash);
await expect(proxyAdmin.getProxyAdmin(walletAddress)).to.eventually.equal(
proxyAdmin.address,
);
expectPubkeysEql(
await getPublicKeyFromHash(vg2, hash),
walletOldVg.PublicKey(),
);
const blsWallet = await ethers.getContractAt("BLSWallet", walletAddress);
// New verification gateway pending
await expect(blsWallet.trustedBLSGateway()).to.eventually.equal(
fx.verificationGateway.address,
);
// Advance time one week
await fx.advanceTimeBy(safetyDelaySeconds + 1);
// set pending
await (await blsWallet.setAnyPending()).wait();
// Check new verification gateway was set
await expect(blsWallet.trustedBLSGateway()).to.eventually.equal(
vg2.address,
);
await walletNewVg.syncWallet(vg2);
// Check new gateway has wallet via static call through new gateway
const bundleResult = await vg2.callStatic.processBundle(
fx.blsWalletSigner.aggregate([
walletNewVg.sign({
nonce: BigNumber.from(2),
gas: BigNumber.from(30_000_000),
actions: [
{
ethValue: 0,
contractAddress: vg2.address,
encodedFunction: vg2.interface.encodeFunctionData(
"walletFromHash",
[hash],
),
},
],
}),
]),
);
const walletFromHashAddress = ethers.utils.defaultAbiCoder.decode(
["address"],
bundleResult.results[0][0], // first and only operation/action result
)[0];
expect(walletFromHashAddress).to.equal(walletAddress);
});
it("should change mapping of an address to hash", async () => {
@@ -115,6 +347,7 @@ describe("Upgrade", async function () {
encodedFunction: vg1.interface.encodeFunctionData("setBLSKeyForWallet", [
addressSignature,
wallet2.PublicKey(),
signatureExpiryTimestamp,
]),
};

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-405e23a",
"bls-wallet-clients": "0.9.0-2a20bfe",
"browser-passworder": "^2.0.3",
"bs58check": "^2.1.2",
"crypto-browserify": "^3.12.0",

View File

@@ -1791,6 +1791,16 @@
version "5.7.0"
resolved "https://registry.yarnpkg.com/@ethersproject/transactions/-/transactions-5.7.0.tgz#91318fc24063e057885a6af13fdb703e1f993d3b"
integrity sha512-kmcNicCp1lp8qanMTC3RIikGgoJ80ztTyvtsFvCYpSCfkjhD0jZ2LOrnbcuxuToLIUYYf+4XwD1rP+B/erDIhQ==
dependencies:
"@ethersproject/address" "^5.7.0"
"@ethersproject/bignumber" "^5.7.0"
"@ethersproject/bytes" "^5.7.0"
"@ethersproject/constants" "^5.7.0"
"@ethersproject/keccak256" "^5.7.0"
"@ethersproject/logger" "^5.7.0"
"@ethersproject/properties" "^5.7.0"
"@ethersproject/rlp" "^5.7.0"
"@ethersproject/signing-key" "^5.7.0"
"@ethersproject/transactions@^5.5.0", "@ethersproject/transactions@^5.6.2":
version "5.6.2"
@@ -2888,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-405e23a:
version "0.9.0-405e23a"
resolved "https://registry.npmjs.org/bls-wallet-clients/-/bls-wallet-clients-0.9.0-405e23a.tgz#b66121f9ec0cb4e821965606ada203e6601b773d"
integrity sha512-cMm6pq35VU30veCAHt6ArSavlqzXu+olQg+dzUH28fvqSeQsfWz2qiuBekGxSWOCfn8gX1j/8jHEhrGxXS509Q==
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"