Skip to content

feat(ledger-cbor): one era-aware ledger CBOR reader (ADR 0003, M0-M6) - #748

Draft
satran004 wants to merge 59 commits into
masterfrom
feat/ledger-cbor
Draft

satran004 wants to merge 59 commits into
masterfrom
feat/ledger-cbor

Conversation

@satran004

Copy link
Copy Markdown
Member

Summary

Implements ADR 0003 (one era-aware ledger CBOR reader), milestones M0 to M6, as one PR, as the maintainer chose. M7
(Dijkstra) and M8 (the listener ADR) are not part of it.

  • New module cardano-client-ledger-cbor. The raw views move into it, and it holds the ledger format in one place:
    • EraSpec and the record, field and certificate tables;
    • BlockEnvelope, RawBlock.of and RawBlock.readAs, RawHeader;
    • views for the body, witness set, auxiliary data, governance, parameter updates and the update before Conway;
    • RawTx.walk with FormatPolicy (D6).
  • transaction-spec maps CCL's models from the reader. The models keep their API (D7).
  • common writers are fixed: an empty indefinite map gets its break, tag 2^64-1 is written in full, and no tags go on
    cbor-java's shared simple values.
  • Three tools in tools/:
    • cddl-table-check checks every table against the pinned CDDL (I3);
    • reader-diff compares two CCL and Yaci builds in one JVM, over a corpus, the dataset or a chain stream, and also
      benches (G7, as a local tool);
    • chain-parse-check now also reads every header, body and witness set through the views, and walks every
      transaction.
  • ADR r4 (section 7.1) records how each exit was met, the decisions taken, corrections to the ADR, and the differences
    that need approval.

Yaci (feat/ledger-cbor-reader) and Yano (feat/ledger-cbor-reader) were built and tested against local snapshots of
this branch at every milestone. Those branches are local and follow once this is released.

Results (final state; details in ADR section 7.1)

Gate Result
G3 chain-parse-check 0 issues on preprod (5,376,728 blocks), preview (4,827,659) and mainnet (9,595,957 blocks, 124,065,266 txs). Every per-era counter equals the pre-ADR run
Full-chain old vs new (reader-diff) CCL equal on every transaction of all three networks. Yaci differs only in listed items: the D8 header hash correction (now the hash of the bytes received, on every block), yaci#191's native script hash, and one preview metadata error message
Corpus, governance blocks 22,319 mainnet blocks (288,763 txs) and 118 update and proposal blocks: 0 differences beyond the listed ones
Dataset (42,675 samples) Every difference is attributed to a listed change. CCL's 1,231 are all the bf → bfff writer fix
I10 speed CCL Transaction.deserialize −40% time, −63% allocation. Yaci BlockSerializer −12.5% time, −24% allocation
Yano report 42,215 → 42,606 of 42,675. The strict side rejects all 31,761 invalid samples. I7 passes at every milestone, with 0 lost
Tests CCL whole repository: 6,204 tests, 0 failures, with the dataset tests on

Points needing a maintainer decision

  1. I10 for Yano's validation path. Yano's strict reading of a Conway block is about +30% (about 40 µs a block),
    because the walk checks every leaf for the policy and the views then read the leaves again. CCL and Yaci are faster
    than before.
  2. Indefinite-length headers (Yano). Released nodes (10.6.x, 10.7.0) decode headers with ouroboros-consensus
    RecD and reject them. cardano-ledger 3b0a49bc90 (in cardano-protocol-0.2.0.0) accepts them, and the dataset
    follows it. Yano follows the newer ledger code, as it did before this work.

Accepted by the maintainer on 2026-11-06: Q6 as (a); costModelsHash from the original bytes; the common writer
fixes; Yaci deprecating its tree entry points rather than its classes; and the differences listed in ADR section 7.1.

Release notes (pre-release API)

  • The raw package moves to cardano-client-ledger-cbor. RawDatum.toPlutusData and RawScript.toScript move to
    RawModels (Q2). RawBlock.of(bytes, era) is replaced by readAs.
  • RawRelay's optional items are Optional. CborSpan.items() and entries() return the same read-only list on each
    call.
  • Strict coin reads reject negatives and bignums, as the ledger's Coin does.
  • On rejected input, some model readers now throw CborRuntimeException or CborDeserializationException where they
    threw IllegalArgumentException or ClassCastException: Anchor, Voter, GovActionId, VotingProcedure,
    Constitution, DRep, ProtocolVersion, CostMdls, Withdrawal.
  • A tree overload such as TransactionWitnessSet.deserialize(Map) loses a PlutusData map's lost-pairs mark, which
    the bytes path keeps.
  • plutus depends on ledger-cbor.

Related

Test plan

  • ./gradlew test -PcborDataset=<dataset> on the whole repository (JDK 21): 6,204 tests, 0 failures, 15 skipped
    (Postgres, benchmarks, one annotation-processor test)
  • tools/cddl-table-check at the pin and at 6176e413
  • G3 on preprod, preview and mainnet
  • reader-diff over the full chains, the corpus, the governance blocks and the dataset, base against this branch
  • Yaci and Yano suites against local snapshots of this branch
  • CI

🤖 Generated with Claude Code

https://claude.ai/code/session_01MJrQUDVVQMG6z1k4eszgth

satran004 and others added 30 commits November 5, 2026 23:24
ADR 0003 M0. The raw views move with their package to a new module with no
model on its classpath, so the build keeps models out of the reader. EraRules
becomes the public EraSpec. RawDatum.toPlutusData and RawScript.toScript need
models, so they become RawModels helpers in transaction-spec (Q2).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MJrQUDVVQMG6z1k4eszgth
ADR 0003 I5 and I6 ask that a migrated entry point accepts the same inputs and returns the same models. Before moving
certificates onto the reader, record for every public certificate decoder, over the dataset's Conway samples of the
rules a certificate is made of, valid and invalid alike, how many samples are accepted and a digest of the outcomes,
models included.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MJrQUDVVQMG6z1k4eszgth
CustomCborEncoder wrote bfff back as bf alone, which is not well-formed CBOR: the BREAK was only written after a map's
entries. The recursive encoder before it did the same, and the iterative one kept that on purpose. ADR 0003 D7 has the
public overloads re-encode their item for the reader, which then cannot read such an item, so the side-by-side run of
the certificate decoders found accepted inputs that became rejected. Close it as the array case already does, here and
in the oracle.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MJrQUDVVQMG6z1k4eszgth
ADR 0003 M1. FormatPolicy, RecordSpec, FieldSpec and FieldType are D6's vocabulary, and RecordSpec.walk is one walk for
keyed records, tagged unions and positional records, driven by the tables. A table's depth bounds the walk's, and
recursive types are leaves, so it is stack-safe.

CertKind is the table of the certificate rule: all 19 tags across eras, read from the CDDL at f649f975. RawCertificate
and its nested views find items through the table by type, so a position is written down only there.

RawCertificate.of reads as D5 says. RawCertificate.lenient reads what the models have always accepted, which D7 and I5
keep; D5 rejects a wrong arity and the models accept a long Conway certificate, so the two need separate entry points.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MJrQUDVVQMG6z1k4eszgth
ADR 0003 M1. Certificate and every other certificate decoder, with Anchor, DRep and the credential decoders, become
mappers over RawCertificate; the public overloads re-encode their item and take the same path (D7). A mapper holds no
key, position or tag.

No input changes outcome: CertificateOutcomesTest passes against the file generated from the old decoders, and a
side-by-side run of old and new found no difference. Rejected inputs may be rejected with another exception:
ClassCastException and IndexOutOfBoundsException became CborRuntimeException. CertificateLeniencyTest names what the
models accept beyond the ledger's rules; the same test passes on the old decoders.

One check in RawModels knows an encoding, against D1: an anchor must have a definite length, as the models have
rejected since #717.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MJrQUDVVQMG6z1k4eszgth
ADR 0003 M1's keyed-record proof. TxBodyField and WitnessSetField are the tables of transaction_body and
transaction_witness_set, Shelley to Conway. A key has a row per era range with its own type or requirement: key 3 is
required only in Shelley, key 6 ends at Babbage, key 14 is required_signers to Conway.

RawTx.walk takes the bytes and not a view, as a view rejects a repeated key when it is created and a policy must see that
entry. The proof is a policy with no key number in it that rejects what Yano's reader rejects before reading a value: a
body with no fee, an unknown body or witness key, a repeated key, equal keys encoded differently, a field outside its
era. I9: a walk and the accessors stay stack-safe at 200,000 levels, on the default stack and on a virtual thread.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MJrQUDVVQMG6z1k4eszgth
ADR 0003 I3. tools/cddl-table-check reads the CDDL of each era with git show and the Java tables as text, and compares
certificate tags and items, transaction body and witness set keys with their required flags and types, and the rules a
certificate is made of. Python with only its standard library, run by hand and on each ledger pin bump.

At f649f975 every table matches, Shelley to Conway.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MJrQUDVVQMG6z1k4eszgth
A view read a record's items again for its tag and its arity, and a number at a lenient position went through a tree
even when it was an integer. Time and allocation of Certificate.deserialize fall by about half and a third, still
above the old decoder's. Removes the tables' spec() accessors, which nothing used.

LedgerReaderConformanceTest: every valid Conway sample of the certificate, body, witness set and transaction rules reads
and walks with no structural complaint (ADR 0003 D5, I13).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MJrQUDVVQMG6z1k4eszgth
…pe's table

A policy checks a value by its type alone (ADR 0003 D6), so it could not reach a pool owner's hash length or
definite encoding through a plain SET. KEY_HASH_SET names the CDDL's set<addr_keyhash>, and the table check now
requires it wherever the CDDL has it. FieldType.record() is public so that a caller can walk one nested record,
such as a credential or a DRep, as it can already walk a certificate.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MJrQUDVVQMG6z1k4eszgth
…e JVM

A manual check for moving the decoders onto one reader (ADR 0003 I5, I6, I10).
Two sides, each a directory of jars, run in isolated class loaders over the same
inputs. Probes loaded in each side return an outcome string: accepted with the
model as canonical JSON, rejected, or a stack overflow. A field coverage walker
pins that the JSON misses no field.

Commands: dump (a sample of raw blocks from a node), diff (over a sample or a
stream, with an approved list whose fields are checked against values computed
from the original bytes), diff-dataset (the cardano-cbor-dataset's rules), and
bench (the I10 baseline). side.sh resolves the jars of a side.

Standalone Gradle build, as with tools/chain-parse-check. Not part of the root
build.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MJrQUDVVQMG6z1k4eszgth
CborSpan.arrayItem finds an item of nested arrays by its index path, reading only the heads on the way, the items
before each index and the item itself, so whatever follows may be malformed or missing. A span must walk all of an
array to be created, which is what a damaged block's header cannot have. ADR 0002 D8 promised this and ADR 0001 left
it for when a need showed. It takes every array form and looks through tags, so Yaci's CborSlice.arrayItem can be it.

Also pins that CborSpan.at does not read trailing bytes and CborSpan.skip does: a caller that must fail on a malformed
value after a block, as Yaci's BlockSerializer does, walks them with skip.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MJrQUDVVQMG6z1k4eszgth
…yout

ADR 0003 M2 on the CCL side. EraSpec is complete for Shelley to Conway: each era names a block layout (PARALLEL_4,
PARALLEL_5) and a header layout (FLAT_15, NESTED_10), each implemented once. EraSpec.newest() names the last row.

BlockEnvelope reads [era, block], optionally in tag 24, and accepts any era index, Byron and unknown ones included.
RawBlock.of(envelope) reads a known era and raises UnsupportedEraException, a CborRuntimeException, for the others.
RawBlock.readAs(envelope, era) reads with a named era's layouts. RawBlock.of(bytes) keeps what it accepts.

RawHeader reads both layouts by table: block number, slot, previous hash, keys, VRF results, body size and hash,
operational certificate, protocol version and signature, each when it is asked for, leniently as D5 sets out. hash()
is blake2b256 of the header's original bytes (I8, D8's correction). RawBlock.header() stays a span, so the view is
RawBlock.rawHeader(). RawHeader.of(span) takes its layout from the shape, for a caller with no era.

BlockEnvelope.eraIndexOf and headerOf read only as far as they need, so a block damaged after its header still
reports its number while RawBlock.of stays strict.

The I3 tool checks the layouts against the pinned CDDL. Its split_top took the '>' of '=>' for a closing bracket, which
merged the items after a map; fixed. Also fixes a stale Javadoc link in RawTx that failed :ledger-cbor:javadoc, and wraps
two long lines of the previous commit.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MJrQUDVVQMG6z1k4eszgth
Per block, RawHeader's block number, slot, hash and body hash against the raw bytes, and the block number and slot
against Yaci's header reader. A block hash that differs from Yaci's is counted, not reported, so a G3 run answers how
many real headers ADR 0003 D8's correction changes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MJrQUDVVQMG6z1k4eszgth
…s it was decoded

ADR 0003 M3 reads a transaction body from spans in place of a tree, and has to keep what the tree accepts.
CborSerializationUtil.checkTags rejects the only thing decoding rejects and a span's walker does not: tag 30 without two
integers and a denominator other than 0, and tag 38 without two text strings, in one pass over the heads. A test holds
it to decoding on hand-made items and on random mutations. deserialize(CborSpan) decodes a span without a copy, and
CborSpan.isTagged tells a tag number of 2^64 - 1, which tag() returns as the -1 of no tag.

The re-encode route of D7 lost two inputs that the old decoders accepted. A tag number of 2^63 or more was written with
cbor-java's signed comparisons, as invalid bytes (ff for 2^64 - 1); it is written in 8 bytes now, here and in the oracle.
cbor-java's decoder also sets a tag on its shared null, true, false, undefined and BREAK for the whole JVM when it reads
d818f6, so a tree's nulls were written tagged, and read back as no null. Such a tag is not written now: Yaci's agent
found this rejecting certificates with a null field.

Walking an item with no indefinite length needs no frames, only a count, so CborSpan.skip allocates nothing for most
items. This keeps ccl-tx within I10 with a body walked at each level it reads.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MJrQUDVVQMG6z1k4eszgth
…s, outputs, values and mint

ADR 0003 M3. RawTx.body(era) is a typed view driven by TxBodyField: inputs, outputs, fee, ttl, certificates, withdrawals,
mint, hashes, network id, collateral, required signers and reference inputs. Update and the governance keys stay spans
until M5. A key a later row gives another meaning reads as absent, through RecordSpec.replacedIn, which is how key 14
stays era-aware, and a field that is only outside its era stays readable (D5).

RawOutput keeps its span accessors in both forms and gains addressBytes, amount, inlineDatumBytes and scriptRefBytes;
RawValue, RawMultiAsset, RawWithdrawal and RawInput are new. As M1 did for certificates, RawTx.lenient, RawTxBody.lenient,
RawOutput.lenient and RawValue.lenient read what the models have always accepted, which D7 keeps.

The tables gain what Yano's policy needs for these records, within D6's walk contract: positional tables for an input and
a legacy output, with an optional last item; RecordSpec.entries, a keyed record of like entries with a key type and a
value type per era, which tells mint from withdrawals; a row's own table, for what a set's items are; and
Shape.BY_MAJOR_TYPE, where the item's major type picks the record, for an output and a value. A policy sees the record
picked, in the shapes it knows. Types the policy has no rule for yet are not checked by it, but it does see the tables
now, so its rules on inputs, outputs and mint apply as they are.

A certificate's items are read once: CertKind.of and the view share one list, a union's walk hands it to its member, and
RawCertificate.walk walks a view without listing them again. On the corpus's certificates a walk and a view go from 4377
to 1466 bytes.

From Yaci's pilot: RawRelay.of, relays read when asked for, public RawItems, RawMir.namedPot, TxBodyField.key and spec, and
Optional in place of null in RawRelay. A relay's accessors change type, which Yaci's mapper has to follow.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MJrQUDVVQMG6z1k4eszgth
ADR 0003 M3, D1 and D7. Transaction, TransactionBody, TransactionInput, TransactionOutput, Value, MultiAsset and Withdrawal
map the views, with no key, position or tag in the mapper. Their signatures stay, and the overloads taking a tree re-encode
it and share the path. Transaction.deserialize no longer decodes the body to a tree or re-encodes a certificate. The
witness set and the aux data stay on the decoded tree until M4, and update and the governance keys are decoded from their
spans until M5.

Accepting and rejecting is unchanged, over the corpus, the dataset and mutated bodies; a rejected input may now raise
another class, and an overload that declares CborDeserializationException raises it for all of them, in place of
ClassCastException and NullPointerException.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MJrQUDVVQMG6z1k4eszgth
…n every transaction

cddl-table-check compares transaction_input, both forms of an output, datum_option, value, multiasset, mint and
withdrawals, era by era. chain-parse-check reads every typed field of a body and walks its tables on each transaction.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MJrQUDVVQMG6z1k4eszgth
RawTxBody.tolerant, RawOutput.tolerant, RawValue.tolerant and
RawWithdrawal.tolerant read what Yaci's serializers read, so they can become
mappers (ADR 0003 M3): a repeated record key means its last value, a value array
may be indefinite or longer than two items, and a withdrawal may be any integer.
RawTxBody.certificateItems gives the certificates as encoded. Nothing that exists
changes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MJrQUDVVQMG6z1k4eszgth
A policy has to tell a transaction index, a block body size and a VRF proof from a
plain uint or byte string, as the CDDL does (uint .size 2, uint .size 4, bytes .size
80), so these get their own FieldTypes. RawHeader.walk walks a header and its body in
the layout it has, for a caller with no era; RawHeader.bodySpan gives the body as
encoded; RawOutput.of is public, for a caller holding one output. No reading changes,
and the table check still matches the pinned CDDL.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MJrQUDVVQMG6z1k4eszgth
Its walk is public, and a policy has no need of the table: the rows hold in every era,
so the walk tells it the newest, with no lookup of a layout's first era.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MJrQUDVVQMG6z1k4eszgth
A record is now read twice, by a policy's walk and by its views, and each listing of a
container skips over every child, so re-listing costs a scan of all it holds at every
level (ADR 0003 I10). A span keeps its listing. The list it hands out cannot be
modified; no caller modifies one. A racing second listing is equal and harmless.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MJrQUDVVQMG6z1k4eszgth
…he whole transaction

ADR 0003 M4. RawTx.witnessSet(era) and RawTx.auxData(era) are typed views over WitnessSetField and the new AuxDataField:
vkey and bootstrap witnesses, scripts, datums, both forms of redeemers, and aux data in all three forms. Scripts, Plutus
data and metadata stay spans. RawWitnessSet.lenient, RawAuxData.lenient and RawRedeemer.lenient read what the models have
always accepted, which D7 keeps: a redeemer map has one entry per key as a decoded map has it, a field of neither
redeemer form holds none, the tag closest to a map says what aux data it is, and a redeemer value and the aux array are
definite unless a tree makes them so (RawItems.indefiniteWhenDecoded). RawTx.redeemers, scripts, witnessDatums and
auxScripts read through the views, with the behaviour they had.

The tables gain what Yano's policy needs, within D6's walk contract. Witness set rows name the table of an item:
positional vkeywitness, bootstrap_witness and redeemer, and a leaf per item for the recursive types. Redeemers are by
major type: a list, or the map of like entries, whose keys and values have tables. Auxiliary data is by major type, with
a row for a map in its tag (RecordSpec.tag). RawTx.walk now walks the transaction's own array, with the form without
the flag having rows to Mary and the form with it from Alonzo, so an out of era form is the policy's to reject. A key of
an array of scalars counts its occurrences as decoded, so [0, 0] and [0, 1800] are one key.

KeyedRecordPolicyTest.acceptAllSeesRepeatedAndUnknownKeysInSourceOrder now looks at the body's entries only, as the walk
reports the transaction's positional entries as well, with no key.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MJrQUDVVQMG6z1k4eszgth
… reader

ADR 0003 M4, D1 and D7. TransactionWitnessSet, VkeyWitness, BootstrapWitness, Redeemer, ExUnits and AuxiliaryData map the
views, with no key, position or tag in the mapper. Their signatures stay, and the overloads taking a tree re-encode it
and share the path. PlutusData, NativeScript, the Plutus script models and CBORMetadata still decode their recursive
content, from the source bytes of its span. Transaction.deserialize reads no tree of its own now, and only M5's
governance items in TransactionBody still decode from spans. plutus now depends on ledger-cbor, for Redeemer and ExUnits.

Accepting and rejecting is unchanged over the corpus, the dataset and mutated witness sets and aux data, and a rejected
input to an overload now raises CborDeserializationException in place of ClassCastException or CborRuntimeException.
One difference is known: a tree's Plutus data map that lost a pair no longer reports it through
PlutusData.mapsThatLostPairs when it goes through an overload taking a tree, as re-encoding drops the mark. From bytes,
as Transaction.deserialize reads it, the mark stays, and WitnessAndAuxMappingTest pins both.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MJrQUDVVQMG6z1k4eszgth
…lk every transaction

cddl-table-check compares the table of each witness field's items, vkeywitness, bootstrap_witness, both forms of
redeemers, ex_units, the three forms of auxiliary data and the transaction's own array, era by era. chain-parse-check
reads the typed witness set and aux data of each transaction and walks all its tables, reporting any field that is
unknown or outside its era and any required field missing.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MJrQUDVVQMG6z1k4eszgth
… without copying

The same reason as listing a span's children once (ADR 0003 I10). A repeated-key check
of small keys needs no set, and a byte string key's identity needs no copy of its bytes.
Nothing a caller sees changes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MJrQUDVVQMG6z1k4eszgth
encodeHexString built a String for every byte in a synchronized StringBuffer, and
decodeHexString a substring for every pair; it was a fifth of a transaction's read in
Yano's parse (ADR 0003 I10). The same characters come out, lower case, and the same
input is rejected.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MJrQUDVVQMG6z1k4eszgth
…r does

ADR 0003 M4, for Yaci. RawWitnessSet.tolerant and RawAuxData.tolerant read what a
decoded tree accepts, as RawTxBody.tolerant does: a repeated key means its last
value, a redeemer map's value may be indefinite, the aux data array may be
indefinite and longer than two items, and an item of no aux data form has nothing
in it. What decoding to a tree rejects is still rejected.

A redeemer keeps its encoding: RawRedeemer.span is the item of the array form or
the value of the map form, and RawRedeemer.key the map form's key. A repeated key
keeps its first encoding with its last value. RawAuxData.has says whether the keyed
form has a field, which an empty list of scripts cannot.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MJrQUDVVQMG6z1k4eszgth
…s in a mask

ADR 0003 I10. RawTx.walk takes the span of a view as well as bytes, so a caller that
walks and then reads lists each container once: a span keeps its listing. A keyed
walk counts unsigned keys below 64 in a mask, however their heads are written, and
keeps a map only for any other key or a repeat, so a record's usual walk allocates
no BigInteger and no map. What a policy is told does not change.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MJrQUDVVQMG6z1k4eszgth
satran004 and others added 6 commits November 6, 2026 05:19
…irectory from the environment

side.sh and the README named one developer's directories; READER_DIFF_HOME now says where m2/ and sides/ are.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MJrQUDVVQMG6z1k4eszgth
The walk compared a key that is no integer, array or byte string by its whole encoding, and a byte string by its
content, so a text key "aa" and the byte string key h'6161' counted as one key. Inside an array key it compared a byte
string by its whole encoding, so a voter, gov action id or committee credential whose hash has a longer length head
counted as another key, where the decoded map has one. Every key that is no integer now goes through RawItems.identity,
alone or as an item of an array key.

Found by Yano's M5 (governance maps keyed by records) and the ADR 0003 review.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MJrQUDVVQMG6z1k4eszgth
…ers take, and tell [+ a] from nonempty_list

A record whose shape follows its item's major type reported nothing of its own, so a form outside its era with an
empty container reached a policy as an enter alone: an array value in Shelley, a map output in Alonzo, aux data in tag
259 or as an array in Shelley, map redeemers in Babbage. A policy with no layout knowledge (ADR 0003 I13) could not
reject them. The walk now reports the row the item picks as an entry with no key, whose key is the major type, before
it walks the row's table, as a positional record reports each position.

NONEMPTY_LIST stood for both Conway's nonempty_list<a>, which may be in tag 258 (witness fields 0, 1, 2 and 4), and the
list form of redeemers, [+ redeemer], which may not (decodeNonEmptyList without allowTag, Alonzo/TxWits.hs at
6176e413). The list form of redeemers is now NONEMPTY_ARRAY, and cddl-table-check maps [+ a] to it.

Found by the ADR 0003 review.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MJrQUDVVQMG6z1k4eszgth
…integers, and keep RawItems to this module

A view read a fee, a collateral, a withdrawal or a parameter's coin as an unsigned integer, and a certificate's deposit,
pledge and cost, a proposal's deposit, a value's coin and a MIR's coin as any integer or bignum. Every coin is now read
as the ledger's Coin decodes one (decodeWord64, Coin.hs at 6176e413), an unsigned integer with tags looked through,
through RawItems.coin. The lenient readings keep what the models have always read (ADR 0003 D7), so CCL's models and
Yaci read what they did; a walk still gives a policy a value's coin of any integer or bignum to check. Delta coins and
asset quantities stay signed.

The strict reading of a redeemer read its index as any 64-bit integer, a negative one included; tag and index are now
read as the lenient readings read them.

RawItems keeps public only what Yaci and plutus call: array, bytes and indefiniteWhenDecoded.

Found by the ADR 0003 review.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MJrQUDVVQMG6z1k4eszgth
… envelope readers stop; fix a block's message

A RawBlock of two items said it was a Dijkstra block in any era; it now says how many items the era's block has, and
EraSpec still names Dijkstra for era 8. RawBlock and BlockEnvelope say that eraIndexOf and headerOf, unlike of, do not
look into tag 24. CborSpan says why its memo fields need not be volatile: each memo is reached only through final
fields, so a thread that sees one sees it whole (JLS 17.5).

Found by the ADR 0003 review.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MJrQUDVVQMG6z1k4eszgth
Section 7.1 records how each milestone's exit was met, with the final G3, full-chain,
dataset, Yano report and I10 numbers; the decisions taken during implementation; the
corrections to D2, D3, D5, D6, D7 and D8; the differences for approval; and what is
left open (I10 for Yano's validation path, indefinite-length headers, yano#238).
The status stays Accepted; its note now says M0 to M6 are implemented. Q6 is
implemented as (a).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MJrQUDVVQMG6z1k4eszgth

Copy link
Copy Markdown
Contributor

Initial automated review · Aster

Hi @satran004, thanks for the PR! 🙏 This is an automatic first check by an AI reviewer. It does not replace maintainer review and may be wrong.

I’ve noted that this is still a draft.

Reviewed commit: 7bdbdf484d08f9997797f3e1ac9a0afc1485e346

Issues found:

  • tools/reader-diff/src/main/java/com/bloxbean/cardano/client/tools/readerdiff/OutcomeDiff.java:135 — Comparing differing models with deeply nested data can overflow the recursive equals/walk and abort the entire comparison run. Could we use an explicit stack for both checks and test a differing deep model?

Not tested: Code was read only; builds, tests, and chain comparisons were not run.

satran004 and others added 11 commits November 6, 2026 10:46
The ledger counts the values of a bootstrap address's unknown attributes in its
attribute size (bootstrapAddressAttrsSize), so a validator needs them from the same
decoder that checks the address. ByronAddress.getUnknownAttributes returns every
attribute other than the derivation path and the protocol magic, by key, as the
ledger's unknownAttributes keeps them.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MJrQUDVVQMG6z1k4eszgth
Haskell decodes by era (eraDecoder at eraProtVerLow), so Conway always
decodes at 9. Section 7.1 "Left open" now says M7 adds Yano's Dijkstra
policy at version 12 and checks the auxiliary data forms whose decoders
change there. No other text changed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MJrQUDVVQMG6z1k4eszgth
…ntal

ADR 0003 r6: new D4a reads Dijkstra and Leios in ledger-cbor as experimental
work for the Leios branch in Yaci, pinned to cardano-ledger-dijkstra 0.5.0.0
(95b36905, codecs equal to 0.4.0.0 in cardano-node 11.2.0) and
ouroboros-consensus 5.2.0.0. The block body hash is one hash over the
original block_body; the Dijkstra header on the wire is the 10-item Praos
header, with the 12-item Leios header also read. M7 splits into M7a (now)
and M7b (era final); new I14 and I15; I12 re-pinned; test vectors pinned
per source.

ADR 0004 r1: Dijkstra in CCL's transaction model as an opt-in era: new
fields, era propagation to nested writers, public constructors kept, Conway
bytes unchanged (W1).

Both reviewed before commit by a Fable pass and a Codex pass.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MJrQUDVVQMG6z1k4eszgth
95e9e7c2 to 4fd977b2. Every Conway file of 95e9e7c2 is byte-identical at
4fd977b2, which adds 1,437; run at both pins, the 59,076 outcomes they
share are identical. The model gate goes from 2,705 to 2,837 valid
samples, all 132 new ones passing; certificate-outcomes.txt gains the new
samples' lines. Its Dijkstra files are those of d4709b12, which the
Dijkstra gate (next commit) lists.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MJrQUDVVQMG6z1k4eszgth
Era.Dijkstra(8) with an EraSpec row: the unified block layout, its body
hash over the whole block_body, and the 10-item Praos header node 11.2.0
sends as well as the 12-item Leios header. Transactions in all three
forms; sub-transactions read and walked with the body table's
sub-transaction rows; guards, keys 23-27, Plutus V4 scripts, the
pool BLS key slot, parameter keys 34-56, Leios and Peras certificates
and endorser-block references. A later era's key in a keyed record is an
unknown key, so Conway walks are as before. Every new API is marked
experimental.

Tests: 43 Haskell or real-network vectors (Musashi, DijkstraNet, Leios
devnet blocks, transactions with sub-transactions and Plutus V4,
endorser-block references), each with expected values from its source;
7,576 Dijkstra dataset samples of 50 rules under a strict test policy
(one dataset label disputed); the Conway gates unchanged.

Reviewed by Fable and Codex over two rounds.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MJrQUDVVQMG6z1k4eszgth
…era (ADR 0004)

Model fields for guards, sub-transactions, required top-level guards,
direct deposits, account balance intervals, Plutus V4 scripts, the pool
BLS key and parameter keys 34-56, and RequireGuard, PlutusV4Script,
Language.PLUTUS_V4 and RedeemerTag.Guarding. They are written only for
Dijkstra: Transaction.serialize(Era) is public, the era reaches proposals
and parameter updates, a Dijkstra-only field written for an earlier era
is an error, and Dijkstra writes the 3-item transaction and Haskell's
encodings. Transaction.deserialize(bytes, Era) reads them. Public
constructors keep their signatures (Lombok's all-args is private), so
814 existing signatures, Conway bytes and toJson output are unchanged.

ledger-cbor: lenient and tolerant reads in Dijkstra accept
indefinite-length records, as Haskell does from version 12; new
RawCertificate.lenient(kind, span, era) and RawBlock.transaction(int)
for Yaci.

Reviewed by Fable and Codex over two rounds.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MJrQUDVVQMG6z1k4eszgth
Q7 decided as (a) by the maintainer: Yaci's models gain Dijkstra's fields
now, mapped from CCL's views, null when absent, with ADR 0004's constructor
rule. requiredSigners in Dijkstra are the key guards, as Haskell's
reqSignerHashesTxBodyG. Keys 38 and 53 round-trip in three states through
any ObjectMapper (CCL's nil-or-value codec made public, with a
deserializer). Yaci's parameter writer writes keys 34-56 and its proposal
reader takes an era. Sub-transaction UTxO application, Plutus V4 and the
guarding purpose, and Yano's proposal reload are listener prerequisites
before Dijkstra. New I16 and M7c.

Reviewed by Fable (two rounds) and Codex (three rounds), both approved.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MJrQUDVVQMG6z1k4eszgth
…SON for Yaci's Dijkstra models (ADR 0003 M7c)

A sub-transaction's body view hides the six top-level-only keys (2, 13,
16, 17, 23, 27), as the ledger guards them with STopTx, in the reader and
in CCL's own models. RawAuxData hides key 5 (Plutus V4) before Dijkstra
in has() and its accessor; keys 0-4 read as before. The nil-or-value JSON
serializer is public, with a matching contextual deserializer, so keys 38
and 53 round-trip in three states through a bare ObjectMapper; Rational
gains a JSON creator and ProtocolParamUpdate's nOpt its "nopt" name, with
emitted JSON unchanged. ADR 0003 r10 records M7c in section 7.3.

Reviewed by Fable (two rounds) and Codex (three rounds), both approved.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MJrQUDVVQMG6z1k4eszgth
9ef2a45 (ADR 0004) added a fourth test to CborCorpusConformanceTest,
so the guard that fails a skipped gate, which wants exactly three,
failed although every test passed. Locally at dataset 4fd977b2:
tests="4" skipped="0" failures="0" errors="0".

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MJrQUDVVQMG6z1k4eszgth
Section 8 gains a row, and I16 and section 7.3's JSON gate list it: the six
pins of three preprod blocks match once metadataCbor is re-encoded
canonically (yaci-store#1215; yaci#243 does the same on Yaci's 0.4 line).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MJrQUDVVQMG6z1k4eszgth
@sonarqubecloud

sonarqubecloud Bot commented Nov 9, 2026

Copy link
Copy Markdown

Quality Gate Failed Quality Gate failed

Failed conditions
C Reliability Rating on New Code (required ≥ A)

See analysis details on SonarQube Cloud

Catch issues before they fail your Quality Gate with our IDE extension SonarQube for IDE

This branch has not been deployed

No deployments
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