Skip to content

Transaction Chaining - #964

Draft
aszepieniec wants to merge 62 commits into
masterfrom
asz/transaction-chaining
Draft

Transaction Chaining#964
aszepieniec wants to merge 62 commits into
masterfrom
asz/transaction-chaining

Conversation

@aszepieniec

Copy link
Copy Markdown
Contributor

Very much work-in-progress. Please see TODO.md for a motivation and progress tracker.

@aszepieniec
aszepieniec marked this pull request as draft August 10, 2026 13:12
@codspeed-hq

codspeed-hq Bot commented Aug 10, 2026

Copy link
Copy Markdown

Merging this PR will not alter performance

✅ 6 untouched benchmarks


Comparing asz/transaction-chaining (92cb17f) with master (28ff10f)

Open in CodSpeed

@aszepieniec

Copy link
Copy Markdown
Contributor Author

Transaction chaining — benchmark results

neptune-consensus/benches/chaintx.rs; cargo bench -p neptune-consensus --bench chaintx.

Measured on my i9-12900K (16 cores / 24 threads, 123 GB RAM) with TVM_LDE_TRACE=no_cache.

Claim 1 — Forge + Fix beats Collect + Raise

One transaction, 2 in / 2 out / 1 announcement, same primitive witness down both routes.

legacy chaining
Σ padded height 2 183 168 1 572 864
prover time 858 s 599 s
breakdown
  chained: Forge -> Fix
    Forge[0]  height  1048576     422.1 s
    Fix       height   524288     177.1 s
    TOTAL     height  1572864     599.2 s

  legacy: Collect -> Raise
    RemovalRecordsIntegrity  height    65536      12.1 s
    CollectLockScripts       height     4096       0.5 s
    KernelToOutputs          height     8192       0.8 s
    CollectTypeScripts       height     8192       0.7 s
    Raise                    height  2097152     844.1 s
    TOTAL                    height  2183168     858.2 s

≈30% cheaper.

Claim 2 — chaining N interactions beats N separate transactions

Not like-for-like: the chained fixture's successor spends its predecessors' unconfirmed outputs, which the legacy pipeline cannot express at all. The closest counterpart is N independent transactions raised and merged.

N = 2 legacy chaining
Σ padded height 5 414 912 3 670 016
prover time 2 119 s 1 361 s
breakdown
  chained: 2 interactions
    Forge[0]  height  1048576     392.5 s
    Forge[1]  height  1048576     391.7 s
    Chain[0]  height  1048576     397.5 s
    Fix       height   524288     178.9 s
    TOTAL     height  3670016    1360.6 s

  legacy: 2 separate transactions
    RemovalRecordsIntegrity[0]  height    65536      11.5 s
    CollectLockScripts[0]       height     4096       0.5 s
    KernelToOutputs[0]          height     8192       0.9 s
    CollectTypeScripts[0]       height     8192       0.8 s
    Raise[0]                    height  2097152     839.9 s
    RemovalRecordsIntegrity[1]  height    65536      11.5 s
    CollectLockScripts[1]       height     4096       0.5 s
    KernelToOutputs[1]          height     8192       0.7 s
    CollectTypeScripts[1]       height     8192       0.8 s
    Raise[1]                    height  2097152     850.4 s
    Merge[0]                    height  1048576     401.7 s
    TOTAL                       height  5414912    2119.2 s

≈36% cheaper. At the margin: one more chained interaction is Forge + Chain ≈790 s, against ≈1265 s for one more legacy transaction.

Why

Not the RRI inlining: proving RRI alone is 12.1 s, 1.4% of the legacy total. It is recursive verification which dominates all of these programs. Forge+Fix nets 3 fewer recursive verifications than Raise.

N = 4 compiles but has not been run. no_cache makes both columns pessimistic in absolute terms; the ratio is unaffected.

@Sword-Smith

Sword-Smith commented Aug 11, 2026

Copy link
Copy Markdown
Member

Thanks for the benchmark. Very cool that this is faster than "collect" + "raise"!

As you mention, all that really matter is the number of in-vm verifications you have to do. Since "collect" alone (without the "raise" step) is still much faster than the "chaining" approach, I think that "collect" should stay as the default proving method that downstream wallets use. That being said, that's of course up to each wallet implementation and outside of our control.

@Sword-Smith

Sword-Smith commented Aug 12, 2026

Copy link
Copy Markdown
Member

The auditing of the entire witness might be a bit excessive. I wonder what you pay for that in clock cycles. I'm thinking about this TASM:

let audit_preloaded_data =
            library.import(Box::new(VerifyNdSiIntegrity::<ChainWitness>::default()));

Maybe it's OK? I think it's worth investigating what the witness auditing costs. We don't want that to become dominant in the future, for any transactions sizes. Consider using more "WitnessMemory" structures if auditing is too expensive.

@Sword-Smith

Sword-Smith commented Aug 12, 2026

Copy link
Copy Markdown
Member

Let me suggest that we don't call the existing transaction pipeline the "legacy" pipeline, since that communicates that it will be retired, which is not the case. Let's instead call it the "single-proof" pipeline.

Just posting a stream of consciousness here. Maybe it'll be useful for others.

Vocabulary

  • confirmed input An input to a LinkTx that comes from the mutator set. So a UTXO that was previously mined, and that is being spent in this LinkTx.
  • thruputs Inputs to a LinkTx that comes from outputs from another LinkTx.

The reason that "update" on a transaction with no inputs was disallowed, was that the "update" of a "nop" transaction would grant composers a fast way to mine empty blocks. With the current ban on updating a transaction with no inputs, composers wanting to mine an empty block are required to create a "nop" transaction from scratch for each new block proposal, going through "raise" for both their coinbase transaction, and their "nop" transaction. Including a transaction from the mempool in their block proposal requires the composer only to go through one "raise" and an "update", as opposed to two "raise".

Let's ensure that composers keep this incentive, to include mempool transactions in their block proposals.

New Transaction Pipeline

image

@Sword-Smith

Sword-Smith commented Aug 12, 2026

Copy link
Copy Markdown
Member

So we need to ban "update" on LinkTx when the LinkTx has no confirmed inputs and no thruputs.

And we should ban negative fees throughout the LinkTx pipeline. Those are only required in single proof's "merge" operation.

Also: The SingleProof program digest could be printed to std out when it is used to construct the SingleProof claim inside of the LinkTxProof program.

edit:
Alan agrees on:

  • ban "update" in chain pipeline when confirmed_inputs.len() + thruputs.len() == 0.
  • Ban negative fees throughout the chain pipeline, transitively banned through entrypoints "cast" and "forge"
  • Agree on the security-in-depth/redundant check that prints the single proof program digest in all the link proof executions. This changes the claim for the LinkProof program.

@Sword-Smith

Copy link
Copy Markdown
Member

Updated to latest triton-vm and tasm-lib version: v8.0.0.

@Sword-Smith

Sword-Smith commented Aug 24, 2026

Copy link
Copy Markdown
Member

Missing internal audits:

LinkProof

  • Forge
    • a. coinbase is None
    • b. merge bit is false
    • c. fee is non-negative
    • d. All confirmed inputs live in the AOCL and match the absolute indices in the witness data -- AOCL membership / absolute index set correspondence
    • thruputs ++ confirmed inputs == input UTXO witness data
    • outputs == output UTXO witness data
    • All output elements match UTXOs from witness field outputs
    • NativeCurrency typescript is always required. Also if inputs, thruputs, and outputs are empty.
    • Type script list constructed correctly
    • All type scripts halt
    • Lock script list constructed correctly
    • All lock scripts halt
    • no bfe-wrap when summing number of thruputs and number of (confirmed) inputs.
    • All fields MAST verified before use
      • fee (during c)
      • coinbase (a)
      • merge bit (b)
      • AOCL MMR from witness (intial verification)
  • Chain
  • Advance
    • a. Previous LinkProof is valid for the relevant link-kernel MH
    • b. no coinbase
    • c. merge-bit not set
    • d. new_timestamp >= old_timestamp
    • e. disallow empty thruput + empty inputs
    • f. Fee unchanged
    • g. announcements unchanged
    • h. outputs unchanged
    • i. mutator set moved forward
    • j. All promoted thruputs are members of the new AOCL
    • k. Promoted thruputs inputs are calculated from the same witness data as the verified AOCL leafs
    • l. All promotions have strictly increasing AOCL leaf indices
    • m. new thruputs are old throughputs minus promotions <=> set(old thruputs) = set(new thruputs) ++ set(promotions)
    • n. new inputs are old inputs plus promotions
    • o. promotion-witness loop iteration only uses bounded jumps.
    • All fields MAST verified before use
      • old mutator set hash / old AOCL <- old lkmh (during i)
      • new mutator set hash new AOCL -< new lkmh (i)
      • old inputs (j/k/l/m)
      • new inputs (j/k/l/m)
      • old thruputs (j/k/l/m)
      • new thruputs (j/k/l/m)
      • old outputs (h)
      • new outputs (h)
      • old announcements (g)
      • new announcements (g)
      • new fee (f)
      • old fee (f)
      • old timestamp (d)
      • new timestamp (d)
      • new coinbase (b)
      • new merge bit (c)
    • Non-MAST authenticated fields don't need to be authenticated
      • old coinbase (Forge and Cast block coinbase from being anything but None)
      • old merge-bit (as above)
  • Cast
    • disallow set coinbase field
    • disallow set merge bit
    • force fee to be non-zero
    • claimed tx-kernel MH matches link-kernel MH in claim (with empty thruputs set)
    • SingleProof for tx-kernel is valid

SingleProof

  • Fix

Everything else is window dressing.

aszepieniec and others added 21 commits August 25, 2026 11:32
…ions diagram

Bring `transaction.md` in line with the current code:
- fix stale witness field names in the field-usage table
  (`PrimitiveWitness`, `lock_scripts_and_witnesses`,
  `type_scripts_and_witnesses`)
- document `merge_bit` semantics: set by the Merge branch, asserted
  false by the Collection/Raise branch
- fix duplicate/skipped section labels (Update is E; Integral Mempool
  is a wishlist, not a validity variant)
- clarify the opaque mutator-set-hash update condition
- add an ASCII diagram of the legacy validity transitions
  (PrimitiveWitness -> ProofCollection -> SingleProof, with Merge and
  Update as self-loops)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
First data structure of the transaction-chaining pipeline.

`LinkKernel` composes a legacy `TransactionKernel` with a `Vec<AdditionRecord>`
of thruputs, reusing the wrapped kernel's MAST leafs verbatim and carrying the
thruputs as one extra trailing leaf (`LinkKernelField::Thruputs`). This keeps
the legacy kernel fields at their original leaf positions so that the view from
the type script is exactly a legacy transaction.

Wires up the `chaintx` module. Tests cover the leaf-position drift guard
against `TransactionKernelField`, verbatim reuse of the kernel leafs, thruput
binding into the MAST hash, and BFieldCodec round-tripping.

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The transaction-chaining analogue to `PrimitiveWitness`. Exposes
secrets. Never broadcast. Consumed by `Forge` which produces a
`LinkTx`.

Stores two input kinds:
 - *confirmed inputs* -- like today's inputs, spent via removal records
   and membership proofs;
 - *thruputs* -- a predecessor's unconfirmed output, but since it is
   unconfirmed the data structure agrees with that of outputs.

The `input_utxos` field is the concatenation of confirmed-inputs and
thruputs. This way the type script sees a legacy transaction, but also
sees the thruputs.

Also: add and use test helper function `LinkKernel::empty()`.

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Transaction-chaining analogs of `TransactionProof` and `Transaction`,
respectively. Contrary to `TransactionProof`, `LinkTxProof` only has
two variants, not three: the proof-collection stage is being skipped.

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ess`

Replace placeholder arbitrary strategy with one obtained from lifting a
`PrimitiveWitness` strategy. Lifting means: reclassify the last k inputs
as thruputs. No rebalancing needed.

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
`from_primitive_witness` reclassifies the tail of a `PrimitiveWitness`'s
inputs as thruputs, which truncates the kernel's `inputs` leaf and thereby
moves the kernel mast root. The type scripts are checked against that root,
so their fee/coinbase/timestamp mast auth paths must be regenerated against
the truncated kernel -- reusing the originals produces witnesses that fail
type-script verification. Mirror of `update_with_new_ms_data`.

Latent because `bfield_codec_round_trip` never runs the VMs; surfaced when
writing the `LinkWitness::validate` reference predicate.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Proof-free, tier-1 validator for a chained transaction -- the analog of
PrimitiveWitness::validate, mirroring its structure check-for-check. Confirmed
inputs are validated against the mutator set (removal-records integrity);
thruputs are checked only for commitment-consistency (Forge does not match a
thruput against a predecessor -- that is Chain's job); type scripts run
unchanged over the combined confirmed || thruput input list. Adds the
LinkWitness-specific rules the thruput/LinkKernel semantics require:
cardinality, thruput commitments, and coinbase rejection.

Three new WitnessValidationError variants (CardinalityMismatch,
ThruputCommitmentMismatch, CoinbaseSet).

Test suite as proptests over random instances (no proving involved, so no need
for deterministic single samples): a positive lift-preserves-validity property,
one negative per reachable branch built by poking a valid witness (or poking the
PrimitiveWitness and lifting, so from_primitive_witness rebuilds the type scripts
and the intended late-stage check fires), and a pinned lock-script coverage gap.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Mirror `SingleProofWitness` for the link pipeline. Only the `Forge`
variant lands here; `Chain`, `Update`, and `Cast` follow with their
witnesses and tasm programs. `Fix` is deliberately absent: it produces a
`SingleProof`, not a `LinkProof`, so it belongs on `SingleProofWitness`.

Discriminant 0 is pinned for `Forge` (with 1/2/3 reserved) since the
`LinkProof` program branches on it. `TasmObject` is hand-written because
the derive rejects enums -- same reason `SingleProofWitness` has one.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The name now says which tier it belongs to. It is the analog of
`PrimitiveWitness`, not of `SingleProofWitness` -- a distinction that
matters now that `LinkProofWitness` exists alongside it.

Renames the module to match. Also picks up rustfmt on the touched file.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
`Forge: LinkPrimitiveWitness -> LinkTx` is the entry point into the
transaction-chaining pipeline. It recursively establishes, against the
`LinkKernel` MAST hash, that a chained transaction's inputs and outputs are
legitimate and that every lock and type script halts.

Input integrity
- Authenticate the mutator-set accumulator, the confirmed removal records, the
  thruputs, the outputs, and the constant coinbase / merge-bit leafs against the
  `LinkKernel` MAST hash.
- Inline `RemovalRecordsIntegrity` non-recursively over the confirmed inputs
  (the cost saving that motivates chaining): AOCL membership plus computed-vs-
  claimed absolute index sets.
- Check thruput commitments and bind `|input_utxos| == |confirmed| + |thruputs|`,
  so a phantom input UTXO cannot inflate the type scripts' input balance.

Output integrity
- `KernelToOutputs` absorbed: each addition record must equal the canonical
  commitment of its output UTXO, binding the type scripts' view of the outputs
  to the mutator set. Without it a prover could show the type scripts cheap
  outputs while the kernel commits to expensive addition records.
- Thruputs and outputs share one `for_all_addition_records` loop (both are
  `AdditionRecord`s equal to a UTXO's canonical commitment).

Recursive script verification
- Divine the inner (height-3) `TransactionKernel` MAST root -- the value the
  scripts were proven against -- and authenticate it as the left child of the
  `LinkKernel` root (one `hash` against the divined right sibling). It is kept
  on the stack, reusing the now-dead `lkmh` slot, so both claim templates read
  it without static memory.
- Lock scripts: verify one halting proof per input UTXO (`StarkVerify`) against
  the inner root, marching `input_utxos` and `lock_scripts_halt` in parallel --
  a thruput is still an input being spent and must be authorized.
- Type scripts: recollect the deduplicated, native-currency-first hash list over
  `input_utxos || output_utxos` (absorbing `CollectTypeScripts`) and verify one
  halting proof per unique type script against
  `[inner_root, H(input_utxos), H(output_utxos)]`.
- Both loops assert their proof counts up front
  (`WRONG_NUMBER_OF_{TYPE,LOCK}_SCRIPT_PROOFS_ERROR`), so a shortfall fails
  cleanly rather than obscurely inside `StarkVerify`.

Witness and constructor
- `ForgeWitness` carries the input/output-integrity data plus `lock_scripts_halt`
  and `type_scripts_halt`. `produce` is the sole production constructor -- it
  proves every lock script and every unique type script -- and shares field
  assembly with a private `build_from_parts`. A `#[cfg(test)]` `without_proofs`
  builds proofless witnesses for the integrity-negative and MAST-derivation
  tests, which never reach script verification (proving in each would time the
  suite out).

Reference predicate
- `ForgeWitness::validate` is the proof-carrying reference predicate (analog of
  `LinkPrimitiveWitness::validate`, sibling of `ProofCollection::verify`): it
  re-derives the inlined integrity natively and *verifies* -- never runs -- the
  lock and type scripts, so a `true` verdict guarantees the tasm `Forge` accepts
  and a caller can gate the expensive proof on it. It splits into a cheap, pure
  `validate_integrity` (exhaustively testable without proving) and the full
  proof-verifying `validate`. Both are hand-written native predicates (the tasm
  `run_rust` shadow stays test-only) and are not feature-gated, since everything
  they reference is production-available.

Copied snippets and drift guards
- The confirmed-input loop is a deliberate copy of `RemovalRecordsIntegrity`'s,
  whose hash is consensus-pinned; `forge_confirmed_loop_matches_rri` guards it.
- The three type-script-hash collection subroutines are copied instruction-for-
  instruction from `CollectTypeScripts`; `forge_collect_type_scripts_matches_cts`
  guards them (normalizing the copies' local call labels).
- `authenticate_mutator_set_acc` is a near-verbatim copy of RRI's, differing only
  in the `MAST_HEIGHT` immediate; left unguarded on purpose (a guard would have
  to special-case that lone immediate).

Also: `LinkPrimitiveWitness::validate` gains the matching output-side checks; the
Rust spec `source()` verifies the scripts in the same order as the tasm; and
Forge's assertion error ids are registered at 1_000_520..528.

Lock- and type-script coverage is complete; the remaining pipeline stages
(`Chain`/`Update`/`Cast`/`Fix`) are tracked in `chaintx/TODO.md`.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
`CollectLockScripts` / `CollectTypeScripts` are absorbed by `Forge` only in the
dual (chaining) pipeline; they remain, unchanged and consensus-pinned, in the
legacy pipeline. Reword the TODO note, which read as if they were removed
outright.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…h `Forge`

`Forge` inlined its own near-verbatim copy of the mutator-set-accumulator
authentication -- the one duplicated snippet that had no drift guard. Replace it
with the existing `AuthenticateMsaAgainstTxk` snippet, shared with the legacy
`SingleProof` pipeline.

Generalize the snippet over MAST height:
- Add a `mast_height` field (was hardcoded to `TransactionKernel::MAST_HEIGHT`);
  the `MutatorSetHash` leaf index (6) is the same in both kernels, so only the
  height varies. The entrypoint keeps its name (no height suffix): imports are
  emitted sorted by name and no single program authenticates against two kernel
  heights, so a stable name keeps each caller's bytes identical -- the sole
  existing caller (`update_branch`) still emits `push 3`, and the `SingleProof`
  program hash is unchanged (its `test_program_snapshot!` verifies this).

Wire `Forge` to import it at `LinkKernel`'s height:
- Drop the ~60-line inline copy for a short call passing the AOCL / bagged-swbfi
  / swbfa pointers and `lkmh`. Imported after the four RRI-matching `kmalloc`s,
  so the confirmed-input loop's static addresses are undisturbed
  (`forge_confirmed_loop_matches_rri` verifies this).
- `ForgeWitnessMemory` now carries `swbfi_bagged` and `swbfa_hash` as digests
  (like `update_branch`'s `UpdateWitness`) instead of the raw swbfi MMR; the
  swbfa hash is no longer divined. `source()` and `nondeterminism()` follow.

The MSA authentication is now the shared, separately-audited snippet
(`AuthenticateMsaAgainstTxk` has its own `MemPreserver` shadow test), removing
`Forge`'s last unguarded copy. This changes `Forge`'s program hash, pinned by
the snapshot in the following commit.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Add `test_program_snapshot!(Forge, ...)`, now that `Forge` is feature-complete
(input/output integrity, recursive lock- and type-script verification, and the
shared mutator-set-accumulator authentication), so any unintended change to the
program hash is caught. Mark the `Forge` tasm item and its snapshot subitem done
in `chaintx/TODO.md`.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Add two cheap (`without_proofs`) tasm negatives for the inlined
`RemovalRecordsIntegrity`:

- `bad_mutator_set_accumulator_is_rejected`: a mutator-set accumulator that
  disagrees with the kernel's `MutatorSetHash` fails authentication
  (`ROOT_MISMATCH`). Also the first negative to exercise the shared
  `AuthenticateMsaAgainstTxk` snippet's rejection path.
- `bad_absolute_index_set_is_rejected`: a confirmed record whose claimed absolute
  index set disagrees with the one recomputed from its UTXO fails
  (`COMPUTED_AND_CLAIMED_INDICES_DISAGREE_ERROR`) -- a double-spend path if
  unchecked. Mirrors `removal_records_fail_on_bad_absolute_indices`.

Update `chaintx/TODO.md`: tick these plus the items already covered (bad MAST
binding via the `unauthenticated_*` tests, phantom UTXO via
`phantom_input_utxo_is_rejected`, lock-script coverage via the count guard), and
drop the items that don't belong on `Forge` -- the balance/fee properties are
`NativeCurrency`'s (verified, not re-implemented, by `Forge`) and partition
misclassification is not an error condition -- keeping a note that records why.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
`Fix` recursively verifies a `LinkProof` and `Cast` recursively verifies a
`SingleProof`, so each edge needs the other program's digest at program-
construction time. Hardcoding both is impossible.

Record the resolution: promote the `SingleProof` digest `D` to a public input
of the `LinkProof` claim, copied verbatim through `Chain`/`Update`, used only
by `Cast`, and instantiated by `Fix` as `own_program_digest()`. Includes the
soundness induction, the audit-critical pass-through invariant, the accepted
upgrade coupling, and a negative test per branch that touches `D`.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
`Forge` was a standalone program with its own claim and its own pinned hash,
even though `LinkProofWitness` already reserved discriminants for the four
branches. Give the pipeline its dispatcher.

`LinkProof` reads the link-kernel MAST hash off stdin, range-checks the witness
discriminant at address 0, and dispatches. `Forge` becomes a `BasicSnippet` with
the stack contract `[lkmh] *witness disc -> [scratch] *witness -1`, mirroring
`SingleProof`'s merge and update branches.

The digest slot belongs to the dispatcher and is scratch: `Forge` reuses it for
the inner kernel root once `lkmh` goes dead, and the dispatcher pops it unread.
Should a post-dispatch check ever need `lkmh`, it stashes it once in `LinkProof`
rather than making each branch hand it back -- a branch returning the inner root
instead of `lkmh` returns a well-formed digest of the same transaction, which
would bind such a check to the wrong tree without crashing.

`Forge` must be imported first: four of its `kmalloc`s have to land at
`RemovalRecordsIntegrity`'s addresses, and `forge_confirmed_loop_matches_rri`
compares emitted instructions, `push`ed addresses included.

The discriminant range check is not redundant with `NO_BRANCH_TAKEN_ERROR`:
branches signal "taken" by leaving `-1` in the discriminant's slot, so a witness
claiming `-1` would otherwise sail through the dispatcher untouched.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…patcher

`Chain` and `Update` recursively verify a `LinkProof` against a claim that
names `LinkProof` itself. Neither can hardcode that digest -- it is not known
while the program is being built -- so both need `own_program_digest()`, the
same move `SingleProof` makes for its self-recursion.

The digest sits at the bottom of the initial stack and `dup` reaches only 16
deep, so the dispatcher must copy it before pushing anything: read it first,
hand it to every branch below `lkmh`, and pop it after dispatch.

`Forge` is unaffected. Its frame sits above the digest and it addresses that
frame relatively, so it simply leaves the digest buried.

Re-pins the `LinkProof` program hash.
`Forge` authenticated its MAST leafs with an inline closure. Every branch has
to do the same thing, so lift it into `AuthenticateLinkKernelField` -- the
`LinkKernel` analog of `AuthenticateTxkField`, differing only in tree height
and leaf enum.

Deliberately a copy of the legacy snippet rather than a shared,
height-parameterized one: that snippet is baked into the consensus-pinned
`SingleProof` hash, so widening it could only ever risk that hash, for no gain
here. (`AuthenticateMsaAgainstTxk` *was* worth parameterizing: its body is
substantial and its one legacy caller could absorb the change.)

The extraction splits two jobs the closure had conflated. Three of `Forge`'s
five leafs hash a witness field, which is what the snippet does. The other two
push a consensus-fixed constant digest straight into `merkle_verify`, which
asserts the field's *value* at the same time -- strictly stronger, and not
something the snippet can express. Those keep their own path, and the two
constants move to `link_proof.rs`: no branch owns them, every branch owes them.

Imported after the four RRI-matching `kmalloc`s, alongside the other
verifier-side snippets, so those addresses cannot shift and
`forge_confirmed_loop_matches_rri` keeps holding.

Re-pins the `LinkProof` program hash.
`Chain: LinkTx * LinkTx -> LinkTx` combines two link transactions, cutting
through the thruputs that the operands' outputs resolve. Transaction-chaining
analog of `single_proof/merge_branch`, with `ChainWitness` playing the part of
`MergeWitness`; unlike `ForgeWitness` it needs no memory projection, since
everything it carries is read from RAM.

Cut-through is one witness-supplied multiset removed from the concatenated
outputs *and* the concatenated thruputs. Same list on both sides, so a record
can only leave the output side by leaving the input side with it -- value
conservation by construction -- and matching is on the addition record, i.e. on
the UTXO's canonical commitment. A thruput no predecessor output resolves can
therefore never be cancelled, which is what makes a fabricated one inert rather
than inflationary.

Also established: inputs and announcements are the concatenations; the fee is
the sum, with both operand fees bounded (hence non-negative); the timestamp is
the later of the two; all three kernels share one mutator-set hash; and the
chained kernel carries no coinbase and no merge bit.

The operands' coinbase and merge-bit leafs are deliberately *not* re-checked.
Every branch asserts both on the kernel it produces, so an operand that
verifies has them by induction. Any branch added later owes the same assertion.

The recursion runs *last*, unlike `merge_branch`, which verifies its operand
proofs first. Ordering is immaterial to soundness -- every assertion must hold
either way -- and putting the cheap self-contained checks first lets a negative
test drive any of them with a proofless witness, the same reason `Forge` ends
with its script verifications. Ten negatives ride on that and run in about a
second; the positive test, which cannot, pays for two real link proofs.

`D`, the `SingleProof` digest, is not yet part of the operand claims: it does
not enter the `LinkProof` claim at all until `Cast` gives it a consumer. See
the tracker.

Re-pins the `LinkProof` program hash.
Cut-through must be maximal: a chained `LinkKernel`'s outputs and its thruputs
are disjoint. The two cut-through equations did not establish that. They hold
for *any* sub-multiset of the intersection, so a prover could name a short
`cut_through` and leave a matching (output, thruput) pair standing -- both sides
still balance, since the record simply never left either one.

Disjointness is what pins the cut-through set to the whole intersection. For a
record the operands hold `a` times as an output and `b` times as a thruput, the
equations force cut-through multiplicity `c <= min(a, b)`; disjointness of the
`a - c` and `b - c` survivors then forces `c = min(a, b)`. So the new assert is
exactly maximality, not an approximation of it.

Implemented as a quadratic scan -- `All` over the chained thruputs, each one
looked up in the chained outputs with `Contains` -- because disjointness does not
reduce to a multiset equation the way the surrounding checks do. An
`AdditionRecord` is a `Digest` in a one-field struct, so records compare
unhashed, unlike the multiset comparisons, which hash first because that is what
`MultisetEqualityDigests` eats. The list being scanned reaches the inner function
through static memory, the inner function taking only its element. Both lists are
already authenticated against `lkmh` by the two `assert_cut_through` calls the
new one follows; it only re-reads them.

`chained_kernel` already cancelled everything it could, so no witness this repo
builds is affected -- only a hostile prover could reach the gap. Mirrored in the
Rust shadow, which `test_assertion_failure` requires to reject too.

`non_maximal_cut_through_is_rejected` drives it: pop a record off `cut_through`
and put it back on *both* sides, leaving both equations satisfied so that
nothing but the new assert stands between the witness and a valid chain.

Renumbers three error ids to keep the constants in program order; they are
file-local. Repins the `LinkProof` program hash, which any new assertion moves.
Also picks up rustfmt's re-indentation of `Forge::authenticate_field`, which the
tree had drifted from.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
aszepieniec and others added 24 commits August 25, 2026 11:32
The TODO item asked for mixed provenance -- `Chain(Forge'd with D₁, Cast'd with
D₂)` -- to be rejected, calling it the case that would otherwise launder a
junk-`D` `Cast` into a real chain. Reading the branch, that hazard does not
exist. `Chain` never compares two digests because there is only one: `D` is read
from the public input into a single static slot, and `generate_link_proof_claim`
appends that one value to both operand claims. Operands supply no digest of
their own, so mixed provenance is unrepresentable rather than rejected, and both
operands are forced into the same `Link[D]` family by construction. How the junk
operand was produced is irrelevant; `Chain` only ever checks the claim.

What that argument does not cover is whether both claims are *checked*, and
there the suite had a real hole. Every operand negative corrupts both operands
at once -- `operand_forged_under_another_single_proof_digest_is_rejected` forges
each under `D'`, `operand_proof_must_attest_to_its_own_operand` swaps them -- so
verifying the left operand alone trips all of them. Deleting the right
`verify_operand` call, a whole missing recursion rather than a subtle slip,
would have gone unnoticed by the entire test suite.

`each_operand_is_verified_against_the_claims_d` closes that. Exactly one operand
is forged under `other_d`, in each direction, so the surviving verification is
the only thing that can fire. It costs cache hits rather than forges: both
digests over both operands are proofs the neighbouring tests already make.

Two construction notes, both recorded in the doc comment. The non-determinism is
a consistent witness's, with the junk operand substituted afterwards -- the
digest stream is extracted per (proof, claim) pair, so it cannot be built for a
mismatched pairing at all, which is the same reason
`operand_proof_must_attest_to_its_own_operand` substitutes rather than
constructs. That also sharpens the test: were the substituted operand's
verification skipped, its proof would go unread and the leftover stream unused,
and the run would succeed. And only the tasm is run, unlike its neighbours,
because the rust shadow's `verify_stark` unwraps FRI errors and so aborts
instead of returning -- the shadow's error handling, not the branch's behaviour.

Not mutation-checked by deleting the right `verify_operand` call: that changes
the program hash, invalidating every cached `LinkProof` and forcing a full
re-forge. The evidence is direct anyway -- with the junk proof on the right, the
left operand is good, so `stark_verify` runs and succeeds for it, and the run
still fails inside `stark_verify`. The right operand's proof is the only other
one in play. Mirrored for the left.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Both used a `[~]` marker whose meaning is written down nowhere, so two resolved
items read as pending. Neither needed work; both needed saying so plainly.

Proof/claim binding -- a valid `LinkProof` for `[lkmh, D₁]` not verifying
against `[lkmh, D₂]` -- is now `[x]` with an explicit resolved status and the
argument spelled out rather than compressed. The check cannot fail: the
challenges a proof answers are derived by hashing a transcript seeded with the
statement itself, so a proof is bound to its statement by construction and the
test would be asserting a property of Triton VM. What the entry was really
aimed at is the risk that is ours -- `D` read at runtime and then never put in
the claim, leaving it decorative and every digest interchangeable, which is
universal forgery. That risk exists only because `D` is a parameter rather than
a constant, and the entry now names the asymmetry behind it: Fiat-Shamir binds a
proof to whatever statement you check it against, and says nothing about whether
you built the right statement. Two tests cover the second part, one per side --
`link_proof_claim_shape_is_pinned` for the prover's claim builder,
`operand_forged_under_another_single_proof_digest_is_rejected` for the verifier's
program -- and both are needed, being separate pieces of code that could each
forget `D`.

The `LinkProof`-hash-stability entry is deleted outright. One of its two
justifications had expired: it said there was no second `SingleProof` version to
parameterise over, which stopped being true when the `Fix` branch made gamma and
delta different programs. The other, the `test_program_snapshot!` backstop, is
already recorded where that pin is logged. And the real reason the hash cannot
drift is already a governing invariant at the top of this file -- `D` is a
parameter of the `LinkProof` claim, never a constant inside it -- which is what
makes `LinkProof` a unit struct with no channel through which a `SingleProof`
version could reach its program text. Nothing was left for the entry to carry.

No `[~]` markers remain.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Every `Fix` test so far stops at running the program, which establishes that
the branch's asserts hold and nothing more. What a block carries is a proof,
answering a claim built by `single_proof_claim` rather than by the witness --
two constructions that have to agree on the program digest and the claim's
Triton VM version both, the version being part of the Fiat-Shamir transcript
and so not a mismatched constant but a proof that answers nothing.

`produce_and_verify_single_proof` proves the fix witness and verifies the
result against that claim, returning it so a caller can say what the
transaction leaving the pipeline turned out to be. Two tests use it.

`forge_then_fix_yields_a_verifying_single_proof` takes the same route as
`fix_accepts_a_resolved_link_transaction` -- the same link transaction, hence a
cache hit -- and adds the last step.

`cast_then_fix_yields_a_verifying_single_proof` is the one where the
`Fix`/`Cast` cycle closes with real proofs on both sides: a genuine
`SingleProof` recursively verified by `Cast` under digest `D`, and that same `D`
instantiated by `Fix` as `own_program_digest()`. The transaction is proven under
delta, not gamma as `cast.rs`'s own fixture is. It has to be: `Fix` names its
own program's digest, so a gamma-proven transaction can enter the chain but
never leave it. The two rule sets share a claim version, so nothing else stands
in the way.

It is also the `Cast` round-trip. The fixed transaction's claim is asserted
equal to the original transaction's: `Cast` adds nothing and `Fix` takes nothing
away, so the composition is the identity on what a block checks.

Both run cold in ~21 minutes together.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
`prop_positive` had never run over a chained witness whose `new_kernel.thruputs`
is non-empty, so nothing showed `Chain` accepting an honest carry-forward. Both
existing positives chain to zero thruputs: one pairs a successor with the single
predecessor resolving all of them, the other uses `from_primitive_witness(pw, 0)`
throughout. The negatives bound the cut-through set from every side --
`cut_through_on_unequal_commitments_is_rejected` below the outputs,
`non_maximal_cut_through_is_rejected` above,
`a_thruput_cannot_be_cut_through_twice` on multiplicity -- with no positive to
be non-vacuous against.

`thruputs_resolve_in_two_stages` supplies it. The successor is funded entirely
by unconfirmed money, one thruput from each of two predecessors. Chaining the
first cancels one thruput and leaves the other standing; that intermediate is
proven and chained onto the second, which empties the list. Both stages assert
their cut-through size and thruput count, and both run through `prop_positive`.
The intermediate has to be proven because `ChainWitness::chain` takes `LinkTx`s
and panics on anything not proof-backed; the second stage is consumed by
nothing, so it is run rather than proven, as `chain_accepts_a_chain_produced_operand`
already does with its outer chaining.

This is also the shape the mempool will produce, parents arriving separately
rather than all at once.

`chainable_link_primitive_witnesses` could not express it: it builds the one
predecessor resolving the whole reclassified tail. The predecessor construction
is extracted to `predecessor_resolving(&pw, range)`, taking a range rather than
a count, and the old function now delegates to it with the tail range -- same
behaviour, and the caller list is unchanged. All 24 `chaintx::chain::` tests
pass.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The two `→ Chain → Fix` entries keep their open boxes but name one remaining
gap instead of two: the proven-`SingleProof` half is done on both legs, leaving
only the `Chain` in the middle. `Cast` round-trip goes to `[x]`. The
thruput-input-integrity entry keeps its box and loses its "needs a fixture that
cannot be expressed" tail, which is no longer true. The block end-to-end entry
records that its verification half is done and parks the rest on Integration.

Separately, the Triton VM proof-version item was describing a state of the world
that no longer exists. It said delta is at `V6`, ahead of the linked dependency,
and that a delta proof produced today would carry version 5 and answer nothing.
Neither holds: there is no `V6` variant, delta tracks the linked VM at `V5`, and
delta proofs are producible -- which is what the two full-pipeline tests rest
on. The tripwire it named does not exist either; it is
`newest_rule_set_tracks_the_linked_triton_vm`.

The mechanism is already documented at the site (`consensus_rule_set.rs`) and
enforced by that test, so the entry keeps only what neither can say -- that a
bump is expected -- in one line.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The four test sections had drifted into prose: entries carried their own
rationale, their provenance from the legacy test they mirror, notes to future
readers, and in several cases an argument for why some neighbouring test exists.
Reading them told you what someone was thinking, not what is covered.

Every entry is now the test's function name and one sentence saying what it
does, standing on its own -- no `←` back-references to legacy tests, no
motivation, no cross-entry commentary. Items with no test yet keep their open
box and describe the test in the same form.

Entries that had never named a test now do: fifteen across `onto Chain` and
`onto Update` described a property and left the reader to find it. Four tests
that exist but were unlisted are added (`chain_accepts_a_predecessor_successor_pair`,
`update_accepts_a_forged_link_transaction`,
`update_accepts_an_all_thruputs_link_transaction`, `forge_confirmed_loop_matches_rri`),
and one name was simply wrong: `chained_outputs_must_be_the_operands_outputs`
is `chained_outputs_must_be_union_of_outputs_of_operands`. Every backticked
identifier in the file now resolves to a real function.

Three entries are not tests and say so in a line each rather than a page: the
proof/claim binding argument, `Chain`'s mixed provenance, and `Update`
re-targeting `D`. The first keeps its "not to be re-opened", which is the only
operational thing it said.

Net -138 lines.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
`extra_output_utxo_fails_cardinality` and `tampered_output_randomness_fails`
have existed in `link_primitive_witness.rs` since the predicate was written and
appeared nowhere in this file, so the output side of the proof-free tier read as
untested next to its input and thruput counterparts.

The second is the output-side inflation path: an output UTXO whose sender
randomness no longer commits it to the kernel's addition record gives
`OutputCommitmentMismatch`. It is the proof-free counterpart of
`output_utxos_unbound_to_addition_records_is_rejected` on `Forge`, and the twin
of `tampered_thruput_randomness_fails` already listed beside it.

Also names the error variant the input-cardinality entry produces, matching its
new output-side neighbour.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Point `BasicSnippet` at its `tasm_lib` path, drop two redundant explicit
link targets, and demote the two links to the `pub(crate)`
`no_thruputs_subtree_root` -- they resolved only under
`--document-private-items`.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
A benchmark target is an external crate, so nothing under `#[cfg(test)]` is
reachable from one. Move `predecessor_resolving`, the chainable-pair
constructors and `forge` into a `test-helpers`-gated `chaintx::test_helpers`,
so the tests that own these fixtures and the benchmark that measures them
build the same thing rather than two fixtures that have to be kept in step.

Adds `fan_in_link_primitive_witnesses`: one successor funded entirely by `n`
predecessors, one thruput apiece -- the shape an N-way chain collapses from.
Also adds `ChainWitness::link_kernel`, mirroring `CastWitness::link_kernel`,
since a caller that proves a chain needs the kernel to pair with the proof and
`new_kernel` is `pub(super)`.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Two benchmarks, one per performance claim the chaining pipeline rests on:
`Forge + Fix` against `Collect + Raise` over one transaction carried both
ways, and `N` chained interactions against the closest thing the legacy
pipeline admits, `N` independent transactions raised and merged.

Each stage is traced for its padded height and then proven, and both numbers
are reported. The prover is called directly rather than through the job queue:
under `test-helpers` -- always on for a benchmark target, via the crate's
dev-dependency on itself -- the queue serves proofs from the on-disk cache,
which would time the disk instead of the prover.

The lock-script and type-script halting proofs are excluded from both routes.
`Forge` and `ProofCollection` prove exactly the same ones, so they cancel;
they are produced through the cache here, as inputs to the stages that recurse
into them.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Both §Benchmarks entries are now measured rather than pending.

§Motivation claimed the cheaper initiation came from inlining
`RemovalRecordsIntegrity` non-recursively. It does not: proving the inlined
programs separately is a rounding error against either route's total. The
saving is that the chained route recursively verifies three fewer proofs --
`Raise` verifies the four collection proofs on top of the script proofs,
`Forge` verifies only the script proofs, and `Fix` gives one back by verifying
the `LinkProof`. Recursive verification is what dominates all of these
programs.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The imports rewritten to point at `chaintx::test_helpers` were inserted where
the old ones sat rather than where rustfmt sorts them.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Revise the plan so the re-targeting branch may move a thruput into the
confirmed inputs once the mutator set has confirmed it, replacing "index
sets unchanged" and "thruputs byte-for-byte" with one coupled multiset
pair. Records the per-entry commitment / AOCL / index-set equations,
forbids promoting two thruputs at one AOCL leaf, and notes that the
branch thereby becomes a second `RemovalRecordsIntegrity` site.

Also: the stranded-successor motivation this fixes, the announcement
payload that makes promotion permissionless for self-perpetuating UTXOs,
the mempool split by preimage availability, and the new test list.

Rename the branch from `Update` to `Advance` while here. `UpdateWitness`
named two different types in this repo -- this one and
`single_proof/update_branch`'s -- and the docs paid for the collision in
circumlocution; `Advance` also survives promotion, which `Update` would
not have. Plan only: percolating the rename into the code is tracked as
its own item. Every remaining `Update` in this file is the `SingleProof`
one.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The witness `Update` will read when a thruput confirmed since the old
mutator set moves into the confirmed inputs. One `Promotion` is the
tuple `Forge`'s confirmed-input loop works on -- item, sender
randomness, receiver preimage, AOCL leaf index -- plus that leaf's
membership proof against the *new* AOCL. It names neither the thruput
it retires nor the input it becomes: both are derived, and the two
multiset equations do the matching.

The set lives in the memory image, `UpdateWitness` being its own, so
the loop count is a list length and the loop re-reads its operands
rather than stashing them in `kmalloc` slots. The path digests still
have to reach the digest stream for the Merkle-traversal instructions;
the memory copy is redundant but harmless.

The constructor now asserts both equations, the strictly ascending leaf
indices and each entry's AOCL membership, so a bad witness fails in
milliseconds rather than after a proof. The branch does not read any of
it yet: every call site passes an empty set, which is the pre-promotion
behaviour exactly.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The Merkle-traversal instructions read authentication paths off the
nondeterministic digest stream, so the paths go there in promotion
order -- after the field authentications, before `StarkVerify`'s
digests, which is where the loop will sit: late enough that the thruput
and input lists it compares are bound to their kernels, early enough to
precede the recursion.

Nothing interleaves and nothing joins the individual-token stream, `P`
being memory-resident: the loop takes one path per iteration off this
stream and everything else out of the entry. An empty `P` contributes
no digests, so every witness built today produces the stream it always
did -- the six cheap negative tests still pass, and the program is
untouched.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The branch now reads the promotion set. A loop over `P` commits each
entry to its addition record, verifies that commitment against the new
AOCL at the leaf index the entry names, derives the absolute index set
from the same four operands, and appends the two results to the lists
the equations compare: a copy of the new thruputs, and the hashed index
sets of the old inputs. Two multiset comparisons then stand where two
byte-equality assertions used to.

No static memory. The loop's whole state is an eight-word stack
invariant -- the accumulator, the two lists, the running lower bound,
the count, the cursor -- and its deepest reach is `dup 14`, so nothing
needs a `kmalloc` slot.

The lower bound makes the leaf indices climb strictly, which is what
forbids two promotions at one AOCL leaf: they would put two removal
records with the same absolute index set in one kernel. Block rule 2.c
rejects that anyway; catching it here keeps a `LinkProof` from ever
attesting to it.

Thruputs and input index sets stop being carried over byte for byte, so
two negatives now fail at the equations instead of at the Merkle root,
as the tracker predicted.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Eight negatives, one per way a promotion can be wrong: a promoted
thruput the old kernel does not list, a promoted input whose index set
is not the computed one, a commitment that is not in the new AOCL, a
membership proof for the wrong leaf, a promotion retiring no thruput at
all, either half of the coupled pair moving alone, two promotions at one
AOCL leaf, and a valid pair in the wrong order.

They run on a deterministic fixture with a mock old link proof, the way
the existing negatives do: the recursion is the last thing the branch
does, so every assertion here fires before a proof is looked at, and
none of the eight pays for proving.

`promotable` now also returns *decoys* -- the same tuples built for the
inputs that stay confirmed. A decoy names a real leaf of the new AOCL
and carries a valid membership proof, so it passes every per-entry
check and still retires no thruput. That is the only shape that reaches
the first promotion equation; every other wrong-UTXO poke fails at the
membership check first.

`update_promotes_every_thruput` adds the rescue in full: a link
transaction with no confirmed inputs, whose thruputs all leave in one
advance. It stops short of asserting the result `Fix`es, `Fix` not
existing yet.

No `promotion_loop_matches_rri` guard, and the tracker now says why.
`Forge` needs its guard because it reimplements RRI's loop inline; this
loop calls the snippets RRI is built from, so the shared core cannot
drift without changing under both.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Rely on two version of tasm-lib/triton-vm.

- Parameterize `NewClaim` and every claim-generator snippet over the
  rule set's proof version, so gamma programs keep pushing 5 while
  delta programs push 7. Entrypoint labels are unchanged: the label
  ordering decides program layout, so renames would break the pinned
  digests.
- Parameterize update, merge, block program
- Stamp every Rust-side sub-claim (proof collection, lock and type
  scripts, update/merge operand claims, block appendix) with the rule
  set's proof version.
Also rename "legacy pipeline" to "singple-proof" pipeline. And fix some
comments, and some indentation.

No consensus programs were changed by this commit.
…eline

- Disallow negative fees throughout the chain pipeline: Cast and Forge,
  the pipeline's entry points, authenticate the kernel's fee leaf and
  assert the fee is a non-negative amount in range; Chain's sum checks
  and Update's unchanged-field check extend the rule by induction.
- Disallow Update when the link's inputs and thruputs are both empty:
  such a link commits to nothing the mutator set moves, and updating one
  would give composers a shortcut past the mempool.
- LinkProof writes the SingleProof program digest D to standard out, and
  every LinkProof claim carries D as its output as well as (reversed) in
  its input: { program, input: [lkmh] || [D], output: [D] }.
- Re-record the LinkProof and delta-SingleProof program digest pins.
An extra proof item is added upstream, for the new (canonical) version
of Triton VM.
Change MempoolTransaction's transaction field to a sum over the two
pipelines (AnyTx), keyed by the wrapped kernel's txid so a link and its
Fix'd form contend for one slot. insert_link follows the standard
replacement contest with links as witness/intermediate tier; a standard
transaction is not replaced by its casted version. Links with a mined
thruput are evicted on block update, as they can never Fix.
Three new p2p message types matching those for transactions in the
single proof pipeline. Shares admission check with that of existing
transactions. Further check: link txs are only accepted from peers if
their thruputs match transactions in the mempool.
- Send chain-transactions, spending outputs from the mempool
- New tx-upgrader task: fix chain-tx. Performs the "fix" transformation
  of chain-transactions with an empty set of thruputs. Done on 3rd party
  transactions.
- Composer checks for fixable (no thruputs chain-tx) before constructing
  expensive nop.
- bump version to v0.16.1 to activate chain p2p messages for integration
  tests.
- resync own chain tx in mempool if they're still confirmable (input
  hasn't been mined and consumed output still in mempool).
@aszepieniec
aszepieniec force-pushed the asz/transaction-chaining branch from 6266eec to 878edf7 Compare August 25, 2026 10:32
@Sword-Smith
Sword-Smith force-pushed the asz/transaction-chaining branch from 12f9a14 to 3ed57f0 Compare August 25, 2026 16:06
In order to clearly be able to distinguish between an "update" on the
single proof-pipeline and on the chain-tx pipeline, we name the
operation that updates the mutator set of a chain-tx "advance". This
operation can also "promote" thruputs to (confirmed) inputs, so it makes
sense it gets a unique name.

Also adds a bound to the jump length used when iterating through
a list of promotion witnesses. This changes some program hash
snapshots.
@Sword-Smith
Sword-Smith force-pushed the asz/transaction-chaining branch from 3ed57f0 to 293aa70 Compare August 26, 2026 09:13
Crash VM execution if some list lengths are not valid u32 numbers. The
main motivation for this is that I saw an `add` of two unchecked list
lengths and wanted to disallow this add wrapping around the b-field
element "max" value.
It can be divined in, or derived. This commit opts for deriving, as that
just feels safer to me.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants