yevm-core 0.1.0

Async, WASM-native Ethereum VM engine: opcode dispatch, gas accounting, precompiles, state traits, structured trace events.
Documentation
use serde::{Deserialize, Serialize};
use yevm_base::{Acc, Int};
use yevm_misc::{buf::Buf, hex::Hex, keccak256};

#[derive(Clone, Debug, Deserialize, Serialize)]
pub struct Call {
    pub by: Acc,
    #[serde(default)]
    #[serde(skip_serializing_if = "Option::is_none")]
    pub to: Option<Acc>,
    pub gas: u64,
    pub eth: Int,
    pub data: Buf,
}

impl Call {
    pub fn is_create(&self) -> bool {
        self.to.is_none()
    }

    pub fn builder() -> Builder {
        Builder::default()
    }
}

#[derive(Clone, Debug, Default)]
pub struct Builder {
    by: Option<Acc>,
    to: Option<Acc>,
    gas: Option<u64>,
    eth: Option<Int>,
    data: Buf,
}

impl Builder {
    pub fn build(self) -> Call {
        Call {
            by: self.by.unwrap_or_default(),
            to: self.to,
            gas: self.gas.unwrap_or_default(),
            eth: self.eth.unwrap_or_default(),
            data: self.data,
        }
    }

    pub fn call(self, abi: &str, args: &[&[u8]]) -> Self {
        let len = 4 + args.iter().map(|arg| arg.len()).sum::<usize>();
        let selector = keccak256(abi.as_bytes());
        let selector = &selector.as_ref()[..4];
        let mut vec = Vec::with_capacity(len);
        vec.extend_from_slice(selector);
        for arg in args {
            vec.extend_from_slice(arg);
        }
        Self {
            data: vec.into(),
            ..self
        }
    }

    pub fn create(self, mut code: Vec<u8>, args: &[&[u8]]) -> Self {
        let len = args.iter().map(|arg| arg.len()).sum::<usize>();
        code.reserve(len);
        for arg in args {
            code.extend_from_slice(arg);
        }
        Self {
            data: code.into(),
            ..self
        }
    }

    pub fn by(self, by: Acc) -> Self {
        Self {
            by: Some(by),
            ..self
        }
    }

    pub fn to(self, to: Acc) -> Self {
        Self {
            to: Some(to),
            ..self
        }
    }

    pub fn gas(self, gas: u64) -> Self {
        Self {
            gas: Some(gas),
            ..self
        }
    }

    pub fn eth(self, eth: Int) -> Self {
        Self {
            eth: Some(eth),
            ..self
        }
    }
}

#[derive(Clone, Debug, Default, Deserialize, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct Head {
    pub number: Hex<8>,
    pub hash: Int,
    pub gas_limit: Int,
    #[serde(alias = "miner")]
    pub coinbase: Acc,
    pub timestamp: Int,
    #[serde(alias = "baseFeePerGas")]
    pub base_fee: Int,
    #[serde(default)]
    #[serde(skip_serializing_if = "Option::is_none")]
    pub excess_blob_gas: Option<Int>,
    #[serde(default)]
    #[serde(skip_serializing_if = "Option::is_none")]
    pub blobhash: Option<Int>,
    #[serde(alias = "mixHash")]
    #[serde(alias = "prevRandao")]
    pub prevrandao: Int,
    pub parent_hash: Int,
    #[serde(default)]
    #[serde(skip_serializing_if = "Option::is_none")]
    pub parent_beacon_block_root: Option<Int>,
}

#[derive(Clone, Debug, Deserialize, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct Tx {
    #[serde(default)]
    #[serde(skip_serializing_if = "Hex::is_zero")]
    pub chain_id: Hex<8>,
    pub nonce: Int,
    pub gas_price: Int,
    #[serde(default)]
    #[serde(skip_serializing_if = "Int::is_zero")]
    pub max_fee_per_gas: Int,
    #[serde(default)]
    #[serde(skip_serializing_if = "Int::is_zero")]
    pub max_priority_fee_per_gas: Int,
    #[serde(default)]
    #[serde(skip_serializing_if = "Vec::is_empty")]
    pub access_list: Vec<AccessListItem>,
    #[serde(default)]
    #[serde(skip_serializing_if = "Vec::is_empty")]
    pub authorization_list: Vec<AuthorizationListItem>,
    #[serde(default)]
    #[serde(skip_serializing_if = "Vec::is_empty")]
    pub blob_versioned_hashes: Vec<Int>,
    #[serde(default)]
    #[serde(skip_serializing_if = "Option::is_none")]
    pub max_fee_per_blob_gas: Option<Int>,
    pub hash: Int,
    #[serde(rename = "transactionIndex")]
    pub index: Int,
}

#[derive(Clone, Debug, Deserialize, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct AccessListItem {
    pub address: Acc,
    pub storage_keys: Vec<Int>,
}

#[derive(Clone, Debug, Deserialize, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct AuthorizationListItem {
    pub address: Acc,
    pub chain_id: Int,
    pub nonce: Int,
    pub r: Int,
    pub s: Int,
    pub y_parity: Int,
}

#[derive(Clone, Debug, Deserialize, Serialize)]
pub struct TxCall {
    #[serde(default)]
    #[serde(skip_serializing_if = "Option::is_none")]
    pub to: Option<Acc>,
    pub from: Acc,
    pub input: Buf,
    pub value: Int,
    pub gas: Int,
}

impl From<TxCall> for Call {
    fn from(call: TxCall) -> Self {
        Call {
            by: call.from,
            to: call.to,
            gas: call.gas.as_u64(),
            eth: call.value,
            data: call.input,
        }
    }
}

#[derive(Clone, Debug, Deserialize, Serialize)]
pub struct TxFull {
    #[serde(flatten)]
    pub call: TxCall,
    #[serde(flatten)]
    pub tx: Tx,
}

pub type TxHash = Int;

#[derive(Clone, Debug, Deserialize, Serialize)]
pub struct Block {
    #[serde(flatten)]
    pub head: Head,
    #[serde(rename = "transactions")]
    pub txs: Vec<TxFull>,
}

#[derive(Clone, Debug, Deserialize, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct Receipt {
    pub block_hash: Int,
    pub block_number: Int,
    #[serde(default)]
    #[serde(skip_serializing_if = "Option::is_none")]
    pub contract_address: Option<Acc>,
    pub cumulative_gas_used: Int,
    pub effective_gas_price: Int,
    pub gas_used: Int,
    pub status: Int,
    #[serde(default)]
    #[serde(skip_serializing_if = "Option::is_none")]
    pub to: Option<Acc>,
    pub r#type: Int,
    pub transaction_hash: Int,
    pub transaction_index: Int,
    pub logs: Vec<Logged>,
    pub logs_bloom: Buf,
}

#[derive(Clone, Debug, Deserialize, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct Logged {
    pub address: Acc,
    pub block_hash: Int,
    pub block_number: Int,
    #[serde(default)]
    #[serde(skip_serializing_if = "Option::is_none")]
    pub block_timestamp: Option<Int>,
    pub data: Buf,
    pub log_index: Int,
    pub removed: bool,
    pub topics: Vec<Int>,
    pub transaction_hash: Int,
    pub transaction_index: Int,
}

// TODO: proper blob handling
/*
/// EIP-4844 BLOBBASEFEE calculation (fake_exponential)
/// (see: https://eips.ethereum.org/EIPS/eip-4844)
pub fn blob_base_fee(excess_blob_gas: u64) -> u128 {
    let d = 5_007_716u128;
    let mut i = 1u64;
    let mut out = 0;
    let mut acc = d;
    while acc > 0 {
        out += acc;
        acc = (acc * excess_blob_gas) / (d * i);
        i += 1;
    }
    out / d
}
*/

#[cfg(test)]
mod tests {
    use super::*;
    #[test]
    fn test_block() {
        const JSON: &str = r#"{
                "baseFeePerGas": "0x246edf3",
                "blobGasUsed": "0x40000",
                "difficulty": "0x0",
                "excessBlobGas": "0xa2a720e",
                "extraData": "0x4275696c6465724e6574202842656176657229",
                "gasLimit": "0x3938700",
                "gasUsed": "0x1a4ed69",
                "hash": "0x6c4c6ea047f0ca4bcb0b0fbbcc2e50061d8dd24ba1ab8a22a6d3583a497c3c82",
                "logsBloom": "0xfffffef97ffdf7dfb75fbbfeffffdfffffbeefffffbbffff6ffffdffbffffaf9df7fff7f7fff6ffbffffdfdffbffffff76fffffcbf7ffbfffffeefffffff7df7bffffffdefffff8fdfdf7f7ceffdffff7ff77affe7ef5ff5fbfff57ff7eeadfffffffabfff7bdffbbfbffcfffdefffeffefffffffff5fffefdffdffbffffdfdffbffe7fffffffffdffebf7e37fdfe977f7fbdfefdffddffffffffff5f97ffffdfbffdf7fffffbff9feff77afffefffeef77ffff7fff6fffeffd77ffffeefefdb7bffffff7f7ffdfbff7ffffffffefdffdffddffffffefffffffefdfbf7fdffef7fffff3bdf7fff6cefffdfffdf7ffffeffbfbffffdafffdfbf7efdbeffff7fff",
                "miner": "0xdadb0d80178819f2319190d340ce9a924f783711",
                "mixHash": "0xf824cbc0bffaa4862fffafa96c2cce90898dda8b97fc3eaa6a85ac38075991e5",
                "nonce": "0x0000000000000000",
                "number": "0x177d354",
                "parentBeaconBlockRoot": "0xd9b957562f8477151f61eddc77bcad7451ef62eab8f4fe470402073a794e8bba",
                "parentHash": "0x63ce16602b2afe4089fa8e0a2c16be1a2dea1c4d30b65bd717d9398ddd5fba23",
                "receiptsRoot": "0x52fc280ab6b1db61781dae4cc5f81b7ddd484eae78320bdd8161d5b2fe8e7719",
                "requestsHash": "0xe3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
                "sha3Uncles": "0x1dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d49347",
                "size": "0x2a8a9",
                "stateRoot": "0x41ebb402f36d0ce15799e98d1aca83bbb5d4b08d1b6572e73b388e4ece192916",
                "timestamp": "0x69b09d6b",
                "transactions": [
                    {
                        "accessList": [
                            {
                                "address": "0xf6e72db5454dd049d0788e411b06cfaf16853042",
                                "storageKeys": [
                                    "0x0000000000000000000000000000000000000000000000000000000000000004"
                                ]
                            }
                        ],
                        "authorizationList": [
                            {
                                "address": "0x66e9d4bc80c1d04d045573c2f4631342fe6fc87d",
                                "chainId": "0x1",
                                "nonce": "0xa28",
                                "r": "0xedba2225e5a2a8958d7530ef3fdd941ba5996c8a92ddb60d649f1ccabb9d686d",
                                "s": "0x2a058bc60a7674c234739fefcbcf40bf846a7c4cf89ed850b86cb871033e7502",
                                "yParity": "0x0"
                            }
                        ],
                        "blockHash": "0x1f5b98ca47015bbfa9bff1b6282fd7753a0b79053876677b22ac178df53d76c7",
                        "blockNumber": "0x177e232",
                        "blockTimestamp": "0x69b1509f",
                        "chainId": "0x1",
                        "from": "0xdadb0d80178819f2319190d340ce9a924f783711",
                        "gas": "0x5208",
                        "gasPrice": "0x4b2f976",
                        "hash": "0xb478179e8e7f2fadfd42103007e9dfb5ea882667b1dd6e65142ef6b2a54b5668",
                        "input": "0x",
                        "maxFeePerGas": "0x4b2f976",
                        "maxPriorityFeePerGas": "0x0",
                        "nonce": "0x22792c",
                        "r": "0x51a8855fd9176012c328891ee0027959fd6480ef7ca09a2aac2e287c1c311566",
                        "s": "0x3e4b0c0cf854f42a0999c2ce6a9dc88b16f1b5f4e201dd61a36ed3a769719e6c",
                        "to": "0x08ec37e2eb451ab6fb29fc14d215b0aeef170040",
                        "transactionIndex": "0x201",
                        "type": "0x2",
                        "v": "0x1",
                        "value": "0x1f92fa85e3ea2a",
                        "yParity": "0x1"
                    }
                ],
                "transactionsRoot": "0x3b889eb9fa0713d2e1d9e90725d45fd04ec4a550a7a390639c243c5f3b2c6215",
                "uncles": [],
                "withdrawals": [
                    {
                    "address": "0xb9d7934878b5fb9610b3fe8a5e441e8fad7e293f",
                    "amount": "0x344741",
                    "index": "0x73e482b",
                    "validatorIndex": "0x21e64c"
                    }
                ],
                "withdrawalsRoot": "0x76abcbef0846a5bd065d0207581dacc3cf8df4ec2ba0a8e4d4fbbe97864b82ee"
            }"#;
        assert!(serde_json::from_str::<Block>(JSON).is_ok());
    }

    #[test]
    fn test_receipt() {
        const JSON: &str = r#"  {
            "blockHash": "0xe80afa4e302440b3f13974819d24027d1c5c7663a9688c5f03caa685660a28c7",
            "blockNumber": "0x178f9f8",
            "contractAddress": null,
            "cumulativeGasUsed": "0x216cc",
            "effectiveGasPrice": "0x1ef66a0",
            "from": "0xe4613af9ecdf8db83707546af9d4bd9da76ce504",
            "gasUsed": "0x216cc",
            "logs": [
            {
                "address": "0xe0f63a424a4439cbe457d80e4f4b51ad25b2c56c",
                "blockHash": "0xe80afa4e302440b3f13974819d24027d1c5c7663a9688c5f03caa685660a28c7",
                "blockNumber": "0x178f9f8",
                "blockTimestamp": "0x69be7e43",
                "data": "0x000000000000000000000000000000000000000000000000000000261a04825d",
                "logIndex": "0x0",
                "removed": false,
                "topics": [
                "0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef",
                "0x00000000000000000000000051c72848c68a965f66fa7a88855f9f7784502a7f",
                "0x00000000000000000000000052c77b0cb827afbad022e6d6caf2c44452edbc39"
                ],
                "transactionHash": "0xd3aafbde18d85a863399c94ffec80af928bb3ebecc3685f1c784245deff04c04",
                "transactionIndex": "0x0"
            }
            ],
            "logsBloom": "0x002000000000000002000000800000010000000000000000000000000000000000000000000000000000000000000000020000000800000000000400000000000000000000000000000000080000002000000000000400000000000000000000000000000000000000000000000000000000000000000000000000100000000000000040000000000000000000000000000000000000000c0000004000000000000000000000000000000000000000004000000000000000000000000000000000000002000000000000000000000000000000000400001000000000000000000000200000000000000000000000000000400000000000000000000040006000",
            "status": "0x1",
            "to": "0x51c72848c68a965f66fa7a88855f9f7784502a7f",
            "transactionHash": "0xd3aafbde18d85a863399c94ffec80af928bb3ebecc3685f1c784245deff04c04",
            "transactionIndex": "0x0",
            "type": "0x2"
        }"#;
        assert!(serde_json::from_str::<Receipt>(JSON).is_ok());
    }
}