Enable nullables in 6 base projects#12155
Conversation
|
Claude finished @flcl42's task in 8m 16s —— View job Review: Enable foundation nullables (#12155)
Overall this is a careful, well-scoped nullability batch. The core fix — splitting FindingsMedium
Low
Positive notes
Verdict: Not blocking on bugs, but the Medium ERA1 behavior change should be explicitly acknowledged (or covered/commented) before merge. I couldn't independently run the build/tests in this review environment; the PR reports Release builds passing with warnings-as-errors. |
| RlpReader ctx = new(buffer.Span); | ||
| // ReceiptMessageDecoder returns null for empty-list (0xC0) items | ||
| return ctx.DecodeArray<TxReceipt>(_receiptDecoder, allowNulls: true); | ||
| return ctx.DecodeArray<TxReceipt>(_receiptDecoder); |
There was a problem hiding this comment.
[Medium] Behavior change: ERA1 receipt decoding switched from tolerant to throwing.
On master this was:
// ReceiptMessageDecoder returns null for empty-list (0xC0) items
return ctx.DecodeArray<TxReceipt>(_receiptDecoder, allowNulls: true);The class-constrained DecodeArray<T> now uses DecodeGuardNotNull, and ReceiptMessageDecoder.DecodeInternal genuinely returns null for an empty-list (0xC0) element. So an ERA1 archive containing such an element now throws RlpException during import instead of producing a null array entry.
For well-formed canonical archives this is fine (receipts are never 0xC0), and arguably stricter-is-better. But the deliberate allowNulls: true plus its explanatory comment were load-bearing-looking, and there is no test asserting the new strict behavior on the ERA path. Please confirm this narrowing is intentional; if so, consider a one-line comment noting that empty-list receipt elements are now rejected. If null-tolerance is actually required here, use DecodeNullableArray instead.
| string? enr = node.Enr; | ||
| if (string.IsNullOrEmpty(enr)) | ||
| { | ||
| return null; | ||
| } | ||
|
|
||
| if (TryGetKnownRecord(node.Id.Hash, out NodeRecord? knownRecord)) | ||
| { | ||
| return IsAcceptableNodeRecord(knownRecord, node.Id.Hash, allowNonRoutableRelays, recordFilter) ? knownRecord : null; | ||
| } |
There was a problem hiding this comment.
[Low] Reordering changes lookup behavior. The new early-return on string.IsNullOrEmpty(enr) runs before TryGetKnownRecord(node.Id.Hash, ...). Previously the cached-record lookup ran first, so a node that has a known/cached NodeRecord but an empty node.Enr string would have returned the cached record; now it returns null and is excluded from FINDNODE responses.
If a cached record can ever exist independently of node.Enr, this silently drops valid relays. Safer to keep the TryGetKnownRecord check first and only guard the empty-enr case around the FromEnrString call:
if (TryGetKnownRecord(node.Id.Hash, out NodeRecord? knownRecord))
return IsAcceptableNodeRecord(...) ? knownRecord : null;
if (string.IsNullOrEmpty(node.Enr)) return null;| public IEnumerable<KeyValuePair<byte[], byte[]>> GetAll(bool ordered = false) => | ||
| ordered ? OrderedDb : _db.Select(static kvp => new KeyValuePair<byte[], byte[]>(kvp.Key, kvp.Value)); |
There was a problem hiding this comment.
[Low] Extra per-element allocation on enumeration. GetAll/OrderedDb previously returned _db (or its ordered view) directly; they now project each entry into a freshly-allocated KeyValuePair<byte[], byte[]> via Select. Since MemDb.Set(key, null) deletes rather than storing null, the values are guaranteed non-null, so the projection is purely to satisfy the non-nullable return type. Functionally correct, but it adds an allocation/copy per entry on every GetAll enumeration (used by pruning/copy-tree/visitor paths). Minor; acceptable for the nullability cleanup, just flagging the cost.
Changes
Visual summary
Green nodes show the new nullable-aware state. Red nodes show replaced/deleted assumptions. Blue nodes show contracts that became stricter non-null.
flowchart LR classDef old fill:#fee2e2,stroke:#dc2626,color:#111827 classDef added fill:#dcfce7,stroke:#16a34a,color:#052e16 CryptoOld["Nethermind.Crypto<br/>Nullable: annotations"] --> CryptoNew["Nethermind.Crypto<br/>Nullable: enable"] DbOld["Nethermind.Db<br/>Nullable: annotations"] --> DbNew["Nethermind.Db<br/>Nullable: enable"] StatsOld["Nethermind.Network.Stats<br/>Nullable: disabled"] --> StatsNew["Nethermind.Network.Stats<br/>Nullable: enable"] JsonOld["Nethermind.Serialization.Json<br/>Nullable: disabled"] --> JsonNew["Nethermind.Serialization.Json<br/>Nullable: enable"] RlpOld["Nethermind.Serialization.Rlp<br/>Nullable: annotations"] --> RlpNew["Nethermind.Serialization.Rlp<br/>Nullable: enable"] SpecsOld["Nethermind.Specs<br/>Nullable: annotations"] --> SpecsNew["Nethermind.Specs<br/>Nullable: enable"] class CryptoOld,DbOld,StatsOld,JsonOld,RlpOld,SpecsOld old class CryptoNew,DbNew,StatsNew,JsonNew,RlpNew,SpecsNew addedflowchart TD classDef added fill:#dcfce7,stroke:#16a34a,color:#052e16 classDef removed fill:#fee2e2,stroke:#dc2626,color:#111827 classDef strict fill:#dbeafe,stroke:#2563eb,color:#111827 RlpOld["RLP old assumptions<br/>generic Decode/Encode and array helpers treated null/default loosely"] --> RlpNew["RLP nullable-aware contracts<br/>MaybeNull Decode, T? Encode/GetLength, nullable arrays explicit"] RlpOld2["Deleted: empty-list element could become default(T)<br/>in delegate DecodeArray and DecodeArrayPoolList"] --> RlpNew2["Added: null/default guard<br/>ThrowNullArrayElement for malformed non-null arrays"] RlpOld3["Deleted: allowNulls flag inside DecodeArray"] --> RlpNew3["Added: DecodeArray for non-null class arrays<br/>and DecodeNullableArray for nullable class arrays"] RlpStrict["Added non-null scalar decode helpers<br/>DecodeKeccakNonNull, DecodeZeroPrefixKeccakNonNull,<br/>DecodeAddressNonNull, DecodeBloomNonNull"] JsonOld["JSON old return shape<br/>Deserialize returned object / T"] --> JsonNew["JSON nullable return shape<br/>Deserialize returns object? / T?"] JsonOld2["Hash256Converter wrote only Hash256"] --> JsonNew2["Hash256Converter handles Hash256?<br/>and writes JSON null"] JsonOld3["ByteArrayArrayConverter handled byte[][]"] --> JsonNew3["ByteArrayArrayConverter handles byte[]?[]?<br/>outer and inner nulls are explicit"] DbOld["DB old shape<br/>column keys unconstrained; GetAll/Values carried nullable values"] --> DbNew["DB stricter shape<br/>column keys are notnull; GetAll/Values enumerate non-null persisted values"] PruneOld["Full pruning old out parameter<br/>out IPruningContext"] --> PruneNew["Full pruning Try pattern<br/>NotNullWhen(true) out IPruningContext?"] CryptoOld["ECIES old decrypt tuple<br/>byte[] PlainText even when Success=false"] --> CryptoNew["ECIES decrypt tuple<br/>byte[]? PlainText tied to Success"] StatsOld["Network.Stats old details<br/>P2P/Eth/Les details assumed present"] --> StatsNew["Network.Stats optional details<br/>P2PNodeDetails?, EthNodeDetails?, LesNodeDetails?"] SpecsOld["Chain spec old optional JSON fields<br/>modeled as non-null references"] --> SpecsNew["Chain spec nullable fields<br/>optional addresses, hashes, genesis, allocations, names are marked ?"] class RlpNew,RlpNew2,RlpNew3,RlpStrict,JsonNew,JsonNew2,JsonNew3,PruneNew,CryptoNew,StatsNew,SpecsNew added class RlpOld,RlpOld2,RlpOld3,JsonOld,JsonOld2,JsonOld3,DbOld,PruneOld,CryptoOld,StatsOld,SpecsOld removed class DbNew strictflowchart LR classDef upgraded fill:#dcfce7,stroke:#16a34a,color:#052e16 classDef consumer fill:#fef9c3,stroke:#ca8a04,color:#111827 Crypto["Crypto"] --> Discovery["Network.Discovery consumers"] Crypto --> Hive["Hive/runtime consumers"] Db["Db"] --> DbRocks["Db.Rocks"] Db --> TxPool["TxPool BlobTxStorage"] Db --> PruningTests["pruning and DB tests"] Stats["Network.Stats"] --> DiscoveryAdapters["discv4/discv5 adapters"] Json["Serialization.Json"] --> JsonRpc["JsonRpc tests and serializers"] Json --> MergeTests["Merge.Plugin tests"] Rlp["Serialization.Rlp"] --> Network["Network eth/snap message serializers"] Rlp --> Era["Era1 reader"] Rlp --> Optimism["Optimism decoders"] Rlp --> Xdc["XDC header decoders"] Specs["Specs"] --> OptimismSpecs["Optimism/Taiko/Shutter chain-spec consumers"] class Crypto,Db,Stats,Json,Rlp,Specs upgraded class Discovery,Hive,DbRocks,TxPool,PruningTests,DiscoveryAdapters,JsonRpc,MergeTests,Network,Era,Optimism,Xdc,OptimismSpecs consumerTypes of changes
What types of changes does your code introduce?
Testing
Requires testing
If yes, did you write tests?
Notes on testing
src/tests; no files undersrc/testswere changed.Documentation
Requires documentation update
Requires explanation in Release Notes
Remarks
This is the first smaller nullable batch. Later nullable batches should continue upward through dependent runtime projects.