Add bls lib

This commit is contained in:
James Zaki
2021-01-20 14:33:27 +11:00
parent 67c8fc86a0
commit ff5adeebaf
21 changed files with 2666 additions and 0 deletions

View File

@@ -19,8 +19,10 @@
"@types/express": "^4.17.11",
"@types/mysql": "^2.15.17",
"connect-livereload": "^0.6.1",
"ethers": "^5.0.26",
"express": "^4.17.1",
"livereload": "^0.9.1",
"mcl-wasm": "^0.7.1",
"mysql": "^2.18.1",
"nodemon": "^2.0.7",
"path": "^0.12.7",

View File

@@ -0,0 +1,108 @@
import { Tree } from "./tree";
import { BlsAccountRegistry } from "../types/ethers-contracts/BlsAccountRegistry";
import { ethers } from "ethers";
import { solG2 } from "./mcl";
import { RegistrationFail, WrongBatchSize } from "./exceptions";
// Tree is 32 level depth, the index is still smaller than Number.MAX_SAFE_INTEGER
export class AccountRegistry {
treeLeft: Tree;
treeRight: Tree;
leftIndex: number = 0;
rightIndex: number = 0;
setSize: number;
batchSize: number;
public static async new(
registry: BlsAccountRegistry
): Promise<AccountRegistry> {
const depth = (await registry.DEPTH()).toNumber();
const batchDepth = (await registry.BATCH_DEPTH()).toNumber();
return new AccountRegistry(registry, depth, batchDepth);
}
constructor(
public readonly registry: BlsAccountRegistry,
private readonly depth: number,
private readonly batchDepth: number
) {
this.treeLeft = Tree.new(depth);
this.treeRight = Tree.new(depth);
this.setSize = 2 ** depth;
this.batchSize = 2 ** batchDepth;
}
public async register(pubkey: solG2): Promise<number> {
const pubkeyID = await this.syncLeftIndex();
await this.registry.register(pubkey);
const leaf = this.pubkeyToLeaf(pubkey);
this.treeLeft.updateSingle(pubkeyID, leaf);
const exist = await this.checkExistence(pubkeyID, pubkey);
if (!exist) throw new RegistrationFail(`PubkeyID ${pubkeyID}`);
await this.syncLeftIndex();
return pubkeyID;
}
public async registerBatch(pubkeys: solG2[]): Promise<number> {
const length = pubkeys.length;
if (length != this.batchSize)
throw new WrongBatchSize(
`Expect ${this.batchSize} pubkeys, got ${length}`
);
const rigthIndex = await this.syncRightIndex();
await this.registry.registerBatch(pubkeys);
const leaves = pubkeys.map(key => this.pubkeyToLeaf(key));
this.treeRight.updateBatch(rigthIndex, leaves);
const firstPubkeyID = rigthIndex + this.setSize;
const lastPubkeyID = firstPubkeyID + length - 1;
const exist = await this.checkExistence(
lastPubkeyID,
pubkeys[length - 1]
);
if (!exist) throw new RegistrationFail(`LastID ${lastPubkeyID}`);
await this.syncRightIndex();
return firstPubkeyID;
}
public async checkExistence(
pubkeyID: number,
pubkey: solG2
): Promise<boolean> {
// To do merkle check on chain, we only need 31 hashes in the witness
// The 32 hash is the root of the left or right tree, which account tree will get it for us.
const _witness = this.witness(pubkeyID).slice(0, 31);
return await this.registry.exists(pubkeyID, pubkey, _witness);
}
private async syncLeftIndex(): Promise<number> {
this.leftIndex = (await this.registry.leafIndexLeft()).toNumber();
return this.leftIndex;
}
private async syncRightIndex(): Promise<number> {
this.rightIndex = (await this.registry.leafIndexRight()).toNumber();
return this.rightIndex;
}
public witness(pubkeyID: number): string[] {
if (pubkeyID < this.treeLeft.setSize) {
const witness = this.treeLeft.witness(pubkeyID).nodes;
witness.push(this.treeRight.root);
return witness;
} else {
const rightTreeID = pubkeyID - this.setSize;
const witness = this.treeRight.witness(rightTreeID).nodes;
witness.push(this.treeLeft.root);
return witness;
}
}
public root() {
const hasher = this.treeLeft.hasher;
return hasher.hash2(this.treeLeft.root, this.treeRight.root);
}
public pubkeyToLeaf(uncompressed: solG2) {
const leaf = ethers.utils.solidityKeccak256(
["uint256", "uint256", "uint256", "uint256"],
uncompressed
);
return leaf;
}
}

View File

@@ -0,0 +1,36 @@
import { TokenRegistry } from "../types/ethers-contracts/TokenRegistry";
import { Pob } from "../types/ethers-contracts/Pob";
import { Transfer } from "../types/ethers-contracts/Transfer";
import { ExampleToken } from "../types/ethers-contracts/ExampleToken";
import { DepositManager } from "../types/ethers-contracts/DepositManager";
import { Rollup } from "../types/ethers-contracts/Rollup";
import { BlsAccountRegistry } from "../types/ethers-contracts/BlsAccountRegistry";
import { MassMigration } from "../types/ethers-contracts/MassMigration";
import { Vault } from "../types/ethers-contracts/Vault";
import { WithdrawManager } from "../types/ethers-contracts/WithdrawManager";
import { SpokeRegistry } from "../types/ethers-contracts/SpokeRegistry";
import { FrontendGeneric } from "../types/ethers-contracts/FrontendGeneric";
import { FrontendTransfer } from "../types/ethers-contracts/FrontendTransfer";
import { FrontendMassMigration } from "../types/ethers-contracts/FrontendMassMigration";
import { FrontendCreate2Transfer } from "../types/ethers-contracts/FrontendCreate2Transfer";
import { Create2Transfer } from "../types/ethers-contracts/Create2Transfer";
import { BurnAuction } from "../types/ethers-contracts/BurnAuction";
export interface allContracts {
frontendGeneric: FrontendGeneric;
frontendTransfer: FrontendTransfer;
frontendMassMigration: FrontendMassMigration;
frontendCreate2Transfer: FrontendCreate2Transfer;
blsAccountRegistry: BlsAccountRegistry;
tokenRegistry: TokenRegistry;
transfer: Transfer;
massMigration: MassMigration;
create2Transfer: Create2Transfer;
chooser: Pob | BurnAuction;
exampleToken: ExampleToken;
spokeRegistry: SpokeRegistry;
vault: Vault;
depositManager: DepositManager;
rollup: Rollup;
withdrawManager: WithdrawManager;
}

View File

@@ -0,0 +1,62 @@
import {
solG2,
Domain,
getPubkey,
g2ToHex,
sign,
g1ToHex,
aggregateRaw,
mclG1,
solG1,
SecretKey,
randFr,
PublicKey
} from "./mcl";
export interface SignatureInterface {
mcl: mclG1;
sol: solG1;
}
export interface BlsSignerInterface {
pubkey: solG2;
sign(message: string): SignatureInterface;
}
export class NullBlsSinger implements BlsSignerInterface {
get pubkey(): solG2 {
throw new Error("NullBlsSinger has no public key");
}
sign(message: string): SignatureInterface {
throw new Error("NullBlsSinger dosen't sign");
}
}
export const nullBlsSigner = new NullBlsSinger();
export class BlsSigner implements BlsSignerInterface {
static new(domain: Domain) {
const secret = randFr();
return new BlsSigner(domain, secret);
}
private _pubkey: PublicKey;
constructor(public domain: Domain, private secret: SecretKey) {
this._pubkey = getPubkey(secret);
}
get pubkey(): solG2 {
return g2ToHex(this._pubkey);
}
public sign(message: string): SignatureInterface {
const { signature } = sign(message, this.secret, this.domain);
const sol = g1ToHex(signature);
return { mcl: signature, sol };
}
}
export function aggregate(
signatures: SignatureInterface[]
): SignatureInterface {
const aggregated = aggregateRaw(signatures.map(s => s.mcl));
return { mcl: aggregated, sol: g1ToHex(aggregated) };
}

View File

@@ -0,0 +1,318 @@
import { BigNumberish, BytesLike, ethers } from "ethers";
import { Rollup } from "../types/ethers-contracts/Rollup";
import { ZERO_BYTES32 } from "./constants";
import { Wei } from "./interfaces";
import { State } from "./state";
import { MigrationTree, StateProvider } from "./stateTree";
import { Tree } from "./tree";
import { serialize, TxMassMigration } from "./tx";
import { sum } from "./utils";
interface CompressedStruct {
stateRoot: BytesLike;
bodyRoot: BytesLike;
}
interface SolStruct {
stateRoot: BytesLike;
body: any;
}
export interface CommitmentInclusionProof {
commitment: CompressedStruct;
path: number;
witness: string[];
}
interface XCommitmentInclusionProof {
commitment: SolStruct;
path: number;
witness: string[];
}
abstract class Commitment {
constructor(public stateRoot: BytesLike) {}
abstract get bodyRoot(): BytesLike;
public hash(): string {
return ethers.utils.solidityKeccak256(
["bytes32", "bytes32"],
[this.stateRoot, this.bodyRoot]
);
}
abstract toSolStruct(): SolStruct;
abstract toBatch(): Batch;
public toCompressedStruct(): CompressedStruct {
return {
stateRoot: this.stateRoot,
bodyRoot: this.bodyRoot
};
}
}
export class BodylessCommitment extends Commitment {
get bodyRoot() {
return ZERO_BYTES32;
}
public toSolStruct() {
return { stateRoot: this.stateRoot, body: {} };
}
public toBatch(): Batch {
return new Batch([this]);
}
}
export function getGenesisProof(
stateRoot: BytesLike
): CommitmentInclusionProof {
return new BodylessCommitment(stateRoot).toBatch().proofCompressed(0);
}
export class TransferCommitment extends Commitment {
public static new(
stateRoot: BytesLike = ethers.constants.HashZero,
accountRoot: BytesLike = ethers.constants.HashZero,
signature: BigNumberish[] = [0, 0],
feeReceiver: BigNumberish = 0,
txs: BytesLike = "0x"
) {
return new TransferCommitment(
stateRoot,
accountRoot,
signature,
feeReceiver,
txs
);
}
constructor(
public stateRoot: BytesLike,
public accountRoot: BytesLike,
public signature: BigNumberish[],
public feeReceiver: BigNumberish,
public txs: BytesLike
) {
super(stateRoot);
}
public get bodyRoot() {
return ethers.utils.solidityKeccak256(
["bytes32", "uint256[2]", "uint256", "bytes"],
[this.accountRoot, this.signature, this.feeReceiver, this.txs]
);
}
public toSolStruct(): SolStruct {
return {
stateRoot: this.stateRoot,
body: {
accountRoot: this.accountRoot,
signature: this.signature,
feeReceiver: this.feeReceiver,
txs: this.txs
}
};
}
public toBatch() {
return new TransferBatch([this]);
}
}
export class MassMigrationCommitment extends Commitment {
public static new(
stateRoot: BytesLike = ethers.constants.HashZero,
accountRoot: BytesLike = ethers.constants.HashZero,
signature: BigNumberish[] = [0, 0],
spokeID: BigNumberish = 0,
withdrawRoot: BytesLike = ethers.constants.HashZero,
tokenID: BigNumberish = 0,
amount: BigNumberish = 0,
feeReceiver: BigNumberish = 0,
txs: BytesLike = "0x"
) {
return new MassMigrationCommitment(
stateRoot,
accountRoot,
signature,
spokeID,
withdrawRoot,
tokenID,
amount,
feeReceiver,
txs
);
}
public static fromStateProvider(
accountRoot: BytesLike,
txs: TxMassMigration[],
signature: BigNumberish[],
feeReceiver: number,
stateProvider: StateProvider
) {
const states = [];
for (const tx of txs) {
const origin = stateProvider.getState(tx.fromIndex).state;
const destination = State.new(
origin.pubkeyID,
origin.tokenID,
tx.amount,
0
);
states.push(destination);
}
const migrationTree = MigrationTree.fromStates(states);
const commitment = new this(
stateProvider.root,
accountRoot,
signature,
txs[0].spokeID,
migrationTree.root,
states[0].tokenID,
sum(txs.map(tx => tx.amount)),
feeReceiver,
serialize(txs)
);
return { commitment, migrationTree };
}
constructor(
public stateRoot: BytesLike,
public accountRoot: BytesLike,
public signature: BigNumberish[],
public spokeID: BigNumberish,
public withdrawRoot: BytesLike,
public tokenID: BigNumberish,
public amount: BigNumberish,
public feeReceiver: BigNumberish,
public txs: BytesLike
) {
super(stateRoot);
}
public get bodyRoot() {
return ethers.utils.solidityKeccak256(
[
"bytes32",
"uint256[2]",
"uint256",
"bytes32",
"uint256",
"uint256",
"uint256",
"bytes"
],
[
this.accountRoot,
this.signature,
this.spokeID,
this.withdrawRoot,
this.tokenID,
this.amount,
this.feeReceiver,
this.txs
]
);
}
public toSolStruct(): SolStruct {
return {
stateRoot: this.stateRoot,
body: {
accountRoot: this.accountRoot,
signature: this.signature,
spokeID: this.spokeID,
withdrawRoot: this.withdrawRoot,
tokenID: this.tokenID,
amount: this.amount,
feeReceiver: this.feeReceiver,
txs: this.txs
}
};
}
public toBatch() {
return new MassMigrationBatch([this]);
}
}
export class Create2TransferCommitment extends TransferCommitment {
public toBatch(): Create2TransferBatch {
return new Create2TransferBatch([this]);
}
}
export class Batch {
private tree: Tree;
constructor(public readonly commitments: Commitment[]) {
this.tree = Tree.merklize(commitments.map(c => c.hash()));
}
get commitmentRoot(): string {
return this.tree.root;
}
witness(leafInfex: number): string[] {
return this.tree.witness(leafInfex).nodes;
}
proof(leafInfex: number): XCommitmentInclusionProof {
return {
commitment: this.commitments[leafInfex].toSolStruct(),
path: leafInfex,
witness: this.witness(leafInfex)
};
}
proofCompressed(leafInfex: number): CommitmentInclusionProof {
return {
commitment: this.commitments[leafInfex].toCompressedStruct(),
path: leafInfex,
witness: this.witness(leafInfex)
};
}
}
export class TransferBatch extends Batch {
constructor(public readonly commitments: TransferCommitment[]) {
super(commitments);
}
async submit(rollup: Rollup, stakingAmount: Wei) {
return await rollup.submitTransfer(
this.commitments.map(c => c.stateRoot),
this.commitments.map(c => c.signature),
this.commitments.map(c => c.feeReceiver),
this.commitments.map(c => c.txs),
{ value: stakingAmount }
);
}
}
export class MassMigrationBatch extends Batch {
constructor(public readonly commitments: MassMigrationCommitment[]) {
super(commitments);
}
async submit(rollup: Rollup, stakingAmount: Wei) {
return await rollup.submitMassMigration(
this.commitments.map(c => c.stateRoot),
this.commitments.map(c => c.signature),
this.commitments.map(c => [
c.spokeID,
c.tokenID,
c.amount,
c.feeReceiver
]),
this.commitments.map(c => c.withdrawRoot),
this.commitments.map(c => c.txs),
{ value: stakingAmount }
);
}
}
export class Create2TransferBatch extends Batch {
constructor(public readonly commitments: TransferCommitment[]) {
super(commitments);
}
async submit(rollup: Rollup, stakingAmount: Wei) {
return await rollup.submitCreate2Transfer(
this.commitments.map(c => c.stateRoot),
this.commitments.map(c => c.signature),
this.commitments.map(c => c.feeReceiver),
this.commitments.map(c => c.txs),
{ value: stakingAmount }
);
}
}

View File

@@ -0,0 +1,38 @@
import { DeploymentParameters } from "./interfaces";
import { toWei } from "./utils";
export const TESTING_PARAMS: DeploymentParameters = {
MAX_DEPTH: 8,
MAX_DEPOSIT_SUBTREE_DEPTH: 1,
STAKE_AMOUNT: toWei("0.1"),
BLOCKS_TO_FINALISE: 5,
MIN_GAS_LEFT: 300000,
USE_BURN_AUCTION: false,
MAX_TXS_PER_COMMIT: 32,
DONATION_ADDRESS: "0x00000000000000000000000000000000000000d0",
DONATION_NUMERATOR: 7500
};
export const PRODUCTION_PARAMS: DeploymentParameters = {
MAX_DEPTH: 20,
MAX_DEPOSIT_SUBTREE_DEPTH: 2,
STAKE_AMOUNT: toWei("0.1"),
BLOCKS_TO_FINALISE: 7 * 24 * 60 * 4, // 7 days of blocks
MIN_GAS_LEFT: 10000,
MAX_TXS_PER_COMMIT: 32,
USE_BURN_AUCTION: true,
DONATION_ADDRESS: "0x00000000000000000000000000000000000000d0",
DONATION_NUMERATOR: 7500
};
export const COMMIT_SIZE = 32;
export const STATE_TREE_DEPTH = 32;
export const BLOCKS_PER_SLOT = 100;
export const DELTA_BLOCKS_INITIAL_SLOT = 1000;
// ethers.utils.keccak256(ethers.constants.HashZero)
export const ZERO_BYTES32 =
"0x290decd9548b62a8d60345a988386fc84ba6bc95484008f6362f93160ef3e563";
export const EMPTY_SIGNATURE = [0, 0];

View File

@@ -0,0 +1,117 @@
import { BigNumber, BigNumberish, BytesLike, ethers } from "ethers";
import { EncodingError } from "./exceptions";
import { randHex } from "./utils";
export class DecimalCodec {
private mantissaMax: BigNumber;
private exponentMax: number;
private exponentMask: BigNumber;
public bytesLength: number;
constructor(
public readonly exponentBits: number,
public readonly mantissaBits: number,
public readonly place: number
) {
this.mantissaMax = BigNumber.from(2 ** mantissaBits - 1);
this.exponentMax = 2 ** exponentBits - 1;
this.exponentMask = BigNumber.from(this.exponentMax << mantissaBits);
this.bytesLength = (mantissaBits + exponentBits) / 8;
}
public rand(): string {
return randHex(this.bytesLength);
}
public randInt(): BigNumber {
return this.decodeInt(this.rand());
}
public randNum(): number {
return this.decode(this.rand());
}
/**
* Given an arbitrary js number returns a js number that can be encoded.
*/
public cast(input: number): number {
if (input == 0) {
return input;
}
const logMantissaMax = Math.log10(this.mantissaMax.toNumber());
const logInput = Math.log10(input);
const exponent = Math.floor(logMantissaMax - logInput);
const mantissa = Math.floor(input * 10 ** exponent);
return mantissa / 10 ** exponent;
}
/**
* Given an arbitrary js number returns a integer that can be encoded
*/
public castInt(input: number): BigNumber {
const validNum = this.cast(input);
return BigNumber.from(Math.round(validNum * 10 ** this.place));
}
/**
* Find a BigNumber that's less than the input and compressable
*/
public castBigNumber(input: BigNumber): BigNumber {
let mantissa = input;
for (let exponent = 0; exponent < this.exponentMax; exponent++) {
if (mantissa.lte(this.mantissaMax))
return mantissa.mul(BigNumber.from(10).pow(exponent));
mantissa = mantissa.div(10);
}
throw new EncodingError(`Can't cast input ${input.toString()}`);
}
public encode(input: number) {
// Use Math.round here to prevent the case
// > 32.3 * 10 ** 6
// 32299999.999999996
const integer = Math.round(input * 10 ** this.place);
return this.encodeInt(integer);
}
public decode(input: BytesLike): number {
const integer = this.decodeInt(input);
const tens = BigNumber.from(10).pow(this.place);
if (integer.gte(Number.MAX_SAFE_INTEGER.toString())) {
return integer.div(tens).toNumber();
} else {
return integer.toNumber() / tens.toNumber();
}
}
public encodeInt(input: BigNumberish): string {
let exponent = 0;
let mantissa = BigNumber.from(input.toString());
for (let i = 0; i < this.exponentMax; i++) {
if (!mantissa.isZero() && mantissa.mod(10).isZero()) {
mantissa = mantissa.div(10);
exponent += 1;
} else {
break;
}
}
if (mantissa.gt(this.mantissaMax)) {
throw new EncodingError(
`Can not encode input ${input}, mantissa ${mantissa} should not be larger than ${this.mantissaMax}`
);
}
const hex = BigNumber.from(exponent)
.shl(this.mantissaBits)
.add(mantissa)
.toHexString();
return ethers.utils.hexZeroPad(hex, this.bytesLength);
}
public decodeInt(input: BytesLike): BigNumber {
const mantissa = this.mantissaMax.and(input);
const exponent = this.exponentMask.and(input).shr(this.mantissaBits);
return mantissa.mul(BigNumber.from(10).pow(exponent));
}
}
export const USDT = new DecimalCodec(4, 12, 6);

View File

@@ -0,0 +1,166 @@
import { TokenRegistryFactory } from "../types/ethers-contracts/TokenRegistryFactory";
import { TransferFactory } from "../types/ethers-contracts/TransferFactory";
import { MassMigrationFactory } from "../types/ethers-contracts/MassMigrationFactory";
import { ExampleTokenFactory } from "../types/ethers-contracts/ExampleTokenFactory";
import { DepositManagerFactory } from "../types/ethers-contracts/DepositManagerFactory";
import { RollupFactory } from "../types/ethers-contracts/RollupFactory";
import { BlsAccountRegistryFactory } from "../types/ethers-contracts/BlsAccountRegistryFactory";
import { Signer, Contract } from "ethers";
import { DeploymentParameters } from "./interfaces";
import { allContracts } from "./allContractsInterfaces";
import {
FrontendGenericFactory,
FrontendTransferFactory,
FrontendMassMigrationFactory,
FrontendCreate2TransferFactory,
SpokeRegistryFactory,
VaultFactory,
WithdrawManagerFactory,
Create2TransferFactory
} from "../types/ethers-contracts";
import { BurnAuctionFactory } from "../types/ethers-contracts/BurnAuctionFactory";
import { BurnAuction } from "../types/ethers-contracts/BurnAuction";
import { ProofOfBurnFactory } from "../types/ethers-contracts/ProofOfBurnFactory";
import { ProofOfBurn } from "../types/ethers-contracts/ProofOfBurn";
import { GenesisNotSpecified } from "./exceptions";
async function waitAndRegister(
contract: Contract,
name: string,
verbose: boolean
) {
await contract.deployed();
if (verbose) console.log("Deployed", name, "at", contract.address);
}
export async function deployAll(
signer: Signer,
parameters: DeploymentParameters,
verbose: boolean = false
): Promise<allContracts> {
// deploy libs
const frontendGeneric = await new FrontendGenericFactory(signer).deploy();
await waitAndRegister(frontendGeneric, "frontendGeneric", verbose);
const frontendTransfer = await new FrontendTransferFactory(signer).deploy();
await waitAndRegister(frontendTransfer, "frontendTransfer", verbose);
const frontendMassMigration = await new FrontendMassMigrationFactory(
signer
).deploy();
await waitAndRegister(
frontendMassMigration,
"frontendMassMigration",
verbose
);
const frontendCreate2Transfer = await new FrontendCreate2TransferFactory(
signer
).deploy();
await waitAndRegister(
frontendCreate2Transfer,
"frontendCreate2Transfer",
verbose
);
// deploy a chooser
let chooser: ProofOfBurn | BurnAuction;
if (parameters.USE_BURN_AUCTION) {
chooser = await new BurnAuctionFactory(signer).deploy(
parameters.DONATION_ADDRESS,
parameters.DONATION_NUMERATOR
);
} else {
chooser = await new ProofOfBurnFactory(signer).deploy();
}
await waitAndRegister(chooser, "chooser", verbose);
const blsAccountRegistry = await new BlsAccountRegistryFactory(
signer
).deploy();
await waitAndRegister(blsAccountRegistry, "blsAccountRegistry", verbose);
// deploy Token registry contract
const tokenRegistry = await new TokenRegistryFactory(signer).deploy();
await waitAndRegister(tokenRegistry, "tokenRegistry", verbose);
const massMigration = await new MassMigrationFactory(signer).deploy();
await waitAndRegister(massMigration, "mass_migs", verbose);
const transfer = await new TransferFactory(signer).deploy();
await waitAndRegister(transfer, "transfer", verbose);
const create2Transfer = await new Create2TransferFactory(signer).deploy();
await waitAndRegister(create2Transfer, "create2transfer", verbose);
// deploy example token
const exampleToken = await new ExampleTokenFactory(signer).deploy();
await waitAndRegister(exampleToken, "exampleToken", verbose);
await tokenRegistry.requestRegistration(exampleToken.address);
await tokenRegistry.finaliseRegistration(exampleToken.address);
const spokeRegistry = await new SpokeRegistryFactory(signer).deploy();
await waitAndRegister(spokeRegistry, "spokeRegistry", verbose);
const vault = await new VaultFactory(signer).deploy(
tokenRegistry.address,
spokeRegistry.address
);
await waitAndRegister(vault, "vault", verbose);
// deploy deposit manager
const depositManager = await new DepositManagerFactory(signer).deploy(
tokenRegistry.address,
vault.address,
parameters.MAX_DEPOSIT_SUBTREE_DEPTH
);
await waitAndRegister(depositManager, "depositManager", verbose);
if (!parameters.GENESIS_STATE_ROOT) throw new GenesisNotSpecified();
// deploy Rollup core
const rollup = await new RollupFactory(signer).deploy(
chooser.address,
depositManager.address,
blsAccountRegistry.address,
transfer.address,
massMigration.address,
create2Transfer.address,
parameters.GENESIS_STATE_ROOT,
parameters.STAKE_AMOUNT,
parameters.BLOCKS_TO_FINALISE,
parameters.MIN_GAS_LEFT,
parameters.MAX_TXS_PER_COMMIT
);
await waitAndRegister(rollup, "rollup", verbose);
await vault.setRollupAddress(rollup.address);
await depositManager.setRollupAddress(rollup.address);
const withdrawManager = await new WithdrawManagerFactory(signer).deploy(
tokenRegistry.address,
vault.address,
rollup.address
);
await waitAndRegister(withdrawManager, "withdrawManager", verbose);
await spokeRegistry.registerSpoke(withdrawManager.address);
return {
frontendGeneric,
frontendTransfer,
frontendMassMigration,
frontendCreate2Transfer,
blsAccountRegistry,
tokenRegistry,
transfer,
massMigration,
create2Transfer,
chooser,
exampleToken,
spokeRegistry,
vault,
depositManager,
rollup,
withdrawManager
};
}

View File

@@ -0,0 +1,50 @@
export class HubbleError extends Error {}
export class EncodingError extends HubbleError {}
export class MismatchByteLength extends HubbleError {}
export class GenesisNotSpecified extends HubbleError {}
export class UserNotExist extends HubbleError {}
export class TreeException extends HubbleError {}
class AccountTreeException extends HubbleError {}
class StateTreeExceptions extends HubbleError {}
// TreeException
export class ExceedTreeSize extends TreeException {}
export class BadMergeAlignment extends TreeException {}
export class EmptyArray extends TreeException {}
export class MismatchLength extends TreeException {}
export class MismatchHash extends TreeException {}
export class NegativeIndex extends TreeException {}
// AccountTreeException
export class RegistrationFail extends AccountTreeException {}
export class WrongBatchSize extends AccountTreeException {}
// StateTreeExceptions
export class ExceedStateTreeSize extends StateTreeExceptions {}
export class SenderNotExist extends StateTreeExceptions {}
export class ReceiverNotExist extends StateTreeExceptions {}
export class StateAlreadyExist extends StateTreeExceptions {}
export class WrongTokenID extends StateTreeExceptions {}
export class InsufficientFund extends StateTreeExceptions {}
export class ZeroAmount extends StateTreeExceptions {}

View File

@@ -0,0 +1,257 @@
import { BigNumber } from "ethers";
import {
aggregate,
BlsSigner,
BlsSignerInterface,
nullBlsSigner
} from "./blsSigner";
import { USDT } from "./decimal";
import { UserNotExist } from "./exceptions";
import { Domain, solG1 } from "./mcl";
import { State } from "./state";
import { nullProvider, StateProvider } from "./stateTree";
import {
TxTransfer,
TxCreate2Transfer,
TxMassMigration,
SignableTx
} from "./tx";
export class User {
static new(stateID: number, pubkeyID: number, domain?: Domain) {
const signer = domain ? BlsSigner.new(domain) : nullBlsSigner;
return new this(signer, stateID, pubkeyID);
}
constructor(
public blsSigner: BlsSignerInterface,
public stateID: number,
public pubkeyID: number
) {}
public sign(tx: SignableTx) {
return this.blsSigner.sign(tx.message());
}
public signRaw(message: string) {
return this.blsSigner.sign(message);
}
public connect(signer: BlsSignerInterface) {
this.blsSigner = signer;
return this;
}
get pubkey() {
return this.blsSigner.pubkey;
}
toString() {
return `<User stateID: ${this.stateID} pubkeyID: ${this.pubkeyID}>`;
}
}
interface GroupOptions {
n: number;
domain?: Domain;
stateProvider?: StateProvider;
initialStateID?: number;
initialPubkeyID?: number;
}
interface createStateOptions {
initialBalance?: BigNumber;
tokenID?: number;
zeroNonce?: boolean;
}
export class Group {
static new(options: GroupOptions) {
const initialStateID = options.initialStateID || 0;
const initialPubkeyID = options.initialPubkeyID || 0;
const stateProvider = options.stateProvider || nullProvider;
const users: User[] = [];
for (let i = 0; i < options.n; i++) {
const stateID = initialStateID + i;
const pubkeyID = initialPubkeyID + i;
users.push(User.new(stateID, pubkeyID, options.domain));
}
return new this(users, stateProvider);
}
constructor(private users: User[], private stateProvider: StateProvider) {}
public connect(provider: StateProvider) {
this.stateProvider = provider;
return this;
}
public setupSigners(domain: Domain) {
for (const user of this.users) {
const signer = BlsSigner.new(domain);
user.connect(signer);
}
}
get size() {
return this.users.length;
}
public *userIterator() {
for (const user of this.users) {
yield user;
}
}
// Useful when want to divide users into sub-groups
public *groupInterator(subgroupSize: number) {
let subgroup = [];
for (const user of this.users) {
subgroup.push(user);
if (subgroup.length == subgroupSize) {
yield new Group(subgroup, this.stateProvider);
subgroup = [];
}
}
}
public pickRandom(): { user: User; index: number } {
const index = Math.floor(Math.random() * this.users.length);
const user = this.users[index];
return { user, index };
}
public join(other: Group) {
const allUsers = [];
for (const user of this.userIterator()) {
allUsers.push(user);
}
for (const user of other.userIterator()) {
allUsers.push(user);
}
return new Group(allUsers, this.stateProvider);
}
public slice(n: number) {
if (n > this.users.length)
throw new UserNotExist(
`Want ${n} users but this group has only ${this.users.length} users`
);
return new Group(this.users.slice(0, n), this.stateProvider);
}
public getUser(i: number) {
if (i >= this.users.length) throw new UserNotExist(`${i}`);
return this.users[i];
}
public getState(user: User) {
return this.stateProvider.getState(user.stateID).state;
}
public getPubkeys() {
return this.users.map(user => user.pubkey);
}
public getPubkeyIDs() {
return this.users.map(user => user.pubkeyID);
}
public syncState(): State[] {
const states: State[] = [];
for (const user of this.users) {
const state = this.stateProvider.getState(user.stateID).state;
states.push(state);
}
return states;
}
public createStates(options?: createStateOptions) {
const initialBalance = options?.initialBalance || USDT.castInt(1000.0);
const tokenID = options?.tokenID === undefined ? 5678 : options.tokenID;
const zeroNonce = options?.zeroNonce || false;
const arbitraryInitialNonce = 9;
for (let i = 0; i < this.users.length; i++) {
const user = this.users[i];
const nonce = zeroNonce ? 0 : arbitraryInitialNonce + i;
const state = State.new(
user.pubkeyID,
tokenID,
initialBalance,
nonce
);
this.stateProvider.createState(user.stateID, state);
}
}
}
// Created n transfers from Group of Users, if n is greater than the size of the group, balance is not guaranteed to be sufficient
export function txTransferFactory(
group: Group,
n: number
): { txs: TxTransfer[]; signature: solG1; senders: User[] } {
const txs: TxTransfer[] = [];
const signatures = [];
const senders = [];
for (let i = 0; i < n; i++) {
const sender = group.getUser(i % group.size);
const receiver = group.getUser((i + 5) % group.size);
const senderState = group.getState(sender);
const amount = USDT.castBigNumber(senderState.balance.div(10));
const fee = USDT.castBigNumber(amount.div(10));
const tx = new TxTransfer(
sender.stateID,
receiver.stateID,
amount,
fee,
senderState.nonce,
USDT
);
txs.push(tx);
signatures.push(sender.sign(tx));
senders.push(sender);
}
const signature = aggregate(signatures).sol;
return { txs, signature, senders };
}
// creates N new transactions with existing sender and non-existent receiver
export function txCreate2TransferFactory(
registered: Group,
unregistered: Group
): { txs: TxCreate2Transfer[]; signature: solG1 } {
const txs: TxCreate2Transfer[] = [];
const signatures = [];
if (registered.size != unregistered.size)
throw new Error("This factory supports same number of users only");
for (let i = 0; i < registered.size; i++) {
const sender = registered.getUser(i);
const reciver = unregistered.getUser(i);
const senderState = registered.getState(sender);
const amount = USDT.castBigNumber(senderState.balance.div(10));
const fee = USDT.castBigNumber(amount.div(10));
const tx = new TxCreate2Transfer(
sender.stateID,
reciver.stateID,
reciver.pubkey,
reciver.pubkeyID,
amount,
fee,
senderState.nonce,
USDT
);
txs.push(tx);
signatures.push(sender.sign(tx));
}
const signature = aggregate(signatures).sol;
return { txs, signature };
}
export function txMassMigrationFactory(
group: Group,
spokeID = 0
): { txs: TxMassMigration[]; signature: solG1; senders: User[] } {
const txs: TxMassMigration[] = [];
const signatures = [];
const senders = [];
for (const sender of group.userIterator()) {
const senderState = group.getState(sender);
const amount = USDT.castBigNumber(senderState.balance.div(10));
const fee = USDT.castBigNumber(amount.div(10));
const tx = new TxMassMigration(
sender.stateID,
amount,
spokeID,
fee,
senderState.nonce,
USDT
);
txs.push(tx);
signatures.push(sender.sign(tx));
senders.push(sender);
}
const signature = aggregate(signatures).sol;
return { txs, signature, senders };
}

View File

@@ -0,0 +1,98 @@
import { BigNumber } from "ethers";
import { sha256, arrayify, zeroPad } from "ethers/lib/utils";
export const FIELD_ORDER = BigNumber.from(
"0x30644e72e131a029b85045b68181585d97816a916871ca8d3c208c16d87cfd47"
);
export function hashToField(
domain: Uint8Array,
msg: Uint8Array,
count: number
): BigNumber[] {
const u = 48;
const _msg = expandMsg(domain, msg, count * u);
const els = [];
for (let i = 0; i < count; i++) {
const el = BigNumber.from(_msg.slice(i * u, (i + 1) * u)).mod(
FIELD_ORDER
);
els.push(el);
}
return els;
}
export function expandMsg(
domain: Uint8Array,
msg: Uint8Array,
outLen: number
): Uint8Array {
if (domain.length > 32) {
throw new Error("bad domain size");
}
const out: Uint8Array = new Uint8Array(outLen);
const len0 = 64 + msg.length + 2 + 1 + domain.length + 1;
const in0: Uint8Array = new Uint8Array(len0);
// zero pad
let off = 64;
// msg
in0.set(msg, off);
off += msg.length;
// l_i_b_str
in0.set([(outLen >> 8) & 0xff, outLen & 0xff], off);
off += 2;
// I2OSP(0, 1)
in0.set([0], off);
off += 1;
// DST_prime
in0.set(domain, off);
off += domain.length;
in0.set([domain.length], off);
const b0 = sha256(in0);
const len1 = 32 + 1 + domain.length + 1;
const in1: Uint8Array = new Uint8Array(len1);
// b0
in1.set(arrayify(b0), 0);
off = 32;
// I2OSP(1, 1)
in1.set([1], off);
off += 1;
// DST_prime
in1.set(domain, off);
off += domain.length;
in1.set([domain.length], off);
const b1 = sha256(in1);
// b_i = H(strxor(b_0, b_(i - 1)) || I2OSP(i, 1) || DST_prime);
const ell = Math.floor((outLen + 32 - 1) / 32);
let bi = b1;
for (let i = 1; i < ell; i++) {
const ini: Uint8Array = new Uint8Array(32 + 1 + domain.length + 1);
const nb0 = zeroPad(arrayify(b0), 32);
const nbi = zeroPad(arrayify(bi), 32);
const tmp = new Uint8Array(32);
for (let i = 0; i < 32; i++) {
tmp[i] = nb0[i] ^ nbi[i];
}
ini.set(tmp, 0);
let off = 32;
ini.set([1 + i], off);
off += 1;
ini.set(domain, off);
off += domain.length;
ini.set([domain.length], off);
out.set(arrayify(bi), 32 * (i - 1));
bi = sha256(ini);
}
out.set(arrayify(bi), 32 * (ell - 1));
return out;
}

View File

@@ -0,0 +1,139 @@
import { allContracts } from "./allContractsInterfaces";
import { DeploymentParameters } from "./interfaces";
import fs from "fs";
import {
BlsAccountRegistryFactory,
BurnAuctionFactory,
Create2TransferFactory,
DepositManagerFactory,
ExampleTokenFactory,
FrontendCreate2TransferFactory,
FrontendGenericFactory,
FrontendMassMigrationFactory,
FrontendTransferFactory,
MassMigrationFactory,
ProofOfBurnFactory,
RollupFactory,
SpokeRegistryFactory,
TokenRegistryFactory,
TransferFactory,
VaultFactory,
WithdrawManagerFactory
} from "../types/ethers-contracts";
import { ethers, Signer } from "ethers";
import { solG2 } from "./mcl";
import { toWei } from "./utils";
function parseGenesis(
parameters: DeploymentParameters,
addresses: { [key: string]: string },
signer: Signer
): allContracts {
const factories = {
frontendGeneric: FrontendGenericFactory,
frontendTransfer: FrontendTransferFactory,
frontendMassMigration: FrontendMassMigrationFactory,
frontendCreate2Transfer: FrontendCreate2TransferFactory,
blsAccountRegistry: BlsAccountRegistryFactory,
tokenRegistry: TokenRegistryFactory,
transfer: TransferFactory,
massMigration: MassMigrationFactory,
create2Transfer: Create2TransferFactory,
exampleToken: ExampleTokenFactory,
spokeRegistry: SpokeRegistryFactory,
vault: VaultFactory,
depositManager: DepositManagerFactory,
rollup: RollupFactory,
withdrawManager: WithdrawManagerFactory,
chooser: parameters.USE_BURN_AUCTION
? BurnAuctionFactory
: ProofOfBurnFactory
};
const contracts: any = {};
for (const [key, factory] of Object.entries(factories)) {
const address = addresses[key];
if (!address) throw `Bad Genesis: Find no address for ${key} contract`;
contracts[key] = factory.connect(address, signer);
}
return contracts;
}
export class Hubble {
private constructor(
public parameters: DeploymentParameters,
public contracts: allContracts,
public signer: Signer
) {}
static fromGenesis(
parameters: DeploymentParameters,
addresses: { [key: string]: string },
signer: Signer
) {
const contracts = parseGenesis(parameters, addresses, signer);
return new Hubble(parameters, contracts, signer);
}
static fromDefault(
providerUrl = "http://localhost:8545",
genesisPath = "./genesis.json"
) {
const genesis = fs.readFileSync(genesisPath).toString();
const { parameters, addresses } = JSON.parse(genesis);
const provider = new ethers.providers.JsonRpcProvider(providerUrl);
const signer = provider.getSigner();
return Hubble.fromGenesis(parameters, addresses, signer);
}
async registerPublicKeys(pubkeys: string[]) {
const registry = this.contracts.blsAccountRegistry;
const accountIDs: number[] = [];
console.log(`Registering ${pubkeys.length} public keys`);
for (const pubkeyRaw of pubkeys) {
const parsedPubkey: solG2 = [
"0x" + pubkeyRaw.slice(64, 128),
"0x" + pubkeyRaw.slice(0, 64),
"0x" + pubkeyRaw.slice(192, 256),
"0x" + pubkeyRaw.slice(128, 192)
];
console.log("Registering", parsedPubkey);
const accID = await registry.callStatic.register(parsedPubkey);
const tx = await registry.register(parsedPubkey);
await tx.wait();
accountIDs.push(accID.toNumber());
console.log(
"Done registering pubkey",
pubkeyRaw.slice(0, 5),
accID.toNumber()
);
}
return accountIDs;
}
async depositFor(pubkeyIDs: number[], tokenID: number, amount: number) {
console.log(
`Depositing tokenID ${tokenID} for pubkeyID ${pubkeyIDs} each with amount ${amount}`
);
const { tokenRegistry, depositManager } = this.contracts;
const tokenAddress = await tokenRegistry.safeGetAddress(tokenID);
const erc20 = ExampleTokenFactory.connect(tokenAddress, this.signer);
// approve depositmanager for amount
const totalAmount = pubkeyIDs.length * amount;
console.log("Approving total amount", totalAmount);
const approveTx = await erc20.approve(
depositManager.address,
toWei(totalAmount.toString())
);
await approveTx.wait();
console.log("token approved", approveTx.hash.toString());
for (const pubkeyID of pubkeyIDs) {
console.log(`Depositing ${amount} for pubkeyID ${pubkeyID}`);
const tx = await depositManager.depositFor(
pubkeyID,
amount,
tokenID
);
await tx.wait();
}
}
}

View File

@@ -0,0 +1,34 @@
export type Wei = string;
export interface DeploymentParameters {
MAX_DEPTH: number;
MAX_DEPOSIT_SUBTREE_DEPTH: number;
STAKE_AMOUNT: Wei;
BLOCKS_TO_FINALISE: number;
MIN_GAS_LEFT: number;
MAX_TXS_PER_COMMIT: number;
USE_BURN_AUCTION: boolean;
GENESIS_STATE_ROOT?: string;
DONATION_ADDRESS: string;
DONATION_NUMERATOR: number;
}
export enum Usage {
Genesis,
Transfer,
MassMigration
}
export enum Result {
Ok,
InvalidTokenAmount,
NotEnoughTokenBalance,
BadFromTokenID,
BadToTokenID,
BadSignature,
MismatchedAmount,
BadWithdrawRoot,
BadCompression,
TooManyTx
}

View File

@@ -0,0 +1,145 @@
const mcl = require("mcl-wasm");
import { BigNumber, ethers } from "ethers";
import { FIELD_ORDER, randHex } from "./utils";
import { hashToField } from "./hashToField";
import { arrayify, hexlify } from "ethers/lib/utils";
export type mclG2 = any;
export type mclG1 = any;
export type mclFP = any;
export type mclFR = any;
export type SecretKey = mclFR;
export type MessagePoint = mclG1;
export type Signature = mclG1;
export type PublicKey = mclG2;
export type solG1 = [string, string];
export type solG2 = [string, string, string, string];
export interface keyPair {
pubkey: PublicKey;
secret: SecretKey;
}
export type Domain = Uint8Array;
export async function init() {
await mcl.init(mcl.BN_SNARK1);
mcl.setMapToMode(mcl.BN254);
}
export function validateDomain(domain: Domain) {
if (domain.length != 32) {
throw new Error("bad domain length");
}
}
export function hashToPoint(msg: string, domain: Domain): MessagePoint {
if (!ethers.utils.isHexString(msg)) {
throw new Error("message is expected to be hex string");
}
const _msg = arrayify(msg);
const [e0, e1] = hashToField(domain, _msg, 2);
const p0 = mapToPoint(e0);
const p1 = mapToPoint(e1);
const p = mcl.add(p0, p1);
p.normalize();
return p;
}
export function mapToPoint(e0: BigNumber): mclG1 {
let e1 = new mcl.Fp();
e1.setStr(e0.mod(FIELD_ORDER).toString());
return e1.mapToG1();
}
export function toBigEndian(p: mclFP): Uint8Array {
// serialize() gets a little-endian output of Uint8Array
// reverse() turns it into big-endian, which Solidity likes
return p.serialize().reverse();
}
export function g1(): mclG1 {
const g1 = new mcl.G1();
g1.setStr("1 0x01 0x02", 16);
return g1;
}
export function g2(): mclG2 {
const g2 = new mcl.G2();
g2.setStr(
"1 0x1800deef121f1e76426a00665e5c4479674322d4f75edadd46debd5cd992f6ed 0x198e9393920d483a7260bfb731fb5d25f1aa493335a9e71297e485b7aef312c2 0x12c85ea5db8c6deb4aab71808dcb408fe3d1e7690c43d37b4ce6cc0166fa7daa 0x090689d0585ff075ec9e99ad690c3395bc4b313370b38ef355acdadcd122975b"
);
return g2;
}
export function g1ToHex(p: mclG1): solG1 {
p.normalize();
const x = hexlify(toBigEndian(p.getX()));
const y = hexlify(toBigEndian(p.getY()));
return [x, y];
}
export function g2ToHex(p: mclG2): solG2 {
p.normalize();
const x = toBigEndian(p.getX());
const x0 = hexlify(x.slice(32));
const x1 = hexlify(x.slice(0, 32));
const y = toBigEndian(p.getY());
const y0 = hexlify(y.slice(32));
const y1 = hexlify(y.slice(0, 32));
return [x0, x1, y0, y1];
}
export function getPubkey(secret: SecretKey): PublicKey {
const pubkey = mcl.mul(g2(), secret);
pubkey.normalize();
return pubkey;
}
export function newKeyPair(): keyPair {
const secret = randFr();
const pubkey = getPubkey(secret);
return { pubkey, secret };
}
export function sign(
message: string,
secret: SecretKey,
domain: Domain
): { signature: Signature; messagePoint: MessagePoint } {
const messagePoint = hashToPoint(message, domain);
const signature = mcl.mul(messagePoint, secret);
signature.normalize();
return { signature, messagePoint };
}
export function aggregateRaw(signatures: Signature[]): Signature {
let aggregated = new mcl.G1();
for (const sig of signatures) {
aggregated = mcl.add(aggregated, sig);
}
aggregated.normalize();
return aggregated;
}
export function randFr(): mclFR {
const r = randHex(12);
let fr = new mcl.Fr();
fr.setHashOf(r);
return fr;
}
export function randG1(): solG1 {
const p = mcl.mul(g1(), randFr());
p.normalize();
return g1ToHex(p);
}
export function randG2(): solG2 {
const p = mcl.mul(g2(), randFr());
p.normalize();
return g2ToHex(p);
}

View File

@@ -0,0 +1,40 @@
import { BigNumber, BigNumberish, ethers } from "ethers";
import { solidityPack } from "ethers/lib/utils";
export class State {
public static new(
pubkeyID: number,
tokenID: number,
balance: BigNumberish,
nonce: number
): State {
return new State(pubkeyID, tokenID, BigNumber.from(balance), nonce);
}
public clone() {
return new State(this.pubkeyID, this.tokenID, this.balance, this.nonce);
}
constructor(
public pubkeyID: number,
public tokenID: number,
public balance: BigNumber,
public nonce: number
) {}
public encode(): string {
return solidityPack(
["uint256", "uint256", "uint256", "uint256"],
[this.pubkeyID, this.tokenID, this.balance, this.nonce]
);
}
public toStateLeaf(): string {
return ethers.utils.solidityKeccak256(
["uint256", "uint256", "uint256", "uint256"],
[this.pubkeyID, this.tokenID, this.balance, this.nonce]
);
}
}
export const ZERO_STATE = State.new(0, 0, 0, 0);

View File

@@ -0,0 +1,382 @@
import { Hasher, Tree } from "./tree";
import { State, ZERO_STATE } from "./state";
import { TxTransfer, TxMassMigration, TxCreate2Transfer } from "./tx";
import { BigNumber, constants } from "ethers";
import { ZERO_BYTES32 } from "./constants";
import { minTreeDepth, sum } from "./utils";
import {
ExceedStateTreeSize,
InsufficientFund,
ReceiverNotExist,
SenderNotExist,
StateAlreadyExist,
WrongTokenID,
ZeroAmount
} from "./exceptions";
export interface StateProvider {
getState(stateID: number): SolStateMerkleProof;
createState(stateID: number, state: State): void;
root: string;
}
class NullProvider implements StateProvider {
getState(stateID: number): SolStateMerkleProof {
throw new Error(
"This is a NullProvider, please connect to a real provider"
);
}
createState(stateID: number, state: State) {
throw new Error(
"This is a NullProvider, please connect to a real provider"
);
}
get root(): string {
throw new Error(
"This is a NullProvider, please connect to a real provider"
);
}
}
export const nullProvider = new NullProvider();
interface SolStateMerkleProof {
state: State;
witness: string[];
}
const STATE_WITNESS_LENGHT = 32;
const PLACEHOLDER_PROOF_WITNESS = Array(STATE_WITNESS_LENGHT).fill(
constants.HashZero
);
const PLACEHOLDER_SOL_STATE_PROOF: SolStateMerkleProof = {
state: ZERO_STATE,
witness: PLACEHOLDER_PROOF_WITNESS
};
function applySender(sender: State, decrement: BigNumber): State {
const state = sender.clone();
state.balance = sender.balance.sub(decrement);
state.nonce = sender.nonce + 1;
return state;
}
function applyReceiver(receiver: State, increment: BigNumber): State {
const state = receiver.clone();
state.balance = receiver.balance.add(increment);
return state;
}
function processNoRaise(
generator: Generator<SolStateMerkleProof>,
expectedNumProofs: number
): { proofs: SolStateMerkleProof[]; safe: boolean } {
let proofs: SolStateMerkleProof[] = [];
let safe = true;
for (let i = 0; i < expectedNumProofs; i++) {
if (!safe) {
proofs.push(PLACEHOLDER_SOL_STATE_PROOF);
continue;
}
try {
proofs.push(generator.next().value);
} catch (error) {
safe = false;
}
}
return { proofs, safe };
}
export class StateTree implements StateProvider {
public static new(stateDepth: number) {
return new StateTree(stateDepth);
}
private stateTree: Tree;
private states: { [key: number]: State } = {};
constructor(stateDepth: number) {
this.stateTree = Tree.new(
stateDepth,
Hasher.new("bytes", ZERO_BYTES32)
);
}
private checkSize(stateID: number) {
if (stateID >= this.stateTree.setSize)
throw new ExceedStateTreeSize(
`Want stateID ${stateID} but the tree has only ${this.stateTree.setSize} leaves`
);
}
public getState(stateID: number): SolStateMerkleProof {
this.checkSize(stateID);
const state = this.states[stateID] || ZERO_STATE;
const witness = this.stateTree.witness(stateID).nodes;
return { state, witness };
}
/** Side effect! */
private updateState(stateID: number, state: State) {
this.checkSize(stateID);
this.states[stateID] = state;
this.stateTree.updateSingle(stateID, state.toStateLeaf());
}
public getVacancyProof(mergeOffsetLower: number, subtreeDepth: number) {
const witness = this.stateTree.witnessForBatch(
mergeOffsetLower,
subtreeDepth
);
const pathAtDepth = mergeOffsetLower >> subtreeDepth;
return {
witness: witness.nodes,
depth: subtreeDepth,
pathAtDepth
};
}
public depth() {
return this.stateTree.depth;
}
public createState(stateID: number, state: State) {
if (this.states[stateID])
throw new StateAlreadyExist(`stateID: ${stateID}`);
this.updateState(stateID, state);
}
public createStateBulk(firstStateID: number, states: State[]) {
for (const [i, state] of states.entries()) {
this.createState(firstStateID + i, state);
}
return this;
}
public get root() {
return this.stateTree.root;
}
private *_processTransferCommit(
txs: TxTransfer[],
feeReceiverID: number
): Generator<SolStateMerkleProof> {
const tokenID = this.states[txs[0].fromIndex].tokenID;
for (const tx of txs) {
const [senderProof, receiverProof] = this.processTransfer(
tx,
tokenID
);
yield senderProof;
yield receiverProof;
}
const proof = this.processReceiver(
feeReceiverID,
sum(txs.map(tx => tx.fee)),
tokenID
);
yield proof;
return;
}
public processTransferCommit(
txs: TxTransfer[],
feeReceiverID: number,
raiseError: boolean = true
): {
proofs: SolStateMerkleProof[];
safe: boolean;
} {
const generator = this._processTransferCommit(txs, feeReceiverID);
if (raiseError) {
return { proofs: Array.from(generator), safe: true };
} else {
return processNoRaise(generator, txs.length * 2 + 1);
}
}
private *_processCreate2TransferCommit(
txs: TxCreate2Transfer[],
feeReceiverID: number
): Generator<SolStateMerkleProof> {
const tokenID = this.states[txs[0].fromIndex].tokenID;
for (const tx of txs) {
const [senderProof, receiverProof] = this.processCreate2Transfer(
tx,
tokenID
);
yield senderProof;
yield receiverProof;
}
const proof = this.processReceiver(
feeReceiverID,
sum(txs.map(tx => tx.fee)),
tokenID
);
yield proof;
return;
}
public processCreate2TransferCommit(
txs: TxCreate2Transfer[],
feeReceiverID: number,
raiseError: boolean = true
): {
proofs: SolStateMerkleProof[];
safe: boolean;
} {
const generator = this._processCreate2TransferCommit(
txs,
feeReceiverID
);
if (raiseError) {
return { proofs: Array.from(generator), safe: true };
} else {
return processNoRaise(generator, txs.length * 2 + 1);
}
}
private *_processMassMigrationCommit(
txs: TxMassMigration[],
feeReceiverID: number
): Generator<SolStateMerkleProof> {
const tokenID = this.states[txs[0].fromIndex].tokenID;
for (const tx of txs) {
const proof = this.processMassMigration(tx, tokenID);
yield proof;
}
const proof = this.processReceiver(
feeReceiverID,
sum(txs.map(tx => tx.fee)),
tokenID
);
yield proof;
return;
}
public processMassMigrationCommit(
txs: TxMassMigration[],
feeReceiverID: number,
raiseError: boolean = true
): {
proofs: SolStateMerkleProof[];
safe: boolean;
} {
const generator = this._processMassMigrationCommit(txs, feeReceiverID);
if (raiseError) {
return { proofs: Array.from(generator), safe: true };
} else {
return processNoRaise(generator, txs.length + 1);
}
}
public processTransfer(
tx: TxTransfer,
tokenID: number
): SolStateMerkleProof[] {
const senderProof = this.processSender(
tx.fromIndex,
tokenID,
tx.amount,
tx.fee
);
const receiverProof = this.processReceiver(
tx.toIndex,
tx.amount,
tokenID
);
return [senderProof, receiverProof];
}
public processMassMigration(
tx: TxMassMigration,
tokenID: number
): SolStateMerkleProof {
return this.processSender(tx.fromIndex, tokenID, tx.amount, tx.fee);
}
public processCreate2Transfer(
tx: TxCreate2Transfer,
tokenID: number
): SolStateMerkleProof[] {
const senderProof = this.processSender(
tx.fromIndex,
tokenID,
tx.amount,
tx.fee
);
const receiverProof = this.processCreate(
tx.toIndex,
tx.toPubkeyID,
tx.amount,
tokenID
);
return [senderProof, receiverProof];
}
private getProofAndUpdate(
stateID: number,
postState: State
): SolStateMerkleProof {
const proofBeforeUpdate = this.getState(stateID);
this.updateState(stateID, postState);
return proofBeforeUpdate;
}
public processSender(
senderIndex: number,
tokenID: number,
amount: BigNumber,
fee: BigNumber
): SolStateMerkleProof {
const state = this.states[senderIndex];
if (!state) throw new SenderNotExist(`stateID: ${senderIndex}`);
if (amount.isZero()) throw new ZeroAmount();
const decrement = amount.add(fee);
if (state.balance.lt(decrement))
throw new InsufficientFund(
`balance: ${state.balance}, tx amount+fee: ${decrement}`
);
if (state.tokenID != tokenID)
throw new WrongTokenID(
`Tx tokenID: ${tokenID}, State tokenID: ${state.tokenID}`
);
const postState = applySender(state, decrement);
const proof = this.getProofAndUpdate(senderIndex, postState);
return proof;
}
public processReceiver(
receiverIndex: number,
increment: BigNumber,
tokenID: number
): SolStateMerkleProof {
const state = this.states[receiverIndex];
if (!state) throw new ReceiverNotExist(`stateID: ${receiverIndex}`);
if (state.tokenID != tokenID)
throw new WrongTokenID(
`Tx tokenID: ${tokenID}, State tokenID: ${state.tokenID}`
);
const postState = applyReceiver(state, increment);
const proof = this.getProofAndUpdate(receiverIndex, postState);
return proof;
}
public processCreate(
createIndex: number,
pubkeyID: number,
balance: BigNumber,
tokenID: number
): SolStateMerkleProof {
if (this.states[createIndex] !== undefined)
throw new StateAlreadyExist(`stateID: ${createIndex}`);
const postState = State.new(pubkeyID, tokenID, balance, 0);
const proof = this.getProofAndUpdate(createIndex, postState);
return proof;
}
}
export class MigrationTree extends StateTree {
public static fromStates(states: State[]) {
const depth = minTreeDepth(states.length);
return new this(depth).createStateBulk(0, states);
}
public getWithdrawProof(stateID: number) {
const { state, witness } = this.getState(stateID);
return { state, witness, path: stateID };
}
}

View File

@@ -0,0 +1,34 @@
import { ethers } from "ethers";
export type Node = string;
const ZERO =
"0x0000000000000000000000000000000000000000000000000000000000000000";
export class Hasher {
static new(leafType = "uint256", zero = ZERO): Hasher {
return new Hasher(leafType, zero);
}
constructor(private leafType = "uint256", private zero = ZERO) {}
public toLeaf(data: string): string {
return ethers.utils.solidityKeccak256([this.leafType], [data]);
}
public hash(x0: string): string {
return ethers.utils.solidityKeccak256(["uint256"], [x0]);
}
public hash2(x0: string, x1: string): string {
return ethers.utils.solidityKeccak256(["uint256", "uint256"], [x0, x1]);
}
public zeros(depth: number): Array<Node> {
const N = depth + 1;
const zeros = Array(N).fill(this.zero);
for (let i = 1; i < N; i++) {
zeros[N - 1 - i] = this.hash2(zeros[N - i], zeros[N - i]);
}
return zeros;
}
}

View File

@@ -0,0 +1,2 @@
export { Hasher, Node } from "./hasher";
export { Tree, Data, Witness } from "./tree";

View File

@@ -0,0 +1,216 @@
import { ZERO_BYTES32 } from "../constants";
import { minTreeDepth } from "../utils";
import {
BadMergeAlignment,
EmptyArray,
ExceedTreeSize,
MismatchHash,
MismatchLength,
NegativeIndex
} from "../exceptions";
import { Hasher, Node } from "./hasher";
type Level = { [node: number]: Node };
export type Data = string;
export type Witness = {
path: Array<boolean>;
nodes: Array<Node>;
leaf: Node;
index: number;
data?: Data;
depth?: number;
};
export class Tree {
public readonly zeros: Array<Node>;
public readonly depth: number;
public readonly setSize: number;
public readonly hasher: Hasher;
private readonly tree: Array<Level> = [];
public static new(depth: number, hasher?: Hasher): Tree {
return new Tree(depth, hasher || Hasher.new());
}
public static merklize(leaves: Node[]): Tree {
const depth = minTreeDepth(leaves.length);
// This ZERO_BYTES32 must match the one we use in the mekle tree utils contract
const hasher = Hasher.new("bytes", ZERO_BYTES32);
const tree = Tree.new(depth, hasher);
tree.updateBatch(0, leaves);
return tree;
}
constructor(depth: number, hasher: Hasher) {
this.depth = depth;
this.setSize = 2 ** this.depth;
this.tree = [];
for (let i = 0; i < depth + 1; i++) {
this.tree.push({});
}
this.hasher = hasher;
this.zeros = this.hasher.zeros(depth);
}
get root(): Node {
return this.tree[0][0] || this.zeros[0];
}
public getNode(level: number, index: number): Node {
return this.tree[level][index] || this.zeros[level];
}
// witnessForBatch given merging subtree offset and depth constructs a witness
public witnessForBatch(
mergeOffsetLower: number,
subtreeDepth: number
): Witness {
const mergeSize = 1 << subtreeDepth;
const mergeOffsetUpper = mergeOffsetLower + mergeSize;
const pathFollower = mergeOffsetLower >> subtreeDepth;
const subtreeRootIndexUpper = (mergeOffsetUpper - 1) >> subtreeDepth;
if (pathFollower != subtreeRootIndexUpper)
throw new BadMergeAlignment(
`pathFollower ${pathFollower}; subtreeRootIndexUpper ${subtreeRootIndexUpper}`
);
return this.witness(pathFollower, this.depth - subtreeDepth);
}
// witness given index and depth constructs a witness
public witness(index: number, depth: number = this.depth): Witness {
const path = Array<boolean>(depth);
const nodes = Array<Node>(depth);
let nodeIndex = index;
const leaf = this.getNode(depth, nodeIndex);
for (let i = 0; i < depth; i++) {
nodeIndex ^= 1;
nodes[i] = this.getNode(depth - i, nodeIndex);
path[i] = (nodeIndex & 1) == 1;
nodeIndex >>= 1;
}
return { path, nodes, leaf, index, depth };
}
// checkInclusion verifies the given witness.
// It performs root calculation rather than just looking up for the leaf or node
public checkInclusion(witness: Witness): boolean {
// we check the form of witness data rather than looking up for the leaf
if (witness.nodes.length == 0) throw new EmptyArray();
if (witness.nodes.length != witness.path.length)
throw new MismatchLength(
`nodes: ${witness.nodes.length}; path: ${witness.path.length}`
);
const data = witness.data;
if (data) {
if (witness.nodes.length != this.depth)
throw new MismatchLength(
`nodes: ${witness.nodes.length}; tree depth: ${this.depth}`
);
const dataHash = this.hasher.hash(data);
if (dataHash != witness.leaf)
throw new MismatchHash(
`hash(data): ${dataHash}; leaf: ${witness.leaf}`
);
}
const depth = witness.depth ? witness.depth : this.depth;
let leaf = witness.leaf;
for (let i = 0; i < depth; i++) {
const node = witness.nodes[i];
if (witness.path[i]) {
leaf = this.hasher.hash2(leaf, node);
} else {
leaf = this.hasher.hash2(node, leaf);
}
}
return leaf == this.root;
}
private checkSetSize(index: number) {
if (index >= this.setSize)
throw new ExceedTreeSize(
`Leaf index ${index}; tree size ${this.setSize}`
);
// Probably an overflow if this error is hit
if (index < 0) throw new NegativeIndex(`${index}`);
}
// insertSingle updates tree with a single raw data at given index
public insertSingle(leafIndex: number, data: Data) {
this.checkSetSize(leafIndex);
this.tree[this.depth][leafIndex] = this.hasher.toLeaf(data);
this.ascend(leafIndex, 1);
}
// updateSingle updates tree with a leaf at given index
public updateSingle(leafIndex: number, leaf: Node) {
this.checkSetSize(leafIndex);
this.tree[this.depth][leafIndex] = leaf;
this.ascend(leafIndex, 1);
}
// insertBatch given multiple raw data updates tree ascending from an offset
public insertBatch(offset: number, data: Array<Data>) {
const len = data.length;
if (len == 0) throw new EmptyArray();
const lastIndex = len + offset - 1;
this.checkSetSize(lastIndex);
for (let i = 0; i < len; i++) {
this.tree[this.depth][offset + i] = this.hasher.toLeaf(data[i]);
}
this.ascend(offset, len);
}
// updateBatch given multiple sequencial data updates tree ascending from an offset
public updateBatch(offset: number, leaves: Array<Node>) {
const len = leaves.length;
if (len == 0) throw new EmptyArray();
const lastIndex = len + offset - 1;
this.checkSetSize(lastIndex);
for (let i = 0; i < len; i++) {
this.tree[this.depth][offset + i] = leaves[i];
}
this.ascend(offset, len);
}
public isZero(level: number, leafIndex: number): boolean {
return this.zeros[level] == this.getNode(level, leafIndex);
}
private ascend(offset: number, len: number) {
for (let level = this.depth; level > 0; level--) {
if (offset & 1) {
offset -= 1;
len += 1;
}
if (len & 1) {
len += 1;
}
for (let node = offset; node < offset + len; node += 2) {
this.updateCouple(level, node);
}
offset >>= 1;
len >>= 1;
}
}
private updateCouple(level: number, leafIndex: number) {
const n = this.hashCouple(level, leafIndex);
this.tree[level - 1][leafIndex >> 1] = n;
}
private hashCouple(level: number, leafIndex: number) {
const X = this.getCouple(level, leafIndex);
return this.hasher.hash2(X.l, X.r);
}
private getCouple(level: number, index: number): { l: Node; r: Node } {
index = index & ~1;
return {
l: this.getNode(level, index),
r: this.getNode(level, index + 1)
};
}
}

View File

@@ -0,0 +1,335 @@
import { BigNumber } from "ethers";
import { randomNum } from "./utils";
import { DecimalCodec, USDT } from "./decimal";
import { MismatchByteLength } from "./exceptions";
import { hexZeroPad, concat, hexlify, solidityPack } from "ethers/lib/utils";
import { COMMIT_SIZE } from "./constants";
const amountLen = 2;
const feeLen = 2;
const stateIDLen = 4;
const nonceLen = 4;
const spokeLen = 4;
export interface Tx {
encode(prefix?: boolean): string;
encodeOffchain(): string;
}
export interface SignableTx extends Tx {
message(): string;
}
export interface OffchainTransfer {
txType: string;
fromIndex: number;
toIndex: number;
amount: BigNumber;
fee: BigNumber;
nonce: number;
}
export interface OffchainMassMigration {
txType: string;
fromIndex: number;
amount: BigNumber;
fee: BigNumber;
spokeID: number;
nonce: number;
}
export interface OffchainCreate2Transfer {
txType: string;
fromIndex: number;
toIndex: number;
toPubkeyID: number;
amount: BigNumber;
fee: BigNumber;
nonce: number;
}
export function serialize(txs: Tx[]): string {
return hexlify(concat(txs.map(tx => tx.encode())));
}
function checkByteLength(
decimal: DecimalCodec,
fieldName: string,
expected: number
) {
if (decimal.bytesLength != expected) {
throw new MismatchByteLength(
`Deciaml: ${decimal.bytesLength} bytes, ${fieldName}: ${expected} bytes`
);
}
}
export class TxTransfer implements SignableTx {
private readonly TX_TYPE = "0x01";
public static rand(): TxTransfer {
const sender = randomNum(stateIDLen);
const receiver = randomNum(stateIDLen);
const amount = USDT.randInt();
const fee = USDT.randInt();
const nonce = randomNum(nonceLen);
return new TxTransfer(sender, receiver, amount, fee, nonce, USDT);
}
public static buildList(n: number = COMMIT_SIZE): TxTransfer[] {
const txs = [];
for (let i = 0; i < n; i++) {
txs.push(TxTransfer.rand());
}
return txs;
}
constructor(
public readonly fromIndex: number,
public readonly toIndex: number,
public readonly amount: BigNumber,
public readonly fee: BigNumber,
public nonce: number,
public readonly decimal: DecimalCodec
) {
checkByteLength(decimal, "amount", amountLen);
checkByteLength(decimal, "fee", feeLen);
}
public message(): string {
return solidityPack(
["uint256", "uint256", "uint256", "uint256", "uint256", "uint256"],
[
this.TX_TYPE,
this.fromIndex,
this.toIndex,
this.nonce,
this.amount,
this.fee
]
);
}
public encodeOffchain() {
return solidityPack(
["uint256", "uint256", "uint256", "uint256", "uint256", "uint256"],
[
this.TX_TYPE,
this.fromIndex,
this.toIndex,
this.amount,
this.fee,
this.nonce
]
);
}
public offchain(): OffchainTransfer {
return {
txType: this.TX_TYPE,
fromIndex: this.fromIndex,
toIndex: this.toIndex,
amount: this.amount,
fee: this.fee,
nonce: this.nonce
};
}
public encode(): string {
const concated = concat([
hexZeroPad(hexlify(this.fromIndex), stateIDLen),
hexZeroPad(hexlify(this.toIndex), stateIDLen),
this.decimal.encodeInt(this.amount),
this.decimal.encodeInt(this.fee)
]);
return hexlify(concated);
}
}
export class TxMassMigration implements SignableTx {
private readonly TX_TYPE = "0x05";
public static rand(): TxMassMigration {
const sender = randomNum(stateIDLen);
const amount = USDT.randInt();
const fee = USDT.randInt();
const nonce = randomNum(nonceLen);
const spokeID = randomNum(spokeLen);
return new TxMassMigration(sender, amount, spokeID, fee, nonce, USDT);
}
public static buildList(n: number = COMMIT_SIZE): TxMassMigration[] {
const txs = [];
for (let i = 0; i < n; i++) {
txs.push(TxMassMigration.rand());
}
return txs;
}
constructor(
public readonly fromIndex: number,
public readonly amount: BigNumber,
public readonly spokeID: number,
public readonly fee: BigNumber,
public nonce: number,
public readonly decimal: DecimalCodec
) {
checkByteLength(decimal, "amount", amountLen);
checkByteLength(decimal, "fee", feeLen);
}
public message(): string {
return solidityPack(
["uint8", "uint32", "uint256", "uint256", "uint32", "uint32"],
[
this.TX_TYPE,
this.fromIndex,
this.amount,
this.fee,
this.nonce,
this.spokeID
]
);
}
public encodeOffchain() {
return solidityPack(
["uint256", "uint256", "uint256", "uint256", "uint256", "uint256"],
[
this.TX_TYPE,
this.fromIndex,
this.amount,
this.fee,
this.spokeID,
this.nonce
]
);
}
public offchain(): OffchainMassMigration {
return {
txType: this.TX_TYPE,
fromIndex: this.fromIndex,
amount: this.amount,
fee: this.fee,
spokeID: this.spokeID,
nonce: this.nonce
};
}
public encode(): string {
const concated = concat([
hexZeroPad(hexlify(this.fromIndex), stateIDLen),
this.decimal.encodeInt(this.amount),
this.decimal.encodeInt(this.fee)
]);
return hexlify(concated);
}
}
export class TxCreate2Transfer implements SignableTx {
private readonly TX_TYPE = "0x03";
public static rand(): TxCreate2Transfer {
const sender = randomNum(stateIDLen);
const receiver = randomNum(stateIDLen);
const receiverPub: string[] = ["0x00", "0x00", "0x00", "0x00"];
const toPubkeyID = randomNum(stateIDLen);
const amount = USDT.randInt();
const fee = USDT.randInt();
const nonce = randomNum(nonceLen);
return new TxCreate2Transfer(
sender,
receiver,
receiverPub,
toPubkeyID,
amount,
fee,
nonce,
USDT
);
}
public static buildList(n: number = COMMIT_SIZE): TxCreate2Transfer[] {
const txs = [];
for (let i = 0; i < n; i++) {
txs.push(TxCreate2Transfer.rand());
}
return txs;
}
constructor(
public readonly fromIndex: number,
public readonly toIndex: number,
public toPubkey: string[],
public readonly toPubkeyID: number,
public readonly amount: BigNumber,
public readonly fee: BigNumber,
public nonce: number,
public readonly decimal: DecimalCodec
) {
checkByteLength(decimal, "amount", amountLen);
checkByteLength(decimal, "fee", feeLen);
}
public message(): string {
return solidityPack(
[
"uint256",
"uint256",
"uint256[4]",
"uint256",
"uint256",
"uint256"
],
[
this.TX_TYPE,
this.fromIndex,
this.toPubkey,
this.nonce,
this.amount,
this.fee
]
);
}
public encodeOffchain() {
return solidityPack(
[
"uint256",
"uint256",
"uint256",
"uint256",
"uint256",
"uint256",
"uint256"
],
[
this.TX_TYPE,
this.fromIndex,
this.toIndex,
this.toPubkeyID,
this.amount,
this.fee,
this.nonce
]
);
}
public offchain(): OffchainCreate2Transfer {
return {
txType: this.TX_TYPE,
fromIndex: this.fromIndex,
toIndex: this.toIndex,
toPubkeyID: this.toPubkeyID,
amount: this.amount,
fee: this.fee,
nonce: this.nonce
};
}
public encode(): string {
const concated = concat([
hexZeroPad(hexlify(this.fromIndex), stateIDLen),
hexZeroPad(hexlify(this.toIndex), stateIDLen),
hexZeroPad(hexlify(this.toPubkeyID), stateIDLen),
this.decimal.encodeInt(this.amount),
this.decimal.encodeInt(this.fee)
]);
return hexlify(concated);
}
}

View File

@@ -0,0 +1,87 @@
import { ethers } from "ethers";
import { BigNumber } from "ethers";
import { randomBytes, hexlify, hexZeroPad, parseEther } from "ethers/lib/utils";
import { Wei } from "./interfaces";
import { ContractTransaction } from "ethers";
import { assert, expect } from "chai";
// import { Rollup } from "../types/ethers-contracts/Rollup";
export const FIELD_ORDER = BigNumber.from(
"0x30644e72e131a029b85045b68181585d97816a916871ca8d3c208c16d87cfd47"
);
export const ZERO = BigNumber.from("0");
export const ONE = BigNumber.from("1");
export const TWO = BigNumber.from("2");
export function randHex(n: number): string {
return hexlify(randomBytes(n));
}
export function sum(xs: BigNumber[]): BigNumber {
return xs.reduce((a, b) => a.add(b));
}
export function to32Hex(n: BigNumber): string {
return hexZeroPad(n.toHexString(), 32);
}
export function hexToUint8Array(h: string): Uint8Array {
return Uint8Array.from(Buffer.from(h.slice(2), "hex"));
}
export function toWei(ether: string): Wei {
return parseEther(ether).toString();
}
export function randFs(): BigNumber {
const r = BigNumber.from(randomBytes(32));
return r.mod(FIELD_ORDER);
}
export function randomNum(numBytes: number): number {
const bytes = randomBytes(numBytes);
return BigNumber.from(bytes).toNumber();
}
export function randomLeaves(num: number): string[] {
const leaves = [];
for (let i = 0; i < num; i++) {
leaves.push(randHex(32));
}
return leaves;
}
// Simulate the tree depth of calling contracts/libs/MerkleTree.sol::MerkleTree.merklise
// Make the depth as shallow as possible
// the length 1 is a special case that the formula doesn't work
export function minTreeDepth(leavesLength: number) {
return leavesLength == 1 ? 1 : Math.ceil(Math.log2(leavesLength));
}
export async function mineBlocks(
provider: ethers.providers.JsonRpcProvider,
numOfBlocks: number
) {
for (let i = 0; i < numOfBlocks; i++) {
await provider.send("evm_mine", []);
}
}
export async function expectRevert(
tx: Promise<ContractTransaction>,
revertReason: string
) {
await tx.then(
() => {
assert.fail(`Expect tx to fail with reason: ${revertReason}`);
},
error => {
expect(error.message).to.have.string(revertReason);
}
);
}
// export async function getBatchID(rollup: Rollup): Promise<number> {
// return Number(await rollup.nextBatchID()) - 1;
// }