xdc eth endpoints#11939
Conversation
|
Claude finished @ak88's task in 5m 24s —— View job PR Review: xdc eth endpoints
SummaryThis PR adds three new FindingsHigh1. 2. Medium3. 4. No tests for 5. Low6. 7. 8. Pruning of RPC rewards not exercised in tests — |
| return Task.FromResult(ResultWrapper<Dictionary<string, Dictionary<string, Dictionary<string, string>>>>.Success([])); | ||
| } | ||
|
|
||
| if (!rewardsStore.TryGetEpochRewards((ulong)header.Number, out Dictionary<string, Dictionary<string, Dictionary<string, string>>>? rewards) |
There was a problem hiding this comment.
High — rewards retrieved by block number, not block hash
The method contract says "Returns epoch reward distribution for the block identified by hash," but the lookup is keyed by header.Number, not by the input hash:
if (!rewardsStore.TryGetEpochRewards((ulong)header.Number, out ...))RewardsStore indexes its data by block number (see BuildEpochRewardsRpcKey). If a chain reorganisation replaces the block at this number, TryGetEpochRewards returns the rewards for the new canonical block — not for the block whose hash was passed in. The caller cannot detect the mismatch.
Options:
- Store rewards by block hash (and add a hash → number index) so that each call is truly hash-scoped.
- At minimum, after the lookup, verify that the canonical block at
header.Numberstill has the same hash, and return an error or empty if it doesn't. Fix this →
| BlockHeader? header = blockFinder.FindHeader(blockHash); | ||
| if (header is null) | ||
| { | ||
| return Task.FromResult(ResultWrapper<Dictionary<string, Dictionary<string, Dictionary<string, string>>>>.Success([])); |
There was a problem hiding this comment.
Medium — silent "not found" masquerades as "no rewards"
When FindHeader(blockHash) returns null the method returns Success([]), which is indistinguishable from a genuine "epoch block exists but has zero reward entries." Callers that pass an invalid or pruned hash will silently receive an empty dict instead of an error.
| return Task.FromResult(ResultWrapper<Dictionary<string, Dictionary<string, Dictionary<string, string>>>>.Success([])); | |
| return Task.FromResult(ResultWrapper<Dictionary<string, Dictionary<string, Dictionary<string, string>>>>.Fail($"Block with hash {blockHash} not found.")); |
| public void SaveEpochRewardsRpc(ulong epochBlockNumber, Dictionary<string, Dictionary<string, Dictionary<string, string>>> rewards) | ||
| { | ||
| byte[] key = BuildEpochRewardsRpcKey(epochBlockNumber); | ||
| byte[] bytes = JsonSerializer.SerializeToUtf8Bytes(rewards); |
There was a problem hiding this comment.
High — SaveEpochRewardsRpc is not atomic with the enclosing write batch in SaveEpochRewards
SaveEpochRewards commits its write batch (sequence keys, prune tombstones, regular reward bytes) atomically. SaveEpochRewardsRpc then issues a separate _rewardsDb.Set(...) outside any batch. A process crash between the two writes leaves the store in an inconsistent state:
HasEpochRewards→true(regular reward key exists)TryGetEpochRewards→false/ empty (RPC key missing)
eth_getRewardByHash returns an empty dict in this case, making it impossible to distinguish "pruned" from "crash-interrupted write."
The fix is to fold the RPC key write into the same IWriteBatch that SaveEpochRewards already creates, either by accepting it as a parameter or by extracting a combined SaveEpochRewardsFull(ulong, BlockReward[], Dictionary<...>) method.
|
|
||
| XdcTransactionAndReceiptProof proof = new() | ||
| { | ||
| BlockHash = block.Hash!, |
There was a problem hiding this comment.
Medium — null-forgiving ! on block.Hash
block.Hash is typed as Hash256?. Using ! suppresses the nullable warning. FindBlock should always return a fully hashed block in practice, but if it doesn't, this silently propagates a null reference into XdcTransactionAndReceiptProof.BlockHash, which will later fail in an uncontrolled way.
| BlockHash = block.Hash!, | |
| BlockHash = block.Hash ?? throw new InvalidOperationException($"Block returned by FindBlock has a null hash for block hash lookup {blockHash}"), |
|
|
||
| private static (string[] Keys, string[] Values) FromProofNodes(byte[][] proofNodes) | ||
| { | ||
| string[] keys = new string[proofNodes.Length]; |
There was a problem hiding this comment.
Low — misleading variable name
keys[i] is set to Keccak.Compute(rlp).ToString() — it holds the hash of the proof node, not a trie path key. Rename to nodeHashes (or proofNodeHashes) to avoid confusion with the trie-index key stored in XdcTransactionAndReceiptProof.Key.
| string[] keys = new string[proofNodes.Length]; | |
| string[] nodeHashes = new string[proofNodes.Length]; |
|
@claude review |
|
Claude finished @ak88's task in 3m 7s —— View job PR Review: xdc eth endpoints
SummaryThis PR adds three new Fixed since last review:
Still outstanding: High1. Medium2. 3. 4. No round-trip test for Low5. 6. Misleading variable name |
| return Task.FromResult(ResultWrapper<Dictionary<string, Dictionary<string, Dictionary<string, string>>>>.Success([])); | ||
| } | ||
|
|
||
| if (!rewardsStore.TryGetEpochRewards((ulong)header.Number, out Dictionary<string, Dictionary<string, Dictionary<string, string>>>? rewards) |
There was a problem hiding this comment.
High — rewards are retrieved by block number, not block hash
TryGetEpochRewards is keyed by header.Number. In a reorg scenario, two different blocks can share the same number, and this returns rewards for whichever number was stored last — not for the specific block whose hash was passed in. The method name and its description ("Returns epoch reward distribution for the block identified by hash") make this a contract violation.
Minimal fix: after the lookup, verify the canonical block at that number still has the same hash:
BlockHeader? canonical = blockFinder.FindHeader(new BlockParameter(header.Number));
if (canonical?.Hash != blockHash)
{
return Task.FromResult(ResultWrapper<...>.Fail($"Block {blockHash} is not canonical at number {header.Number}."));
}XDC's consensus makes deep reorgs unlikely, but the API should honour what it declares.
| BlockHeader? header = blockFinder.FindHeader(blockHash); | ||
| if (header is null) | ||
| { | ||
| return Task.FromResult(ResultWrapper<Dictionary<string, Dictionary<string, Dictionary<string, string>>>>.Success([])); |
There was a problem hiding this comment.
Medium — unknown block hash returns Success([]), indistinguishable from "no rewards"
Callers cannot tell whether the empty dict means "unknown hash" or "epoch block had no reward entries." Use Fail here:
| return Task.FromResult(ResultWrapper<Dictionary<string, Dictionary<string, Dictionary<string, string>>>>.Success([])); | |
| return Task.FromResult(ResultWrapper<Dictionary<string, Dictionary<string, Dictionary<string, string>>>>.Fail($"Block with hash {blockHash} not found.")); |
Co-authored-by: claude[bot] <209825114+claude[bot]@users.noreply.github.com>
Co-authored-by: claude[bot] <209825114+claude[bot]@users.noreply.github.com>
|
@claude review again |
|
Claude finished @ak88's task in 2m 39s —— View job PR Review: xdc eth endpoints
SummaryGood progress since the last review. The two previous High findings are now resolved:
One new Critical issue was introduced by an incomplete variable rename, and two Medium findings from prior reviews remain unaddressed. Critical1. The variable was renamed to Medium2. Flagged in both prior reviews, still unaddressed. Callers cannot distinguish "unknown block hash" from "epoch block has zero reward entries." Inline comment → Fix this → 3. No round-trip test for Flagged in both prior reviews, still unaddressed. Only the not-found path is covered. The proof format (key encoding, Low4. Flagged in both prior reviews. Marking methods sharable asserts concurrent call safety. Confirm the thread-safety guarantees of |
| for (int i = 0; i < proofNodes.Length; i++) | ||
| { | ||
| byte[] rlp = proofNodes[i]; | ||
| keys[i] = Keccak.Compute(rlp).ToString(); |
There was a problem hiding this comment.
Critical — keys is not declared; this is a compile error
The variable was renamed to nodeHashes on line 118 but lines 123 and 127 still reference the old name keys. This produces CS0103: The name 'keys' does not exist in the current context.
| keys[i] = Keccak.Compute(rlp).ToString(); | |
| nodeHashes[i] = Keccak.Compute(rlp).ToString(); |
| values[i] = Bytes.ToHexString(rlp); | ||
| } | ||
|
|
||
| return (keys, values); |
There was a problem hiding this comment.
Critical — keys is not declared here either
Same incomplete rename: keys was never declared (it was renamed to nodeHashes).
| return (keys, values); | |
| return (nodeHashes, values); |
| BlockHeader? header = blockFinder.FindHeader(blockHash); | ||
| if (header is null) | ||
| { | ||
| return Task.FromResult(ResultWrapper<Dictionary<string, Dictionary<string, Dictionary<string, string>>>>.Success([])); |
There was a problem hiding this comment.
Medium — unknown hash returns Success([]), indistinguishable from "epoch exists but has no rewards"
This was flagged in both prior reviews and is still unaddressed. Callers that pass a wrong or pruned hash receive an empty dict with no way to tell it apart from a legitimate empty-rewards epoch.
| return Task.FromResult(ResultWrapper<Dictionary<string, Dictionary<string, Dictionary<string, string>>>>.Success([])); | |
| return Task.FromResult(ResultWrapper<Dictionary<string, Dictionary<string, Dictionary<string, string>>>>.Fail($"Block with hash {blockHash} not found.")); |
There was a problem hiding this comment.
this matches the behavior in XDC client
|
@claude review |
|
Claude finished @ak88's task in 2m 15s —— View job PR Review: xdc eth endpoints
SummaryGood progress. The Critical compile error from the previous review ( Fixed since last review:
Acknowledged with rationale (not blocking):
Medium1. No round-trip test for Only the not-found path is covered. The proof format (key RLP-encoding, Low2. The 3. Flagged in 3 prior reviews. Marking methods sharable asserts concurrent call safety. Confirm |
| } | ||
|
|
||
| [Test] | ||
| public async Task eth_getTransactionAndReceiptProof_returns_null_when_tx_is_unknown() |
There was a problem hiding this comment.
Medium — no round-trip test for eth_getTransactionAndReceiptProof with a valid transaction (flagged 4 reviews)
Only the not-found path is tested. The proof format — key RLP encoding, FromProofNodes hash computation, TxRoot/ReceiptRoot derivation — is entirely untested. Given this is custom proof-format code, at least one test with a real block + known transaction should be added to verify the round-trip.
Suggested structure (using TestBlockchain or a minimal in-memory setup):
[Test]
public async Task eth_getTransactionAndReceiptProof_returns_valid_proof_for_known_transaction()
{
// Arrange: build a block with at least one transaction, set up real receipt finder
// Act: call eth_getTransactionAndReceiptProof with the known tx hash
// Assert: result is non-null, TxRoot matches the block's TxRoot, ReceiptRoot matches,
// Key == RLP-encode(index), TxProofKeys/Values are non-empty
}| return Task.FromResult(ResultWrapper<Dictionary<string, Dictionary<string, Dictionary<string, string>>>>.Success([])); | ||
| } | ||
|
|
||
| if (!rewardsStore.TryGetEpochRewards(header.Hash!, out Dictionary<string, Dictionary<string, Dictionary<string, string>>>? rewards) |
There was a problem hiding this comment.
Low — header.Hash! null-forgiving operator
header came from FindHeader(blockHash) so its hash should equal blockHash in practice, but the null-forgiving operator hides a potential NullReferenceException. Consistent with the fix applied to block.Hash in this same PR:
| if (!rewardsStore.TryGetEpochRewards(header.Hash!, out Dictionary<string, Dictionary<string, Dictionary<string, string>>>? rewards) | |
| if (!rewardsStore.TryGetEpochRewards(header.Hash ?? blockHash, out Dictionary<string, Dictionary<string, Dictionary<string, string>>>? rewards) |
Using header.Hash ?? blockHash is safe here since the block was found by that hash.
Missing eth_ endpoints for XDC.