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(client): Oracle-backed derive traits #252

Merged
merged 2 commits into from
Jun 16, 2024
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
56 changes: 46 additions & 10 deletions bin/host/src/fetcher/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,14 +9,12 @@ use alloy_provider::{Provider, ReqwestProvider};
use alloy_rlp::Decodable;
use alloy_rpc_types::{Block, BlockTransactions};
use anyhow::{anyhow, Result};
use kona_client::HintType;
use kona_preimage::{PreimageKey, PreimageKeyType};
use std::sync::Arc;
use tokio::sync::RwLock;
use tracing::debug;

mod hint;
pub use hint::HintType;

mod precompiles;

/// The [Fetcher] struct is responsible for fetching preimages from a remote source.
Expand Down Expand Up @@ -191,7 +189,7 @@ where
anyhow::bail!("Invalid hint data length: {}", hint_data.len());
}

// Fetch the raw header from the L1 chain provider.
// Fetch the raw header from the L2 chain provider.
let hash: B256 = hint_data
.as_ref()
.try_into()
Expand All @@ -210,7 +208,43 @@ where
raw_header.into(),
);
}
HintType::L2Transactions => todo!(),
HintType::L2Transactions => {
// Validate the hint data length.
if hint_data.len() != 32 {
anyhow::bail!("Invalid hint data length: {}", hint_data.len());
}

// Fetch the block from the L2 chain provider and store the transactions within its
// body in the key-value store.
let hash: B256 = hint_data
.as_ref()
.try_into()
.map_err(|e| anyhow!("Failed to convert bytes to B256: {e}"))?;
let Block { transactions, .. } = self
.l2_provider
.get_block_by_hash(hash, false)
.await
.map_err(|e| anyhow!("Failed to fetch block: {e}"))?
.ok_or(anyhow!("Block not found."))?;

match transactions {
BlockTransactions::Hashes(transactions) => {
let mut encoded_transactions = Vec::with_capacity(transactions.len());
for tx_hash in transactions {
let tx = self
.l2_provider
.client()
.request::<&[B256; 1], Bytes>("debug_getRawTransaction", &[tx_hash])
.await
.map_err(|e| anyhow!("Error fetching transaction: {e}"))?;
encoded_transactions.push(tx);
}

self.store_trie_nodes(encoded_transactions.as_slice()).await?;
}
_ => anyhow::bail!("Only BlockTransactions::Hashes are supported."),
};
}
HintType::L2Code => {
// geth hashdb scheme code hash key prefix
const CODE_PREFIX: u8 = b'c';
Expand Down Expand Up @@ -277,11 +311,11 @@ where
.await
.map_err(|e| anyhow!("Failed to fetch account proof: {e}"))?;

let mut raw_output = [0u8; 97];
raw_output[0] = OUTPUT_ROOT_VERSION;
raw_output[1..33].copy_from_slice(header.state_root.as_ref());
raw_output[33..65].copy_from_slice(l2_to_l1_message_passer.storage_hash.as_ref());
raw_output[65..97].copy_from_slice(self.l2_head.as_ref());
let mut raw_output = [0u8; 128];
raw_output[31] = OUTPUT_ROOT_VERSION;
raw_output[32..64].copy_from_slice(header.state_root.as_ref());
raw_output[64..96].copy_from_slice(l2_to_l1_message_passer.storage_hash.as_ref());
raw_output[96..128].copy_from_slice(self.l2_head.as_ref());
let output_root = keccak256(raw_output);

let mut kv_write_lock = self.kv_store.write().await;
Expand Down Expand Up @@ -405,9 +439,11 @@ where
let mut hb = kona_mpt::ordered_trie_with_encoder(nodes, |node, buf| {
buf.put_slice(node.as_ref());
});
hb.root();
let intermediates = hb.take_proofs();

let mut kv_write_lock = self.kv_store.write().await;

for (_, value) in intermediates.into_iter() {
let value_hash = keccak256(value.as_ref());
let key = PreimageKey::new(*value_hash, PreimageKeyType::Keccak256);
Expand Down
3 changes: 2 additions & 1 deletion bin/host/src/util.rs
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
//! Contains utility functions and helpers for the host program.

use crate::{fetcher::HintType, types::NativePipeFiles};
use crate::types::NativePipeFiles;
use alloy_primitives::{hex, Bytes};
use alloy_provider::ReqwestProvider;
use alloy_rpc_client::RpcClient;
use alloy_transport_http::Http;
use anyhow::{anyhow, Result};
use kona_client::HintType;
use kona_common::FileDescriptor;
use kona_preimage::PipeHandle;
use reqwest::Client;
Expand Down
16 changes: 5 additions & 11 deletions bin/programs/client/src/comms/caching_oracle.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,33 +17,27 @@ use spin::Mutex;
///
/// [OracleReader]: kona_preimage::OracleReader
#[derive(Debug, Clone)]
pub struct CachingOracle<const N: usize> {
pub struct CachingOracle {
/// The spin-locked cache that stores the responses from the oracle.
cache: Arc<Mutex<LruCache<PreimageKey, Vec<u8>>>>,
}

impl<const N: usize> CachingOracle<N> {
impl CachingOracle {
/// Creates a new [CachingOracle] that wraps the given [OracleReader] and stores up to `N`
/// responses in the cache.
///
/// [OracleReader]: kona_preimage::OracleReader
pub fn new() -> Self {
pub fn new(cache_size: usize) -> Self {
Self {
cache: Arc::new(Mutex::new(LruCache::new(
NonZeroUsize::new(N).expect("N must be greater than 0"),
NonZeroUsize::new(cache_size).expect("N must be greater than 0"),
))),
}
}
}

impl<const N: usize> Default for CachingOracle<N> {
fn default() -> Self {
Self::new()
}
}

#[async_trait]
impl<const N: usize> PreimageOracleClient for CachingOracle<N> {
impl PreimageOracleClient for CachingOracle {
async fn get(&self, key: PreimageKey) -> Result<Vec<u8>> {
let mut cache_lock = self.cache.lock();
if let Some(value) = cache_lock.get(&key) {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
//! This module contains the [HintType] enum.

use std::fmt::Display;
use core::fmt::Display;

use alloc::{string::String, vec::Vec};
use alloy_primitives::hex;

/// The [HintType] enum is used to specify the type of hint that was received.
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
Expand Down Expand Up @@ -32,6 +35,14 @@ pub enum HintType {
L2AccountStorageProof,
}

impl HintType {
/// Encodes the hint type as a string.
pub fn encode_with(&self, data: &[&[u8]]) -> String {
let concatenated = data.iter().map(hex::encode).collect::<Vec<_>>().join(" ");
alloc::format!("{} {}", self, concatenated)
}
}

impl TryFrom<&str> for HintType {
type Error = anyhow::Error;

Expand Down Expand Up @@ -74,7 +85,7 @@ impl From<HintType> for &str {
}

impl Display for HintType {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
let s: &str = (*self).into();
write!(f, "{}", s)
}
Expand Down
141 changes: 141 additions & 0 deletions bin/programs/client/src/l1/chain_provider.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
//! Contains the concrete implementation of the [ChainProvider] trait for the client program.

use crate::{BootInfo, CachingOracle, HintType, HINT_WRITER};
use alloc::{boxed::Box, sync::Arc, vec::Vec};
use alloy_consensus::{Header, Receipt, ReceiptEnvelope, TxEnvelope};
use alloy_eips::eip2718::Decodable2718;
use alloy_primitives::{Bytes, B256};
use alloy_rlp::Decodable;
use anyhow::{anyhow, Result};
use async_trait::async_trait;
use kona_derive::traits::ChainProvider;
use kona_mpt::{OrderedListWalker, TrieDBFetcher};
use kona_preimage::{HintWriterClient, PreimageKey, PreimageKeyType, PreimageOracleClient};
use kona_primitives::BlockInfo;

/// The oracle-backed L1 chain provider for the client program.
#[derive(Debug)]
pub struct OracleL1ChainProvider {
/// The boot information
boot_info: Arc<BootInfo>,
/// The preimage oracle client.
oracle: Arc<CachingOracle>,
}

impl OracleL1ChainProvider {
/// Creates a new [OracleL1ChainProvider] with the given boot information and oracle client.
pub fn new(boot_info: Arc<BootInfo>, oracle: Arc<CachingOracle>) -> Self {
Self { boot_info, oracle }
}
}

#[async_trait]
impl ChainProvider for OracleL1ChainProvider {
async fn header_by_hash(&mut self, hash: B256) -> Result<Header> {
// Send a hint for the block header.
HINT_WRITER.write(&HintType::L1BlockHeader.encode_with(&[hash.as_ref()])).await?;

// Fetch the header RLP from the oracle.
let header_rlp =
self.oracle.get(PreimageKey::new(*hash, PreimageKeyType::Keccak256)).await?;

// Decode the header RLP into a Header.
Header::decode(&mut header_rlp.as_slice())
.map_err(|e| anyhow!("Failed to decode header RLP: {e}"))
}

async fn block_info_by_number(&mut self, block_number: u64) -> Result<BlockInfo> {
// Fetch the starting block header.
let mut header = self.header_by_hash(self.boot_info.l1_head).await?;

// Check if the block number is in range. If not, we can fail early.
if block_number > header.number {
anyhow::bail!("Block number past L1 head.");
}

// Walk back the block headers to the desired block number.
while header.number > block_number {
header = self.header_by_hash(header.parent_hash).await?;
}

Ok(BlockInfo {
hash: header.hash_slow(),
number: header.number,
parent_hash: header.parent_hash,
timestamp: header.timestamp,
})
}

async fn receipts_by_hash(&mut self, hash: B256) -> Result<Vec<Receipt>> {
// Fetch the block header to find the receipts root.
let header = self.header_by_hash(hash).await?;

// Send a hint for the block's receipts, and walk through the receipts trie in the header to
// verify them.
HINT_WRITER.write(&HintType::L1Receipts.encode_with(&[hash.as_ref()])).await?;
let trie_walker = OrderedListWalker::try_new_hydrated(header.receipts_root, self)?;

// Decode the receipts within the transactions trie.
let receipts = trie_walker
.into_iter()
.map(|(_, rlp)| {
let envelope = ReceiptEnvelope::decode_2718(&mut rlp.as_ref())
.map_err(|e| anyhow!("Failed to decode ReceiptEnvelope RLP: {e}"))?;
Ok(envelope.as_receipt().expect("Infalliable").clone())
})
.collect::<Result<Vec<_>>>()?;

Ok(receipts)
}

async fn block_info_and_transactions_by_hash(
&mut self,
hash: B256,
) -> Result<(BlockInfo, Vec<TxEnvelope>)> {
// Fetch the block header to construct the block info.
let header = self.header_by_hash(hash).await?;
let block_info = BlockInfo {
hash,
number: header.number,
parent_hash: header.parent_hash,
timestamp: header.timestamp,
};

// Send a hint for the block's transactions, and walk through the transactions trie in the
// header to verify them.
HINT_WRITER.write(&HintType::L1Transactions.encode_with(&[hash.as_ref()])).await?;
let trie_walker = OrderedListWalker::try_new_hydrated(header.transactions_root, self)?;

// Decode the transactions within the transactions trie.
let transactions = trie_walker
.into_iter()
.map(|(_, rlp)| {
TxEnvelope::decode_2718(&mut rlp.as_ref())
.map_err(|e| anyhow!("Failed to decode TxEnvelope RLP: {e}"))
})
.collect::<Result<Vec<_>>>()?;

Ok((block_info, transactions))
}
}

impl TrieDBFetcher for OracleL1ChainProvider {
fn trie_node_preimage(&self, key: B256) -> Result<Bytes> {
// On L1, trie node preimages are stored as keccak preimage types in the oracle. We assume
// that a hint for these preimages has already been sent, prior to this call.
kona_common::block_on(async move {
self.oracle
.get(PreimageKey::new(*key, PreimageKeyType::Keccak256))
.await
.map(Into::into)
})
}

fn bytecode_by_hash(&self, _: B256) -> Result<Bytes> {
unimplemented!("TrieDBFetcher::bytecode_by_hash unimplemented for OracleL1ChainProvider")
}

fn header_by_hash(&self, _: B256) -> Result<Header> {
unimplemented!("TrieDBFetcher::header_by_hash unimplemented for OracleL1ChainProvider")
}
}
4 changes: 4 additions & 0 deletions bin/programs/client/src/l1/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
//! Contains the L1 constructs of the client program.

mod chain_provider;
pub use chain_provider::OracleL1ChainProvider;
Loading
Loading