Skip to content
This repository has been archived by the owner on Nov 5, 2023. It is now read-only.

Use registries #552

Merged
merged 16 commits into from
Mar 15, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
62 changes: 62 additions & 0 deletions contracts/clients/src/BundleCompressor.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
import { ethers } from "ethers";
import { encodeVLQ, hexJoin } from "./encodeUtils";
import IOperationCompressor from "./IOperationCompressor";
import { Bundle, Operation, PublicKey } from "./signer";
import Range from "./helpers/Range";

export default class BundleCompressor {
compressors: [number, IOperationCompressor][] = [];

addCompressor(expanderIndex: number, compressor: IOperationCompressor) {
this.compressors.push([expanderIndex, compressor]);
}

async compressOperation(
blsPublicKey: PublicKey,
operation: Operation,
): Promise<string> {
let expanderIndexAndData: [number, string] | undefined;

for (const [expanderIndex, compressor] of this.compressors) {
const data = await compressor.compress(blsPublicKey, operation);

if (data === undefined) {
continue;
}

expanderIndexAndData = [expanderIndex, data];
break;
}

if (expanderIndexAndData === undefined) {
throw new Error("Failed to compress operation");
}

const [expanderIndex, data] = expanderIndexAndData;

return hexJoin([encodeVLQ(expanderIndex), data]);
}

async compress(bundle: Bundle): Promise<string> {
const len = bundle.operations.length;

if (bundle.senderPublicKeys.length !== len) {
throw new Error("ops vs keys length mismatch");
}

const compressedOperations = await Promise.all(
Range(len).map((i) =>
this.compressOperation(
bundle.senderPublicKeys[i],
bundle.operations[i],
),
),
);

return hexJoin([
encodeVLQ(bundle.operations.length),
...compressedOperations,
ethers.utils.defaultAbiCoder.encode(["uint256[2]"], [bundle.signature]),
]);
}
}
209 changes: 209 additions & 0 deletions contracts/clients/src/FallbackCompressor.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,209 @@
/* eslint-disable camelcase */

import { ethers, Signer } from "ethers";
import {
AddressRegistry__factory,
BLSPublicKeyRegistry__factory,
FallbackExpander,
FallbackExpander__factory,
} from "../typechain-types";
import AddressRegistryWrapper from "./AddressRegistryWrapper";
import BlsPublicKeyRegistryWrapper from "./BlsPublicKeyRegistryWrapper";
import {
encodeBitStream,
encodePseudoFloat,
encodeRegIndex,
encodeVLQ,
hexJoin,
} from "./encodeUtils";
import SignerOrProvider from "./helpers/SignerOrProvider";
import IOperationCompressor from "./IOperationCompressor";
import SafeSingletonFactory, {
SafeSingletonFactoryViewer,
} from "./SafeSingletonFactory";
import { Operation, PublicKey } from "./signer";

export default class FallbackCompressor implements IOperationCompressor {
private constructor(
public fallbackExpander: FallbackExpander,
public blsPublicKeyRegistry: BlsPublicKeyRegistryWrapper,
public addressRegistry: AddressRegistryWrapper,
) {}

static async wrap(
fallbackExpander: FallbackExpander,
): Promise<FallbackCompressor> {
const [blsPublicKeyRegistryAddress, addressRegistryAddress] =
await Promise.all([
fallbackExpander.blsPublicKeyRegistry(),
fallbackExpander.addressRegistry(),
]);

return new FallbackCompressor(
fallbackExpander,
new BlsPublicKeyRegistryWrapper(
BLSPublicKeyRegistry__factory.connect(
blsPublicKeyRegistryAddress,
fallbackExpander.signer,
),
),
new AddressRegistryWrapper(
AddressRegistry__factory.connect(
addressRegistryAddress,
fallbackExpander.signer,
),
),
);
}

static async deployNew(signer: Signer): Promise<FallbackCompressor> {
const blsPublicKeyRegistryFactory = new BLSPublicKeyRegistry__factory(
signer,
);

const addressRegistryFactory = new AddressRegistry__factory(signer);

const [blsPublicKeyRegistryContract, addressRegistryContract] =
await Promise.all([
blsPublicKeyRegistryFactory.deploy(),
addressRegistryFactory.deploy(),
]);

const fallbackExpanderFactory = new FallbackExpander__factory(signer);

const fallbackExpanderContract = await fallbackExpanderFactory.deploy(
blsPublicKeyRegistryContract.address,
addressRegistryContract.address,
);

return new FallbackCompressor(
fallbackExpanderContract,
new BlsPublicKeyRegistryWrapper(blsPublicKeyRegistryContract),
new AddressRegistryWrapper(addressRegistryContract),
);
}

static async connectOrDeploy(
signerOrFactory: Signer | SafeSingletonFactory,
salt: ethers.utils.BytesLike = ethers.utils.solidityPack(["uint256"], [0]),
): Promise<FallbackCompressor> {
const factory = await SafeSingletonFactory.from(signerOrFactory);

const [blsPublicKeyRegistry, addressRegistry] = await Promise.all([
BlsPublicKeyRegistryWrapper.connectOrDeploy(factory, salt),
AddressRegistryWrapper.connectOrDeploy(factory, salt),
]);

const fallbackExpanderContract = await factory.connectOrDeploy(
FallbackExpander__factory,
[blsPublicKeyRegistry.registry.address, addressRegistry.registry.address],
salt,
);

return new FallbackCompressor(
fallbackExpanderContract,
blsPublicKeyRegistry,
addressRegistry,
);
}

static async connectIfDeployed(
signerOrProvider: SignerOrProvider,
salt: ethers.utils.BytesLike = ethers.utils.solidityPack(["uint256"], [0]),
): Promise<FallbackCompressor | undefined> {
const factoryViewer = await SafeSingletonFactoryViewer.from(
signerOrProvider,
);

const blsPublicKeyRegistry = await factoryViewer.connectIfDeployed(
BLSPublicKeyRegistry__factory,
[],
salt,
);

if (!blsPublicKeyRegistry) {
return undefined;
}

const addressRegistry = await factoryViewer.connectIfDeployed(
AddressRegistry__factory,
[],
salt,
);

if (!addressRegistry) {
return undefined;
}

const fallbackExpander = await factoryViewer.connectIfDeployed(
FallbackExpander__factory,
[blsPublicKeyRegistry.address, addressRegistry.address],
salt,
);

if (!fallbackExpander) {
return undefined;
}

return new FallbackCompressor(
fallbackExpander,
new BlsPublicKeyRegistryWrapper(blsPublicKeyRegistry),
new AddressRegistryWrapper(addressRegistry),
);
}

async compress(blsPublicKey: PublicKey, operation: Operation) {
const result: string[] = [];

const resultIndexForRegUsageBitStream = result.length;
const regUsageBitStream: boolean[] = [];
result.push("0x"); // Placeholder to overwrite

const blsPublicKeyId = await this.blsPublicKeyRegistry.reverseLookup(
blsPublicKey,
);

if (blsPublicKeyId === undefined) {
regUsageBitStream.push(false);

result.push(
ethers.utils.defaultAbiCoder.encode(["uint256[4]"], [blsPublicKey]),
);
} else {
regUsageBitStream.push(true);
result.push(encodeRegIndex(blsPublicKeyId));
}

result.push(encodeVLQ(operation.nonce));
result.push(encodePseudoFloat(operation.gas));

result.push(encodeVLQ(operation.actions.length));

for (const action of operation.actions) {
result.push(encodePseudoFloat(action.ethValue));

const addressId = await this.addressRegistry.reverseLookup(
action.contractAddress,
);

if (addressId === undefined) {
regUsageBitStream.push(false);
result.push(action.contractAddress);
} else {
regUsageBitStream.push(true);
result.push(encodeRegIndex(addressId));
}

const fnHex = ethers.utils.hexlify(action.encodedFunction);
const fnLen = (fnHex.length - 2) / 2;

result.push(encodeVLQ(fnLen));
result.push(fnHex);
}

result[resultIndexForRegUsageBitStream] =
encodeBitStream(regUsageBitStream);

return hexJoin(result);
}
}
10 changes: 10 additions & 0 deletions contracts/clients/src/IOperationCompressor.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { Operation, PublicKey } from "./signer";

type IOperationCompressor = {
compress(
blsPublicKey: PublicKey,
operation: Operation,
): Promise<string | undefined>;
};

export default IOperationCompressor;
Original file line number Diff line number Diff line change
@@ -1,54 +1,10 @@
import { ethers, BigNumber, BigNumberish } from "ethers";
import { PublicKey, Operation, Signature } from "../../clients/src";
import { BigNumber, BigNumberish, ethers } from "ethers";

export function bundleCompressedOperations(
compressedOperations: string[],
signature: Signature,
) {
return hexJoin([
encodeVLQ(compressedOperations.length),
...compressedOperations,
ethers.utils.defaultAbiCoder.encode(["uint256[2]"], [signature]),
]);
}

export function compressAsFallback(
fallbackExpanderIndex: number,
blsPublicKey: PublicKey,
operation: Operation,
): string {
const result: string[] = [];

result.push(encodeVLQ(fallbackExpanderIndex));

result.push(
ethers.utils.defaultAbiCoder.encode(["uint256[4]"], [blsPublicKey]),
);

result.push(encodeVLQ(operation.nonce));
result.push(encodePseudoFloat(operation.gas));

result.push(encodeVLQ(operation.actions.length));

for (const action of operation.actions) {
result.push(encodePseudoFloat(action.ethValue));
result.push(action.contractAddress);

const fnHex = ethers.utils.hexlify(action.encodedFunction);
const fnLen = (fnHex.length - 2) / 2;

result.push(encodeVLQ(fnLen));
result.push(fnHex);
}

return hexJoin(result);
}

function hexJoin(hexStrings: string[]) {
export function hexJoin(hexStrings: string[]) {
return "0x" + hexStrings.map(remove0x).join("");
}

function remove0x(hexString: string) {
export function remove0x(hexString: string) {
if (!hexString.startsWith("0x")) {
throw new Error("Expected 0x prefix");
}
Expand Down Expand Up @@ -119,3 +75,45 @@ export function encodeRegIndex(regIndex: BigNumberish) {
`0x${fixedValue.toString(16).padStart(4, "0")}`,
]);
}

/**
* Bit streams are just the bits of a uint256 encoded as a VLQ.
* (Technically the encoding is unbounded, but 256 booleans is a lot and it's
* much easier to just decode the VLQ into a uint256 in the EVM.)
*
* Notably, the bits are little endian - the first bit is the *lowest* bit. This
* is because the lowest bit is clearly the 1-valued bit, but the highest valued
* bit could be anywhere - there's infinitely many zero-bits to choose from.
*
* If it wasn't for this need to be little endian, we'd definitely use big
* endian (like our other encodings generally do), since that's preferred by the
* EVM and the ecosystem:
*
* ```ts
* const abi = new ethers.utils.AbiCoder():
* console.log(abi.encode(["uint"], [0xff]));
* // 0x00000000000000000000000000000000000000000000000000000000000000ff
*
* // If Ethereum used little endian (like x86), it would instead be:
* // 0xff00000000000000000000000000000000000000000000000000000000000000
* ```
*/
export function encodeBitStream(bitStream: boolean[]) {
let stream = 0;
let bitValue = 1;

const abi = new ethers.utils.AbiCoder();
abi.encode(["uint"], [0xff]);

for (const bit of bitStream) {
if (bit) {
stream += bitValue;
}

bitValue *= 2;
}

const streamVLQ = encodeVLQ(stream);

return streamVLQ;
}
3 changes: 3 additions & 0 deletions contracts/clients/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,9 @@ export {

export { default as AddressRegistryWrapper } from "./AddressRegistryWrapper";
export { default as BlsPublicKeyRegistryWrapper } from "./BlsPublicKeyRegistryWrapper";
export { default as FallbackCompressor } from "./FallbackCompressor";
export { default as BundleCompressor } from "./BundleCompressor";
export * from "./encodeUtils";

const Experimental_ = {
BlsProvider,
Expand Down
Loading