Summary
dolos_testing::synthetic::build_synthetic_blocks produces transactions that are not value-balanced: each tx's output coin equals its input coin while also declaring a fee and registration-deposit certs that are never funded from the inputs. Dolos doesn't phase-1-validate in sync/import (it trusts the chain), so it records the fees/deposits anyway. This conjures value and breaks the pots supply invariant the first time a chain of these blocks crosses an epoch boundary.
This surfaced while building the #1018 reproduction test (tests/boundary_resume.rs, added in #1019): those tests must run --release to compile out the pots.is_consistent debug_assert in crates/cardano/src/estart/reset.rs, because crossing a boundary trips it. The invariant is orthogonal to what those tests exercise (entity snapshot rotation), so release-only is an acceptable status quo — but it means the boundary-resume tests cannot run in debug, and the generator is not boundary-safe for future tests.
Root cause
- Per-block UTxO accounting:
utxo_delta = Σ outputs.coin − Σ resolved_inputs.coin (crates/cardano/src/roll/epochs.rs:143-169), folded into the pots at the boundary (apply_delta, crates/cardano/src/pots.rs:413-414).
- Pots invariant:
reserves + treasury + utxos + rewards + fees + obligations == max_supply (crates/cardano/src/pots.rs:72).
- In
crates/testing/src/synthetic.rs::sample_transaction: input = one seed UTxO of seed_amount (MIN_UTXO_AMOUNT); output coin = lovelace (MIN_UTXO_AMOUNT, equal to input); fee: 7 plus stake/pool/DRep registration certs.
- Result:
utxo_delta == 0, but fees and obligations grow with no offsetting decrease in utxos. The invariant overshoots by exactly fees + obligations (observed: 896,199,386 at the first boundary; produced_utxos: 0 / consumed_utxos: 0 in the boundary PotDelta).
What a fix requires
- Balance every tx:
input_value = output_value + fee + deposits_charged_by_this_tx, so utxo_delta = −(fee+deposits) and the utxos pot drops by what fees/obligations gain. (Sufficient for is_consistent, which checks pot arithmetic, not the actual UTxO-set total.)
- Handle deposit conditionality (the hard part): a deposit is charged only on the first registration of a credential. The generator currently re-includes the same registration certs in every tx, but only the first charges a deposit (observed
pool_count == 1 despite a pool cert in every block). So either emit registration certs once and fund that tx, or use distinct fresh credentials per registration and fund each.
- (Optional, fidelity only) trace the seed funds back to genesis (reduce genesis reserves / add to the genesis utxos pot) so the actual UTxO-set total also reconciles — not needed to pass the debug_assert.
Risk / scope
build_synthetic_blocks is shared by tests/bootstrap.rs, SyntheticVectors-based assertions, and others that hardcode the current amounts/cert shapes — changing tx values and the cert strategy will likely require re-greening those. A lower-risk alternative is a dedicated, minimal, properly-funded block builder for boundary tests instead of reworking the shared generator.
Acceptance
References
Summary
dolos_testing::synthetic::build_synthetic_blocksproduces transactions that are not value-balanced: each tx's output coin equals its input coin while also declaring a fee and registration-deposit certs that are never funded from the inputs. Dolos doesn't phase-1-validate in sync/import (it trusts the chain), so it records the fees/deposits anyway. This conjures value and breaks the pots supply invariant the first time a chain of these blocks crosses an epoch boundary.This surfaced while building the #1018 reproduction test (
tests/boundary_resume.rs, added in #1019): those tests must run--releaseto compile out thepots.is_consistentdebug_assertincrates/cardano/src/estart/reset.rs, because crossing a boundary trips it. The invariant is orthogonal to what those tests exercise (entity snapshot rotation), so release-only is an acceptable status quo — but it means the boundary-resume tests cannot run in debug, and the generator is not boundary-safe for future tests.Root cause
utxo_delta = Σ outputs.coin − Σ resolved_inputs.coin(crates/cardano/src/roll/epochs.rs:143-169), folded into the pots at the boundary (apply_delta,crates/cardano/src/pots.rs:413-414).reserves + treasury + utxos + rewards + fees + obligations == max_supply(crates/cardano/src/pots.rs:72).crates/testing/src/synthetic.rs::sample_transaction: input = one seed UTxO ofseed_amount(MIN_UTXO_AMOUNT); output coin =lovelace(MIN_UTXO_AMOUNT, equal to input);fee: 7plus stake/pool/DRep registration certs.utxo_delta == 0, butfeesandobligationsgrow with no offsetting decrease inutxos. The invariant overshoots by exactlyfees + obligations(observed: 896,199,386 at the first boundary;produced_utxos: 0 / consumed_utxos: 0in the boundaryPotDelta).What a fix requires
input_value = output_value + fee + deposits_charged_by_this_tx, soutxo_delta = −(fee+deposits)and theutxospot drops by whatfees/obligationsgain. (Sufficient foris_consistent, which checks pot arithmetic, not the actual UTxO-set total.)pool_count == 1despite a pool cert in every block). So either emit registration certs once and fund that tx, or use distinct fresh credentials per registration and fund each.Risk / scope
build_synthetic_blocksis shared bytests/bootstrap.rs,SyntheticVectors-based assertions, and others that hardcode the current amounts/cert shapes — changing tx values and the cert strategy will likely require re-greening those. A lower-risk alternative is a dedicated, minimal, properly-funded block builder for boundary tests instead of reworking the shared generator.Acceptance
pots.is_consistent.tests/boundary_resume.rsruns in debug (drop therequire_release()gate).References
crates/testing/src/synthetic.rs,crates/cardano/src/roll/epochs.rs,crates/cardano/src/pots.rs