Skip to content

Commit

Permalink
docs: improve rlp transaction encoding docs (#5922)
Browse files Browse the repository at this point in the history
  • Loading branch information
Rjected authored Jan 2, 2024
1 parent 5f53545 commit dcf1d50
Show file tree
Hide file tree
Showing 11 changed files with 168 additions and 54 deletions.
8 changes: 4 additions & 4 deletions crates/net/eth-wire/src/disconnect.rs
Original file line number Diff line number Diff line change
Expand Up @@ -104,9 +104,9 @@ impl TryFrom<u8> for DisconnectReason {
}
}

/// The [`Encodable`] implementation for [`DisconnectReason`] encodes the disconnect reason in a
/// single-element RLP list.
impl Encodable for DisconnectReason {
/// The [`Encodable`] implementation for [`DisconnectReason`] encodes the disconnect reason in
/// a single-element RLP list.
fn encode(&self, out: &mut dyn BufMut) {
vec![*self as u8].encode(out);
}
Expand All @@ -115,9 +115,9 @@ impl Encodable for DisconnectReason {
}
}

/// The [`Decodable`] implementation for [`DisconnectReason`] supports either a disconnect reason
/// encoded a single byte or a RLP list containing the disconnect reason.
impl Decodable for DisconnectReason {
/// The [`Decodable`] implementation for [`DisconnectReason`] supports either a disconnect
/// reason encoded a single byte or a RLP list containing the disconnect reason.
fn decode(buf: &mut &[u8]) -> alloy_rlp::Result<Self> {
if buf.is_empty() {
return Err(RlpError::InputTooShort)
Expand Down
20 changes: 10 additions & 10 deletions crates/net/eth-wire/src/p2pstream.rs
Original file line number Diff line number Diff line change
Expand Up @@ -643,11 +643,11 @@ impl P2PMessage {
}
}

/// The [`Encodable`] implementation for [`P2PMessage::Ping`] and [`P2PMessage::Pong`] encodes the
/// message as RLP, and prepends a snappy header to the RLP bytes for all variants except the
/// [`P2PMessage::Hello`] variant, because the hello message is never compressed in the `p2p`
/// subprotocol.
impl Encodable for P2PMessage {
/// The [`Encodable`] implementation for [`P2PMessage::Ping`] and [`P2PMessage::Pong`] encodes
/// the message as RLP, and prepends a snappy header to the RLP bytes for all variants except
/// the [`P2PMessage::Hello`] variant, because the hello message is never compressed in the
/// `p2p` subprotocol.
fn encode(&self, out: &mut dyn BufMut) {
(self.message_id() as u8).encode(out);
match self {
Expand Down Expand Up @@ -680,13 +680,13 @@ impl Encodable for P2PMessage {
}
}

/// The [`Decodable`] implementation for [`P2PMessage`] assumes that each of the message variants
/// are snappy compressed, except for the [`P2PMessage::Hello`] variant since the hello message is
/// never compressed in the `p2p` subprotocol.
///
/// The [`Decodable`] implementation for [`P2PMessage::Ping`] and [`P2PMessage::Pong`] expects a
/// snappy encoded payload, see [`Encodable`] implementation.
impl Decodable for P2PMessage {
/// The [`Decodable`] implementation for [`P2PMessage`] assumes that each of the message
/// variants are snappy compressed, except for the [`P2PMessage::Hello`] variant since the
/// hello message is never compressed in the `p2p` subprotocol.
///
/// The [`Decodable`] implementation for [`P2PMessage::Ping`] and [`P2PMessage::Pong`] expects
/// a snappy encoded payload, see [`Encodable`] implementation.
fn decode(buf: &mut &[u8]) -> alloy_rlp::Result<Self> {
/// Removes the snappy prefix from the Ping/Pong buffer
fn advance_snappy_ping_pong_payload(buf: &mut &[u8]) -> alloy_rlp::Result<()> {
Expand Down
8 changes: 4 additions & 4 deletions crates/net/eth-wire/src/types/message.rs
Original file line number Diff line number Diff line change
Expand Up @@ -105,9 +105,9 @@ impl ProtocolMessage {
}
}

/// Encodes the protocol message into bytes.
/// The message type is encoded as a single byte and prepended to the message.
impl Encodable for ProtocolMessage {
/// Encodes the protocol message into bytes. The message type is encoded as a single byte and
/// prepended to the message.
fn encode(&self, out: &mut dyn BufMut) {
self.message_type.encode(out);
self.message.encode(out);
Expand All @@ -130,9 +130,9 @@ pub struct ProtocolBroadcastMessage {
pub message: EthBroadcastMessage,
}

/// Encodes the protocol message into bytes.
/// The message type is encoded as a single byte and prepended to the message.
impl Encodable for ProtocolBroadcastMessage {
/// Encodes the protocol message into bytes. The message type is encoded as a single byte and
/// prepended to the message.
fn encode(&self, out: &mut dyn BufMut) {
self.message_type.encode(out);
self.message.encode(out);
Expand Down
10 changes: 10 additions & 0 deletions crates/primitives/src/transaction/eip1559.rs
Original file line number Diff line number Diff line change
Expand Up @@ -138,6 +138,10 @@ impl TxEip1559 {

/// Inner encoding function that is used for both rlp [`Encodable`] trait and for calculating
/// hash that for eip2718 does not require rlp header
///
/// This encodes the transaction as:
/// `rlp(chain_id, nonce, max_priority_fee_per_gas, max_fee_per_gas, gas_limit to, value, input,
/// access_list, y_parity, r, s)`
pub(crate) fn encode_with_signature(
&self,
signature: &Signature,
Expand Down Expand Up @@ -192,6 +196,12 @@ impl TxEip1559 {
}

/// Encodes the legacy transaction in RLP for signing.
///
/// This encodes the transaction as:
/// `tx_type || rlp(chain_id, nonce, max_priority_fee_per_gas, max_fee_per_gas, gas_limit, to,
/// value, input, access_list)`
///
/// Note that there is no rlp header before the transaction type byte.
pub(crate) fn encode_for_signing(&self, out: &mut dyn bytes::BufMut) {
out.put_u8(self.tx_type() as u8);
Header { list: true, payload_length: self.fields_len() }.encode(out);
Expand Down
8 changes: 8 additions & 0 deletions crates/primitives/src/transaction/eip2930.rs
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,9 @@ impl TxEip2930 {

/// Inner encoding function that is used for both rlp [`Encodable`] trait and for calculating
/// hash that for eip2718 does not require rlp header
///
/// This encodes the transaction as:
/// `rlp(nonce, gas_price, gas_limit, to, value, input, access_list, y_parity, r, s)`
pub(crate) fn encode_with_signature(
&self,
signature: &Signature,
Expand Down Expand Up @@ -157,6 +160,11 @@ impl TxEip2930 {
}

/// Encodes the legacy transaction in RLP for signing.
///
/// This encodes the transaction as:
/// `tx_type || rlp(chain_id, nonce, gas_price, gas_limit, to, value, input, access_list)`
///
/// Note that there is no rlp header before the transaction type byte.
pub(crate) fn encode_for_signing(&self, out: &mut dyn bytes::BufMut) {
out.put_u8(self.tx_type() as u8);
Header { list: true, payload_length: self.fields_len() }.encode(out);
Expand Down
6 changes: 6 additions & 0 deletions crates/primitives/src/transaction/eip4844.rs
Original file line number Diff line number Diff line change
Expand Up @@ -295,6 +295,12 @@ impl TxEip4844 {
}

/// Encodes the legacy transaction in RLP for signing.
///
/// This encodes the transaction as:
/// `tx_type || rlp(chain_id, nonce, max_priority_fee_per_gas, max_fee_per_gas, gas_limit, to,
/// value, input, access_list, max_fee_per_blob_gas, blob_versioned_hashes)`
///
/// Note that there is no rlp header before the transaction type byte.
pub(crate) fn encode_for_signing(&self, out: &mut dyn bytes::BufMut) {
out.put_u8(self.tx_type() as u8);
Header { list: true, payload_length: self.fields_len() }.encode(out);
Expand Down
14 changes: 14 additions & 0 deletions crates/primitives/src/transaction/legacy.rs
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,11 @@ impl TxLegacy {

/// Inner encoding function that is used for both rlp [`Encodable`] trait and for calculating
/// hash.
///
/// This encodes the transaction as:
/// `rlp(nonce, gas_price, gas_limit, to, value, input, v, r, s)`
///
/// The `v` value is encoded according to EIP-155 if the `chain_id` is not `None`.
pub(crate) fn encode_with_signature(&self, signature: &Signature, out: &mut dyn bytes::BufMut) {
let payload_length =
self.fields_len() + signature.payload_len_with_eip155_chain_id(self.chain_id);
Expand All @@ -105,6 +110,9 @@ impl TxLegacy {

/// Encodes EIP-155 arguments into the desired buffer. Only encodes values for legacy
/// transactions.
///
/// If a `chain_id` is `Some`, this encodes the `chain_id`, followed by two zeroes, as defined
/// by [EIP-155](https://eips.ethereum.org/EIPS/eip-155).
pub(crate) fn encode_eip155_fields(&self, out: &mut dyn bytes::BufMut) {
// if this is a legacy transaction without a chain ID, it must be pre-EIP-155
// and does not need to encode the chain ID for the signature hash encoding
Expand All @@ -131,6 +139,12 @@ impl TxLegacy {
}

/// Encodes the legacy transaction in RLP for signing, including the EIP-155 fields if possible.
///
/// If a `chain_id` is `Some`, this encodes the transaction as:
/// `rlp(nonce, gas_price, gas_limit, to, value, input, chain_id, 0, 0)`
///
/// Otherwise, this encodes the transaction as:
/// `rlp(nonce, gas_price, gas_limit, to, value, input)`
pub(crate) fn encode_for_signing(&self, out: &mut dyn bytes::BufMut) {
Header { list: true, payload_length: self.fields_len() + self.eip155_fields_len() }
.encode(out);
Expand Down
102 changes: 71 additions & 31 deletions crates/primitives/src/transaction/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -669,9 +669,9 @@ impl Default for Transaction {
}
}

/// This encodes the transaction _without_ the signature, and is only suitable for creating a hash
/// intended for signing.
impl Encodable for Transaction {
/// This encodes the transaction _without_ the signature, and is only suitable for creating a
/// hash intended for signing.
fn encode(&self, out: &mut dyn bytes::BufMut) {
match self {
Transaction::Legacy(legacy_tx) => {
Expand Down Expand Up @@ -771,12 +771,19 @@ impl Compact for TransactionKind {
}

impl Encodable for TransactionKind {
/// This encodes the `to` field of a transaction request.
/// If the [TransactionKind] is a [TransactionKind::Call] it will encode the inner address:
/// `rlp(address)`
///
/// If the [TransactionKind] is a [TransactionKind::Create] it will encode an empty list:
/// `rlp([])`, which is also
fn encode(&self, out: &mut dyn alloy_rlp::BufMut) {
match self {
TransactionKind::Call(to) => to.encode(out),
TransactionKind::Create => out.put_u8(EMPTY_STRING_CODE),
}
}

fn length(&self) -> usize {
match self {
TransactionKind::Call(to) => to.length(),
Expand Down Expand Up @@ -1092,9 +1099,10 @@ impl TransactionSigned {
/// Encodes the transaction into the "raw" format (e.g. `eth_sendRawTransaction`).
/// This format is also referred to as "binary" encoding.
///
/// For legacy transactions, it encodes the RLP of the transaction into the buffer: `rlp(tx)`
/// For legacy transactions, it encodes the RLP of the transaction into the buffer:
/// `rlp(tx-data)`
/// For EIP-2718 typed it encodes the type of the transaction followed by the rlp of the
/// transaction: `type || rlp(tx)`
/// transaction: `tx-type || rlp(tx-data)`
pub fn encode_enveloped(&self, out: &mut dyn bytes::BufMut) {
self.encode_inner(out, false)
}
Expand Down Expand Up @@ -1211,12 +1219,12 @@ impl TransactionSigned {
///
/// This should be used _only_ be used internally in general transaction decoding methods,
/// which have already ensured that the input is a typed transaction with the following format:
/// `tx_type || rlp(tx)`
/// `tx-type || rlp(tx-data)`
///
/// Note that this format does not start with any RLP header, and instead starts with a single
/// byte indicating the transaction type.
///
/// CAUTION: this expects that `data` is `tx_type || rlp(tx)`
/// CAUTION: this expects that `data` is `tx-type || rlp(tx-data)`
pub fn decode_enveloped_typed_transaction(
data: &mut &[u8],
) -> alloy_rlp::Result<TransactionSigned> {
Expand Down Expand Up @@ -1276,13 +1284,13 @@ impl TransactionSigned {
///
/// A raw transaction is either a legacy transaction or EIP-2718 typed transaction.
///
/// For legacy transactions, the format is encoded as: `rlp(tx)`. This format will start with a
/// RLP list header.
/// For legacy transactions, the format is encoded as: `rlp(tx-data)`. This format will start
/// with a RLP list header.
///
/// For EIP-2718 typed transactions, the format is encoded as the type of the transaction
/// followed by the rlp of the transaction: `type || rlp(tx)`.
/// followed by the rlp of the transaction: `type || rlp(tx-data)`.
///
/// To decode EIP-4844 transactions in `eth_sendRawTransaction`, use
/// To decode EIP-4844 transactions from `eth_sendRawTransaction`, use
/// [PooledTransactionsElement::decode_enveloped].
pub fn decode_enveloped(data: &mut &[u8]) -> alloy_rlp::Result<Self> {
if data.is_empty() {
Expand Down Expand Up @@ -1325,6 +1333,14 @@ impl From<TransactionSignedEcRecovered> for TransactionSigned {
}

impl Encodable for TransactionSigned {
/// This encodes the transaction _with_ the signature, and an rlp header.
///
/// For legacy transactions, it encodes the transaction data:
/// `rlp(tx-data)`
///
/// For EIP-2718 typed transactions, it encodes the transaction type followed by the rlp of the
/// transaction:
/// `rlp(tx-type || rlp(tx-data))`
fn encode(&self, out: &mut dyn bytes::BufMut) {
self.encode_inner(out, true);
}
Expand All @@ -1334,29 +1350,37 @@ impl Encodable for TransactionSigned {
}
}

/// This `Decodable` implementation only supports decoding rlp encoded transactions as it's used by
/// p2p.
///
/// The p2p encoding format always includes an RLP header, although the type RLP header depends on
/// whether or not the transaction is a legacy transaction.
///
/// If the transaction is a legacy transaction, it is just encoded as a RLP list: `rlp(tx)`.
///
/// If the transaction is a typed transaction, it is encoded as a RLP string:
/// `rlp(type || rlp(tx))`
///
/// This cannot be used for decoding EIP-4844 transactions in p2p, since the EIP-4844 variant of
/// [TransactionSigned] does not include the blob sidecar. For a general purpose decoding method
/// suitable for decoding transactions from p2p, see [PooledTransactionsElement].
///
/// CAUTION: Due to a quirk in [Header::decode], this method will succeed even if a typed
/// transaction is encoded in the RPC format, and does not start with a RLP header. This is because
/// [Header::decode] does not advance the buffer, and returns a length-1 string header if the first
/// byte is less than `0xf7`. This causes this decode implementation to pass unaltered buffer to
/// [TransactionSigned::decode_enveloped_typed_transaction], which expects the RPC format. Despite
/// this quirk, this should **not** be used for RPC methods that accept raw transactions.
impl Decodable for TransactionSigned {
/// This `Decodable` implementation only supports decoding rlp encoded transactions as it's used
/// by p2p.
///
/// The p2p encoding format always includes an RLP header, although the type RLP header depends
/// on whether or not the transaction is a legacy transaction.
///
/// If the transaction is a legacy transaction, it is just encoded as a RLP list:
/// `rlp(tx-data)`.
///
/// If the transaction is a typed transaction, it is encoded as a RLP string:
/// `rlp(tx-type || rlp(tx-data))`
///
/// This can be used for decoding all signed transactions in p2p `BlockBodies` responses.
///
/// This cannot be used for decoding EIP-4844 transactions in p2p `PooledTransactions`, since
/// the EIP-4844 variant of [TransactionSigned] does not include the blob sidecar.
///
/// For a method suitable for decoding pooled transactions, see [PooledTransactionsElement].
///
/// CAUTION: Due to a quirk in [Header::decode], this method will succeed even if a typed
/// transaction is encoded in this format, and does not start with a RLP header:
/// `tx-type || rlp(tx-data)`.
///
/// This is because [Header::decode] does not advance the buffer, and returns a length-1 string
/// header if the first byte is less than `0xf7`.
fn decode(buf: &mut &[u8]) -> alloy_rlp::Result<Self> {
if buf.is_empty() {
return Err(RlpError::InputTooShort)
}

// decode header
let mut original_encoding = *buf;
let header = Header::decode(buf)?;
Expand Down Expand Up @@ -1480,6 +1504,9 @@ impl TransactionSignedEcRecovered {
}

impl Encodable for TransactionSignedEcRecovered {
/// This encodes the transaction _with_ the signature, and an rlp header.
///
/// Refer to docs for [TransactionSigned::encode] for details on the exact format.
fn encode(&self, out: &mut dyn bytes::BufMut) {
self.signed_transaction.encode(out)
}
Expand Down Expand Up @@ -1569,6 +1596,19 @@ mod tests {
assert_eq!(RlpError::InputTooShort, res);
}

#[test]
fn raw_kind_encoding_sanity() {
// check the 0x80 encoding for Create
let mut buf = Vec::new();
TransactionKind::Create.encode(&mut buf);
assert_eq!(buf, vec![0x80]);

// check decoding
let buf = [0x80];
let decoded = TransactionKind::decode(&mut &buf[..]).unwrap();
assert_eq!(decoded, TransactionKind::Create);
}

#[test]
fn test_decode_create_goerli() {
// test that an example create tx from goerli decodes properly
Expand Down
16 changes: 13 additions & 3 deletions crates/primitives/src/transaction/pooled.rs
Original file line number Diff line number Diff line change
Expand Up @@ -321,17 +321,27 @@ impl PooledTransactionsElement {

impl Encodable for PooledTransactionsElement {
/// Encodes an enveloped post EIP-4844 [PooledTransactionsElement].
///
/// For legacy transactions, this encodes the transaction as `rlp(tx-data)`.
///
/// For EIP-2718 transactions, this encodes the transaction as `rlp(tx_type || rlp(tx-data)))`.
fn encode(&self, out: &mut dyn bytes::BufMut) {
// The encoding of `tx-data` depends on the transaction type. Refer to these docs for more
// information on the exact format:
// - Legacy: TxLegacy::encode_with_signature
// - EIP-2930: TxEip2930::encode_with_signature
// - EIP-1559: TxEip1559::encode_with_signature
// - EIP-4844: BlobTransaction::encode_with_type_inner
match self {
Self::Legacy { transaction, signature, .. } => {
transaction.encode_with_signature(signature, out)
}
Self::Eip2930 { transaction, signature, .. } => {
// encodes with header
// encodes with string header
transaction.encode_with_signature(signature, out, true)
}
Self::Eip1559 { transaction, signature, .. } => {
// encodes with header
// encodes with string header
transaction.encode_with_signature(signature, out, true)
}
Self::BlobTransaction(blob_tx) => {
Expand Down Expand Up @@ -377,7 +387,7 @@ impl Encodable for PooledTransactionsElement {
impl Decodable for PooledTransactionsElement {
/// Decodes an enveloped post EIP-4844 [PooledTransactionsElement].
///
/// CAUTION: this expects that `buf` is `[id, rlp(tx)]`
/// CAUTION: this expects that `buf` is `rlp(tx_type || rlp(tx-data))`
fn decode(buf: &mut &[u8]) -> alloy_rlp::Result<Self> {
// From the EIP-4844 spec:
// Blob transactions have two network representations. During transaction gossip responses
Expand Down
Loading

0 comments on commit dcf1d50

Please sign in to comment.