diff --git a/CIPs/cip-172.md b/CIPs/cip-172.md new file mode 100644 index 0000000..864f0a1 --- /dev/null +++ b/CIPs/cip-172.md @@ -0,0 +1,81 @@ +--- +CIP No.: 172 +Title: Canonical Transaction RLP Encoding +Author: Peilun Li (@peilun-conflux) +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. + +### 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/).