Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Update EIP-6404: rm chain_id from txs wrapper #6665

Merged
merged 1 commit into from
Mar 9, 2023
Merged
Show file tree
Hide file tree
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
Update EIP-6404: rm chain_id from txs wrapper
Chain ID is already mixed into each transaction signature, no need to
replicate it as part of the transactions list.
  • Loading branch information
etan-status committed Mar 9, 2023
commit 417c98a7fa029642789e1440a4bc983f34f2ca4d
188 changes: 91 additions & 97 deletions EIPS/eip-6404.md
Original file line number Diff line number Diff line change
Expand Up @@ -556,12 +556,13 @@ def normalize_signed_transaction(encoded_signed_tx: bytes, cfg: ExecutionConfig)

### Consensus `ExecutionPayload` changes

The [consensus `ExecutionPayload`'s `transactions`](https://github.com/ethereum/consensus-specs/blob/67c2f9ee9eb562f7cc02b2ff90d92c56137944e1/specs/capella/beacon-chain.md#executionpayload) list is replaced with a `Transactions` container.
The [consensus `ExecutionPayload`'s `transactions`](https://github.com/ethereum/consensus-specs/blob/67c2f9ee9eb562f7cc02b2ff90d92c56137944e1/specs/capella/beacon-chain.md#executionpayload) list is now based on the normalized `Transaction` SSZ container.

```python
class Transactions(Container):
tx_list: List[Transaction, MAX_TRANSACTIONS_PER_PAYLOAD]
chain_id: uint256
class ExecutionPayload(Container):
...
transactions: List[Transaction, MAX_TRANSACTIONS_PER_PAYLOAD]
...
```

```python
Expand All @@ -570,13 +571,10 @@ cfg = ExecutionConfig(...)
encoded_signed_txs = List[ByteList[MAX_BYTES_PER_TRANSACTION], MAX_TRANSACTIONS_PER_PAYLOAD](
encoded_signed_tx_0, encoded_signed_tx_1, encoded_signed_tx_2, ...)

payload.transactions = Transactions(
tx_list=[
normalize_signed_transaction(encoded_signed_tx, cfg)
for encoded_signed_tx in encoded_signed_txs
],
chain_id=cfg.chain_id,
)
payload.transactions = List[Transaction, MAX_TRANSACTIONS_PER_PAYLOAD](*[
normalize_signed_transaction(encoded_signed_tx, cfg)
for encoded_signed_tx in encoded_signed_txs
])
```

### Consensus `ExecutionPayloadHeader` changes
Expand Down Expand Up @@ -734,78 +732,75 @@ def recover_encoded_signed_tx(tx: Transaction, cfg: ExecutionConfig) -> bytes:

### Consensus `ExecutionPayload` validation

As part of the `engine_newPayload` duties, the `transactions` field of an `ExecutionPayload` is validated.
As part of the `engine_newPayload` duties, all `Transaction` SSZ containers within the `transactions` field of the `ExecutionPayload` are validated.

```python
def validate_transactions(transactions: Transactions, cfg: ExecutionConfig):
assert transactions.chain_id == cfg.chain_id

for tx in transactions.tx_list:
assert ecdsa_validate_signature(tx.payload.signature)

tx_type = tx.payload.sig_type.tx_type
if tx_type == TRANSACTION_TYPE_EIP4844:
signed_tx = recover_eip4844_signed_tx(tx, cfg)
assert tx.payload.tx_from == ecdsa_recover_tx_from(
tx.payload.signature,
compute_eip4844_sig_hash(signed_tx),
)
assert tx.tx_hash == compute_eip4844_tx_hash(signed_tx)
elif tx_type == TRANSACTION_TYPE_EIP1559:
signed_tx = recover_eip1559_signed_tx(tx, cfg)
assert tx.payload.tx_from == ecdsa_recover_tx_from(
tx.payload.signature,
compute_eip1559_sig_hash(signed_tx),
)
assert tx.tx_hash == compute_eip1559_tx_hash(signed_tx)
elif tx_type == TRANSACTION_TYPE_EIP2930:
signed_tx = recover_eip2930_signed_tx(tx, cfg)
assert tx.payload.tx_from == ecdsa_recover_tx_from(
tx.payload.signature,
compute_eip1559_sig_hash(signed_tx),
)
assert tx.tx_hash == compute_eip2930_tx_hash(signed_tx)
elif tx_type == TRANSACTION_TYPE_LEGACY:
signed_tx = recover_legacy_signed_tx(tx, cfg)
assert tx.payload.tx_from == ecdsa_recover_tx_from(
tx.payload.signature,
compute_eip1559_sig_hash(signed_tx),
)
assert tx.tx_hash == compute_legacy_tx_hash(signed_tx)
else:
assert False

destination_type = tx.payload.tx_to.destination_type
if destination_type == DESTINATION_TYPE_REGULAR:
pass
elif destination_type == DESTINATION_TYPE_CREATE:
assert tx.payload.tx_to.address == compute_contract_address(
tx.payload.tx_from,
tx.payload.nonce,
)
else:
assert False
def validate_transaction(transaction: Transaction, cfg: ExecutionConfig):
assert ecdsa_validate_signature(tx.payload.signature)

tx_type = tx.payload.sig_type.tx_type
if tx_type == TRANSACTION_TYPE_EIP4844:
signed_tx = recover_eip4844_signed_tx(tx, cfg)
assert tx.payload.tx_from == ecdsa_recover_tx_from(
tx.payload.signature,
compute_eip4844_sig_hash(signed_tx),
)
assert tx.tx_hash == compute_eip4844_tx_hash(signed_tx)
elif tx_type == TRANSACTION_TYPE_EIP1559:
signed_tx = recover_eip1559_signed_tx(tx, cfg)
assert tx.payload.tx_from == ecdsa_recover_tx_from(
tx.payload.signature,
compute_eip1559_sig_hash(signed_tx),
)
assert tx.tx_hash == compute_eip1559_tx_hash(signed_tx)
elif tx_type == TRANSACTION_TYPE_EIP2930:
signed_tx = recover_eip2930_signed_tx(tx, cfg)
assert tx.payload.tx_from == ecdsa_recover_tx_from(
tx.payload.signature,
compute_eip1559_sig_hash(signed_tx),
)
assert tx.tx_hash == compute_eip2930_tx_hash(signed_tx)
elif tx_type == TRANSACTION_TYPE_LEGACY:
signed_tx = recover_legacy_signed_tx(tx, cfg)
assert tx.payload.tx_from == ecdsa_recover_tx_from(
tx.payload.signature,
compute_eip1559_sig_hash(signed_tx),
)
assert tx.tx_hash == compute_legacy_tx_hash(signed_tx)
else:
assert False

if tx.payload.sig_type.tx_type != TRANSACTION_TYPE_LEGACY:
assert not tx.payload.sig_type.no_replay_protection
destination_type = tx.payload.tx_to.destination_type
if destination_type == DESTINATION_TYPE_REGULAR:
pass
elif destination_type == DESTINATION_TYPE_CREATE:
assert tx.payload.tx_to.address == compute_contract_address(
tx.payload.tx_from,
tx.payload.nonce,
)
else:
assert False

if tx.payload.sig_type.tx_type == TRANSACTION_TYPE_EIP4844:
assert tx.payload.details.blob.get() is not None
continue
assert tx.payload.details.blob.get() is None
if tx.payload.sig_type.tx_type != TRANSACTION_TYPE_LEGACY:
assert not tx.payload.sig_type.no_replay_protection

if tx.payload.sig_type.tx_type == TRANSACTION_TYPE_EIP1559:
continue
assert tx.payload.details.limits.max_priority_fee_per_gas == \
tx.payload.details.limits.max_fee_per_gas
if tx.payload.sig_type.tx_type == TRANSACTION_TYPE_EIP4844:
assert tx.payload.details.blob.get() is not None
return
assert tx.payload.details.blob.get() is None

if tx.payload.sig_type.tx_type == TRANSACTION_TYPE_EIP2930:
continue
assert len(tx.payload.details.access_list) == 0
if tx.payload.sig_type.tx_type == TRANSACTION_TYPE_EIP1559:
return
assert tx.payload.details.limits.max_priority_fee_per_gas == \
tx.payload.details.limits.max_fee_per_gas

if tx.payload.sig_type.tx_type == TRANSACTION_TYPE_LEGACY:
continue
assert False
if tx.payload.sig_type.tx_type == TRANSACTION_TYPE_EIP2930:
return
assert len(tx.payload.details.access_list) == 0

if tx.payload.sig_type.tx_type == TRANSACTION_TYPE_LEGACY:
return
assert False
```

## Rationale
Expand Down Expand Up @@ -845,18 +840,18 @@ Applications that rely on the replaced MPT `transactions_root` in the block head
The following representations of the consensus `ExecutionPayload`'s `transactions` field are compared:

1. **Baseline:** [Opaque `ByteList`](https://github.com/ethereum/consensus-specs/blob/67c2f9ee9eb562f7cc02b2ff90d92c56137944e1/specs/bellatrix/beacon-chain.md#custom-types) containing the transaction's original representation
2. **SSZ Union:** RLP encoded transactions are converted to SSZ objects
3. **Normalized:** Normalized `Transactions` container with extra commitments
2. **SSZ Union:** RLP encoded transactions converted to SSZ objects
3. **Normalized:** Normalized `Transaction` container (proposed design)

### `ExecutionPayload` transaction size

| Transaction | Native | Baseline | SSZ Union | Normalized | Base + Snappy | Union + Snappy | Norm + Snappy |
| - | :-: | :-: | :-: | :-: | :-: | :-: | :-: |
| Legacy | RLP | 106 bytes | 210 bytes | 272 bytes | 109 bytes | 138 bytes | 195 bytes |
| [EIP-155](./eip-155.md) | RLP | 108 bytes | 210 bytes | 272 bytes | 111 bytes | 139 bytes | 194 bytes |
| [EIP-2930](./eip-2930.md) | RLP | 111 bytes | 215 bytes | 272 bytes | 114 bytes | 145 bytes | 194 bytes |
| [EIP-1559](./eip-1559.md) | RLP | 117 bytes | 247 bytes | 272 bytes | 117 bytes | 148 bytes | 194 bytes |
| [EIP-4844](./eip-4844.md) | SSZ | 315 bytes | 315 bytes (\*) | 340 bytes | 186 bytes | 186 bytes | 234 bytes |
| Legacy | RLP | 106 bytes | 210 bytes | 272 bytes | 109 bytes | 138 bytes | 196 bytes |
| [EIP-155](./eip-155.md) | RLP | 108 bytes | 210 bytes | 272 bytes | 111 bytes | 139 bytes | 195 bytes |
| [EIP-2930](./eip-2930.md) | RLP | 111 bytes | 215 bytes | 272 bytes | 114 bytes | 145 bytes | 195 bytes |
| [EIP-1559](./eip-1559.md) | RLP | 117 bytes | 247 bytes | 272 bytes | 117 bytes | 148 bytes | 195 bytes |
| [EIP-4844](./eip-4844.md) | SSZ | 315 bytes | 315 bytes (\*) | 340 bytes | 186 bytes | 186 bytes | 235 bytes |

- [Baseline](../assets/eip-6404/tests/create_transactions.py)
- [SSZ Union](../assets/eip-6404/tests/union/convert_transactions.py)
Expand Down Expand Up @@ -913,42 +908,41 @@ The SSZ Union representation needs more functionality to obtain certain info:

The cost of cryptographic operations varies across environments. Therefore, the complexity of proof verification is analyzed in number of cryptographic operations and number of branches.

| Proof | SSZ Union (\*) | Normalized (\*\*) |
| Proof | SSZ Union (\*) | Normalized |
| - | - | - |
| Transaction | 22 SHA256 | 23 SHA256 |
| Amount | 23 SHA256 + {6, 4, 6, 6} SHA256 | 29 SHA256 |
| Sender | 23 SHA256 + {9, 7, 9, 9} SHA256 + 1 secp256k1 + 1 keccak256 | 29 SHA256 |
| Info | 23 SHA256 + {10, 10, 12, 12} SHA256 + 1 secp256k1 + 1 keccak256 + {0, 1} keccak256 | 34 SHA256 |
| Transaction | 22 SHA256 | 22 SHA256 |
| Amount | 23 SHA256 + {6, 4, 6, 6} SHA256 | 28 SHA256 |
| Sender | 23 SHA256 + {9, 7, 9, 9} SHA256 + 1 secp256k1 + 1 keccak256 | 28 SHA256 |
| Info | 23 SHA256 + {10, 10, 12, 12} SHA256 + 1 secp256k1 + 1 keccak256 + {0, 1} keccak256 | 33 SHA256 |

- [SSZ Union](../assets/eip-6404/tests/union/verify_proofs.py)
- [Normalized](../assets/eip-6404/tests/normalized/verify_proofs.py)

For the SSZ Union representation, the verifier logic contains branching depending on the [EIP-2718](./eip-2718.md) transaction type, and for the "Info" proof, depending on whether or not the transaction deploys a new contract. The number per branch does not exploit coincidental similarity of branches to have a better understanding of the cost that an entirely new transaction type could add.

- (\*) The SSZ Union estimates assume that `sig_hash` is equivalent to `payload.hash_tree_root()`. This is a simplified model and unviable in praxis due to signature malleability. A real signature scheme would require additional SHA256 hashes to mix in additional static data.
- (\*\*) The `Transactions` SSZ container contains the `chain_id`, requiring one extra SHA256 compared to the SSZ Union to mix in.

### SSZ proof verification on Embedded

An industry-standard 64 MHz ARM Cortex-M4F core serves as the reference platform for this section. The following table lists the flash size required to deploy each verifier program, without proof data. The base test harness and RTOS require 19,778 bytes of flash and 5,504 bytes of RAM with 1 KB of stack memory for the main task. For these measurements, this base amount is already subtracted. The worst result was selected when there was minor jitter between test cases.

| Proof | SSZ Union | Normalized |
| - | :-: | :-: |
| Transaction | 2,635 bytes | 2,668 bytes |
| Amount | 3,598 bytes | 3,011 bytes |
| Sender | 30,065 bytes (\*) | 3,071 bytes |
| Info | 31,294 bytes (\*) | 3,651 bytes |
| Transaction | 3,344 bytes | 3,360 bytes |
| Amount | 4,468 bytes | 3,812 bytes |
| Sender | 30,960 bytes (\*) | 3,872 bytes |
| Info | 32,236 bytes (\*) | 4,560 bytes |

- (\*) The secp256k1 library also requires increasing the stack memory for the main task from 1 KB to 6 KB.

The CPU cycle count is measured while verifying sample data for each proofs. That cycle count is then divided by 64 MHz to obtain the elapsed time. All columns except "Normalized" are measured using the SSZ Union approach.

| Proof | Legacy | [EIP-155](./eip-155.md) | [EIP-2930](./eip-2930.md) | [EIP-1559](./eip-1559.md) | [EIP-4844](./eip-4844.md) | Normalized (\*) |
| - | :-: | :-: | :-: | :-: | :-: | :-: |
| Transaction | 2.083203 ms | 2.083046 ms | 2.083046 ms | 2.082890 ms | 2.083046 ms | 2.177125 ms |
| Amount | 2.784296 ms | 2.783875 ms | 2.591125 ms | 2.785203 ms | 2.783781 ms | 2.686578 ms |
| Sender | 26.333531 ms | 26.113796 ms | 25.836078 ms | 26.275093 ms | 26.099609 ms | 2.699390 ms |
| Info | 26.824125 ms | 26.603312 ms | 26.504875 ms | 26.951562 ms | 26.782187 ms | 3.291203 ms |
| Transaction | 2.083203 ms | 2.083046 ms | 2.083046 ms | 2.082890 ms | 2.083046 ms | 2.080640 ms |
| Amount | 2.784296 ms | 2.783875 ms | 2.591125 ms | 2.785203 ms | 2.783781 ms | 2.597109 ms |
| Sender | 26.333531 ms | 26.113796 ms | 25.836078 ms | 26.275093 ms | 26.099609 ms | 2.640890 ms |
| Info | 26.824125 ms | 26.603312 ms | 26.504875 ms | 26.951562 ms | 26.782187 ms | 3.197406 ms |

- [SSZ Union](../assets/eip-6404/tests/union/verify_proofs.c)
- [Normalized](../assets/eip-6404/tests/normalized/verify_proofs.c)
Expand Down
15 changes: 6 additions & 9 deletions assets/eip-6404/tests/normalized/convert_transactions.py
Original file line number Diff line number Diff line change
Expand Up @@ -190,21 +190,18 @@ def normalize_signed_transaction(encoded_signed_tx: bytes, cfg: ExecutionConfig)

assert False

transactions = Transactions(
tx_list=[
normalize_signed_transaction(encoded_signed_tx, cfg)
for encoded_signed_tx in encoded_signed_txs
],
chain_id=cfg.chain_id,
)
transactions = List[Transaction, MAX_TRANSACTIONS_PER_PAYLOAD](*[
normalize_signed_transaction(encoded_signed_tx, cfg)
for encoded_signed_tx in encoded_signed_txs
])
transactions_root = transactions.hash_tree_root()

if __name__ == '__main__':
print('transactions_root')
print(f'0x{transactions_root.hex()}')

for tx_index in range(len(transactions.tx_list)):
tx = transactions.tx_list[tx_index]
for tx_index in range(len(transactions)):
tx = transactions[tx_index]
encoded = tx.encode_bytes()
print(f'{tx_index} - {len(encoded)} bytes (Snappy: {len(compress(encoded))})')
print(f'0x{tx.tx_hash.hex()}')
Expand Down
Loading