prove static method

Future<PoolChainFixture> prove({
  1. required List<int> withdrawalPKH,
  2. bool production = false,
  3. bool verbose = false,
  4. bool aggregate = true,
})

With aggregate false the spend proofs are made and the trees advanced, but no round is aggregated: proof and proof2 are bare and transfers1 and transfers2 are what a coordinator, which aggregates itself, takes in. That saves the two aggregations (about 12 minutes at production) when only the transfers are wanted.

Implementation

static Future<PoolChainFixture> prove(
    {required List<int> withdrawalPKH, bool production = false, bool verbose = false, bool aggregate = true}) async {
  final rng = Random(71);
  List<int> lanes(int n) => List.generate(n, (_) => rng.nextInt(M31.p));
  final agg = plan(production: production);
  final n = agg.transfers;
  final stmt = PoolStatement.of(agg.tree);
  final v = ShieldedPoolTool.poolVerifier(stmt,
      verifier: StarkVerifierGen(agg.rootP, agg.rootAir(List.filled(stmt.numPublics, 0))));

  final cmTree = NoteCommitmentTree();
  final nullifiers = NullifierTree();
  final g = PoolHeader.genesis(
      emptyCmRoot: SlotScript.lanesBytes(cmTree.root), emptyNfRoot: SlotScript.lanesBytes(nullifiers.root));
  final ring = List.filled(4, cmTree.root);

  SpendNote dummy() => SpendNote.dummy(sk: lanes(5), rho: lanes(3));
  final wallet = PoolWalletKeys(lanes(5)), stranger = PoolWalletKeys(lanes(5));
  final walletD = PoolHash.diversifier(wallet.ivk, 0);
  final walletAddr = await NoteAddress.derive(wallet.ivk, walletD);
  final strangerAddr = await NoteAddress.at(stranger.ivk, 0);
  NotePlaintext plain(List<int> d, int v) => NotePlaintext(asset: PoolHash.bsvAsset, d: d, value: v, rho: lanes(3), rcm: lanes(4));

  /// A real transfer's two outputs, [v1] to the wallet and [v2] to the
  /// stranger, and the bundle carrying them, sent under [ovk].
  Future<(OutputNote, OutputNote, NotePlaintext, List<int>)> outputs(int v1, int v2, List<int> ovk) async {
    final p1 = plain(walletD, v1), p2 = plain(strangerAddr.d, v2);
    final bundle = [
      ...(await NoteEncryption.encrypt(p1, walletAddr, ovk, rng: rng)).bytes,
      ...(await NoteEncryption.encrypt(p2, strangerAddr, ovk, rng: rng)).bytes,
    ];
    return (p1.toOutputNote(walletAddr.pkd), p2.toOutputNote(strangerAddr.pkd), p1, bundle);
  }

  List<List<int>> bundlesOf(List<int> first) => [first, for (int t = 1; t < n; t++) const <int>[]];

  /// Proves one round of [witnesses] against the pool's trees as they
  /// stand, and advances them.
  Future<(PoolRoundProof, List<PoolPublicInputs>, List<StarkProof>)> prove(
      List<PoolSpendWitness> witnesses, List<List<int>> c, List<List<int>> ring, List<int> receiptTransfers) async {
    final publics = [for (final w in witnesses) w.publics];
    final sw = Stopwatch()..start();
    final spendProofs = [
      for (int t = 0; t < n; t++)
        StarkProver.prove(agg.spendP, PoolSpendAir.air(publics[t]), witnesses[t].rows, rng: Random(1), hash: const Poseidon2ProofHash())
    ];
    if (verbose) print('  $n spend proofs in ${sw.elapsedMilliseconds} ms');
    sw.reset();
    final rootBefore = cmTree.root;
    final j = cmTree.nextSubtree, paths = <List<List<int>>>[];
    final spendLanes = [for (final p in publics) p.toLanes()];
    for (int s = 0; s < agg.tree.subtrees; s++) {
      paths.add(cmTree.subtreePath(j + s));
      cmTree.appendSubtree([for (final l in agg.tree.subtreeLeavesOf(spendLanes, s)) l ?? MerkleFrontier.emptyLeaf]);
    }
    if (!aggregate) {
      // the aggregation is what inserts the nullifiers; do that by hand
      for (final p in publics) {
        if (p.real1) nullifiers.insert(p.nf1);
        if (p.real2) nullifiers.insert(p.nf2);
      }
      return (PoolRoundProof.bare(List.filled(stmt.numPublics, 0), c), publics, spendProofs);
    }
    final (rootProof, wide) = await agg.aggregate(publics, spendProofs,
        rootBefore: rootBefore,
        rootAfter: cmTree.root,
        index: j,
        paths: paths,
        ring: ring,
        nullifiers: nullifiers,
        receiptTransfers: receiptTransfers,
        rng: Random(3),
        verbose: verbose);
    if (verbose) print('  aggregated in ${sw.elapsedMilliseconds} ms');
    return (PoolRoundProof.root(agg.rootP, agg.rootAir(wide), rootProof, c), publics, spendProofs);
  }

  // ---- round 1: a deposit of 500, whose note is the wallet's, and
  // padding transfers
  final (depositNote, depositZero, depositPlain, depositBundle) = await outputs(500, 0, stranger.ovk);
  final perTransfer = bundlesOf(depositBundle);
  final c = [for (final b in perTransfer) PoolOutHash.bundleHash(b)];
  final bundles = PoolOutHash.encodeBundles(perTransfer);
  final (proof, publics1, spends1) = await prove([
    PoolSpendAir.witness(dummy(), dummy(), depositNote, depositZero, -500,
        outHash: PoolOutHash.transferLanes(c[0]), anchor: lanes(8)),
    for (int t = 1; t < n; t++) ShieldedTransfer.paddingWitness(rng: rng),
  ], c, ring, const [0]);
  final receipt = PoolReceipt(SlotScript.lanesBytes(publics1[0].cmOut1), BigInt.from(500));
  final h1 = g.advance(
      cmRoot: SlotScript.lanesBytes(cmTree.root),
      nfRoot: SlotScript.lanesBytes(nullifiers.root),
      size: stmt.leavesAppended,
      balance: g.balance + BigInt.from(500),
      outHash: PoolOutHash.roundOutHash(c));
  final transfers1 = [for (int t = 0; t < n; t++) ShieldedTransfer(publics1[t], spends1[t], perTransfer[t])];

  // ---- round 2: the deposit's note, the tree's first leaf, spent into a
  // 200 change note to the wallet and a withdrawal of 300, anchored to
  // header 1's root
  final walletPkd = walletAddr.pkd;
  if (publics1[0].cmOut1.toString() != depositPlain.cmUnder(walletPkd).toString()) {
    throw StateError('round 1\'s first output is not the deposit note');
  }
  final path = cmTree.path(0);
  final spent = SpendNote(
      sk: wallet.sk, d: walletD, value: 500, rho: depositPlain.rho, rcm: depositPlain.rcm, siblings: path.siblings, position: path.position);
  final withdrawal = PoolWithdrawal(withdrawalPKH, BigInt.from(300));
  final (changeNote, changeZero, _, changeBundle) = await outputs(200, 0, wallet.ovk);
  final perTransfer2 = bundlesOf(changeBundle);
  final c2 = [for (final b in perTransfer2) PoolOutHash.bundleHash(b)];
  final bundles2 = PoolOutHash.encodeBundles(perTransfer2);
  final (proof2, publics2, spends2) = await prove([
    PoolSpendAir.witness(spent, dummy(), changeNote, changeZero, 300,
        outHash: PoolOutHash.transferLanes(c2[0], withdrawal: withdrawal)),
    for (int t = 1; t < n; t++) ShieldedTransfer.paddingWitness(rng: rng),
  ], c2, [for (final r in h1.ring) _lanesOf(r)], const []);
  final h2 = h1.advance(
      cmRoot: SlotScript.lanesBytes(cmTree.root),
      nfRoot: SlotScript.lanesBytes(nullifiers.root),
      size: 2 * stmt.leavesAppended,
      balance: h1.balance - BigInt.from(300),
      outHash: PoolOutHash.roundOutHash(c2));
  if (h2.nfRoot.toString() == h1.nfRoot.toString()) {
    throw StateError('round 2 spends a real note, so its nullifier goes in');
  }
  final transfers2 = [
    for (int t = 0; t < n; t++) ShieldedTransfer(publics2[t], spends2[t], perTransfer2[t], withdrawal: t == 0 ? withdrawal : null)
  ];
  for (final t in [...transfers1, ...transfers2]) {
    final why = t.refusal();
    if (why != null) throw StateError('the fixture built a transfer that refuses itself: $why');
  }
  return PoolChainFixture._(agg, v, v.body(), g, h1, h2, proof, proof2, bundles, bundles2, receipt, withdrawal, transfers1, transfers2,
      wallet, walletD);
}