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

Proposer duties taken from CL #51

Merged
merged 7 commits into from
Jul 17, 2024
Merged
Show file tree
Hide file tree
Changes from 5 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
1,451 changes: 1,310 additions & 141 deletions Node/Cargo.lock

Large diffs are not rendered by default.

4 changes: 4 additions & 0 deletions Node/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -21,3 +21,7 @@ reqwest = "0.12"
hex = "0.4"
tiny-keccak = "2.0"
secp256k1 = "0.29"
beacon-api-client = { git = "https://github.com/ralexstokes/ethereum-consensus", package = "beacon-api-client" }

[dev-dependencies]
mockito = "1.4"
82 changes: 82 additions & 0 deletions Node/src/ethereum_l1/consensus_layer.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
#![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 })
}

// First iteration we get the next lookahead by checking the actual epoch number
// this will be improved so we keep synchronization with the CL
pub async fn get_latest_lookahead(&self) -> Result<Vec<ProposerDuty>, Error> {
let header = self.client.get_beacon_header_at_head().await?;
let slot = header.header.message.slot;
let epoch = slot / 32;
self.get_lookahead(epoch + 1).await
}

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 mut 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 mut 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
}
}
205 changes: 205 additions & 0 deletions Node/src/ethereum_l1/execution_layer.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,205 @@
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,
}

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,
) -> 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,
})
}

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),
mskrzypkows marked this conversation as resolved.
Show resolved Hide resolved
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 {
const SECONDS_PER_SLOT: u64 = 12;
mskrzypkows marked this conversation as resolved.
Show resolved Hide resolved
self.genesis_timestamp_sec + slot * SECONDS_PER_SLOT
}

#[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,
})
}

#[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();
}
}
Loading