fix(rpc): null v/r/s for undecodable EVM tx sigs instead of silent zeros#703
Conversation
Audit M-6 follow-up against #692. `extract_vrs_from_rlp` returns `Option<(String,String,String)>`; the caller used `unwrap_or_else` to substitute `("0x0","0x0","0x0")` on decode failure. All-zero r/s parses as a valid ECDSA point pair and ecrecover returns a deterministic junk address — a caller doing best-effort sender check via the signature could trust that junk. Surface v/r/s as `null` instead. EIP-1474 clients already treat null sig fields as "not recoverable" (matches the pending-tx convention), so ethers / viem behave correctly without changes. Log a `warn!` when this path fires — successful decode is the steady state for any tx that came in via `eth_sendRawTransaction`, so a failure here means the stored signature field is corrupted or the wire format drifted. For native (non-EVM) txs the same null fields apply; there is no Ethereum-shaped signature to surface, and shape-consistency between the two paths simplifies downstream parsers. No behavior change for healthy EVM txs (which is everything that landed via the standard sender-recovered path).
📝 WalkthroughWalkthroughThis PR contains two changes: a workspace version bump from 2.2.12 to 2.2.13, and a logic modification in the Estimated code review effort🎯 2 (Simple) | ⏱️ ~8 minutes Possibly related PRs
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 inconclusive)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Comment |
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@crates/sentrix-rpc/src/jsonrpc/eth.rs`:
- Around line 338-345: The current match assumes extract_vrs_from_rlp returns
Option and logs only txid on failure; change extract_vrs_from_rlp to return
Result<(String,String,String), E> (or propagate its error type), update the call
in eth.rs to match on Ok((v,r,s)) and Err(err), and include the err in the
tracing::warn message (e.g., tracing::warn!(txid, error = %err,
"eth_getTransactionByHash: failed to decode v/r/s from stored EVM-tx signature
field; returning null sig")). Ensure callers of extract_vrs_from_rlp are updated
to handle Result and that the error type implements Display for logging.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: ASSERTIVE
Plan: Pro Plus
Run ID: 133c732f-b3ae-4f55-b18a-07ccf6dcc6d2
📒 Files selected for processing (2)
Cargo.tomlcrates/sentrix-rpc/src/jsonrpc/eth.rs
| match extract_vrs_from_rlp(tx_obj["signature"].as_str().unwrap_or("")) { | ||
| Some((v, r, s)) => (Value::String(v), Value::String(r), Value::String(s)), | ||
| None => { | ||
| tracing::warn!( | ||
| txid, | ||
| "eth_getTransactionByHash: failed to decode v/r/s from stored \ | ||
| EVM-tx signature field; returning null sig" | ||
| ); |
There was a problem hiding this comment.
Include the decode failure reason in the warning log.
On Line 341, the warning includes txid but not the decode error reason, which misses part of the stated behavior and weakens diagnosability. Return an error from extract_vrs_from_rlp and log it here.
Proposed fix
- let (v_value, r_value, s_value): (Value, Value, Value) = if is_evm {
- match extract_vrs_from_rlp(tx_obj["signature"].as_str().unwrap_or("")) {
+ let (v_value, r_value, s_value): (Value, Value, Value) = if is_evm {
+ match extract_vrs_from_rlp(tx_obj["signature"].as_str().unwrap_or("")) {
Some((v, r, s)) => (Value::String(v), Value::String(r), Value::String(s)),
- None => {
+ Err(err) => {
tracing::warn!(
txid,
+ error = %err,
"eth_getTransactionByHash: failed to decode v/r/s from stored \
EVM-tx signature field; returning null sig"
);
(Value::Null, Value::Null, Value::Null)
}
}
} else {
(Value::Null, Value::Null, Value::Null)
};-fn extract_vrs_from_rlp(sig_hex: &str) -> Option<(String, String, String)> {
+fn extract_vrs_from_rlp(sig_hex: &str) -> Result<(String, String, String), String> {
use alloy_consensus::TxEnvelope;
use alloy_eips::eip2718::Decodable2718;
- let raw_bytes = hex::decode(sig_hex).ok()?;
- let envelope = TxEnvelope::decode_2718(&mut raw_bytes.as_slice()).ok()?;
+ let raw_bytes = hex::decode(sig_hex).map_err(|e| format!("hex decode failed: {e}"))?;
+ let envelope = TxEnvelope::decode_2718(&mut raw_bytes.as_slice())
+ .map_err(|e| format!("2718 decode failed: {e}"))?;
let sig = envelope.signature();
let v_val: u64 = if sig.v() { 1 } else { 0 };
let r_u256 = sig.r();
let s_u256 = sig.s();
- Some((to_hex(v_val), format!("0x{r_u256:x}"), format!("0x{s_u256:x}")))
+ Ok((to_hex(v_val), format!("0x{r_u256:x}"), format!("0x{s_u256:x}")))
}🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@crates/sentrix-rpc/src/jsonrpc/eth.rs` around lines 338 - 345, The current
match assumes extract_vrs_from_rlp returns Option and logs only txid on failure;
change extract_vrs_from_rlp to return Result<(String,String,String), E> (or
propagate its error type), update the call in eth.rs to match on Ok((v,r,s)) and
Err(err), and include the err in the tracing::warn message (e.g.,
tracing::warn!(txid, error = %err, "eth_getTransactionByHash: failed to decode
v/r/s from stored EVM-tx signature field; returning null sig")). Ensure callers
of extract_vrs_from_rlp are updated to handle Result and that the error type
implements Display for logging.
Codecov Report✅ All modified and coverable lines are covered by tests. 📢 Thoughts on this report? Let us know! |
* fix(rpc): eth_getTransactionCount + eth_call + logsBloom length Three discrete chain-RPC fixes bundled because they share one cause (strict-but-non-spec behavior breaking off-the-shelf EVM agents). All discovered together standing up the Hyperlane relayer for the Base Sepolia ↔ Sentrix Testnet bridge (audit fix H-4). 1) eth_getTransactionCount accepts any block tag. Relayer + ethers + viem all pin nonce queries to a recent past block. A stale nonce is self-correcting (chain rejects wrong-nonce tx, caller retries) so this method serves current nonce regardless of block tag. 2) eth_call accepts any block tag. Same pattern: Hyperlane queries Mailbox.delivered(msgId) and recipientIsm(addr) at past blocks. Returns current-state. The strict gate stays on eth_getBalance / eth_getCode / eth_getStorageAt where wrong = wrong protocol decision. 3) Empty logsBloom is now actually 256 bytes (Ethereum spec). The EMPTY_LOGS_BLOOM const was 304 bytes (608 hex chars) because of an off-by-one in the original hand-typed string. The doc comment said 256 but the literal was 304. ethers' fee-oracle middleware strict-parses Block.logsBloom — Hyperlane gas estimation SerdeJson'd on this with "invalid length 608, expected 256 bytes" before any process() tx could be submitted. Bumps workspace 2.2.13 -> 2.2.14. * docs(changelog): add 2.2.14 entry — five RPC compat fixes 2.2.14 went live on mainnet 2026-05-21 17:02 UTC carrying PRs #692/#696/#702/#703/#704. Document the five fixes + flag that the eth_call + logsBloom fixes on this branch are scheduled for 2.2.15.
…bytes (#707) * fix(rpc): eth_getTransactionCount + eth_call + logsBloom length Three discrete chain-RPC fixes bundled because they share one cause (strict-but-non-spec behavior breaking off-the-shelf EVM agents). All discovered together standing up the Hyperlane relayer for the Base Sepolia ↔ Sentrix Testnet bridge (audit fix H-4). 1) eth_getTransactionCount accepts any block tag. Relayer + ethers + viem all pin nonce queries to a recent past block. A stale nonce is self-correcting (chain rejects wrong-nonce tx, caller retries) so this method serves current nonce regardless of block tag. 2) eth_call accepts any block tag. Same pattern: Hyperlane queries Mailbox.delivered(msgId) and recipientIsm(addr) at past blocks. Returns current-state. The strict gate stays on eth_getBalance / eth_getCode / eth_getStorageAt where wrong = wrong protocol decision. 3) Empty logsBloom is now actually 256 bytes (Ethereum spec). The EMPTY_LOGS_BLOOM const was 304 bytes (608 hex chars) because of an off-by-one in the original hand-typed string. The doc comment said 256 but the literal was 304. ethers' fee-oracle middleware strict-parses Block.logsBloom — Hyperlane gas estimation SerdeJson'd on this with "invalid length 608, expected 256 bytes" before any process() tx could be submitted. Bumps workspace 2.2.13 -> 2.2.14. * docs(changelog): add 2.2.14 entry — five RPC compat fixes 2.2.14 went live on mainnet 2026-05-21 17:02 UTC carrying PRs #692/#696/#702/#703/#704. Document the five fixes + flag that the eth_call + logsBloom fixes on this branch are scheduled for 2.2.15.
Audit M-6 follow-up against #692.
Problem
extract_vrs_from_rlpreturnsOption<(String,String,String)>; the caller usedunwrap_or_elseto substitute("0x0","0x0","0x0")on decode failure. All-zero r/s parses as a valid ECDSA point pair and ecrecover returns a deterministic junk address — a caller doing best-effort sender check via the signature could trust that junk.Fix
Surface v/r/s as
nullinstead of zero strings. EIP-1474 clients already treat null sig fields as "not recoverable" (matches the pending-tx convention), so ethers / viem behave correctly without changes. Log awarn!when this path fires — successful decode is the steady state for any tx that came in viaeth_sendRawTransaction, so a failure here means the stored signature field is corrupted or the wire format drifted.For native (non-EVM) txs the same null fields apply; there is no Ethereum-shaped signature to surface, and shape-consistency between the two paths simplifies downstream parsers.
Impact
No behavior change for healthy EVM txs (the steady state). Failed-decode path goes from "silent zeros that look like a valid signature" to "explicit null + warn log".
Test plan
cargo check --releasepassesSummary by CodeRabbit
Chores
Bug Fixes