momiji.ts 9.42 KB
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([]);
  private batch: types.RecursionInputs[] = [];
  private confirmed: types.Confirmed = {};
  private publishing: boolean = false;
  private proving: boolean = false;
  private sweeping: boolean = false;

  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 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.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)
    const gasEstimate: bigint = await this.contracts.state.publish.estimateGas(contractPublish.proof, contractPublish.batch)
    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);
    })
  }

  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;
  }
}