Bug Description
RejectUnprotectedTxsMiddleware is intended to reject pre-EIP-155 / replay-unprotected legacy transactions submitted through public JSON-RPC when allow_unprotected_txs is false. However, the positional parameter parser currently extracts raw transaction bytes with an exact one-element tuple parse:
req.params().parse::<(Bytes,)>().ok().map(|(bytes,)| bytes)
A JSON-RPC request with a valid legacy raw transaction plus an extra positional parameter can make this middleware extraction fail. The middleware then treats the request as unparseable/malformed and forwards it to the inner RPC handler instead of applying Arc's replay-protection rejection.
If the downstream JSON-RPC method ignores the extra positional parameter and consumes the first raw transaction argument, this bypasses Arc's public RPC policy and allows pre-EIP-155 raw transaction submissions despite the default rejection setting.
Affected methods:
eth_sendRawTransaction
eth_sendRawTransactionSync
Code Evidence
crates/evm-node/src/rpc_middleware.rs:
fn error_if_unprotected_send_raw_tx<'a>(req: &Request<'a>) -> Result<(), ErrorObject<'a>> {
if !is_raw_transaction_submission(req.method_name()) {
return Ok(());
}
let Some(bytes) = extract_raw_tx_bytes(req) else {
return Ok(());
};
let Ok(envelope) = TxEnvelope::decode_2718_exact(bytes.as_ref()) else {
return Ok(());
};
if envelope.is_replay_protected() {
return Ok(());
}
Err(ErrorObjectOwned::owned::<()>(
UNPROTECTED_TX_ERROR_CODE,
UNPROTECTED_TX_ERROR_MSG,
None,
))
}
extract_raw_tx_bytes handles object params and exact one-element positional params, but does not reject malformed positional shapes at the policy boundary:
fn extract_raw_tx_bytes(req: &Request<'_>) -> Option<Bytes> {
#[derive(serde::Deserialize)]
struct SendRawTransactionParams {
bytes: Bytes,
}
if req.params().is_object() {
req.params()
.parse::<SendRawTransactionParams>()
.ok()
.map(|p| p.bytes)
} else {
req.params().parse::<(Bytes,)>().ok().map(|(bytes,)| bytes)
}
}
When the tuple parse fails, error_if_unprotected_send_raw_tx returns Ok(()), which allows the request to continue to the inner service.
Steps to Reproduce
- Check out current upstream main:
git clone https://github.com/circlefin/arc-node arc-node-triage
cd arc-node-triage
git checkout de76122a1c4756e747accc27db35ffc8ec8981a8
- Add this focused regression test inside the existing
#[cfg(test)] mod tests in crates/evm-node/src/rpc_middleware.rs:
#[tokio::test]
async fn poc_extra_positional_param_bypasses_pre_eip155_rejection() {
let middleware = RejectUnprotectedTxsMiddleware::new(MockRpcService);
let raw_hex = encode_legacy_raw(None);
let request = send_raw_tx_request_with_params(format!(r#"["{raw_hex}", "ignored"]"#), 999);
let response = middleware.call(request).await;
assert!(
response.as_error_code().is_none(),
"extra positional parameter makes middleware forward an unprotected raw tx"
);
let json: serde_json::Value = serde_json::from_str(response.into_json().get()).unwrap();
assert_eq!(json["result"], "success");
}
- Run the focused test:
RUSTUP_TOOLCHAIN=1.94.0 cargo test -p arc-evm-node poc_extra_positional_param_bypasses_pre_eip155_rejection -- --nocapture
Actual Behavior
A pre-EIP-155 raw transaction with an extra positional parameter is forwarded by the middleware instead of being rejected by Arc's replay-protection policy.
Example request shape:
{
"jsonrpc": "2.0",
"id": 1,
"method": "eth_sendRawTransaction",
"params": [
"0x<valid legacy raw transaction signed without EIP-155 chain_id>",
"ignored"
]
}
Expected Behavior
RejectUnprotectedTxsMiddleware should not silently allow policy-sensitive malformed parameter shapes. It should either:
- extract and validate the first raw transaction argument consistently with the downstream handler, or
- reject raw transaction submission calls whose params are not one of the explicitly supported safe shapes.
In all cases, pre-EIP-155 raw transactions should be rejected when allow_unprotected_txs is false, regardless of trailing ignored params.
Impact
This can bypass Arc's default replay-protection policy for public RPC raw transaction submission paths. A public RPC client may submit replay-unprotected legacy transactions by adding an ignored second positional parameter, undermining the node operator's expectation that pre-EIP-155 raw transactions are blocked unless explicitly allowed.
Duplicate Check
Searched public issues and PRs in circlefin/arc-node for:
sendRawTransaction extra positional
unprotected transaction extra params
pre-EIP-155 raw transaction
extract_raw_tx_bytes
allow_unprotected_txs
trailing raw transaction
RPC replay-protection
No matching issue or PR for this root cause was found. The visible matches were unrelated docs/consensus items, not this middleware bypass.
Environment
- Repository:
circlefin/arc-node
- Commit tested:
de76122a1c4756e747accc27db35ffc8ec8981a8
- Package:
arc-evm-node
- Component:
crates/evm-node/src/rpc_middleware.rs
Bug Description
RejectUnprotectedTxsMiddlewareis intended to reject pre-EIP-155 / replay-unprotected legacy transactions submitted through public JSON-RPC whenallow_unprotected_txsis false. However, the positional parameter parser currently extracts raw transaction bytes with an exact one-element tuple parse:A JSON-RPC request with a valid legacy raw transaction plus an extra positional parameter can make this middleware extraction fail. The middleware then treats the request as unparseable/malformed and forwards it to the inner RPC handler instead of applying Arc's replay-protection rejection.
If the downstream JSON-RPC method ignores the extra positional parameter and consumes the first raw transaction argument, this bypasses Arc's public RPC policy and allows pre-EIP-155 raw transaction submissions despite the default rejection setting.
Affected methods:
eth_sendRawTransactioneth_sendRawTransactionSyncCode Evidence
crates/evm-node/src/rpc_middleware.rs:extract_raw_tx_byteshandles object params and exact one-element positional params, but does not reject malformed positional shapes at the policy boundary:When the tuple parse fails,
error_if_unprotected_send_raw_txreturnsOk(()), which allows the request to continue to the inner service.Steps to Reproduce
git clone https://github.com/circlefin/arc-node arc-node-triage cd arc-node-triage git checkout de76122a1c4756e747accc27db35ffc8ec8981a8#[cfg(test)] mod testsincrates/evm-node/src/rpc_middleware.rs:RUSTUP_TOOLCHAIN=1.94.0 cargo test -p arc-evm-node poc_extra_positional_param_bypasses_pre_eip155_rejection -- --nocaptureActual Behavior
A pre-EIP-155 raw transaction with an extra positional parameter is forwarded by the middleware instead of being rejected by Arc's replay-protection policy.
Example request shape:
{ "jsonrpc": "2.0", "id": 1, "method": "eth_sendRawTransaction", "params": [ "0x<valid legacy raw transaction signed without EIP-155 chain_id>", "ignored" ] }Expected Behavior
RejectUnprotectedTxsMiddlewareshould not silently allow policy-sensitive malformed parameter shapes. It should either:In all cases, pre-EIP-155 raw transactions should be rejected when
allow_unprotected_txsis false, regardless of trailing ignored params.Impact
This can bypass Arc's default replay-protection policy for public RPC raw transaction submission paths. A public RPC client may submit replay-unprotected legacy transactions by adding an ignored second positional parameter, undermining the node operator's expectation that pre-EIP-155 raw transactions are blocked unless explicitly allowed.
Duplicate Check
Searched public issues and PRs in
circlefin/arc-nodefor:sendRawTransaction extra positionalunprotected transaction extra paramspre-EIP-155 raw transactionextract_raw_tx_bytesallow_unprotected_txstrailing raw transactionRPC replay-protectionNo matching issue or PR for this root cause was found. The visible matches were unrelated docs/consensus items, not this middleware bypass.
Environment
circlefin/arc-nodede76122a1c4756e747accc27db35ffc8ec8981a8arc-evm-nodecrates/evm-node/src/rpc_middleware.rs