IntersectMBO / IntersectMBO/ouroboros-consensus
[BUG] - ShelleyGenesis CBOR schema changed under unchanged ShelleyNodeToClientVersion 13/14/15
- Dominant language
- Haskell
- Stars
- 67
- Forks
- 43
- Avg merge
- 5d 13h
- Merged PRs (30d)
- 43
Description
**Internal/External**
*External* — `cardano-wallet`.
**Summary**
`GetGenesisConfig` results are encoded through `genesisConfigEnDecoding`:
```haskell
-- ouroboros-consensus-cardano/src/shelley/Ouroboros/Consensus/Shelley/Ledger/Query.hs
genesisConfigEnDecoding v
| v >= ShelleyNodeToClientVersion13 = (toCBOR, fromCBOR)
| otherwise = (encodeLegacyShelleyGenesis . getCompactGenesis,
compactGenesis <$> decodeLegacyShelleyGenesis)
```
`CompactGenesis` derives `ToCBOR`/`FromCBOR` through `ShelleyGenesis`, so the
`>= 13` arm resolves to the linked `cardano-ledger-shelley`.
`cardano-ledger-shelley` 1.19.0.0 added `sgExtraConfig` to `ShelleyGenesis`,
changing its encoding from `encodeListLen 15` / `decodeRecordNamed … (const 15)`
to `encodeListLen 16` / `(const 16)`.
`ShelleyNodeToClientVersion` 13, 14 and 15 therefore select two incompatible
schemas depending on the linked ledger, and the version mapping is unchanged:
| released | consensus | ledger-shelley | `Result_GenesisConfig` golden |
|---|---|---|---|
| `cardano-node` 11.0.1 | 3.0.1.0 | 1.18.1.0 | `0x8f…`, 78 bytes |
| `cardano-node` 11.1.0 (**pre-release**) | 4.1.0.0 | 1.19.0.0 | `0x90…`, 79 bytes |
Both define Shelley versions through 15 and map `NodeToClientV_21`/`_22`/`_23`
to Shelley 13/14/15. No Shelley version 16 exists in either release or on
current `main`; `ouroboros-network` `main` also ends at outer
`NodeToClientV_23`.
A peer negotiating `NodeToClientV_21`, `_22` or `_23` cannot determine which
`ShelleyGenesis` schema the other side will use.
**Steps to reproduce**
1. Build a node-to-client consumer linking `ouroboros-consensus-cardano` 4.1.0.0
(which links `cardano-ledger-shelley` 1.19.0.0).
2. Run it against `cardano-node` 11.0.1.
3. Issue `GetGenesisConfig` over LocalStateQuery at `NodeToClientV_21`, `_22`
or `_23`.
The node encodes 15 elements; the consumer's decoder expects 16.
**Expected behavior**
A change to a node-to-client payload schema is selected by a negotiated version,
so that both peers agree on the schema after handshake.
**System info**
- OS: Linux x86_64 (NixOS), kernel 6.12.67
- Consensus: 3.0.1.0 and 4.1.0.0; current `main` at `586b4294e6`
- `cardano-ledger-shelley`: 1.18.1.0 and 1.19.0.0
- `cardano-node`: 11.0.1 (release) and 11.1.0 (**pre-release**; GitHub reports `prerelease=true`)
**Screenshots and attachments**
`Result_GenesisConfig` goldens, comparing `6de6c8a6279ef4f072e768724fa3606dd4cb85d5`
with its parent `f205a7103deb732cc07cabd51fa76ce22f84f0d0`. All three paths are
`M`:
```
golden/shelley/QueryVersion3/ShelleyNodeToClientVersion13/Result_GenesisConfig
golden/shelley/QueryVersion3/ShelleyNodeToClientVersion14/Result_GenesisConfig
golden/shelley/QueryVersion3/ShelleyNodeToClientVersion15/Result_GenesisConfig
```
```
parent : 78 bytes, begins 8f 83 19 07 e4 18 87 00 00 00 82 09 0a 0a 18 64
commit : 79 bytes, begins 90 83 19 07 e4 18 87 00 00 00 82 09 0a 0a 18 64
```
Bytes 2–78 are identical. Byte 1 changes from `0x8f` to `0x90` — CBOR definite
array of 15 to 16 — and byte 79 is appended as `0x80`, the encoding of
`sgExtraConfig = SNothing`. Current `main` carries the 79-byte form.
**Observed outcomes, and source-derived mechanism**
Four pairings were run, two matched pairs as controls. Both controls synced, so
the two failures are properties of the pairing rather than of the harness.
| consumer ledger | node 11.0.1 | node 11.1.0 |
|---|---|---|
| 1.18.1.0 | syncs *(control)* | **process dies** |
| 1.19.0.0 | **silent stall** | syncs *(control)* |
In the passing 1.19.0.0 × 11.1.0 control, `GetGenesisConfig` is answered and
decoded including `sgExtraConfig = SNothing`, so the 16-field round trip is
exercised.
**Consumer on 1.18.1.0, node on 1.19.0.0.** *Mechanism, from source:*
`decodeRecordNamedT` runs the body
before checking size, so the 15-field body succeeds and `matchSize` then compares
the announced 16 against the expected 15. Observed:
```
ExceptionInLinkedThread (ThreadId 11) DecoderFailure
(LocalStateQuery HardForkBlock (…)) Query (BlockQuery …)
ServerHasAgency (SingQuerying)
(DeserialiseFailure 117
"Size mismatch when decoding Record ShelleyGenesis.\nExpected 16, but found 15.")
```
Process exit code 1 at approximately 5 seconds; a repeat run produced the
identical error.
**Consumer on 1.19.0.0, node on 1.18.1.0.** *Mechanism, from source:* the decoder
reads the announced
length 15, runs the 16-field body, and requests input for `sgExtraConfig`.
`runDecoderWithChannel` handles `DecodePartial` by calling `recv`. After
`MsgResult` the protocol returns to `StAcquired`, where the client holds agency,
so the server waits for the client's next message and the mini-protocol stays
open; no EOF arrives and both peers block.
Observed: `NodeToClientV_22` negotiated; the querying client's last logged action
is `Send MsgQuery … GetGenesisConfig` with no corresponding `Recv`; the
enabled chain-sync trace recorded no sent messages during the five-minute
observation; the tip remains at genesis. The consumer's
HTTP status endpoint was polled 39 times over five minutes and did not answer
once, while the same harness against the passing control answered 15 times
reporting readiness.
Node-side `StateQueryServer` counters were not obtainable under the tracing
configurations used, and are not part of this evidence.
Consumer revisions. Old side `346786a112`, a public `master` commit eight commits
behind the current head. New side `dfc45fad7f81204d50b7e680df832e725b08c157`, **a
pre-rebase commit no longer reachable from the public branch** — the remote returns
no object for it, so this exact binary is not reproducible from the remote as it
stands; an archival copy can be supplied. The public branch head is `33ccafc`.
The pinned `cardano-ledger-shelley` version — the property under test — is
`1.18.1.0` at both old-side revisions and `1.19.0.0` at both new-side revisions.
**Additional context**
A repair needs to cover both directions:
1. `ShelleyNodeToClientVersion` 13, 14 and 15 encode and decode the 15-field
schema again. The existing `< 13` legacy path cannot be reused directly
because its field 12 encodes `LegacyPParams`; a distinct 15-field
`ShelleyGenesis` codec retaining the v13-era nested `PParams` encoding is
needed for 13–15.
2. A new outer `NodeToClientVersion`, with corresponding Cardano and Shelley
versions, selects the 16-field schema.
3. **Both** codecs own and freeze their complete nested wire representations —
the restored 15-field codec for versions 13–15 as much as the new 16-field one.
Freezing only an outer list length leaves the same class of defect one level
down, where a linked-ledger change can again alter a fixed protocol version.
**This does not preserve every pairing, and the report does not claim it does.**
| pairing after the change | negotiated | result |
|---|---|---|
| repaired consumer ↔ 11.0.1 node | ≤ 23 | works |
| consensus-3.0.1.0 / ledger-1.18.1.0 consumer ↔ repaired node | ≤ 23 | works |
| repaired consumer ↔ **unmodified** consensus-4.1.0.0 node | ≤ 23 | still broken — node sends 16, consumer expects 15 |
| **unmodified** consensus-4.1.0.0 / ledger-1.19.0.0 consumer ↔ repaired node | ≤ 23 | still broken — consumer expects 16, node sends 15 |
| repaired consumer ↔ repaired node | new outer version | works |
No encoder selected only by version 21, 22 or 23 can determine whether an
unmodified peer expects the 15- or 16-field meaning of that version. A permissive
decoder can accept both lengths; an encoder cannot satisfy both. Since 11.1.0 is
a pre-release, superseding or withdrawing that build is one available policy —
the two broken rows above need a stated rollout decision either way.
An alternative design avoids the new version: `compactGenesis` overwrites
`sgExtraConfig` with `SNothing` unconditionally, so the 16th field carries no
information for this query. A permanently frozen 15-field query representation,
independent of the linked ledger, would also resolve it.
**Test coverage.** The existing `Result_GenesisConfig` goldens are per-version
encodings of the current fixture set. They record that bytes changed; they do not
exercise a decoder of one version against an encoder of another. Cross-version
decode tests — old encoder against new decoder and the reverse, for every
retained outer version — would cover the interoperability property directly.
`currentPParamsEnDecoding` has the same two-arm shape: `(toCBOR, fromCBOR)` for
`>= ShelleyNodeToClientVersion13`, explicit `LegacyPParams` codecs below it. This
is a structural observation; no claim is made here that `GetCurrentPParams`
changed between these two releases.
Contributor guide
Research direction
Start in ouroboros-consensus-cardano/src/shelley/Ouroboros/Consensus/Shelley/Ledger/Query.hs and trace genesisConfigEnDecoding alongside the ShelleyNodeToClientVersion mappings. Review the Result_GenesisConfig goldens and add cross-version encoder/decoder coverage for retained outer versions. Done means the selected rollout and frozen representations preserve interoperability, with the new version path tested.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- haskell
- Domain
- blockchain, distributed-systems, networking
- Issue type
- Bug
- Difficulty
- 5/5
- Estimated time
- Over a week
- Activity status
- Active
- Clarity
- Mostly clear
- Newbie friendliness
- 35/100