diff --git a/crates/net/eth-wire/src/disconnect.rs b/crates/net/eth-wire/src/disconnect.rs index e03f99f07f9f..b3d5867bfa12 100644 --- a/crates/net/eth-wire/src/disconnect.rs +++ b/crates/net/eth-wire/src/disconnect.rs @@ -104,9 +104,9 @@ impl TryFrom 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); } @@ -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 { if buf.is_empty() { return Err(RlpError::InputTooShort) diff --git a/crates/net/eth-wire/src/p2pstream.rs b/crates/net/eth-wire/src/p2pstream.rs index 5809de260284..d46ece6836cf 100644 --- a/crates/net/eth-wire/src/p2pstream.rs +++ b/crates/net/eth-wire/src/p2pstream.rs @@ -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 { @@ -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 { /// Removes the snappy prefix from the Ping/Pong buffer fn advance_snappy_ping_pong_payload(buf: &mut &[u8]) -> alloy_rlp::Result<()> { diff --git a/crates/net/eth-wire/src/types/message.rs b/crates/net/eth-wire/src/types/message.rs index fe247c0a892b..bf9d0bdcbf62 100644 --- a/crates/net/eth-wire/src/types/message.rs +++ b/crates/net/eth-wire/src/types/message.rs @@ -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); @@ -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); diff --git a/crates/primitives/src/transaction/eip1559.rs b/crates/primitives/src/transaction/eip1559.rs index 43f9e0db682b..89f78e72364c 100644 --- a/crates/primitives/src/transaction/eip1559.rs +++ b/crates/primitives/src/transaction/eip1559.rs @@ -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, @@ -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); diff --git a/crates/primitives/src/transaction/eip2930.rs b/crates/primitives/src/transaction/eip2930.rs index 5cd4d7803473..1e8c51f47b73 100644 --- a/crates/primitives/src/transaction/eip2930.rs +++ b/crates/primitives/src/transaction/eip2930.rs @@ -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, @@ -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); diff --git a/crates/primitives/src/transaction/eip4844.rs b/crates/primitives/src/transaction/eip4844.rs index 87d9cee9e05d..bafe7c0196ea 100644 --- a/crates/primitives/src/transaction/eip4844.rs +++ b/crates/primitives/src/transaction/eip4844.rs @@ -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); diff --git a/crates/primitives/src/transaction/legacy.rs b/crates/primitives/src/transaction/legacy.rs index 98153c6bd6da..2a5b0b19a9ea 100644 --- a/crates/primitives/src/transaction/legacy.rs +++ b/crates/primitives/src/transaction/legacy.rs @@ -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); @@ -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 @@ -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); diff --git a/crates/primitives/src/transaction/mod.rs b/crates/primitives/src/transaction/mod.rs index 82b08a863548..01bf2e82d11c 100644 --- a/crates/primitives/src/transaction/mod.rs +++ b/crates/primitives/src/transaction/mod.rs @@ -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) => { @@ -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(), @@ -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) } @@ -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 { @@ -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 { if data.is_empty() { @@ -1325,6 +1333,14 @@ impl From 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); } @@ -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 { + if buf.is_empty() { + return Err(RlpError::InputTooShort) + } + // decode header let mut original_encoding = *buf; let header = Header::decode(buf)?; @@ -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) } @@ -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 diff --git a/crates/primitives/src/transaction/pooled.rs b/crates/primitives/src/transaction/pooled.rs index bff710968902..72b0e1c8838d 100644 --- a/crates/primitives/src/transaction/pooled.rs +++ b/crates/primitives/src/transaction/pooled.rs @@ -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) => { @@ -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 { // From the EIP-4844 spec: // Blob transactions have two network representations. During transaction gossip responses diff --git a/crates/rpc/rpc-types/src/eth/block.rs b/crates/rpc/rpc-types/src/eth/block.rs index 124a2a322f63..91370b7cf70e 100644 --- a/crates/rpc/rpc-types/src/eth/block.rs +++ b/crates/rpc/rpc-types/src/eth/block.rs @@ -646,8 +646,8 @@ impl From for BlockHashOrNumber { } } -/// Allows for RLP encoding of either a block hash or block number impl Encodable for BlockHashOrNumber { + /// RLP encodes either the block hash or block number, depending on the variant. fn encode(&self, out: &mut dyn bytes::BufMut) { match self { Self::Hash(block_hash) => block_hash.encode(out), @@ -662,8 +662,10 @@ impl Encodable for BlockHashOrNumber { } } -/// Allows for RLP decoding of a block hash or block number impl Decodable for BlockHashOrNumber { + /// RLP decodes the data into a block hash or number. + /// If the data is exactly 32 bytes and a RLP string, it will be decoded into a block hash. + /// Otherwise, this will try to decode a `u64` from the data as a block number. fn decode(buf: &mut &[u8]) -> alloy_rlp::Result { let header: u8 = *buf.first().ok_or(RlpError::InputTooShort)?; // if the byte string is exactly 32 bytes, decode it into a Hash diff --git a/crates/rpc/rpc-types/src/eth/transaction/typed.rs b/crates/rpc/rpc-types/src/eth/transaction/typed.rs index 6dd0314cbe62..65c577680105 100644 --- a/crates/rpc/rpc-types/src/eth/transaction/typed.rs +++ b/crates/rpc/rpc-types/src/eth/transaction/typed.rs @@ -107,6 +107,12 @@ impl 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 BufMut) { match self { TransactionKind::Call(to) => to.encode(out), @@ -148,3 +154,21 @@ pub struct BlobTransactionSidecar { /// The blob proofs. pub proofs: Vec, } + +#[cfg(test)] +mod tests { + use super::*; + + #[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); + } +}