-
Notifications
You must be signed in to change notification settings - Fork 8
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #51 from NethermindEth/proposer_duties
Proposer duties taken from CL
- Loading branch information
Showing
13 changed files
with
1,902 additions
and
407 deletions.
There are no files selected for viewing
Large diffs are not rendered by default.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,79 @@ | ||
#![allow(dead_code)] // TODO: remove | ||
use anyhow::Error; | ||
use beacon_api_client::{mainnet::MainnetClientTypes, Client, GenesisDetails, ProposerDuty}; | ||
use reqwest; | ||
|
||
pub struct ConsensusLayer { | ||
client: Client<MainnetClientTypes>, | ||
} | ||
|
||
impl ConsensusLayer { | ||
pub fn new(rpc_url: &str) -> Result<Self, Error> { | ||
let client = Client::new(reqwest::Url::parse(rpc_url)?); | ||
Ok(Self { client }) | ||
} | ||
|
||
pub async fn get_current_epoch(&self) -> Result<u64, Error> { | ||
let header = self.client.get_beacon_header_at_head().await?; | ||
let slot = header.header.message.slot; | ||
Ok(slot / 32) | ||
} | ||
|
||
pub async fn get_lookahead(&self, epoch: u64) -> Result<Vec<ProposerDuty>, Error> { | ||
let (_, duties) = self.client.get_proposer_duties(epoch).await?; | ||
Ok(duties) | ||
} | ||
|
||
pub async fn get_genesis_data(&self) -> Result<GenesisDetails, Error> { | ||
self.client.get_genesis_details().await.map_err(Error::new) | ||
} | ||
} | ||
|
||
#[cfg(test)] | ||
pub mod tests { | ||
use super::*; | ||
use tokio; | ||
|
||
#[tokio::test] | ||
async fn test_get_lookahead() { | ||
let server = setup_server().await; | ||
let cl = ConsensusLayer::new(server.url().as_str()).unwrap(); | ||
let duties = cl.get_lookahead(1).await.unwrap(); | ||
|
||
assert_eq!(duties.len(), 32); | ||
assert_eq!(duties[0].slot, 32); | ||
} | ||
|
||
#[tokio::test] | ||
async fn test_get_genesis_data() { | ||
let server = setup_server().await; | ||
let cl = ConsensusLayer::new(server.url().as_str()).unwrap(); | ||
let genesis_data = cl.get_genesis_data().await.unwrap(); | ||
|
||
assert_eq!(genesis_data.genesis_time, 1590832934); | ||
assert_eq!( | ||
genesis_data.genesis_validators_root.to_string(), | ||
"0xcf8e0d4e9587369b2301d0790347320302cc0943d5a1884560367e8208d920f2" | ||
); | ||
assert_eq!(genesis_data.genesis_fork_version, [0; 4]); | ||
} | ||
|
||
pub async fn setup_server() -> mockito::ServerGuard { | ||
let mut server = mockito::Server::new_async().await; | ||
server | ||
.mock("GET", "/eth/v1/beacon/genesis") | ||
.with_body(r#"{ | ||
"data": { | ||
"genesis_time": "1590832934", | ||
"genesis_validators_root": "0xcf8e0d4e9587369b2301d0790347320302cc0943d5a1884560367e8208d920f2", | ||
"genesis_fork_version": "0x00000000" | ||
} | ||
}"#) | ||
.create(); | ||
server | ||
.mock("GET", "/eth/v1/validator/duties/proposer/1") | ||
.with_body(include_str!("lookahead_test_response.json")) | ||
.create(); | ||
server | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,208 @@ | ||
use alloy::{ | ||
network::{Ethereum, EthereumWallet, NetworkWallet}, | ||
primitives::{Address, Bytes, FixedBytes, U256}, | ||
providers::ProviderBuilder, | ||
signers::local::PrivateKeySigner, | ||
sol, | ||
sol_types::SolValue, | ||
}; | ||
use anyhow::Error; | ||
use beacon_api_client::ProposerDuty; | ||
use std::str::FromStr; | ||
|
||
pub struct ExecutionLayer { | ||
rpc_url: reqwest::Url, | ||
wallet: EthereumWallet, | ||
taiko_preconfirming_address: Address, | ||
genesis_timestamp_sec: u64, | ||
slot_duration_sec: u64, | ||
} | ||
|
||
sol!( | ||
#[allow(missing_docs)] | ||
#[sol(rpc)] | ||
PreconfTaskManager, | ||
"src/ethereum_l1/abi/PreconfTaskManager.json" | ||
); | ||
|
||
sol! { | ||
/// @dev Hook and it's data (currently used only during proposeBlock) | ||
struct HookCall { | ||
address hook; | ||
bytes data; | ||
} | ||
|
||
/// @dev Represents proposeBlock's _data input parameter | ||
struct BlockParams { | ||
address assignedProver; // DEPRECATED, value ignored. | ||
address coinbase; | ||
bytes32 extraData; | ||
bytes32 parentMetaHash; | ||
HookCall[] hookCalls; // DEPRECATED, value ignored. | ||
bytes signature; | ||
uint32 l1StateBlockNumber; | ||
uint64 timestamp; | ||
} | ||
} | ||
|
||
impl ExecutionLayer { | ||
pub fn new( | ||
rpc_url: &str, | ||
private_key: &str, | ||
taiko_preconfirming_address: &str, | ||
genesis_timestamp_sec: u64, | ||
slot_duration_sec: u64, | ||
) -> Result<Self, Error> { | ||
let signer = PrivateKeySigner::from_str(private_key)?; | ||
let wallet = EthereumWallet::from(signer); | ||
|
||
Ok(Self { | ||
rpc_url: rpc_url.parse()?, | ||
wallet, | ||
taiko_preconfirming_address: taiko_preconfirming_address.parse()?, | ||
genesis_timestamp_sec, | ||
slot_duration_sec, | ||
}) | ||
} | ||
|
||
pub async fn propose_new_block( | ||
&self, | ||
tx_list: Vec<u8>, | ||
parent_meta_hash: [u8; 32], | ||
lookahead_set: Vec<ProposerDuty>, | ||
) -> Result<(), Error> { | ||
let provider = ProviderBuilder::new() | ||
.with_recommended_fillers() | ||
.wallet(self.wallet.clone()) | ||
.on_http(self.rpc_url.clone()); | ||
|
||
let contract = PreconfTaskManager::new(self.taiko_preconfirming_address, provider); | ||
|
||
let block_params = BlockParams { | ||
assignedProver: Address::ZERO, | ||
coinbase: <EthereumWallet as NetworkWallet<Ethereum>>::default_signer_address( | ||
&self.wallet, | ||
), | ||
extraData: FixedBytes::from(&[0u8; 32]), | ||
parentMetaHash: FixedBytes::from(&parent_meta_hash), | ||
hookCalls: vec![], | ||
signature: Bytes::from(vec![0; 32]), | ||
l1StateBlockNumber: 0, | ||
timestamp: 0, | ||
}; | ||
|
||
let encoded_block_params = Bytes::from(BlockParams::abi_encode_sequence(&block_params)); | ||
|
||
let tx_list = Bytes::from(tx_list); | ||
let lookahead_set_param: Vec<PreconfTaskManager::LookaheadSetParam> = lookahead_set | ||
.iter() | ||
.map(|duty| PreconfTaskManager::LookaheadSetParam { | ||
timestamp: U256::from(self.calculate_slot_timestamp(duty.slot)), | ||
preconfer: Address::ZERO, //TODO: Replace it with a BLS key when the contract is ready. | ||
}) | ||
.collect(); | ||
|
||
let builder = contract.newBlockProposal( | ||
encoded_block_params, | ||
tx_list, | ||
U256::from(0), //TODO: Replace it with the proper lookaheadPointer when the contract is ready. | ||
lookahead_set_param, | ||
); | ||
|
||
let tx_hash = builder.send().await?.watch().await?; | ||
tracing::debug!("Proposed new block: {tx_hash}"); | ||
|
||
Ok(()) | ||
} | ||
|
||
fn calculate_slot_timestamp(&self, slot: u64) -> u64 { | ||
self.genesis_timestamp_sec + slot * self.slot_duration_sec | ||
} | ||
|
||
#[cfg(test)] | ||
pub fn new_from_pk( | ||
rpc_url: reqwest::Url, | ||
private_key: elliptic_curve::SecretKey<k256::Secp256k1>, | ||
) -> Result<Self, Error> { | ||
let signer = PrivateKeySigner::from_signing_key(private_key.into()); | ||
let wallet = EthereumWallet::from(signer); | ||
|
||
Ok(Self { | ||
rpc_url, | ||
wallet, | ||
taiko_preconfirming_address: "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2" // some random address for test | ||
.parse()?, | ||
genesis_timestamp_sec: 0, | ||
slot_duration_sec: 12, | ||
}) | ||
} | ||
|
||
#[cfg(test)] | ||
async fn call_test_contract(&self) -> Result<(), Error> { | ||
sol! { | ||
#[allow(missing_docs)] | ||
#[sol(rpc, bytecode="6080806040523460135760df908160198239f35b600080fdfe6080806040526004361015601257600080fd5b60003560e01c9081633fb5c1cb1460925781638381f58a146079575063d09de08a14603c57600080fd5b3460745760003660031901126074576000546000198114605e57600101600055005b634e487b7160e01b600052601160045260246000fd5b600080fd5b3460745760003660031901126074576020906000548152f35b34607457602036600319011260745760043560005500fea2646970667358221220e978270883b7baed10810c4079c941512e93a7ba1cd1108c781d4bc738d9090564736f6c634300081a0033")] | ||
contract Counter { | ||
uint256 public number; | ||
|
||
function setNumber(uint256 newNumber) public { | ||
number = newNumber; | ||
} | ||
|
||
function increment() public { | ||
number++; | ||
} | ||
} | ||
} | ||
|
||
let provider = ProviderBuilder::new() | ||
.with_recommended_fillers() | ||
.wallet(self.wallet.clone()) | ||
.on_http(self.rpc_url.clone()); | ||
|
||
let contract = Counter::deploy(&provider).await?; | ||
|
||
let builder = contract.setNumber(U256::from(42)); | ||
let tx_hash = builder.send().await?.watch().await?; | ||
println!("Set number to 42: {tx_hash}"); | ||
|
||
let builder = contract.increment(); | ||
let tx_hash = builder.send().await?.watch().await?; | ||
println!("Incremented number: {tx_hash}"); | ||
|
||
let builder = contract.number(); | ||
let number = builder.call().await?.number.to_string(); | ||
|
||
assert_eq!(number, "43"); | ||
|
||
Ok(()) | ||
} | ||
} | ||
|
||
#[cfg(test)] | ||
mod tests { | ||
use super::*; | ||
use alloy::node_bindings::Anvil; | ||
|
||
#[tokio::test] | ||
async fn test_call_contract() { | ||
// Ensure `anvil` is available in $PATH. | ||
let anvil = Anvil::new().try_spawn().unwrap(); | ||
let rpc_url: reqwest::Url = anvil.endpoint().parse().unwrap(); | ||
let private_key = anvil.keys()[0].clone(); | ||
let el = ExecutionLayer::new_from_pk(rpc_url, private_key).unwrap(); | ||
el.call_test_contract().await.unwrap(); | ||
} | ||
|
||
#[tokio::test] | ||
async fn test_propose_new_block() { | ||
let anvil = Anvil::new().try_spawn().unwrap(); | ||
let rpc_url: reqwest::Url = anvil.endpoint().parse().unwrap(); | ||
let private_key = anvil.keys()[0].clone(); | ||
let el = ExecutionLayer::new_from_pk(rpc_url, private_key).unwrap(); | ||
|
||
el.propose_new_block(vec![0; 32], [0; 32], vec![]) | ||
.await | ||
.unwrap(); | ||
} | ||
} |
Oops, something went wrong.