Skip to main content

eth_valkyoth_protocol/transaction/
signing.rs

1//! Canonical transaction signing preimage encoding.
2
3use eth_valkyoth_primitives::ChainId;
4
5use super::encode::{
6    TransactionEncodeError, encode_list_envelope, encode_typed_envelope,
7    encoded_access_list_to_len, encoded_legacy_to_len, encoded_list_envelope_len,
8    encoded_typed_envelope_len, encoded_u64_len, encoded_u256_len, sum_lengths, write_access_list,
9    write_address, write_authorization_list, write_blob_hashes, write_legacy_to, write_scalar,
10    write_transaction_to, write_u64, write_u256, write_wei,
11};
12use super::{
13    ACCESS_LIST_TRANSACTION_TYPE, BLOB_TRANSACTION_TYPE, DYNAMIC_FEE_TRANSACTION_TYPE,
14    SET_CODE_TRANSACTION_TYPE, SetCodeAuthorization, UnvalidatedAccessListTransaction,
15    UnvalidatedBlobTransaction, UnvalidatedDynamicFeeTransaction, UnvalidatedLegacyTransaction,
16    UnvalidatedSetCodeTransaction,
17};
18
19/// EIP-7702 authorization signing magic byte.
20pub const SET_CODE_AUTHORIZATION_MAGIC: u8 = 0x05;
21
22/// Returns the EIP-155 legacy signing preimage length.
23pub fn encoded_legacy_eip155_signing_preimage_len(
24    transaction: &UnvalidatedLegacyTransaction<'_>,
25    chain_id: ChainId,
26) -> Result<usize, TransactionEncodeError> {
27    encoded_list_envelope_len(legacy_eip155_payload_len(transaction, chain_id)?).map_err(Into::into)
28}
29
30/// Encodes the EIP-155 legacy signing preimage.
31pub fn encode_legacy_eip155_signing_preimage(
32    transaction: &UnvalidatedLegacyTransaction<'_>,
33    chain_id: ChainId,
34    output: &mut [u8],
35) -> Result<usize, TransactionEncodeError> {
36    let payload_len = legacy_eip155_payload_len(transaction, chain_id)?;
37    encode_list_envelope(payload_len, output, |fields| {
38        write_u64(transaction.nonce.get(), fields)?;
39        write_wei(transaction.gas_price.to_be_bytes(), fields)?;
40        write_u64(transaction.gas_limit.get(), fields)?;
41        write_legacy_to(transaction.to, fields)?;
42        write_wei(transaction.value.to_be_bytes(), fields)?;
43        write_scalar(transaction.input, fields)?;
44        write_u64(chain_id.get(), fields)?;
45        write_u64(0, fields)?;
46        write_u64(0, fields)
47    })
48    .map_err(Into::into)
49}
50
51/// Returns the EIP-2930 signing preimage length.
52pub fn encoded_access_list_signing_preimage_len(
53    transaction: &UnvalidatedAccessListTransaction<'_>,
54) -> Result<usize, TransactionEncodeError> {
55    encoded_typed_envelope_len(access_list_signing_payload_len(transaction)?).map_err(Into::into)
56}
57
58/// Encodes the EIP-2930 signing preimage.
59pub fn encode_access_list_signing_preimage(
60    transaction: &UnvalidatedAccessListTransaction<'_>,
61    output: &mut [u8],
62) -> Result<usize, TransactionEncodeError> {
63    let payload_len = access_list_signing_payload_len(transaction)?;
64    encode_typed_envelope(
65        ACCESS_LIST_TRANSACTION_TYPE,
66        payload_len,
67        output,
68        |fields| {
69            write_u64(transaction.chain_id.get(), fields)?;
70            write_u64(transaction.nonce.get(), fields)?;
71            write_wei(transaction.gas_price.to_be_bytes(), fields)?;
72            write_u64(transaction.gas_limit.get(), fields)?;
73            write_transaction_to(transaction.to, fields)?;
74            write_wei(transaction.value.to_be_bytes(), fields)?;
75            write_scalar(transaction.input, fields)?;
76            write_access_list(transaction.access_list, fields)
77        },
78    )
79    .map_err(Into::into)
80}
81
82/// Returns the EIP-1559 signing preimage length.
83pub fn encoded_dynamic_fee_signing_preimage_len(
84    transaction: &UnvalidatedDynamicFeeTransaction<'_>,
85) -> Result<usize, TransactionEncodeError> {
86    encoded_typed_envelope_len(dynamic_fee_signing_payload_len(transaction)?).map_err(Into::into)
87}
88
89/// Encodes the EIP-1559 signing preimage.
90pub fn encode_dynamic_fee_signing_preimage(
91    transaction: &UnvalidatedDynamicFeeTransaction<'_>,
92    output: &mut [u8],
93) -> Result<usize, TransactionEncodeError> {
94    let payload_len = dynamic_fee_signing_payload_len(transaction)?;
95    encode_typed_envelope(
96        DYNAMIC_FEE_TRANSACTION_TYPE,
97        payload_len,
98        output,
99        |fields| {
100            write_u64(transaction.chain_id.get(), fields)?;
101            write_u64(transaction.nonce.get(), fields)?;
102            write_wei(transaction.max_priority_fee_per_gas.to_be_bytes(), fields)?;
103            write_wei(transaction.max_fee_per_gas.to_be_bytes(), fields)?;
104            write_u64(transaction.gas_limit.get(), fields)?;
105            write_transaction_to(transaction.to, fields)?;
106            write_wei(transaction.value.to_be_bytes(), fields)?;
107            write_scalar(transaction.input, fields)?;
108            write_access_list(transaction.access_list, fields)
109        },
110    )
111    .map_err(Into::into)
112}
113
114/// Returns the EIP-4844 signing preimage length.
115pub fn encoded_blob_signing_preimage_len(
116    transaction: &UnvalidatedBlobTransaction<'_>,
117) -> Result<usize, TransactionEncodeError> {
118    encoded_typed_envelope_len(blob_signing_payload_len(transaction)?).map_err(Into::into)
119}
120
121/// Encodes the EIP-4844 signing preimage.
122pub fn encode_blob_signing_preimage(
123    transaction: &UnvalidatedBlobTransaction<'_>,
124    output: &mut [u8],
125) -> Result<usize, TransactionEncodeError> {
126    let payload_len = blob_signing_payload_len(transaction)?;
127    encode_typed_envelope(BLOB_TRANSACTION_TYPE, payload_len, output, |fields| {
128        write_u64(transaction.chain_id.get(), fields)?;
129        write_u64(transaction.nonce.get(), fields)?;
130        write_wei(transaction.max_priority_fee_per_gas.to_be_bytes(), fields)?;
131        write_wei(transaction.max_fee_per_gas.to_be_bytes(), fields)?;
132        write_u64(transaction.gas_limit.get(), fields)?;
133        write_scalar(&transaction.to.to_bytes(), fields)?;
134        write_wei(transaction.value.to_be_bytes(), fields)?;
135        write_scalar(transaction.input, fields)?;
136        write_access_list(transaction.access_list, fields)?;
137        write_wei(transaction.max_fee_per_blob_gas.to_be_bytes(), fields)?;
138        write_blob_hashes(transaction.blob_versioned_hashes, fields)
139    })
140    .map_err(Into::into)
141}
142
143/// Returns the EIP-7702 set-code transaction signing preimage length.
144pub fn encoded_set_code_signing_preimage_len(
145    transaction: &UnvalidatedSetCodeTransaction<'_>,
146) -> Result<usize, TransactionEncodeError> {
147    encoded_typed_envelope_len(set_code_signing_payload_len(transaction)?).map_err(Into::into)
148}
149
150/// Encodes the EIP-7702 set-code transaction signing preimage.
151pub fn encode_set_code_signing_preimage(
152    transaction: &UnvalidatedSetCodeTransaction<'_>,
153    output: &mut [u8],
154) -> Result<usize, TransactionEncodeError> {
155    let payload_len = set_code_signing_payload_len(transaction)?;
156    encode_typed_envelope(SET_CODE_TRANSACTION_TYPE, payload_len, output, |fields| {
157        write_u64(transaction.chain_id.get(), fields)?;
158        write_u64(transaction.nonce.get(), fields)?;
159        write_wei(transaction.max_priority_fee_per_gas.to_be_bytes(), fields)?;
160        write_wei(transaction.max_fee_per_gas.to_be_bytes(), fields)?;
161        write_u64(transaction.gas_limit.get(), fields)?;
162        write_address(transaction.to, fields)?;
163        write_wei(transaction.value.to_be_bytes(), fields)?;
164        write_scalar(transaction.input, fields)?;
165        write_access_list(transaction.access_list, fields)?;
166        write_authorization_list(transaction.authorization_list, fields)
167    })
168    .map_err(Into::into)
169}
170
171/// Returns the EIP-7702 authorization signing preimage length.
172pub fn encoded_set_code_authorization_signing_preimage_len(
173    authorization: SetCodeAuthorization,
174) -> Result<usize, TransactionEncodeError> {
175    eth_valkyoth_codec::checked_len_add(
176        1,
177        encoded_list_envelope_len(set_code_authorization_signing_payload_len(authorization)?)?,
178    )
179    .map_err(Into::into)
180}
181
182/// Encodes the EIP-7702 authorization signing preimage.
183pub fn encode_set_code_authorization_signing_preimage(
184    authorization: SetCodeAuthorization,
185    output: &mut [u8],
186) -> Result<usize, TransactionEncodeError> {
187    let payload_len = set_code_authorization_signing_payload_len(authorization)?;
188    let total_len = encoded_set_code_authorization_signing_preimage_len(authorization)?;
189    let payload = output
190        .get_mut(1..total_len)
191        .ok_or(eth_valkyoth_codec::DecodeError::OffsetOutOfBounds)?;
192    encode_list_envelope(payload_len, payload, |fields| {
193        write_u256(authorization.chain_id.to_be_bytes(), fields)?;
194        write_address(authorization.address, fields)?;
195        write_u64(authorization.nonce.get(), fields)
196    })?;
197    let magic = output
198        .first_mut()
199        .ok_or(eth_valkyoth_codec::DecodeError::OffsetOutOfBounds)?;
200    *magic = SET_CODE_AUTHORIZATION_MAGIC;
201    Ok(total_len)
202}
203
204fn legacy_eip155_payload_len(
205    tx: &UnvalidatedLegacyTransaction<'_>,
206    chain_id: ChainId,
207) -> Result<usize, TransactionEncodeError> {
208    sum_lengths(&[
209        encoded_u64_len(tx.nonce.get())?,
210        encoded_u256_len(tx.gas_price.to_be_bytes())?,
211        encoded_u64_len(tx.gas_limit.get())?,
212        encoded_legacy_to_len(tx.to)?,
213        encoded_u256_len(tx.value.to_be_bytes())?,
214        eth_valkyoth_codec::encoded_rlp_scalar_len(tx.input)?,
215        encoded_u64_len(chain_id.get())?,
216        encoded_u64_len(0)?,
217        encoded_u64_len(0)?,
218    ])
219    .map_err(Into::into)
220}
221
222fn access_list_signing_payload_len(
223    tx: &UnvalidatedAccessListTransaction<'_>,
224) -> Result<usize, TransactionEncodeError> {
225    sum_lengths(&[
226        encoded_u64_len(tx.chain_id.get())?,
227        encoded_u64_len(tx.nonce.get())?,
228        encoded_u256_len(tx.gas_price.to_be_bytes())?,
229        encoded_u64_len(tx.gas_limit.get())?,
230        encoded_access_list_to_len(tx.to)?,
231        encoded_u256_len(tx.value.to_be_bytes())?,
232        eth_valkyoth_codec::encoded_rlp_scalar_len(tx.input)?,
233        tx.access_list.encoded_rlp_len(),
234    ])
235    .map_err(Into::into)
236}
237
238fn dynamic_fee_signing_payload_len(
239    tx: &UnvalidatedDynamicFeeTransaction<'_>,
240) -> Result<usize, TransactionEncodeError> {
241    sum_lengths(&[
242        encoded_u64_len(tx.chain_id.get())?,
243        encoded_u64_len(tx.nonce.get())?,
244        encoded_u256_len(tx.max_priority_fee_per_gas.to_be_bytes())?,
245        encoded_u256_len(tx.max_fee_per_gas.to_be_bytes())?,
246        encoded_u64_len(tx.gas_limit.get())?,
247        encoded_access_list_to_len(tx.to)?,
248        encoded_u256_len(tx.value.to_be_bytes())?,
249        eth_valkyoth_codec::encoded_rlp_scalar_len(tx.input)?,
250        tx.access_list.encoded_rlp_len(),
251    ])
252    .map_err(Into::into)
253}
254
255fn blob_signing_payload_len(
256    tx: &UnvalidatedBlobTransaction<'_>,
257) -> Result<usize, TransactionEncodeError> {
258    sum_lengths(&[
259        encoded_u64_len(tx.chain_id.get())?,
260        encoded_u64_len(tx.nonce.get())?,
261        encoded_u256_len(tx.max_priority_fee_per_gas.to_be_bytes())?,
262        encoded_u256_len(tx.max_fee_per_gas.to_be_bytes())?,
263        encoded_u64_len(tx.gas_limit.get())?,
264        eth_valkyoth_codec::encoded_rlp_scalar_len(&tx.to.to_bytes())?,
265        encoded_u256_len(tx.value.to_be_bytes())?,
266        eth_valkyoth_codec::encoded_rlp_scalar_len(tx.input)?,
267        tx.access_list.encoded_rlp_len(),
268        encoded_u256_len(tx.max_fee_per_blob_gas.to_be_bytes())?,
269        tx.blob_versioned_hashes.encoded_rlp_len(),
270    ])
271    .map_err(Into::into)
272}
273
274fn set_code_signing_payload_len(
275    tx: &UnvalidatedSetCodeTransaction<'_>,
276) -> Result<usize, TransactionEncodeError> {
277    sum_lengths(&[
278        encoded_u64_len(tx.chain_id.get())?,
279        encoded_u64_len(tx.nonce.get())?,
280        encoded_u256_len(tx.max_priority_fee_per_gas.to_be_bytes())?,
281        encoded_u256_len(tx.max_fee_per_gas.to_be_bytes())?,
282        encoded_u64_len(tx.gas_limit.get())?,
283        eth_valkyoth_codec::encoded_rlp_scalar_len(&tx.to.to_bytes())?,
284        encoded_u256_len(tx.value.to_be_bytes())?,
285        eth_valkyoth_codec::encoded_rlp_scalar_len(tx.input)?,
286        tx.access_list.encoded_rlp_len(),
287        tx.authorization_list.encoded_rlp_len(),
288    ])
289    .map_err(Into::into)
290}
291
292fn set_code_authorization_signing_payload_len(
293    authorization: SetCodeAuthorization,
294) -> Result<usize, TransactionEncodeError> {
295    sum_lengths(&[
296        encoded_u256_len(authorization.chain_id.to_be_bytes())?,
297        eth_valkyoth_codec::encoded_rlp_scalar_len(&authorization.address.to_bytes())?,
298        encoded_u64_len(authorization.nonce.get())?,
299    ])
300    .map_err(Into::into)
301}
302
303#[cfg(test)]
304mod tests {
305    use super::*;
306    use eth_valkyoth_codec::DecodeLimits;
307    use eth_valkyoth_primitives::{Address, B256, Gas, Nonce, Wei};
308
309    const TEST_LIMITS: DecodeLimits = DecodeLimits {
310        max_input_bytes: 128,
311        max_list_items: 16,
312        max_nesting_depth: 8,
313        max_total_allocation: 128,
314        max_proof_nodes: 4,
315        max_total_items: 32,
316    };
317    const EIP155_SIGNING_DATA: &[u8] = &[
318        0xec, 0x09, 0x85, 0x04, 0xa8, 0x17, 0xc8, 0x00, 0x82, 0x52, 0x08, 0x94, 0x35, 0x35, 0x35,
319        0x35, 0x35, 0x35, 0x35, 0x35, 0x35, 0x35, 0x35, 0x35, 0x35, 0x35, 0x35, 0x35, 0x35, 0x35,
320        0x35, 0x35, 0x88, 0x0d, 0xe0, 0xb6, 0xb3, 0xa7, 0x64, 0x00, 0x00, 0x80, 0x01, 0x80, 0x80,
321    ];
322
323    #[test]
324    fn encodes_eip155_published_signing_data() {
325        let tx = UnvalidatedLegacyTransaction {
326            nonce: Nonce::new(9),
327            gas_price: Wei::from_u128(20_000_000_000),
328            gas_limit: Gas::new(21_000),
329            to: super::super::LegacyTransactionTo::Call(Address::from_bytes([0x35; 20])),
330            value: Wei::from_u128(1_000_000_000_000_000_000),
331            input: &[],
332            v: [0_u8; 32],
333            r: [0_u8; 32],
334            s: [0_u8; 32],
335        };
336        let mut output = [0_u8; 64];
337
338        assert_eq!(
339            encoded_legacy_eip155_signing_preimage_len(&tx, ChainId::new(1)),
340            Ok(EIP155_SIGNING_DATA.len())
341        );
342        let written = encode_legacy_eip155_signing_preimage(&tx, ChainId::new(1), &mut output);
343        assert_eq!(written, Ok(EIP155_SIGNING_DATA.len()));
344        assert_eq!(
345            output.get(..EIP155_SIGNING_DATA.len()),
346            Some(EIP155_SIGNING_DATA)
347        );
348    }
349
350    #[test]
351    fn typed_preimages_drop_signature_fields() {
352        let access_list_tx = [
353            0x01, 0xcd, 0x01, 0x02, 0x03, 0x82, 0x52, 0x08, 0x80, 0x05, 0x80, 0xc0, 0x01, 0x01,
354            0x02,
355        ];
356        let tx = super::super::decode_access_list_transaction(&access_list_tx, TEST_LIMITS);
357        assert!(tx.is_ok());
358        if let Ok(tx) = tx {
359            let mut output = [0_u8; 32];
360            let expected = [
361                0x01, 0xca, 0x01, 0x02, 0x03, 0x82, 0x52, 0x08, 0x80, 0x05, 0x80, 0xc0,
362            ];
363            let written = encode_access_list_signing_preimage(&tx, &mut output);
364            assert_eq!(written, Ok(expected.len()));
365            assert_eq!(output.get(..expected.len()), Some(expected.as_slice()));
366        }
367    }
368
369    #[test]
370    fn dynamic_and_blob_preimages_include_type_byte() {
371        let dynamic_fee_tx = [
372            0x02, 0xce, 0x01, 0x02, 0x03, 0x04, 0x82, 0x52, 0x08, 0x80, 0x05, 0x80, 0xc0, 0x01,
373            0x01, 0x02,
374        ];
375        let blob_tx = [
376            0x03, 0xf8, 0x45, 0x01, 0x02, 0x03, 0x04, 0x82, 0x52, 0x08, 0x94, 0x11, 0x11, 0x11,
377            0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11,
378            0x11, 0x11, 0x11, 0x05, 0x80, 0xc0, 0x06, 0xe1, 0xa0, 0x01, 0x01, 0x01, 0x01, 0x01,
379            0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01,
380            0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01,
381            0x01, 0x02,
382        ];
383        let dynamic = super::super::decode_dynamic_fee_transaction(&dynamic_fee_tx, TEST_LIMITS);
384        let blob = super::super::decode_blob_transaction(&blob_tx, TEST_LIMITS);
385        assert!(dynamic.is_ok());
386        assert!(blob.is_ok());
387        if let (Ok(dynamic), Ok(blob)) = (dynamic, blob) {
388            let mut dynamic_output = [0_u8; 32];
389            let mut blob_output = [0_u8; 96];
390            let dynamic_written =
391                encode_dynamic_fee_signing_preimage(&dynamic, &mut dynamic_output);
392            let blob_written = encode_blob_signing_preimage(&blob, &mut blob_output);
393
394            assert_eq!(dynamic_written, Ok(13));
395            assert_eq!(blob_written, Ok(69));
396            assert_eq!(dynamic_output.first(), Some(&DYNAMIC_FEE_TRANSACTION_TYPE));
397            assert_eq!(blob_output.first(), Some(&BLOB_TRANSACTION_TYPE));
398            assert_ne!(
399                dynamic_output.get(1..dynamic_written.unwrap_or(1)),
400                dynamic_fee_tx.get(1..dynamic_fee_tx.len())
401            );
402            assert_eq!(blob.blob_versioned_hashes.hashes().count(), 1);
403            assert_eq!(B256::from_bytes([1_u8; 32]).to_bytes()[0], 1);
404        }
405    }
406}