Skip to content

Commit

Permalink
feat(l2): deposit verification (#1000)
Browse files Browse the repository at this point in the history
**Motivation**

<!-- Why does this pull request exist? What are its goals? -->
Deposits information must be saved on L1 until it gets verified so we
can trust the proposer don't put any invalid mint transaction.

**Description**

<!-- A clear and concise general description of the changes this PR
introduces -->
When send the commit, the proposer will also send the deposit logs, a
hash with format `N || keccak(d1 || d2 || ... || dN)[2..32]`, with `N`
being the number of deposits executed encoded in 2 bytes, and `dX =
keccack(to, value)` of the transaction `X`. That info is stored on an
array until a `verify` call removes it.

<!-- Link to issues: Resolves #111, Resolves #222 -->

Closes #977
  • Loading branch information
ManuelBilbao authored Oct 29, 2024
1 parent f92b378 commit 08f5a7c
Show file tree
Hide file tree
Showing 9 changed files with 122 additions and 21 deletions.
2 changes: 1 addition & 1 deletion cmd/ethereum_rust_l2/src/commands/wallet.rs
Original file line number Diff line number Diff line change
Expand Up @@ -354,7 +354,7 @@ impl Command {
// .estimate_gas(transfer_transaction.clone())
// .await?;

transfer_transaction.gas_limit = 21000 * 2;
transfer_transaction.gas_limit = 21000 * 5;

let tx_hash = if l1 {
eth_client
Expand Down
19 changes: 19 additions & 0 deletions crates/l2/contracts/src/l1/CommonBridge.sol
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@ contract CommonBridge is ICommonBridge, Ownable, ReentrancyGuard {
/// that the logs were published on L1, and that that block was committed.
mapping(uint256 => bytes32) public blockWithdrawalsLogs;

bytes32[] public depositLogs;

address public ON_CHAIN_PROPOSER;

modifier onlyOnChainProposer() {
Expand Down Expand Up @@ -59,13 +61,30 @@ contract CommonBridge is ICommonBridge, Ownable, ReentrancyGuard {

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

receive() external payable {
deposit(msg.sender);
}

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

for (uint i = 0; i < depositLogs.length - number; i++) {
depositLogs[i] = depositLogs[i + number];
}

for (uint _i = 0; _i < number; _i++) {
depositLogs.pop();
}
}

/// @inheritdoc ICommonBridge
function publishWithdrawals(
uint256 withdrawalLogsBlockNumber,
Expand Down
35 changes: 29 additions & 6 deletions crates/l2/contracts/src/l1/OnChainProposer.sol
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,16 @@ import {ICommonBridge} from "./interfaces/ICommonBridge.sol";
/// @title OnChainProposer contract.
/// @author LambdaClass
contract OnChainProposer is IOnChainProposer, ReentrancyGuard {
struct BlockCommitmentInfo {
bytes32 commitmentHash;
bytes32 depositLogs;
}

/// @notice The commitments of the committed blocks.
/// @dev If a block is committed, the commitment is stored here.
/// @dev If a block was not committed yet, it won't be here.
/// @dev It is used by other contracts to verify if a block was committed.
mapping(uint256 => bytes32) public blockCommitments;
mapping(uint256 => BlockCommitmentInfo) public blockCommitments;

/// @notice The verified blocks.
/// @dev If a block is verified, the block hash is stored here.
Expand Down Expand Up @@ -45,20 +50,29 @@ contract OnChainProposer is IOnChainProposer, ReentrancyGuard {
function commit(
uint256 blockNumber,
bytes32 newL2StateRoot,
bytes32 withdrawalsLogsMerkleRoot
bytes32 withdrawalsLogsMerkleRoot,
bytes32 depositLogs
) external override {
require(
!verifiedBlocks[blockNumber],
"OnChainProposer: block already verified"
);
require(
blockCommitments[blockNumber] == bytes32(0),
blockCommitments[blockNumber].commitmentHash == bytes32(0),
"OnChainProposer: block already committed"
);
bytes32 blockCommitment = keccak256(
abi.encode(blockNumber, newL2StateRoot, withdrawalsLogsMerkleRoot)
abi.encode(
blockNumber,
newL2StateRoot,
withdrawalsLogsMerkleRoot,
depositLogs
)
);
blockCommitments[blockNumber] = BlockCommitmentInfo(
blockCommitment,
depositLogs
);
blockCommitments[blockNumber] = blockCommitment;
if (withdrawalsLogsMerkleRoot != bytes32(0)) {
ICommonBridge(BRIDGE).publishWithdrawals(
blockNumber,
Expand All @@ -74,14 +88,23 @@ contract OnChainProposer is IOnChainProposer, ReentrancyGuard {
bytes calldata // blockProof
) external override {
require(
blockCommitments[blockNumber] != bytes32(0),
blockCommitments[blockNumber].commitmentHash != bytes32(0),
"OnChainProposer: block not committed"
);
require(
!verifiedBlocks[blockNumber],
"OnChainProposer: block already verified"
);

verifiedBlocks[blockNumber] = true;
ICommonBridge(BRIDGE).removeDepositLogs(
// The first 2 bytes are the number of deposits.
uint16(uint256(blockCommitments[blockNumber].depositLogs >> 240))
);

// Remove previous block commitment as it is no longer needed.
delete blockCommitments[blockNumber - 1];

emit BlockVerified(blockNumber);
}
}
8 changes: 8 additions & 0 deletions crates/l2/contracts/src/l1/interfaces/ICommonBridge.sol
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,14 @@ interface ICommonBridge {
/// @param to, the address in L2 to which the tokens will be minted to.
function deposit(address to) external payable;

/// @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;

/// @notice Publishes the L2 withdrawals on L1.
/// @dev This method is used by the L2 OnChainOperator to publish the L2
/// withdrawals when an L2 block is committed.
Expand Down
3 changes: 2 additions & 1 deletion crates/l2/contracts/src/l1/interfaces/IOnChainProposer.sol
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,8 @@ interface IOnChainProposer {
function commit(
uint256 blockNumber,
bytes32 newL2StateRoot,
bytes32 withdrawalsLogsMerkleRoot
bytes32 withdrawalsLogsMerkleRoot,
bytes32 depositLogs
) external;

/// @notice Method used to verify an L2 block proof.
Expand Down
6 changes: 5 additions & 1 deletion crates/l2/docs/state_diffs.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,8 @@ The full state diff sent on every block will then be a sequence of bytes encoded
- Note that values `8` and `16` are mutually exclusive, and if `type` is greater or equal to `4`, then the address is a contract. Each address can only appear once in the list.
- 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)`.
- 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 @@ -46,14 +48,16 @@ version_header_u8 ||
// Modified Accounts
number_of_modified_accounts_u16 ||
(
type_u8 || address_u20 || [balance_u256] || [nonce_increase_u16] ||
type_u8 || address_u160 || [balance_u256] || [nonce_increase_u16] ||
[number_of_modified_storage_slots_u16 || (key_u256 || value_u256)... ] ||
[bytecode_len_u16 || bytecode ...] ||
[code_hash_u256]
)...
// Withdraw Logs
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]
```

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:
Expand Down
26 changes: 18 additions & 8 deletions crates/l2/proposer/l1_watcher.rs
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ pub struct L1Watcher {
max_block_step: U256,
last_block_fetched: U256,
l2_proposer_pk: SecretKey,
l2_proposer_address: Address,
}

impl L1Watcher {
Expand All @@ -52,6 +53,7 @@ impl L1Watcher {
max_block_step: watcher_config.max_block_step,
last_block_fetched: U256::zero(),
l2_proposer_pk: watcher_config.l2_proposer_private_key,
l2_proposer_address: watcher_config.l2_proposer_address,
}
}

Expand Down Expand Up @@ -94,7 +96,20 @@ impl L1Watcher {
logs: Vec<RpcLog>,
store: &Store,
) -> Result<Vec<H256>, L1WatcherError> {
if logs.is_empty() {
return Ok(Vec::new());
}

let mut deposit_txs = Vec::new();
let mut operator_nonce = store
.get_account_info(
self.eth_client.get_block_number().await?.as_u64(),
self.l2_proposer_address,
)
.map_err(|e| L1WatcherError::FailedToRetrieveDepositorAccountInfo(e.to_string()))?
.map(|info| info.nonce)
.unwrap_or_default();

for log in logs {
let mint_value = format!("{:#x}", log.log.topics[1])
.parse::<U256>()
Expand Down Expand Up @@ -123,14 +138,9 @@ impl L1Watcher {
..Default::default()
};

mint_transaction.nonce = store
.get_account_info(
self.eth_client.get_block_number().await?.as_u64(),
beneficiary,
)
.map_err(|e| L1WatcherError::FailedToRetrieveDepositorAccountInfo(e.to_string()))?
.map(|info| info.nonce)
.unwrap_or_default();
mint_transaction.nonce = operator_nonce;
operator_nonce += 1;

mint_transaction.max_fee_per_gas = self.eth_client.get_gas_price().await?.as_u64();
// TODO(IMPORTANT): gas_limit should come in the log and must
// not be calculated in here. The reason for this is that the
Expand Down
43 changes: 39 additions & 4 deletions crates/l2/proposer/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ use bytes::Bytes;
use errors::ProposerError;
use ethereum_rust_blockchain::constants::TX_GAS_COST;
use ethereum_rust_core::types::{
Block, EIP1559Transaction, GenericTransaction, Transaction, TxKind,
Block, EIP1559Transaction, GenericTransaction, PrivilegedTxType, Transaction, TxKind,
};
use ethereum_rust_dev::utils::engine_client::{config::EngineApiConfig, EngineClient};
use ethereum_rust_rlp::encode::RLPEncode;
Expand All @@ -25,7 +25,7 @@ pub mod prover_server;

pub mod errors;

const COMMIT_FUNCTION_SELECTOR: [u8; 4] = [28, 217, 139, 206];
const COMMIT_FUNCTION_SELECTOR: [u8; 4] = [132, 97, 12, 179];
const VERIFY_FUNCTION_SELECTOR: [u8; 4] = [133, 133, 44, 228];

pub struct Proposer {
Expand Down Expand Up @@ -122,6 +122,38 @@ impl Proposer {
H256::zero()
};

let deposit_hashes: Vec<[u8; 32]> = block
.body
.transactions
.iter()
.filter_map(|tx| match tx {
Transaction::PrivilegedL2Transaction(tx)
if tx.tx_type == PrivilegedTxType::Deposit =>
{
let to = match tx.to {
TxKind::Call(to) => to,
TxKind::Create => Address::zero(),
};
let value_bytes = &mut [0u8; 32];
tx.value.to_big_endian(value_bytes);
Some(keccak([H256::from(to).0, H256::from(value_bytes).0].concat()).0)
}
_ => None,
})
.collect();
let deposit_logs_hash = if deposit_hashes.is_empty() {
H256::zero()
} else {
H256::from_slice(
[
&(deposit_hashes.len() as u16).to_be_bytes(),
&keccak(deposit_hashes.concat()).0[2..32],
]
.concat()
.as_slice(),
)
};

let new_state_root_hash = store
.state_trie(block.header.compute_block_hash())
.unwrap()
Expand All @@ -134,6 +166,7 @@ impl Proposer {
block.header.number,
new_state_root_hash,
withdrawals_logs_merkle_root,
deposit_logs_hash,
)
.await
{
Expand Down Expand Up @@ -243,15 +276,17 @@ impl Proposer {
block_number: u64,
new_l2_state_root: H256,
withdrawal_logs_merkle_root: H256,
deposit_logs_hash: H256,
) -> Result<H256, ProposerError> {
info!("Sending commitment");
let mut calldata = Vec::with_capacity(68);
let mut calldata = Vec::with_capacity(132);
calldata.extend(COMMIT_FUNCTION_SELECTOR);
let mut block_number_bytes = [0_u8; 32];
U256::from(block_number).to_big_endian(&mut block_number_bytes);
calldata.extend(block_number_bytes);
calldata.extend(new_l2_state_root.0);
calldata.extend(withdrawal_logs_merkle_root.0);
calldata.extend(deposit_logs_hash.0);

let commit_tx_hash = self
.send_transaction_with_calldata(self.on_chain_proposer_address, calldata.into())
Expand Down Expand Up @@ -285,7 +320,7 @@ impl Proposer {
calldata.extend(H256::from_low_u64_be(32).as_bytes());
calldata.extend(H256::from_low_u64_be(block_proof.len() as u64).as_bytes());
calldata.extend(block_proof);
let leading_zeros = 32 - (calldata.len() % 32);
let leading_zeros = 32 - ((calldata.len() - 4) % 32);
calldata.extend(vec![0; leading_zeros]);

let verify_tx_hash = self
Expand Down
1 change: 1 addition & 0 deletions crates/l2/utils/config/l1_watcher.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ pub struct L1WatcherConfig {
pub max_block_step: U256,
#[serde(deserialize_with = "secret_key_deserializer")]
pub l2_proposer_private_key: SecretKey,
pub l2_proposer_address: Address,
}

impl L1WatcherConfig {
Expand Down

0 comments on commit 08f5a7c

Please sign in to comment.