Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
81 changes: 81 additions & 0 deletions CIPs/cip-172.md
Original file line number Diff line number Diff line change
@@ -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/).