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

feat(l2): implement state diffs #1015

Merged
merged 25 commits into from
Nov 7, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
0b73b5d
Check deposit logs on commit to L1
ManuelBilbao Oct 30, 2024
3570e8b
Fix docs
ManuelBilbao Oct 30, 2024
9e632eb
Add StateDiff struct
ManuelBilbao Oct 30, 2024
a01075d
Implement KZG commitment for blob
ManuelBilbao Oct 30, 2024
6e39be3
Add deposits and withdrawals to state diff
ManuelBilbao Oct 31, 2024
d4801dc
Fix contracts
ManuelBilbao Oct 31, 2024
e8dfcd2
Improve state diff preparation
ManuelBilbao Oct 31, 2024
a6f3bc6
Format
ManuelBilbao Oct 31, 2024
d0afb73
Fix clippy
ManuelBilbao Oct 31, 2024
9a597cb
Remove unwraps
ManuelBilbao Oct 31, 2024
e42a26e
Pass expected_blobs_versioned_hashes to newPayload
ManuelBilbao Nov 1, 2024
4fdf598
Change max_fee_per_blob_gas type to U256
ManuelBilbao Nov 1, 2024
64f01d0
Send commitment with EIP-4844 transaction
ManuelBilbao Nov 1, 2024
7f63def
Change commitment versioned hash to sha256
ManuelBilbao Nov 4, 2024
4b70162
Use kzg API from lambdaworks
ManuelBilbao Nov 5, 2024
e3e4340
Replace lambdaworks with c-kzg-4844
ManuelBilbao Nov 5, 2024
c31deea
Merge branch 'main' into l2_state_diffs
ManuelBilbao Nov 5, 2024
4ff35d3
Remove points_path from proposer config
ManuelBilbao Nov 6, 2024
775a871
Add 0x00 byte before every 31 bytes to fit field modulus
ManuelBilbao Nov 6, 2024
e0680fb
Merge branch 'main' into l2_state_diffs
ManuelBilbao Nov 6, 2024
5d887c8
Improve error handling
ManuelBilbao Nov 6, 2024
809cc5d
Add TODO comment
ManuelBilbao Nov 6, 2024
93cb0c1
Add blob_from_bytes
ManuelBilbao Nov 6, 2024
fcffe1b
Remove TODOs
ManuelBilbao Nov 6, 2024
bd269b0
Add BLS limitation doc
ManuelBilbao Nov 6, 2024
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
2 changes: 2 additions & 0 deletions crates/blockchain/dev/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ ethereum-types.workspace = true
hex.workspace = true
reqwest = { version = "0.12.7", features = ["json"] }
envy = "0.4.2"
keccak-hash = "0.10.0"
sha2 = "0.10.8"

[lib]
path = "./dev.rs"
14 changes: 13 additions & 1 deletion crates/blockchain/dev/block_producer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ use crate::utils::engine_client::EngineClient;
use bytes::Bytes;
use ethereum_rust_rpc::types::fork_choice::{ForkChoiceState, PayloadAttributesV3};
use ethereum_types::H256;
use sha2::{Digest, Sha256};
use std::{
net::SocketAddr,
time::{SystemTime, UNIX_EPOCH},
Expand Down Expand Up @@ -62,7 +63,18 @@ pub async fn start_block_producer(
let payload_status = match engine_client
.engine_new_payload_v3(
execution_payload_response.execution_payload,
Default::default(),
execution_payload_response
.blobs_bundle
.commitments
.iter()
.map(|commitment| {
let mut hasher = Sha256::new();
hasher.update(commitment);
let mut hash = hasher.finalize();
hash[0] = 0x01;
H256::from_slice(&hash)
})
.collect(),
Default::default(),
)
.await
Expand Down
52 changes: 50 additions & 2 deletions crates/common/types/transaction.rs
Original file line number Diff line number Diff line change
Expand Up @@ -851,6 +851,9 @@ impl PrivilegedTxType {
}

impl PrivilegedL2Transaction {
/// Returns the formated hash of the withdrawal transaction,
/// or None if the transaction is not a withdrawal.
/// The hash is computed as keccak256(to || value || tx_hash)
pub fn get_withdrawal_hash(&self) -> Option<H256> {
match self.tx_type {
PrivilegedTxType::Withdrawal => {
Expand All @@ -872,6 +875,26 @@ impl PrivilegedL2Transaction {
_ => None,
}
}

/// Returns the formated hash of the deposit transaction,
/// or None if the transaction is not a deposit.
/// The hash is computed as keccak256(to || value)
pub fn get_deposit_hash(&self) -> Option<H256> {
match self.tx_type {
PrivilegedTxType::Deposit => {
let to = match self.to {
TxKind::Call(to) => to,
_ => return None,
};

let value = &mut [0u8; 32];
self.value.to_big_endian(value);

Some(keccak_hash::keccak([to.as_bytes(), value].concat()))
}
_ => None,
}
}
}

/// Canonical Transaction Encoding
Expand Down Expand Up @@ -1602,8 +1625,7 @@ mod serde_impl {
pub max_priority_fee_per_gas: Option<u64>,
#[serde(default, with = "crate::serde_utils::u64::hex_str_opt")]
pub max_fee_per_gas: Option<u64>,
#[serde(default, with = "crate::serde_utils::u64::hex_str_opt")]
pub max_fee_per_blob_gas: Option<u64>,
pub max_fee_per_blob_gas: Option<U256>,
#[serde(default)]
pub access_list: Vec<AccessListEntry>,
#[serde(default)]
Expand Down Expand Up @@ -1639,6 +1661,32 @@ mod serde_impl {
}
}
}

impl From<EIP4844Transaction> for GenericTransaction {
fn from(value: EIP4844Transaction) -> Self {
Self {
r#type: TxType::EIP4844,
nonce: value.nonce,
to: TxKind::Call(value.to),
gas: Some(value.gas),
value: value.value,
input: value.data,
gas_price: value.max_fee_per_gas,
max_priority_fee_per_gas: Some(value.max_priority_fee_per_gas),
max_fee_per_gas: Some(value.max_fee_per_gas),
max_fee_per_blob_gas: Some(value.max_fee_per_blob_gas),
access_list: value
.access_list
.iter()
.map(AccessListEntry::from)
.collect(),
blob_versioned_hashes: value.blob_versioned_hashes,
blobs: vec![],
chain_id: None,
..Default::default()
}
}
}
}

mod mempool {
Expand Down
2 changes: 2 additions & 0 deletions crates/l2/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,8 @@ libsecp256k1 = "0.7.1"
keccak-hash = "0.10.0"
envy = "0.4.2"
thiserror.workspace = true
sha2 = "0.10.8"
c-kzg = "^1.0.3"

# risc0
risc0-zkvm = { version = "1.1.2" }
Expand Down
28 changes: 25 additions & 3 deletions crates/l2/contracts/src/l1/CommonBridge.sol
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,9 @@ contract CommonBridge is ICommonBridge, Ownable, ReentrancyGuard {

// TODO: Build the tx.
bytes32 l2MintTxHash = keccak256(abi.encodePacked("dummyl2MintTxHash"));
depositLogs.push(keccak256(abi.encodePacked(to, msg.value)));
depositLogs.push(
keccak256(bytes.concat(bytes20(to), bytes32(msg.value)))
);
emit DepositInitiated(msg.value, to, l2MintTxHash);
}

Expand All @@ -70,10 +72,30 @@ contract CommonBridge is ICommonBridge, Ownable, ReentrancyGuard {
}

/// @inheritdoc ICommonBridge
function removeDepositLogs(uint number) public onlyOnChainProposer {
function getDepositLogsVersionedHash(
uint16 number
) public view returns (bytes32) {
require(number > 0, "CommonBridge: number is zero (get)");
require(
uint256(number) <= depositLogs.length,
"CommonBridge: number is greater than the length of depositLogs (get)"
);

bytes memory logs;
for (uint i = 0; i < number; i++) {
logs = bytes.concat(logs, depositLogs[i]);
}

return
bytes32(bytes2(number)) |
bytes32(uint256(uint240(uint256(keccak256(logs)))));
}

/// @inheritdoc ICommonBridge
function removeDepositLogs(uint16 number) public onlyOnChainProposer {
require(
number <= depositLogs.length,
"CommonBridge: number is greater than the length of depositLogs"
"CommonBridge: number is greater than the length of depositLogs (remove)"
);

for (uint i = 0; i < depositLogs.length - number; i++) {
Expand Down
32 changes: 17 additions & 15 deletions crates/l2/contracts/src/l1/OnChainProposer.sol
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ contract OnChainProposer is IOnChainProposer, ReentrancyGuard {
/// @inheritdoc IOnChainProposer
function commit(
uint256 blockNumber,
bytes32 newL2StateRoot,
bytes32 commitment,
bytes32 withdrawalsLogsMerkleRoot,
bytes32 depositLogs
) external override {
Expand All @@ -62,25 +62,27 @@ contract OnChainProposer is IOnChainProposer, ReentrancyGuard {
blockCommitments[blockNumber].commitmentHash == bytes32(0),
"OnChainProposer: block already committed"
);
bytes32 blockCommitment = keccak256(
abi.encode(
blockNumber,
newL2StateRoot,
withdrawalsLogsMerkleRoot,
depositLogs
)
);
blockCommitments[blockNumber] = BlockCommitmentInfo(
blockCommitment,
depositLogs
);
// Check if commitment is equivalent to blob's KZG commitment.

if (depositLogs != bytes32(0)) {
bytes32 savedDepositLogs = ICommonBridge(BRIDGE)
.getDepositLogsVersionedHash(uint16(bytes2(depositLogs)));
require(
savedDepositLogs == depositLogs,
"OnChainProposer: invalid deposit logs"
);
}
if (withdrawalsLogsMerkleRoot != bytes32(0)) {
ICommonBridge(BRIDGE).publishWithdrawals(
blockNumber,
withdrawalsLogsMerkleRoot
);
}
emit BlockCommitted(blockCommitment);
blockCommitments[blockNumber] = BlockCommitmentInfo(
commitment,
depositLogs
);
emit BlockCommitted(commitment);
}

/// @inheritdoc IOnChainProposer
Expand All @@ -100,7 +102,7 @@ contract OnChainProposer is IOnChainProposer, ReentrancyGuard {
lastVerifiedBlock = blockNumber;
ICommonBridge(BRIDGE).removeDepositLogs(
// The first 2 bytes are the number of deposits.
uint16(uint256(blockCommitments[blockNumber].depositLogs >> 240))
uint16(bytes2(blockCommitments[blockNumber].depositLogs))
);

// Remove previous block commitment as it is no longer needed.
Expand Down
8 changes: 7 additions & 1 deletion crates/l2/contracts/src/l1/interfaces/ICommonBridge.sol
Original file line number Diff line number Diff line change
Expand Up @@ -54,13 +54,19 @@ interface ICommonBridge {
/// @param to, the address in L2 to which the tokens will be minted to.
function deposit(address to) external payable;

/// @notice Method to retrieve the versioned hash of the first `number` deposit logs.
/// @param number of deposit logs to retrieve the versioned hash.
function getDepositLogsVersionedHash(
uint16 number
) external view returns (bytes32);

/// @notice Remove deposit from depositLogs queue.
/// @dev This method is used by the L2 OnChainOperator to remove the deposit
/// logs from the queue after the deposit is verified.
/// @param number of deposit logs to remove.
/// As deposits are processed in order, we don't need to specify
/// the deposit logs to remove, only the number of them.
function removeDepositLogs(uint number) external;
function removeDepositLogs(uint16 number) external;

/// @notice Publishes the L2 withdrawals on L1.
/// @dev This method is used by the L2 OnChainOperator to publish the L2
Expand Down
5 changes: 3 additions & 2 deletions crates/l2/contracts/src/l1/interfaces/IOnChainProposer.sol
Original file line number Diff line number Diff line change
Expand Up @@ -27,12 +27,13 @@ interface IOnChainProposer {
/// @dev Committing to an L2 block means to store the block's commitment
/// and to publish withdrawals if any.
/// @param blockNumber the number of the block to be committed.
/// @param newL2StateRoot the new L2 state root of the block to be committed.
/// @param commitment of the block to be committed.
/// @param withdrawalsLogsMerkleRoot the merkle root of the withdrawal logs
/// of the block to be committed.
/// @param depositLogs the deposit logs of the block to be committed.
function commit(
uint256 blockNumber,
bytes32 newL2StateRoot,
bytes32 commitment,
bytes32 withdrawalsLogsMerkleRoot,
bytes32 depositLogs
) external;
Expand Down
8 changes: 6 additions & 2 deletions crates/l2/docs/state_diffs.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ The full state diff sent on every block will then be a sequence of bytes encoded
- Next the `WithdrawalLogs` field:
- First two bytes are the number of entries, then come the tuples `(to_u160, amount_u256, tx_hash_u256)`.
- Next the `DepositLogs` field:
- First two bytes are the number of entries, then come the last 30 bytes of the `keccak` encoding of the concatenation of deposits with form `keccack256(to_u160 || value_u256)`.
- First two bytes are the number of entries, then come the tuples `(to_u160, value_u256)`.
- In case of the only changes on an account are produced by withdrawals, the `ModifiedAccounts` for that address field must be omitted. In this case, the state diff can be computed by incrementing the nonce in one unit and subtracting the amount from the balance.

To recap, using `||` for byte concatenation and `[]` for optional parameters, the full encoding for state diffs is:
Expand All @@ -57,10 +57,14 @@ number_of_modified_accounts_u16 ||
number_of_withdraw_logs_u16 ||
(to_u160 || amount_u256 || tx_hash_u256) ...
// Deposit Logs
number_of_deposit_logs_u16 || keccak256(keccack256(to_u160 || value_u256) || ...)[2:32]
number_of_deposit_logs_u16 ||
(to_u160 || value_u256) ...
```

The sequencer will then make a commitment to this encoded state diff (explained in the EIP 4844 section how this is done) and send on the `commit` transaction:

- Through calldata, the state diff commitment (which is part of the public input to the proof).
- Through the blob, the encoded state diff.

> [!NOTE]
> As the blob is encoded as 4096 BLS12-381 field elements, every 32-bytes chunk cannot be greater than the subgroup `r` size: `0x73eda753299d7d483339d80809a1d80553bda402fffe5bfeffffffff00000001`. _i.e._, the most significant byte must be less than `0x73`. To avoid conflicts, we insert a `0x00` byte before every 31-bytes chunk to ensure this condition is met.
27 changes: 27 additions & 0 deletions crates/l2/proposer/errors.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
use crate::utils::{config::errors::ConfigError, eth_client::errors::EthClientError};
use ethereum_rust_dev::utils::engine_client::errors::EngineClientError;
use ethereum_rust_vm::EvmError;

#[derive(Debug, thiserror::Error)]
pub enum L1WatcherError {
Expand Down Expand Up @@ -35,4 +36,30 @@ pub enum ProposerError {
FailedToGetSystemTime(#[from] std::time::SystemTimeError),
#[error("Proposer failed to serialize block: {0}")]
FailedToRetrieveBlockFromStorage(String),
#[error("Proposer failed to encode state diff: {0}")]
FailedToEncodeStateDiff(#[from] StateDiffError),
#[error("Proposer failed to open Points file: {0}")]
FailedToOpenPointsFile(#[from] std::io::Error),
#[error("Proposer failed to re-execute block: {0}")]
FailedToReExecuteBlock(#[from] EvmError),
#[error("Proposer failed to make KZG operations: {0}")]
KZGError(#[from] c_kzg::Error),
}

#[derive(Debug, thiserror::Error)]
pub enum StateDiffError {
#[error("StateDiff failed to deserialize: {0}")]
FailedToDeserializeStateDiff(String),
#[error("StateDiff failed to serialize: {0}")]
FailedToSerializeStateDiff(String),
#[error("StateDiff failed to get config: {0}")]
FailedToGetConfig(#[from] ConfigError),
#[error("StateDiff invalid account state diff type: {0}")]
InvalidAccountStateDiffType(u8),
#[error("StateDiff unsupported version: {0}")]
UnsupportedVersion(u8),
#[error("Both bytecode and bytecode hash are set")]
BytecodeAndBytecodeHashSet,
#[error("Empty account diff")]
EmptyAccountDiff,
}
Loading
Loading