DC-EVIEW-02
DC derived enforcedTyped, era-gated stake-reference classification. Given canonical address bytes and a TYPED era / protocol-version context BOUND to the block being processed (CardanoEra, from era_schedule.locate(slot).era -- never inferred from the address bytes, local config, wall-clock, or a caller-selected flag), classify_output_stake_ref returns ONE deterministic typed result StakeRefClass = Base(StakeCredential) | Pointer(PointerRef) | Null | Reject(StakeRefReject). It is the per-output attribution PRIMITIVE: it extracts the stake reference only, resolves nothing, sums nothing, and NO result directly changes stake totals (aggregation is Slice 3). No fixed byte offset is the contract across variants/eras: classification routes through the typed decode_address chokepoint plus per-form structural validation. (a) Base 0-3: the staking credential is read (key/script per header bit 5) ONLY after the form is validated to header(1) + payment(28) + stake(28) = 57 bytes. (b) Pointer 4-5 is ERA-GATED -- pre-Conway it decodes to an UNRESOLVED PointerRef{slot,txIx,certIx} that implies no credential / contribution / eligibility; at Conway (protocol major 9+, i.e. era >= CardanoEra::Conway) pointer stake is RETIRED -> Null (the address is spendable, contributes 0). (c) Enterprise 6-7 and Byron 8 are the semantic Null (valid non-staking forms, all eras). (d) Reward 14-15 is fail-closed Reject(RewardAddressNotValidAsOutput): a reward address is not a valid output payment address and must never be ordinary output stake (the full ledger rule is proven in Slice 3; here it is decoder-complete + fail-closed). Reject is DISTINCT from Null: Null is a valid non-staking form, Reject is invalid input or a wrong-position form -- a malformed / under-length / malformed-but-prefix-valid address is Reject(..), NEVER silently Null. Total, pure, deterministic (no HashMap/wall-clock/rand/float).
- Source
docs/clusters/EPOCH-CONSENSUS-VIEW/SLICE-2-stake-reference-classification.md; docs/clusters/EPOCH-CONSENSUS-VIEW/EPOCH-CONSENSUS-VIEW-design-analysis.md (Deliverable 1/2, classification matrix)
- Introduced in
- EPOCH-CONSENSUS-VIEW-S2
Enforcement trace
Tests 18
- base_type0_is_stake_key_hash
- base_type1_is_stake_key_hash
- base_type2_is_stake_script_hash
- base_type3_is_stake_script_hash
- pointer_is_decoded_pre_conway_and_retired_at_conway
- pointer_multibyte_varint_pre_conway
- pointer_result_exposes_no_credential
- enterprise_and_byron_are_null_all_eras
- reward_address_is_rejected_not_summed
- empty_is_reject_not_null
- unknown_type_is_reject
- malformed_but_prefix_valid_base_is_reject_not_null
- pointer_truncated_varint_is_reject_pre_conway
- pointer_trailing_bytes_is_reject_pre_conway
- pointer_overflow_varint_is_reject_pre_conway
- pointer_too_short_is_reject_pre_conway
- real_preview_addresses_classify_without_reject
- classification_is_deterministic
Cross-references
Attack rationale
The classifier must not (a) infer the pointer-retirement era from anything but a bound consensus context -- it takes a typed CardanoEra, never the address bytes / config / wall-clock / a caller flag, so the SAME pointer bytes classify deterministically to Pointer pre-Conway and Null at Conway+ on every node; (b) treat a malformed input as Null -- Null is reserved for valid non-staking forms (enterprise/Byron/Conway-retired-pointer), and a malformed / under-length / malformed-but-prefix-valid address is a DISTINCT Reject, so a truncated base can never be silently counted as no-stake when it is actually corrupt; (c) read a staking part from an unchecked offset -- the base form is validated to 57 bytes before [29..57] is read, and the pointer varints are exact-consumption + overflow-guarded; (d) treat a reward address as output stake -- reward-as-output is fail-closed Reject, decoder-complete but never Base; or (e) resolve, sum, or mutate -- it is a pure reference-extraction primitive that changes no stake totals (resolution + aggregation are Slice 3). Conway pointer-stake retirement (spec 9.1.2) is the era-gate most able to diverge if the era source is loose, which is why the bound-context signature is the load-bearing invariant.
Evidence notes
Introduced at EPOCH-CONSENSUS-VIEW Slice 2 (2026-06-20). The second slice of the native epoch-transition cluster (after DC-EVIEW-01, the transient substrate): the per-output stake-reference classification, isolated as the riskiest correctness surface (Conway pointer retirement; key/script/reward/null per era) and oracle-able on its own BEFORE the heavier aggregation + replay-window wiring (Slice 3). Pure BLUE, no leader slot. Grounded: Ade's ade_codec::address::decode_address already classifies the 5 header-byte forms but stores only raw bytes (no staking-part extraction, no length check) -- the net-new is the typed staking-part decode + era-gated classification + fail-closed malformed handling. CardanoEra (ade_types::era, Conway=7, derives Ord) is the typed bound context; the gate keys on era>=Conway (the whole Conway era is PV9+, == pointer retirement). Ground truth cited in the design record: Conway formal ledger spec 9.1.2 (pointer stake retired: spendable, contributes 0) verified against cardano-ledger code; CIP-19 header/byte layout. 17 hermetic tests: CIP-19 vectors for base x4 (key/script x key/script via header bit 5), the deliberate era fixture (same pointer bytes -> Pointer pre-Conway {Alonzo/Mary/Babbage} and Null at Conway), multi-byte varint, Pointer-exposes-no-credential, enterprise/Byron Null all eras, reward fail-closed (types 14+15), empty/unknown-type/malformed-but-prefix-valid-base/truncated-varint/trailing-bytes/too-short-pointer all Reject-distinct-from-Null, determinism, and a REAL-INTEROP fixture (5 bech32-decoded live preview addresses -- base types 0/1/3 + enterprise 6/7 -- classify with zero Reject, the type-0 staking key hash asserted exactly, proving the [29..57] extraction is right on real wire data, per the synthetic-tests-miss-real-wire-bugs lesson). cargo test -p ade_ledger stake_ref green. Slice-2 boundary held: NO pointer resolution, NO aggregation/sum, NO EpochConsensusView, NO transient-store/replay wiring, NO live wiring, NO track_utxo (all Slice 3). NO BA02 / leader-schedule oracle claim here -- the full pool-distribution match vs cardano-cli is Slice 3 (needs aggregation); Slice 2's oracle is CIP-19 vectors + the Conway pointer fixture + the real-preview sample. SCOPE (binding): DC-EVIEW-02 proves typed, era-aware stake-reference classification ONLY. It does not prove any stake contribution, pool attribution, snapshot, or leader-election semantics. Slice-3 obligation (authoritative-parser rule): the pointer-varint rule must be RESOLVED (not deferred) and must MATCH cardano-ledger EXACTLY (grounded 2026-06-20 vs master + its tests). Ground truth: bounded leading-zero-group aliasing (e.g. [0x80,0x01]==[0x01]) is ACCEPTED in ALL eras (the strict check is a WIDTH check, not a minimal-form check -- so reject-all-non-canonical would FALSE-REJECT txs cardano-node accepts = divergence). The rule is ERA-PARAMETERIZED: Conway+ (PV9+) rejects OVER-WIDTH (u32 slot / u16 txIx / u16 certIx, bounded group counts) + trailing bytes; Babbage (PV7-8) NORMALIZES (clamp the whole 3-tuple to (0,0,0) if any coordinate overflows its width) + rejects trailing; <=Alonzo (PV2-6) normalizes + crops trailing. Slice-2's decode_varint here diverges from BOTH regimes (it rejects >u64) and must be REPLACED by the era-parameterized decoder in S3a; it stays correct for Slice 2 (pre-Conway, resolves nothing).