From fea6fbbbe7fe0ec5a9d38254de61b4f475e056e6 Mon Sep 17 00:00:00 2001 From: Peilun Li Date: Mon, 6 Jul 2026 12:15:10 +0800 Subject: [PATCH 1/4] Add draft CIP: Canonical Transaction RLP Encoding Conflux computes a transaction hash over the exact received bytes while its RLP decoder accepts non-minimal encodings, so one transaction can carry multiple hashes, breaking the unique-hash assumption behind transactions_root, receipts, inclusion proofs, deduplication, and RPC; most visibly, blocks that pack a malleated transaction are rejected by other nodes over the root mismatch. The fix (conflux-rust #3561) rejects non-canonically encoded transactions, which tightens block validity and is therefore a spec-breaking consensus change that requires a CIP before it can merge; this drafts that CIP as a height-gated consensus rule plus an immediate node-local mempool policy. CIP number is left "" for an editor. Co-Authored-By: Claude Fable 5 --- CIPs/CIP-canonical_tx_encoding.md | 82 +++++++++++++++++++++++++++++++ 1 file changed, 82 insertions(+) create mode 100644 CIPs/CIP-canonical_tx_encoding.md diff --git a/CIPs/CIP-canonical_tx_encoding.md b/CIPs/CIP-canonical_tx_encoding.md new file mode 100644 index 0000000..48575f4 --- /dev/null +++ b/CIPs/CIP-canonical_tx_encoding.md @@ -0,0 +1,82 @@ +--- +CIP No.: +Title: Canonical Transaction RLP Encoding +Author: Peilun Li (@peilun-conflux) +Discussions: https://github.com/Conflux-Chain/conflux-rust/pull/3561 +Status: Draft +Type: Spec Breaking +Created: 2026-07-06 +--- + +## Simple Summary + +Conflux computes a transaction's hash from the exact bytes the transaction was decoded from, while the RLP decoder also accepts non-minimal encodings, so the same transaction can have more than one hash. This CIP requires every transaction in a block to be canonically encoded from an activation height; a block containing a non-canonically encoded transaction becomes invalid. + +## Abstract + +A transaction's hash is computed at decode time as `keccak` of its hash preimage: the RLP list bytes for a legacy transaction, or the type-prefixed payload for a typed transaction. Because the RLP decoder is lenient, a byte string that is not minimal RLP (for example, one with a trailing partial item) can decode to a transaction with identical fields but a different hash, so the same transaction can appear under more than one hash. + +The transaction hash is the transaction's identity throughout the protocol: the block `transactions_root` is a Merkle root over these hashes, and receipts, inclusion proofs, transaction pool deduplication, and RPC all key off them. A malleable hash breaks all of these — for example, a block that packs a non-canonically encoded transaction commits a `transactions_root` that other nodes compute differently, so the block is rejected. + +This CIP makes a block invalid, from an activation height, if any transaction in its body is not canonically encoded. It also recommends that clients reject non-canonically encoded transactions from the transaction pool immediately upon upgrade, without waiting for activation. + +## Motivation + +The protocol assumes each transaction has a unique hash. The decode-time hash is used widely: `transactions_root` and transaction inclusion proofs, receipts and the execution environment's `transaction_hash`, cross-space phantom transactions, transaction pool deduplication, and the hash reported over RPC. The bug arises from two implementation choices that are each harmless alone: to avoid a re-serialization, the hash is computed over the exact bytes a transaction was received in, and the RLP library used for decoding does not require those bytes to be the minimal encoding (it ignores, for example, a trailing partial item within the declared length). Ethereum's clients decode transactions strictly, so no equivalent gap exists there. + +Together these break the unique-hash assumption: an attacker can re-encode any valid transaction (unchanged fields and signature) into a non-canonical form with a different hash, confusing every hash-keyed component — most visibly, blocks that pack such a transaction are rejected by other nodes due to the `transactions_root` mismatch. + +Under the current rules nothing forbids non-canonical encodings: block body verification only checks that the header root matches the trie rebuilt from the decode-time hashes, so such blocks pass all consensus checks today. Fixing this therefore requires a consensus rule constraining transaction encodings, gated at a hardfork height. + +## Specification + +### Parameters + +`CANONICAL_TX_RLP_HEIGHT`: the block height at which this CIP activates (the `canonical_tx_rlp` transition height in the reference implementation). + +### Definitions + +A transaction's **hash preimage** is the byte string over which its hash is computed at decode time: + +- for a legacy (native CIP-155 or eSpace EIP-155 / pre-EIP-155) transaction, the RLP list bytes; +- for a typed (native CIP-2930 / CIP-1559, eSpace EIP-2930 / EIP-1559 / EIP-7702) transaction, the type-prefixed payload carried inside the RLP string wrapper. The wrapper's own length framing is not part of the preimage. + +The transaction hash is `keccak(hash preimage)`. + +A received transaction is **canonically encoded** if and only if its hash preimage, as received, is byte-for-byte identical to the hash preimage produced by re-encoding the decoded transaction canonically (minimal RLP, no surplus bytes). An implementation MAY check this as an equality of hashes: the cached decode-time hash equals the hash of the canonical re-encoding. + +For a typed transaction, only the payload is constrained; an implementation MUST NOT additionally require the RLP string wrapper to be minimal, since the wrapper is outside the hash preimage and constraining it would reject bodies other implementations accept. + +### Consensus rule + +For any block whose header height is at least `CANONICAL_TX_RLP_HEIGHT`, including non-pivot blocks, the block is invalid if any transaction in its body is not canonically encoded. Implementations MUST reject such a block during body verification. Blocks below the activation height are unaffected. + +The rule applies to every supported transaction type. + +### Transaction pool policy + +Independently of the activation height, an upgraded client SHOULD reject a non-canonically encoded transaction at transaction pool insertion, at all heights. This node-local policy protects an upgraded miner from packing such a transaction before activation; it is a recommendation, not a consensus rule. + +## Rationale + +Rejecting non-canonical encodings is preferred over accepting them and re-hashing to the canonical form: re-hashing would be a consensus change of the same magnitude while silently rewriting the identity of non-canonical transactions, whereas rejection keeps the existing hash semantics unchanged for every canonical transaction and removes malleable encodings outright. + +## Backwards Compatibility + +This CIP is spec-breaking: nodes must upgrade before the activation height, or they may accept blocks that the upgraded network rejects. Standard signing and transaction-construction tools emit canonical encodings, so honest transactions are unaffected. + +## Test Cases + +Test cases are provided in the reference implementation (conflux-rust PR [#3561](https://github.com/Conflux-Chain/conflux-rust/pull/3561)). + +## Implementation + +Reference implementation: conflux-rust PR [#3561](https://github.com/Conflux-Chain/conflux-rust/pull/3561). + +## Security Considerations + +This CIP closes a transaction-hash malleability vector: without it, an attacker can give one transaction multiple hashes, causing blocks that pack a malleated transaction to be rejected by other nodes, bypassing hash-keyed deduplication and caches, and making the RPC-reported hash diverge from the canonical one. The check costs one RLP serialization and one `keccak` per transaction — linear in the transaction size and negligible next to the ECDSA sender recovery already performed for every transaction — and it can only reject encodings, so it introduces no new attack surface. + +## Copyright + +Copyright and related rights waived via [CC0](https://creativecommons.org/publicdomain/zero/1.0/). From 9de9c4c40f384980de24be5682b35f0b175c0250 Mon Sep 17 00:00:00 2001 From: Peilun Li Date: Mon, 6 Jul 2026 12:52:12 +0800 Subject: [PATCH 2/4] Assign CIP number 172 Per the README, the PR ID serves as the CIP ID at the submitter's discretion; this PR is #172, so the file is renamed to cip-172.md and the header updated. Co-Authored-By: Claude Fable 5 --- CIPs/{CIP-canonical_tx_encoding.md => cip-172.md} | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) rename CIPs/{CIP-canonical_tx_encoding.md => cip-172.md} (99%) diff --git a/CIPs/CIP-canonical_tx_encoding.md b/CIPs/cip-172.md similarity index 99% rename from CIPs/CIP-canonical_tx_encoding.md rename to CIPs/cip-172.md index 48575f4..4e01884 100644 --- a/CIPs/CIP-canonical_tx_encoding.md +++ b/CIPs/cip-172.md @@ -1,5 +1,5 @@ --- -CIP No.: +CIP No.: 172 Title: Canonical Transaction RLP Encoding Author: Peilun Li (@peilun-conflux) Discussions: https://github.com/Conflux-Chain/conflux-rust/pull/3561 From e344dff6170806e104a2d961a5f1fec3e65a5855 Mon Sep 17 00:00:00 2001 From: Peilun Li Date: Mon, 6 Jul 2026 13:18:16 +0800 Subject: [PATCH 3/4] Drop Discussions header CIP-1 forbids the Discussions header from pointing to a GitHub pull request, and the header is optional for CIPs discussed within the dev team; the reference-implementation PR remains linked in the Implementation section. Co-Authored-By: Claude Fable 5 --- CIPs/cip-172.md | 1 - 1 file changed, 1 deletion(-) diff --git a/CIPs/cip-172.md b/CIPs/cip-172.md index 4e01884..1ca52a9 100644 --- a/CIPs/cip-172.md +++ b/CIPs/cip-172.md @@ -2,7 +2,6 @@ CIP No.: 172 Title: Canonical Transaction RLP Encoding Author: Peilun Li (@peilun-conflux) -Discussions: https://github.com/Conflux-Chain/conflux-rust/pull/3561 Status: Draft Type: Spec Breaking Created: 2026-07-06 From 184d42830fbae603d523e0e2eb8ed14afdb691ee Mon Sep 17 00:00:00 2001 From: Peilun Li Date: Mon, 6 Jul 2026 16:37:29 +0800 Subject: [PATCH 4/4] Drop implementation identifier from Parameters CIP bodies should not reference code-level identifiers; the config key name lives in the reference PR, and the spec-level parameter name suffices. Co-Authored-By: Claude Fable 5 --- CIPs/cip-172.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CIPs/cip-172.md b/CIPs/cip-172.md index 1ca52a9..864f0a1 100644 --- a/CIPs/cip-172.md +++ b/CIPs/cip-172.md @@ -31,7 +31,7 @@ Under the current rules nothing forbids non-canonical encodings: block body veri ### Parameters -`CANONICAL_TX_RLP_HEIGHT`: the block height at which this CIP activates (the `canonical_tx_rlp` transition height in the reference implementation). +`CANONICAL_TX_RLP_HEIGHT`: the block height at which this CIP activates. ### Definitions