mirror of
https://github.com/getwax/bls-wallet.git
synced 2026-01-09 15:48:11 -05:00
Add bls dependency
This commit is contained in:
108
lib/hubble-contracts/ts/accountTree.ts
Normal file
108
lib/hubble-contracts/ts/accountTree.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
40
lib/hubble-contracts/ts/allContractsInterfaces.ts
Normal file
40
lib/hubble-contracts/ts/allContractsInterfaces.ts
Normal file
@@ -0,0 +1,40 @@
|
||||
import { ParamManager } from "../types/ethers-contracts/ParamManager";
|
||||
import { NameRegistry } from "../types/ethers-contracts/NameRegistry";
|
||||
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 {
|
||||
paramManager: ParamManager;
|
||||
frontendGeneric: FrontendGeneric;
|
||||
frontendTransfer: FrontendTransfer;
|
||||
frontendMassMigration: FrontendMassMigration;
|
||||
frontendCreate2Transfer: FrontendCreate2Transfer;
|
||||
nameRegistry: NameRegistry;
|
||||
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;
|
||||
}
|
||||
62
lib/hubble-contracts/ts/blsSigner.ts
Normal file
62
lib/hubble-contracts/ts/blsSigner.ts
Normal 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) };
|
||||
}
|
||||
318
lib/hubble-contracts/ts/commitments.ts
Normal file
318
lib/hubble-contracts/ts/commitments.ts
Normal 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 }
|
||||
);
|
||||
}
|
||||
}
|
||||
38
lib/hubble-contracts/ts/constants.ts
Normal file
38
lib/hubble-contracts/ts/constants.ts
Normal 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];
|
||||
117
lib/hubble-contracts/ts/decimal.ts
Normal file
117
lib/hubble-contracts/ts/decimal.ts
Normal 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);
|
||||
258
lib/hubble-contracts/ts/deploy.ts
Normal file
258
lib/hubble-contracts/ts/deploy.ts
Normal file
@@ -0,0 +1,258 @@
|
||||
import { ParamManagerFactory } from "../types/ethers-contracts/ParamManagerFactory";
|
||||
import { NameRegistryFactory } from "../types/ethers-contracts/NameRegistryFactory";
|
||||
import { NameRegistry } from "../types/ethers-contracts/NameRegistry";
|
||||
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,
|
||||
nameRegistry?: NameRegistry,
|
||||
registryKey?: string
|
||||
) {
|
||||
await contract.deployed();
|
||||
if (verbose) console.log("Deployed", name, "at", contract.address);
|
||||
if (nameRegistry) {
|
||||
if (!registryKey) throw Error(`Need registry key for ${name}`);
|
||||
const tx = await nameRegistry.registerName(
|
||||
registryKey,
|
||||
contract.address
|
||||
);
|
||||
await tx.wait();
|
||||
if (verbose) console.log("Registered", name, "on nameRegistry");
|
||||
}
|
||||
}
|
||||
|
||||
export async function deployAll(
|
||||
signer: Signer,
|
||||
parameters: DeploymentParameters,
|
||||
verbose: boolean = false
|
||||
): Promise<allContracts> {
|
||||
// deploy libs
|
||||
|
||||
const paramManager = await new ParamManagerFactory(signer).deploy();
|
||||
await waitAndRegister(paramManager, "paramManager", verbose);
|
||||
|
||||
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 name registry
|
||||
const nameRegistry = await new NameRegistryFactory(signer).deploy();
|
||||
await waitAndRegister(nameRegistry, "nameRegistry", 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,
|
||||
nameRegistry,
|
||||
await paramManager.chooser()
|
||||
);
|
||||
|
||||
const allLinkRefs = {
|
||||
__$b941c30c0f5422d8b714f571f17d94a5fd$__: paramManager.address
|
||||
};
|
||||
|
||||
const blsAccountRegistry = await new BlsAccountRegistryFactory(
|
||||
signer
|
||||
).deploy();
|
||||
await waitAndRegister(
|
||||
blsAccountRegistry,
|
||||
"blsAccountRegistry",
|
||||
verbose,
|
||||
nameRegistry,
|
||||
await paramManager.accountRegistry()
|
||||
);
|
||||
|
||||
// deploy Token registry contract
|
||||
const tokenRegistry = await new TokenRegistryFactory(signer).deploy();
|
||||
await waitAndRegister(
|
||||
tokenRegistry,
|
||||
"tokenRegistry",
|
||||
verbose,
|
||||
nameRegistry,
|
||||
await paramManager.tokenRegistry()
|
||||
);
|
||||
|
||||
const massMigration = await new MassMigrationFactory(signer).deploy();
|
||||
await waitAndRegister(
|
||||
massMigration,
|
||||
"mass_migs",
|
||||
verbose,
|
||||
nameRegistry,
|
||||
await paramManager.massMigration()
|
||||
);
|
||||
|
||||
const transfer = await new TransferFactory(signer).deploy();
|
||||
await waitAndRegister(
|
||||
transfer,
|
||||
"transfer",
|
||||
verbose,
|
||||
nameRegistry,
|
||||
await paramManager.transferSimple()
|
||||
);
|
||||
|
||||
const create2Transfer = await new Create2TransferFactory(signer).deploy();
|
||||
await waitAndRegister(
|
||||
create2Transfer,
|
||||
"create2transfer",
|
||||
verbose,
|
||||
nameRegistry,
|
||||
await paramManager.create2Transfer()
|
||||
);
|
||||
|
||||
// deploy example token
|
||||
const exampleToken = await new ExampleTokenFactory(signer).deploy();
|
||||
await waitAndRegister(
|
||||
exampleToken,
|
||||
"exampleToken",
|
||||
verbose,
|
||||
nameRegistry,
|
||||
await paramManager.exampleToken()
|
||||
);
|
||||
await tokenRegistry.requestRegistration(exampleToken.address);
|
||||
await tokenRegistry.finaliseRegistration(exampleToken.address);
|
||||
|
||||
const spokeRegistry = await new SpokeRegistryFactory(signer).deploy();
|
||||
await waitAndRegister(
|
||||
spokeRegistry,
|
||||
"spokeRegistry",
|
||||
verbose,
|
||||
nameRegistry,
|
||||
await paramManager.spokeRegistry()
|
||||
);
|
||||
|
||||
const vault = await new VaultFactory(allLinkRefs, signer).deploy(
|
||||
nameRegistry.address
|
||||
);
|
||||
await waitAndRegister(
|
||||
vault,
|
||||
"vault",
|
||||
verbose,
|
||||
nameRegistry,
|
||||
await paramManager.vault()
|
||||
);
|
||||
|
||||
// deploy deposit manager
|
||||
const depositManager = await new DepositManagerFactory(
|
||||
allLinkRefs,
|
||||
signer
|
||||
).deploy(nameRegistry.address, parameters.MAX_DEPOSIT_SUBTREE_DEPTH);
|
||||
await waitAndRegister(
|
||||
depositManager,
|
||||
"depositManager",
|
||||
verbose,
|
||||
nameRegistry,
|
||||
await paramManager.depositManager()
|
||||
);
|
||||
|
||||
if (!parameters.GENESIS_STATE_ROOT) throw new GenesisNotSpecified();
|
||||
|
||||
// deploy Rollup core
|
||||
const rollup = await new RollupFactory(allLinkRefs, signer).deploy(
|
||||
nameRegistry.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,
|
||||
nameRegistry,
|
||||
await paramManager.rollupCore()
|
||||
);
|
||||
|
||||
await vault.setRollupAddress();
|
||||
|
||||
const withdrawManager = await new WithdrawManagerFactory(
|
||||
allLinkRefs,
|
||||
signer
|
||||
).deploy(nameRegistry.address);
|
||||
await waitAndRegister(
|
||||
withdrawManager,
|
||||
"withdrawManager",
|
||||
verbose,
|
||||
nameRegistry,
|
||||
await paramManager.withdrawManager()
|
||||
);
|
||||
await spokeRegistry.registerSpoke(withdrawManager.address);
|
||||
|
||||
return {
|
||||
paramManager,
|
||||
frontendGeneric,
|
||||
frontendTransfer,
|
||||
frontendMassMigration,
|
||||
frontendCreate2Transfer,
|
||||
nameRegistry,
|
||||
blsAccountRegistry,
|
||||
tokenRegistry,
|
||||
transfer,
|
||||
massMigration,
|
||||
create2Transfer,
|
||||
chooser,
|
||||
exampleToken,
|
||||
spokeRegistry,
|
||||
vault,
|
||||
depositManager,
|
||||
rollup,
|
||||
withdrawManager
|
||||
};
|
||||
}
|
||||
50
lib/hubble-contracts/ts/exceptions.ts
Normal file
50
lib/hubble-contracts/ts/exceptions.ts
Normal 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 {}
|
||||
257
lib/hubble-contracts/ts/factory.ts
Normal file
257
lib/hubble-contracts/ts/factory.ts
Normal 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 };
|
||||
}
|
||||
98
lib/hubble-contracts/ts/hashToField.ts
Normal file
98
lib/hubble-contracts/ts/hashToField.ts
Normal 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;
|
||||
}
|
||||
143
lib/hubble-contracts/ts/hubble.ts
Normal file
143
lib/hubble-contracts/ts/hubble.ts
Normal file
@@ -0,0 +1,143 @@
|
||||
import { allContracts } from "./allContractsInterfaces";
|
||||
import { DeploymentParameters } from "./interfaces";
|
||||
import fs from "fs";
|
||||
import {
|
||||
BlsAccountRegistryFactory,
|
||||
BurnAuctionFactory,
|
||||
Create2TransferFactory,
|
||||
DepositManagerFactory,
|
||||
ExampleTokenFactory,
|
||||
FrontendCreate2TransferFactory,
|
||||
FrontendGenericFactory,
|
||||
FrontendMassMigrationFactory,
|
||||
FrontendTransferFactory,
|
||||
MassMigrationFactory,
|
||||
NameRegistryFactory,
|
||||
ParamManagerFactory,
|
||||
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 = {
|
||||
paramManager: ParamManagerFactory,
|
||||
frontendGeneric: FrontendGenericFactory,
|
||||
frontendTransfer: FrontendTransferFactory,
|
||||
frontendMassMigration: FrontendMassMigrationFactory,
|
||||
frontendCreate2Transfer: FrontendCreate2TransferFactory,
|
||||
nameRegistry: NameRegistryFactory,
|
||||
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();
|
||||
}
|
||||
}
|
||||
}
|
||||
34
lib/hubble-contracts/ts/interfaces.ts
Normal file
34
lib/hubble-contracts/ts/interfaces.ts
Normal 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
|
||||
}
|
||||
145
lib/hubble-contracts/ts/mcl.ts
Normal file
145
lib/hubble-contracts/ts/mcl.ts
Normal 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);
|
||||
}
|
||||
40
lib/hubble-contracts/ts/state.ts
Normal file
40
lib/hubble-contracts/ts/state.ts
Normal 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);
|
||||
382
lib/hubble-contracts/ts/stateTree.ts
Normal file
382
lib/hubble-contracts/ts/stateTree.ts
Normal 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 };
|
||||
}
|
||||
}
|
||||
34
lib/hubble-contracts/ts/tree/hasher.ts
Normal file
34
lib/hubble-contracts/ts/tree/hasher.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
2
lib/hubble-contracts/ts/tree/index.ts
Normal file
2
lib/hubble-contracts/ts/tree/index.ts
Normal file
@@ -0,0 +1,2 @@
|
||||
export { Hasher, Node } from "./hasher";
|
||||
export { Tree, Data, Witness } from "./tree";
|
||||
216
lib/hubble-contracts/ts/tree/tree.ts
Normal file
216
lib/hubble-contracts/ts/tree/tree.ts
Normal 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)
|
||||
};
|
||||
}
|
||||
}
|
||||
335
lib/hubble-contracts/ts/tx.ts
Normal file
335
lib/hubble-contracts/ts/tx.ts
Normal 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);
|
||||
}
|
||||
}
|
||||
87
lib/hubble-contracts/ts/utils.ts
Normal file
87
lib/hubble-contracts/ts/utils.ts
Normal 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;
|
||||
}
|
||||
@@ -9,6 +9,7 @@
|
||||
"ethereum-waffle": "^3.0.0",
|
||||
"ethers": "^5.0.0",
|
||||
"hardhat": "^2.0.7",
|
||||
"mcl-wasm": "^0.4.5",
|
||||
"ts-node": "^9.1.1",
|
||||
"typescript": "^4.1.3"
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user