import * as types from "../../momiji-helpers/utils/types" import { BatchBuilder } from "../../momiji-helpers/utils/batchBuilder"; import { keccak_tx } from '../../momiji-helpers/circuits/helpers/codegen/keccak_tx'; import { tx_as_hash } from '../../momiji-helpers/circuits/helpers/codegen/tx_as_hash'; export class Publisher extends BatchBuilder { // private publishTimeout: NodeJS.Timeout; private signer: types.EthersSigner | undefined; private mempool: types.Mempool = {}; private provingQueue: Promise<types.RecursionInputs[]> = Promise.resolve([]); public batch: types.RecursionInputs[] = []; private confirmed: types.Confirmed = {}; private publishing: boolean = false; private proving: boolean = false; private sweeping: boolean = false; public queue: types.Transaction[] = []; public accumulator: string = types.ZERO_VALUE; private contractPublish: types.ContractPublish = {} as types.ContractPublish; constructor(config: types.GlobalConfig) { super(config); // this.publishTimeout = setInterval(this.publish, 60 * 1000); this.mempool = {}; this.confirmed = {}; } initializePublisher = async () => { await this.initializeBatchBuilder((_tx: types.Transaction) => this.newTransactionReceived(_tx)); await this.setupListeners(); console.log(`โ Event watcher started.`) await this.printRoot(); console.log(`๐ฃ Welcome to the Offshift Prover Network.`) this.signer && console.log(`๐ Publisher Address: ${await this.signer.getAddress()}.`) } async addToMempool(tx: types.Transaction, txid: string): Promise<boolean> { let pi_hash = await tx_as_hash(tx.public_inputs); if (pi_hash != tx.proof_artifacts.proofData.publicInputs[0]) return false; if (!this.mempool.hasOwnProperty(txid) && !this.confirmed.hasOwnProperty(txid)) { this.mempool[txid] = tx; let verified: boolean = false; console.log(`โ Verifying ${txid}.`) const proofU8: Uint8Array = Uint8Array.from(Object.values(tx.proof_artifacts.proofData.proof)) tx.proof_artifacts.proofData.proof = proofU8 verified = await this.backends.transaction.verifyProof(tx.proof_artifacts.proofData) if (!verified) { return false; } return true; } return false; } async newTransactionReceived(tx: types.Transaction): Promise<void> { if (this.batch.length >= 15) { console.log(`๐ด Transaction rejected -- batch is full`); return; } let txid = await keccak_tx(tx.public_inputs); let _verified = await this.addToMempool(tx, txid) if (_verified) this.queueToProve(tx) else console.log(`๐ด Transaction rejected -- failed to verify: ${txid}`); } async newTransactionReceivedSingle(tx: types.Transaction): Promise<void> { let txid = await keccak_tx(tx.public_inputs); let _verified = await this.addToMempool(tx, txid) if (_verified) { await this.proveSingle(tx); } else console.log(`๐ด Transaction rejected -- failed to verify: ${txid}`); } async newTransactionReceivedMultiple(tx: types.Transaction): Promise<boolean> { let txid = await keccak_tx(tx.public_inputs); let _verified = await this.addToMempool(tx, txid) if (_verified) { this.queue.push(tx) return true } else { console.log(`๐ด Transaction rejected -- failed to verify: ${txid}`); return false }; } async proveMultiple(): Promise<void> { for (let i = 0; i < this.queue.length; i ++) { if (this.batch.length >= 15) { console.log(`โ ๏ธ Batch is full. Cannot prove additional transactions.`) this.queue = this.queue.slice(i) return } this.batch = await this.proveSingle(this.queue[i]) } this.queue = [] return } async setupListeners(): Promise<void> { if (!this.contracts) await this.initializePublisher(); if (!this.contracts) return; this.contracts.state.on(this.contracts.state.filters.TransactionPublish(undefined, undefined, undefined), async (tx: any, _: any, txId: any) => { this.confirmed[txId] = true; console.log(`โ๏ธ Transaction confirmed: ${txId}.`); }); this.contracts.state.on(this.contracts.state.filters.TransactionBroadcast(undefined, undefined, undefined), async (tx: any, _: any, txId: any) => { const public_inputs = { current_root: tx.transaction.current_root, utxo_root: tx.transaction.utxo_root, deposit_amount: tx.transaction.amount, withdrawals: new types.NoirFr(tx.transaction.withdrawals.map((w: any) => BigInt(w)).reduce((a: any, b: any) => a + b)).toString(), commitment_in: tx.transaction.commitments_in, commitment_out: tx.transaction.commitments, nullifier_hashes: tx.transaction.nullifier_hashes, contract_only_inputs: txId } const txAsHash: string = await tx_as_hash(public_inputs) let newTx: types.Transaction = { public_inputs: public_inputs, contract_inputs: tx.transaction, proof_artifacts: { proofData: { proof: Uint8Array.from(Buffer.from(tx.proofU8.slice(2), "hex")), publicInputs: [txAsHash] }, proofAsFields: tx.proof, vkAsFields: types.tx_vk, vkHash: types.tx_vk_hash } } this.newTransactionReceived(newTx); }); this.contracts.state.on(this.contracts.state.filters.BatchPublish(undefined, undefined, undefined, undefined, undefined), async (event: any) => { console.log(`๐ฏ Batch published: ${event}.`); // this.provingQueue = Promise.resolve([]); this.queue = [] this.batch = []; this.printRoot(); return; }); } printRoot = async () => console.log(`๐ณ Current State Root: ${await this.contracts.state.merkleRoot()}`) sweepProfit = async (txs: types.Transaction[] | types.Transaction): Promise<types.Transaction> => { if (!Array.isArray(txs)) txs = [txs]; const utxo_commitments: types.UTXO_Commitment[] = txs.map(tx => tx.contract_inputs.encrypted_utxo.filter(utxo => utxo.amount != types.ZERO_VALUE)).flat().map(utxo => { return { secret: utxo.secret as string, amount: utxo.amount as string, asset_type: utxo.data as string, spend_in_same_batch: true } }) const utxo_encrypted: types.UTXO_Encrypted[] = await this._generateUTXOEncrypted([{ amount: types.toFixedHex(0, true) }]) const withdrawalAmount: bigint = utxo_commitments.map(utxo => BigInt(utxo.amount)).reduce((prev, curr) => prev + curr) const withdrawals: types.WithdrawalSwap[] = await this._generateWithdrawals([{ amount: (new types.NoirFr(withdrawalAmount)).toString(), recipient: types.toFixedHex(1, true), swap_percentage: 100 }], 1) const proverTx: types.Transaction = await this._generateTransactionProof(utxo_commitments, utxo_encrypted, withdrawals); return proverTx; } publishReady = async (txs: types.RecursionInputs[], contractPublish: types.ContractPublish): Promise<boolean> => { console.log(`๐ฐ Calculating prover fees...`) const transactions: types.Transaction[] = txs.filter(tx => tx.transaction !== undefined).map(tx => tx.transaction as types.Transaction) const utxo_commitments: types.UTXO_Commitment[] = transactions.map(tx => tx.contract_inputs.encrypted_utxo.filter(utxo => utxo.amount != types.ZERO_VALUE)).flat().map(utxo => { return { secret: utxo.secret as string, amount: utxo.amount as string, asset_type: utxo.data as string } }) const withdrawalAmount: bigint = utxo_commitments.map(utxo => BigInt(utxo.amount)).reduce((prev, curr) => prev + curr) const withdrawalAmountEther: bigint = await this._getEtherFromXFT(withdrawalAmount) let gasEstimate: bigint try { gasEstimate = await this.contracts.state.publish.estimateGas(contractPublish.proof, contractPublish.batch) } catch { this.batch.pop() return false } const maxFeePerGas: bigint = await this.config.provider.getFeeData().then(feeData => feeData.maxFeePerGas as bigint) const maxFeePerGasAdjusted: bigint = (this.config.profit) ? maxFeePerGas + BigInt(this.config.profit * 1e9) : maxFeePerGas const txFeeEstimate: bigint = gasEstimate * maxFeePerGasAdjusted const publishReady: boolean = (withdrawalAmountEther >= txFeeEstimate) return publishReady } queueToProve = async (tx: types.Transaction) => { if (this.publishing) { console.log(`โ ๏ธ Batch is publishing. Cannot prove additional transactions.`); return; } this.provingQueue = this.provingQueue.then(async (data): Promise<types.RecursionInputs[]> => { return new Promise(async (res, rej) => { if (data.length >= 15) { console.log(`โ ๏ธ Batch is full. Cannot prove additional transactions.`); res(data); } this.proving = true; let _newData = await this.prove(data, tx); this.batch = _newData; this.proving = false; res(_newData); }) }).catch(async e => { console.log(e); throw new Error(`โ An unknown error occured while proving transactions.`); }) } prove = async (data: types.RecursionInputs[], tx: types.Transaction): Promise<types.RecursionInputs[]> => { return new Promise(async (res, rej) => { console.log(`โณ Proving ${await keccak_tx(tx.public_inputs)}.`) const proof = await this.rollupTransaction(data, tx); console.log(`โ๏ธ Proved ${await keccak_tx(tx.public_inputs)}.`) return res(proof); }) } async proveSingle(tx: types.Transaction): Promise<types.RecursionInputs[]> { this.proving = true; console.log(`โณ Proving ${await keccak_tx(tx.public_inputs)}.`) const proof = await this.rollupTransaction(this.batch, tx); console.log(`โ๏ธ Proved ${await keccak_tx(tx.public_inputs)}.`) this.proving = false return proof } publish = async () => { if (this.publishing) return; if (this.proving) return; if (this.sweeping) return; if (this.batch.length === 0) return; // await this.provingQueue; // const contractPublish: types.ContractPublish = await this.preparePublish(this.batch) // if (!(await this.publishReady(this.batch, contractPublish))) return; console.log(`๐งน Sweeping prover fees...`) this.sweeping = true const transactions: types.Transaction[] = this.batch.filter(tx => tx.transaction !== undefined).map(tx => tx.transaction as types.Transaction) const proverTx: types.Transaction = await this.sweepProfit(transactions) this.sweeping = false this.queueToProve(proverTx) this.publishing = true; await this.provingQueue console.log(`๐๏ธ Publishing batch...`) const contractPublishProver: types.ContractPublish = await this.preparePublish(this.batch) // clearInterval(this.publishTimeout); await this.contracts.state.publish(contractPublishProver.proof, contractPublishProver.batch) .then((tx: any) => console.log(`๐ก Batch published: ${tx.hash}.`), (error: any) => console.log(error.message)); this.provingQueue = Promise.resolve([]); this.batch = []; // this.publishTimeout = setInterval(this.publish, 60 * 1000); this.publishing = false; return; } async publishSingle() { if (this.publishing) return; if (this.proving) return; if (this.batch.length === 0) return; // If the accumulator has changed since the last pass // ie. a new transaction has been added to the batch if (this.accumulator !== this.batch[this.batch.length - 1].accumulator) { console.log(`๐ Preparing batch publish`) this.proving = true this.contractPublish = await this.preparePublish(this.batch) this.accumulator = this.batch[this.batch.length - 1].accumulator this.proving = false return; } if (!(await this.publishReady(this.batch, this.contractPublish))) return; this.publishing = true console.log(`๐งน Sweeping prover fees...`) const transactions: types.Transaction[] = this.batch.filter(tx => tx.transaction !== undefined).map(tx => tx.transaction as types.Transaction) const proverTx: types.Transaction = await this.sweepProfit(transactions) const proverBatch: types.RecursionInputs[] = await this.proveSingle(proverTx) const contractPublishProver: types.ContractPublish = await this.preparePublish(proverBatch) console.log(`๐๏ธ Publishing batch...`) await this.contracts.state.publish(contractPublishProver.proof, contractPublishProver.batch) .then((tx: any) => console.log(`๐ก Batch published: ${tx.hash}.`), (error: any) => console.log(error.message)); this.queue = []; this.batch = []; this.accumulator = types.ZERO_VALUE; this.contractPublish = {} as types.ContractPublish if (Object.keys(this.mempool).length >= 1000) this.mempool = {} if (Object.keys(this.confirmed).length >= 1000) this.confirmed = {} this.publishing = false; return; } }