Skip to content

Commit d81dd7d

Browse files
committed
fix: import short-epoch ledger snapshots
1 parent 638d320 commit d81dd7d

12 files changed

Lines changed: 300 additions & 19 deletions

File tree

README.md

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,9 @@ amaru-bootstrap-producer-<github-sha>.tar.gz
9191
producer race.
9292
- A CI-gated `amaru-run-bootstrap` proof that Amaru can open the
9393
produced ledger/chain stores and reach ledger startup.
94+
- A short-epoch Antithesis golden gate that samples slots `9`, `129`,
95+
and `249` from a generated 120-slot-epoch ChainDB and imports them
96+
through Amaru.
9497

9598
The architecture, state machine, release boundary, and concurrency model
9699
are documented with diagrams in `docs/architecture.md`.
@@ -135,6 +138,8 @@ the Amaru bootstrap projection of the node-10.7.1 state:
135138
- Conway/Dijkstra pool state projected to the fields Amaru imports
136139
- Conway/Dijkstra account state projected to Amaru's legacy delegation
137140
wrapper
141+
- empty reward-update state projected as a completed zero reward update,
142+
matching Amaru's `has_rewards=true` import path
138143

139144
## Inputs / outputs
140145

@@ -160,6 +165,7 @@ running.
160165
├── chain.<network>.db/ # populated by amaru import-headers/import-nonces
161166
├── ledger.<network>.db/ # populated by amaru import-ledger-state
162167
├── snapshots/<slot>.<hash>.cbor # target plus two prior epoch snapshots
168+
├── snapshots/history.<slot>.<hash>.json # testnet era history sidecars
163169
├── nonces.json # tail rewritten to previous-epoch header hash
164170
└── headers/header.<slot>.<hash>.cbor # headers needed by Amaru
165171
```
@@ -168,6 +174,11 @@ The latest snapshot's `<slot>.<hash>` must have a matching
168174
`headers/header.<slot>.<hash>.cbor`; Amaru uses that exact header when
169175
aligning its chain store to the ledger tip at startup.
170176
177+
For custom testnets, the producer also corrects each open-ended current
178+
era history sidecar to the `epochLength` from the node's Shelley genesis.
179+
This matters for short-epoch Antithesis networks because Amaru imports
180+
testnet snapshots using the sidecar history file next to each snapshot.
181+
171182
## Local verification
172183
173184
```bash
@@ -180,6 +191,9 @@ Phase 0 smoke verdict and accepts either `PASS` or the expected
180191
bootstrap-producer verifier. The pure producer-specific end-to-end check
181192
is `.#checks.x86_64-linux.bootstrap-producer-synthesized`; the startup
182193
proof is `.#checks.x86_64-linux.amaru-run-bootstrap`.
194+
The short-epoch Antithesis regression checks are
195+
`.#checks.x86_64-linux.antithesis-short-epoch-samples` and
196+
`.#checks.x86_64-linux.antithesis-short-epoch-golden`.
183197

184198
These checks prove bundle production, Amaru import, and Amaru startup
185199
alignment. They are not a full mainnet ledger-content coverage suite for

amaru-bootstrap.cabal

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ library
4040
, cardano-ledger-shelley
4141
, cardano-binary
4242
, cardano-slotting
43+
, cardano-strict-containers
4344
, cborg
4445
, containers
4546
, directory

docs/bootstrap-producer.md

Lines changed: 20 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -54,13 +54,15 @@ signal for downstream Amaru services.
5454
4. Run `ledger-state-emitter` at the selected target slot and the two
5555
preceding epoch slots.
5656
5. Run `amaru convert-ledger-state` for all emitted ledger states.
57-
6. Run `header-extractor list-blocks` and `get-header` to collect the
57+
6. Correct the converted current-era history sidecars from the node
58+
genesis `epochLength`.
59+
7. Run `header-extractor list-blocks` and `get-header` to collect the
5860
headers Amaru needs.
59-
7. Rewrite `nonces.json` so `tail` points at the previous-epoch header
61+
8. Rewrite `nonces.json` so `tail` points at the previous-epoch header
6062
hash.
61-
8. Run `amaru import-ledger-state`, `amaru import-headers`, and
63+
9. Run `amaru import-ledger-state`, `amaru import-headers`, and
6264
`amaru import-nonces`.
63-
9. Atomically rename the unique temp directory into the final bundle
65+
10. Atomically rename the unique temp directory into the final bundle
6466
path.
6567

6668
The final layout is:
@@ -87,6 +89,14 @@ If the latest converted snapshot is
8789
`chain.<network>.db/`; otherwise `amaru run` fails during startup with
8890
`ledger tip header not found`.
8991

92+
For custom testnets, `amaru import-ledger-state` reads
93+
`snapshots/history.<slot>.<hash>.json` next to each snapshot. Amaru's
94+
converter currently fills the open-ended current era with the network
95+
default epoch size, so the producer rewrites that sidecar to the
96+
`epochLength` from the mounted Shelley genesis before import. Without
97+
that correction a 120-slot Antithesis testnet imports slot 9, then fails
98+
at slot 129 because the sidecar still maps slot 129 to epoch 0.
99+
90100
## Node-Release Target
91101

92102
This implementation targets `cardano-node 10.7.1`. The repository pins
@@ -154,6 +164,9 @@ node-10.7.1 ledger state:
154164
- Conway/Dijkstra account state is projected into Amaru's legacy
155165
delegation-state wrapper while preserving rewards, deposits,
156166
stake-pool delegation, and DRep delegation.
167+
- Empty reward-update state is projected as a completed zero reward
168+
update because Amaru's import command decodes snapshots with
169+
`has_rewards=true`.
157170

158171
The detailed contract is in
159172
`specs/003-amaru-bootstrap-producer/research.md#r-011`.
@@ -200,7 +213,9 @@ shape a long-running public network can contain.
200213
`antithesis-short-epoch-samples` generates a deterministic short-epoch
201214
ChainDB corpus from the pinned node 10.7.1 tooling, emits the observed
202215
early bootstrap slots `9`, `129`, and `249`, and converts them through
203-
`amaru convert-ledger-state`. The source ChainDB is generated during the
216+
`amaru convert-ledger-state`. The check also rewrites the current-era
217+
history sidecars to the generated Shelley genesis `epochLength`, matching
218+
the production producer path. The source ChainDB is generated during the
204219
Nix build; the repository does not commit bulky database artifacts.
205220

206221
`antithesis-short-epoch-golden` imports those converted snapshots into

docs/what-amaru-needs.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ Reverse-engineered from
1111
├── chain.<network>.db/ # amaru's chain store, prepopulated
1212
├── ledger.<network>.db/ # amaru's ledger store, prepopulated
1313
├── snapshots/<slot>.<hash>.cbor # target plus two prior epoch boundaries
14+
├── snapshots/history.<slot>.<hash>.json # testnet era history sidecar
1415
├── nonces.json # tail points to previous-epoch header hash
1516
└── headers/header.<slot>.<hash>.cbor # includes the latest snapshot header
1617
```
@@ -94,3 +95,9 @@ dependency set and emits the Amaru bootstrap projection documented in
9495
it three times: `target_slot`, `target_slot - epochLength`, and
9596
`target_slot - 2 * epochLength`. `amaru convert-ledger-state` still owns
9697
the final snapshot slicing, history JSON, and nonce JSON formats.
98+
99+
For custom testnets, the producer corrects the converted
100+
`history.<slot>.<hash>.json` files before import: the open-ended current
101+
era's `epoch_size_slots` is set to the mounted Shelley genesis
102+
`epochLength`. This keeps short-epoch networks consistent with the
103+
ledger snapshot epoch number that Amaru checks during import.

justfile

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,8 @@ build-gate:
4040
.#checks.x86_64-linux.bootstrap-producer-bats \
4141
.#checks.x86_64-linux.bootstrap-producer-synthesized \
4242
.#checks.x86_64-linux.amaru-run-bootstrap \
43+
.#checks.x86_64-linux.antithesis-short-epoch-samples \
44+
.#checks.x86_64-linux.antithesis-short-epoch-golden \
4345
.#checks.x86_64-linux.bootstrap-producer-image
4446
4547
# Run the unit-style bats checks.

lib/LedgerStateEmitter.hs

Lines changed: 23 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,9 @@ adjusted where Amaru's importer still expects older ledger shapes:
3939
and DRep delegation are preserved; historical pointer indexes and the
4040
intermediate deposits accumulator are omitted because Amaru skips them
4141
during bootstrap.
42+
* Empty reward-update state is projected as a completed zero reward
43+
update. Amaru's import command runs in @has_rewards=true@ mode and
44+
unconditionally decodes a @Complete RewardUpdate@ at this position.
4245
-}
4346
module LedgerStateEmitter (
4447
emitLedgerSnapshot,
@@ -58,6 +61,7 @@ import Cardano.Ledger.Conway.State qualified as Conway
5861
import Cardano.Ledger.Core qualified as Core
5962
import Cardano.Ledger.Dijkstra.State ()
6063
import Cardano.Ledger.Shelley.LedgerState qualified as SL
64+
import Cardano.Ledger.Shelley.RewardUpdate qualified as ShelleyReward
6165
import Cardano.Ledger.State qualified as Ledger
6266
import Cardano.Slotting.Slot (SlotNo)
6367
import Cardano.Tools.DBAnalyser.Block.Cardano (
@@ -81,11 +85,13 @@ import Control.ResourceRegistry (
8185
import Data.ByteString.Lazy qualified as LBS
8286
import Data.Functor.Contravariant ((>$<))
8387
import Data.Map.Strict qualified as Map
88+
import Data.Maybe.Strict (StrictMaybe (..))
8489
import Data.Proxy (Proxy (Proxy))
8590
import Data.SOP.BasicFunctors (K (K))
8691
import Data.SOP.Functors (Flip (unFlip))
8792
import Data.SOP.Strict (NP (Nil, (:*)), fn, type (-.->))
8893
import HeaderExtractor (NodeConfig (NodeConfig))
94+
import Lens.Micro ((^.))
8995
import Ouroboros.Consensus.Block (
9096
GetHeader,
9197
blockNo,
@@ -190,7 +196,6 @@ import Ouroboros.Consensus.Util.CBOR (
190196
)
191197
import Ouroboros.Consensus.Util.Versioned (encodeVersion)
192198
import Ouroboros.Network.Block (genesisPoint)
193-
import Lens.Micro ((^.))
194199
import System.Directory (createDirectoryIfMissing)
195200
import System.FilePath (takeDirectory)
196201

@@ -499,7 +504,8 @@ encodeShelleyLedgerStateCanonicalWith ::
499504
(SL.NewEpochState era -> CBOR.Encoding) ->
500505
LedgerState (ShelleyBlock proto era) EmptyMK ->
501506
CBOR.Encoding
502-
encodeShelleyLedgerStateCanonicalWith encodeNewEpochState
507+
encodeShelleyLedgerStateCanonicalWith
508+
encodeNewEpochState
503509
ShelleyLedgerState
504510
{ shelleyLedgerTip
505511
, shelleyLedgerState
@@ -562,18 +568,29 @@ encodeNewEpochStateCanonicalLedgerWith encodeCertState (SL.NewEpochState e bp bc
562568
, encCBOR bp
563569
, encCBOR bc
564570
, encodeEpochStateCanonicalWith encodeCertState es
565-
, encCBOR ru
571+
, encodeRewardUpdateAmaru ru
566572
, encCBOR pd
567573
, encCBOR av
568574
]
569575

576+
encodeRewardUpdateAmaru ::
577+
StrictMaybe ShelleyReward.PulsingRewUpdate ->
578+
LedgerEncoding
579+
encodeRewardUpdateAmaru =
580+
encCBOR . \case
581+
SNothing ->
582+
SJust $
583+
ShelleyReward.Complete ShelleyReward.emptyRewardUpdate
584+
SJust ru -> SJust ru
585+
570586
encodeEpochStateCanonicalWith ::
571587
forall era.
572588
(CanonicalShelleyLedgerCbor era) =>
573589
(SL.CertState era -> LedgerEncoding) ->
574590
SL.EpochState era ->
575591
LedgerEncoding
576-
encodeEpochStateCanonicalWith encodeCertState
592+
encodeEpochStateCanonicalWith
593+
encodeCertState
577594
SL.EpochState
578595
{ SL.esChainAccountState = esChainAccountState
579596
, SL.esLState = esLState
@@ -594,7 +611,8 @@ encodeLedgerStateCanonicalWith ::
594611
(SL.CertState era -> LedgerEncoding) ->
595612
SL.LedgerState era ->
596613
LedgerEncoding
597-
encodeLedgerStateCanonicalWith encodeCertState
614+
encodeLedgerStateCanonicalWith
615+
encodeCertState
598616
SL.LedgerState
599617
{ SL.lsUTxOState = lsUTxOState
600618
, SL.lsCertState = lsCertState

nix/checks.nix

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -204,6 +204,7 @@ let
204204
pkgs.bash
205205
pkgs.coreutils
206206
pkgs.findutils
207+
pkgs.jq
207208
amaruPkg
208209
headerExtractorPkgs.ledger-state-emitter
209210
];
@@ -229,6 +230,16 @@ let
229230
--target-dir "$out/snapshots"
230231
done
231232
233+
epoch_length=$(jq -r '.epochLength' \
234+
"$TMPDIR/config/shelley-genesis.json")
235+
for history in "$out"/snapshots/history.*.json; do
236+
tmp="$history.tmp"
237+
jq --argjson epochLength "$epoch_length" \
238+
'(.eras[] | select(.end == null) | .params.epoch_size_slots) = $epochLength' \
239+
"$history" >"$tmp"
240+
mv "$tmp" "$history"
241+
done
242+
232243
cbor_count=$(find "$out/snapshots" -maxdepth 1 \
233244
-name '*.cbor' | wc -l)
234245
if [ "$cbor_count" -ne 3 ]; then
@@ -354,6 +365,8 @@ in
354365
BOOTSTRAP_PRODUCER_CHAIN_DB=$TMPDIR/chain-db \
355366
bats --tap tests/test-bootstrap-producer-concurrent.bats
356367
368+
bats --tap tests/test-bootstrap-producer-history.bats
369+
357370
mkdir -p $out
358371
'';
359372

scripts/bootstrap-producer.sh

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,32 @@ tail_phase_log() {
102102
printf -- '--- end %s ---\n' "${f}" >&2
103103
}
104104

105+
patch_converted_era_history() {
106+
local history tmp count=0
107+
108+
for history in "${UNIQUE_TMP}/snapshots"/history.*.json; do
109+
[[ -e "${history}" ]] || continue
110+
count=$(( count + 1 ))
111+
tmp="${history}.tmp"
112+
if ! jq --argjson epochLength "${EPOCH_LENGTH}" \
113+
'(.eras[] | select(.end == null) | .params.epoch_size_slots) = $epochLength' \
114+
"${history}" >"${tmp}" 2>"${BUNDLE_DIR}/.logs/history.stderr"
115+
then
116+
printf 'era-history patch failed for %s; see %s\n' \
117+
"${history}" "${BUNDLE_DIR}/.logs/history.stderr" >&2
118+
rm -f "${tmp}"
119+
exit 6
120+
fi
121+
mv "${tmp}" "${history}"
122+
done
123+
124+
if (( count == 0 )); then
125+
printf 'amaru convert-ledger-state produced no history.*.json files in %s\n' \
126+
"${UNIQUE_TMP}/snapshots" >&2
127+
exit 6
128+
fi
129+
}
130+
105131
# ─── 8-step state diagram (functions stubbed, T018+T019 fill them) ─
106132

107133
# Step 1: pre-flight (wait + validate + era-readiness predicate).
@@ -342,6 +368,9 @@ phase_emit() {
342368
# Writes <slot>.<hash>.cbor + nonces.<slot>.<hash>.json + history.<slot>.<hash>.json
343369
# into <staging>/snapshots/. amaru's import-ledger-state requires the
344370
# era-history file to live alongside the snapshot for testnet variants.
371+
# Amaru's converter currently fills the open-ended current era with the
372+
# network default epoch size; for custom short-epoch testnets the
373+
# producer corrects that sidecar from the node genesis before import.
345374
phase_convert() {
346375
local snapshot slot rc
347376
for snapshot in "${LEGACY_SNAPSHOT_FILES[@]}"; do
@@ -361,6 +390,8 @@ phase_convert() {
361390
fi
362391
done
363392

393+
patch_converted_era_history
394+
364395
local cbor_count
365396
cbor_count=$(find "${UNIQUE_TMP}/snapshots" -maxdepth 1 \
366397
-name '*.cbor' 2>/dev/null | wc -l)

specs/004-short-epoch-golden-states/plan.md

Lines changed: 14 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -11,15 +11,19 @@
1111
- Created issue #29 and moved it to WIP.
1212
- Reproduced the local import failure with generated short-epoch states:
1313
`unexpected type map at position 2: expected u32`.
14+
- Fixed the reward-update projection mismatch by emitting a completed
15+
zero reward update when the node ledger state carries `SNothing`.
16+
- Fixed the short-epoch era-history mismatch by rewriting converted
17+
current-era history sidecars from the node genesis `epochLength`.
18+
- Local `just build-gate` passes with the short-epoch checks included.
1419

1520
**Current**:
1621

17-
- Add generated sample and golden checks to Nix/CI.
22+
- Push the passing PR and let GitHub CI re-run the same Build Gate.
1823

1924
**Blockers**:
2025

21-
- The golden check is expected to fail until the Haskell projection or
22-
Amaru importer contract is fixed.
26+
- None known locally.
2327

2428
## Technical Context
2529

@@ -39,6 +43,11 @@ The repository should not grow committed ChainDB artifacts.
3943
chain is dense enough to expose immutable blocks in a small CI budget.
4044
- Sample slots `9`, `129`, and `249`, matching the observed early
4145
bootstrap emission points.
46+
- Project empty reward-update state as `Complete emptyRewardUpdate`
47+
because Amaru's CLI imports snapshots with `has_rewards=true`.
48+
- Correct the converted open-ended current era history sidecar to the
49+
Shelley genesis `epochLength`; Amaru's converter currently uses the
50+
network default there, which is wrong for 120-slot custom testnets.
4251
- Split conversion and import:
4352
- `antithesis-short-epoch-samples` proves sample generation and
4453
conversion.
@@ -52,5 +61,5 @@ nix build .#checks.x86_64-linux.antithesis-short-epoch-samples
5261
nix build .#checks.x86_64-linux.antithesis-short-epoch-golden
5362
```
5463

55-
Before the fix, the second command is expected to fail at
56-
`amaru import-ledger-state` with the current CBOR/import mismatch.
64+
After the fix, both commands pass locally. `just build-gate` also passes
65+
with both short-epoch checks included in the gate.

specs/004-short-epoch-golden-states/spec.md

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,9 @@ fail before the projection/import mismatch is fixed and pass afterwards.
3535
immutable blocks for `header-extractor` and `ledger-state-emitter`.
3636
- A check that only proves conversion is insufficient; the import step is
3737
the failing boundary.
38+
- Converted testnet history sidecars must agree with the node genesis
39+
epoch length; otherwise Amaru maps slot 129 to epoch 0 while the
40+
ledger snapshot declares epoch 1.
3841
- The corpus must remain pinned to the node 10.7.1 dependency set.
3942

4043
## Requirements
@@ -51,13 +54,16 @@ fail before the projection/import mismatch is fixed and pass afterwards.
5154
`amaru import-ledger-state`.
5255
- **FR-005**: The repository MUST document the generated corpus profile
5356
and explain why the database is not committed.
57+
- **FR-006**: The converted current-era history sidecars MUST use the
58+
Shelley genesis `epochLength` before Amaru import on custom testnets.
5459

5560
### Key Entities
5661

5762
- **Short-epoch ChainDB corpus**: Generated Cardano ChainDB configured to
5863
exercise the early Conway-from-genesis bootstrap state family.
5964
- **Golden snapshots**: Converted Amaru snapshots derived from the
60-
sampled ledger states.
65+
sampled ledger states, with history sidecars corrected to the sampled
66+
genesis epoch length.
6167
- **Import gate**: CI check that requires Amaru to consume the snapshots.
6268

6369
## Success Criteria

0 commit comments

Comments
 (0)