ethportal-api 0.1.3

Definitions for various Ethereum Portal Network JSONRPC APIs
Documentation
use crate::types::bytes::Bytes;
use ethereum_types::{Address, Bloom, H256, H64, U256, U64};
use rlp::{Decodable, DecoderError, Encodable, Rlp, RlpStream};
use serde::{Deserialize, Deserializer, Serialize, Serializer};
use ssz::{Decode, Encode, SszDecoderBuilder, SszEncoder};
use ssz_derive::{Decode, Encode};
use ssz_types::{typenum, VariableList};
use std::ops::Deref;

pub type ByteList = VariableList<u8, typenum::U2048>;

const LONDON_BLOCK_NUMBER: U64 = U64([12965000]);

/// BlockHeaderWithProof portal history content item
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct BlockHeaderWithProof {
    pub header: Header,
    pub proof: BlockHeaderProof,
}

impl Serialize for BlockHeaderWithProof {
    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
    where
        S: Serializer,
    {
        let ssz_block_header_with_proof = &self.as_ssz_bytes();
        serializer.serialize_str(&format!("0x{}", hex::encode(ssz_block_header_with_proof)))
    }
}

impl<'de> Deserialize<'de> for BlockHeaderWithProof {
    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
    where
        D: Deserializer<'de>,
    {
        let s = String::deserialize(deserializer)?;
        let block_header_with_proof = BlockHeaderWithProof::from_ssz_bytes(
            &hex::decode(s.strip_prefix("0x").unwrap_or(&s)).map_err(serde::de::Error::custom)?,
        )
        .map_err(|_| {
            serde::de::Error::custom("Unable to decode EpochAccumulator from ssz bytes")
        })?;

        Ok(block_header_with_proof)
    }
}

impl ssz::Encode for BlockHeaderWithProof {
    fn is_ssz_fixed_len() -> bool {
        true
    }

    fn ssz_append(&self, buf: &mut Vec<u8>) {
        let header_rlp = rlp::encode(&self.header);
        let header_rlp: ByteList = VariableList::from(header_rlp.to_vec());

        let offset =
            <ByteList as Encode>::ssz_fixed_len() + <BlockHeaderProof as Encode>::ssz_fixed_len();

        let mut encoder = SszEncoder::container(buf, offset);
        encoder.append(&header_rlp);
        encoder.append(&self.proof);
        encoder.finalize();
    }

    fn ssz_bytes_len(&self) -> usize {
        self.as_ssz_bytes().len()
    }
}

impl ssz::Decode for BlockHeaderWithProof {
    fn is_ssz_fixed_len() -> bool {
        false
    }

    fn from_ssz_bytes(bytes: &[u8]) -> Result<Self, ssz::DecodeError> {
        let mut builder = SszDecoderBuilder::new(bytes);

        builder.register_type::<ByteList>()?;
        builder.register_type::<BlockHeaderProof>()?;

        let mut decoder = builder.build()?;

        let header_rlp: Vec<u8> = decoder.decode_next()?;
        let proof = decoder.decode_next()?;
        let header: Header = rlp::decode(&header_rlp).map_err(|_| {
            ssz::DecodeError::BytesInvalid("Unable to decode bytes into header.".to_string())
        })?;

        Ok(Self { header, proof })
    }
}

/// The BlockHeaderProof allows to provide headers without a proof (None).
#[derive(Debug, Clone, PartialEq, Eq, Encode, Decode)]
#[ssz(enum_behaviour = "union")]
// Ignore clippy herre, since "box"-ing the accumulator proof breaks the Decode trait
#[allow(clippy::large_enum_variant)]
pub enum BlockHeaderProof {
    None(SszNone),
    AccumulatorProof(AccumulatorProof),
}

/// Struct to represent encodable/decodable None value for an SSZ enum
// In rust, None is a variant not a type,
// so we must use Option here to represent a ssz None value
#[derive(Clone, Debug, Default, PartialEq, Eq)]
pub struct SszNone(pub Option<()>);

impl ssz::Decode for SszNone {
    fn is_ssz_fixed_len() -> bool {
        true
    }

    fn from_ssz_bytes(bytes: &[u8]) -> Result<Self, ssz::DecodeError> {
        match bytes.len() {
            0 => Ok(Self(None)),
            _ => Err(ssz::DecodeError::BytesInvalid(
                "Expected None value to be empty, found bytes.".to_string(),
            )),
        }
    }
}

impl ssz::Encode for SszNone {
    fn is_ssz_fixed_len() -> bool {
        true
    }

    fn ssz_append(&self, _buf: &mut Vec<u8>) {}

    fn ssz_bytes_len(&self) -> usize {
        0
    }
}

#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct AccumulatorProof {
    pub proof: [H256; 15],
}

impl ssz::Encode for AccumulatorProof {
    fn is_ssz_fixed_len() -> bool {
        true
    }

    fn ssz_append(&self, buf: &mut Vec<u8>) {
        let raw_proof: Vec<[u8; 32]> = self
            .proof
            .iter()
            .map(|item| item.to_fixed_bytes())
            .collect();

        raw_proof.ssz_append(buf)
    }

    fn ssz_bytes_len(&self) -> usize {
        self.as_ssz_bytes().len()
    }
}

impl ssz::Decode for AccumulatorProof {
    fn is_ssz_fixed_len() -> bool {
        true
    }

    fn from_ssz_bytes(bytes: &[u8]) -> Result<Self, ssz::DecodeError> {
        let vec: Vec<[u8; 32]> = Vec::from_ssz_bytes(bytes)?;
        let mut proof: [H256; 15] = [H256::zero(); 15];
        let raw_proof: [[u8; 32]; 15] = vec
            .try_into()
            .map_err(|_| ssz::DecodeError::BytesInvalid("Invalid proof length".to_string()))?;
        for (idx, val) in raw_proof.iter().enumerate() {
            proof[idx] = H256::from_slice(val);
        }
        Ok(Self { proof })
    }
}

/// Depreciated history network block header content.
#[derive(Debug, Clone, Eq, PartialEq)]
pub struct BlockHeader(pub Header);

impl From<BlockHeader> for Header {
    fn from(v: BlockHeader) -> Self {
        v.0
    }
}

impl Deref for BlockHeader {
    type Target = Header;

    fn deref(&self) -> &Self::Target {
        &self.0
    }
}

impl Serialize for BlockHeader {
    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
    where
        S: Serializer,
    {
        let rlp_header = rlp::encode(&self.0);
        serializer.serialize_str(&format!("0x{}", hex::encode(&rlp_header)))
    }
}

impl<'de> Deserialize<'de> for BlockHeader {
    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
    where
        D: Deserializer<'de>,
    {
        let s = String::deserialize(deserializer)?;
        let header: Header = rlp::decode(
            &hex::decode(s.strip_prefix("0x").unwrap_or(&s)).map_err(serde::de::Error::custom)?,
        )
        .map_err(serde::de::Error::custom)?;

        Ok(Self(header))
    }
}

/// A block header
#[derive(Debug, Clone, Eq, PartialEq, Deserialize, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct Header {
    /// Block parent hash.
    pub parent_hash: H256,
    /// Block uncles hash.
    pub sha3_uncles: H256,
    /// Block miner.
    pub miner: Address,
    /// Block state root.
    pub state_root: H256,
    /// Block transactions root.
    pub transactions_root: H256,
    /// Block receipts root.
    pub receipts_root: H256,
    /// Block bloom filter.
    pub logs_bloom: Bloom,
    /// Block difficulty.
    pub difficulty: U256,
    /// Block number.
    pub number: U64,
    /// Block gas limit.
    pub gas_limit: U256,
    /// Block gas used.
    pub gas_used: U256,
    /// Block timestamp.
    pub timestamp: U64,
    /// Block extra data.
    pub extra_data: Bytes,
    /// Block PoW mix hash.
    pub mix_hash: Option<H256>,
    /// Block PoW nonce.
    pub nonce: Option<H64>,
    /// Block base fee per gas. Introduced by EIP-1559.
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub base_fee_per_gas: Option<U256>,
}

// Based on https://github.com/openethereum/openethereum/blob/main/crates/ethcore/types/src/header.rs
impl Header {
    /// Returns the Keccak-256 hash of the header.
    pub fn hash(&self) -> keccak_hash::H256 {
        keccak_hash::keccak(rlp::encode(self))
    }

    /// Append header to RLP stream `s`, optionally `with_seal`.
    fn stream_rlp(&self, s: &mut RlpStream, with_seal: bool) {
        let stream_length_without_seal = if self.base_fee_per_gas.is_some() {
            14
        } else {
            13
        };

        if with_seal && self.mix_hash.is_some() && self.nonce.is_some() {
            s.begin_list(stream_length_without_seal + 2);
        } else {
            s.begin_list(stream_length_without_seal);
        }

        s.append(&self.parent_hash)
            .append(&self.sha3_uncles)
            .append(&self.miner)
            .append(&self.state_root)
            .append(&self.transactions_root)
            .append(&self.receipts_root)
            .append(&self.logs_bloom)
            .append(&self.difficulty)
            .append(&self.number)
            .append(&self.gas_limit)
            .append(&self.gas_used)
            .append(&self.timestamp)
            .append(&self.extra_data);

        if with_seal && self.mix_hash.is_some() && self.nonce.is_some() {
            s.append(&self.mix_hash.unwrap())
                .append(self.nonce.as_ref().unwrap());
        }

        if self.base_fee_per_gas.is_some() {
            s.append(&self.base_fee_per_gas.unwrap());
        }
    }
}

impl Encodable for Header {
    fn rlp_append(&self, s: &mut RlpStream) {
        self.stream_rlp(s, true);
    }
}

impl Decodable for Header {
    /// Attempt to decode a header from RLP bytes.
    fn decode(rlp: &Rlp) -> Result<Self, DecoderError> {
        let mut header = Header {
            parent_hash: rlp.val_at(0)?,
            sha3_uncles: rlp.val_at(1)?,
            miner: rlp.val_at(2)?,
            state_root: rlp.val_at(3)?,
            transactions_root: rlp.val_at(4)?,
            receipts_root: rlp.val_at(5)?,
            logs_bloom: rlp.val_at(6)?,
            difficulty: rlp.val_at(7)?,
            number: rlp.val_at(8)?,
            gas_limit: rlp.val_at(9)?,
            gas_used: rlp.val_at(10)?,
            timestamp: rlp.val_at(11)?,
            extra_data: rlp.val_at(12)?,
            mix_hash: Some(rlp.val_at(13)?),
            nonce: Some(rlp.val_at(14)?),
            base_fee_per_gas: None,
        };

        if header.number >= LONDON_BLOCK_NUMBER {
            header.base_fee_per_gas = Some(rlp.val_at(15)?);
        }

        Ok(header)
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use serde_json::{json, Value};
    use ssz::Decode;
    use std::fs;

    #[test]
    fn rlp_encode_decode_header() {
        // Mainnet block #1 rlp encoded header
        // sourced from mainnetMM data dump
        // https://www.dropbox.com/s/y5n36ztppltgs7x/mainnetMM.zip?dl=0
        let header_rlp = hex::decode("f90211a0d4e56740f876aef8c010b86a40d5f56745a118d0906a34e69aec8c0db1cb8fa3a01dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d493479405a56e2d52c817161883f50c441c3228cfe54d9fa0d67e4d450343046425ae4271474353857ab860dbc0a1dde64b41b5cd3a532bf3a056e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421a056e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421b90100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000008503ff80000001821388808455ba422499476574682f76312e302e302f6c696e75782f676f312e342e32a0969b900de27b6ac6a67742365dd65f55a0526c41fd18e1b16f1a1215c2e66f5988539bd4979fef1ec4").unwrap();

        let header: Header = rlp::decode(&header_rlp).expect("error decoding header");
        assert_eq!(header.number, U64::from(1));
        assert_eq!(
            header.hash(),
            keccak_hash::H256::from_slice(
                // https://etherscan.io/block/1
                &hex::decode("88e96d4537bea4d9c05d12549907b32561d3bf31f45aae734cdc119f13406cb6")
                    .unwrap()
            )
        );

        let encoded_header = rlp::encode(&header);
        assert_eq!(header_rlp, encoded_header);
    }

    #[test]
    fn rlp_encode_decode_header_after_1559() {
        // RLP encoded block header #14037611
        let header_rlp = hex::decode("f90214a02320c9ca606618919c2a4cf5c6012cfac99399446c60a07f084334dea25f69eca01dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d4934794ea674fdde714fd979de3edf0f56aa9716b898ec8a0604a0ab7fe0d434943fbf2c525c4086818b8305349d91d6f4b205aca0759a2b8a0fdfe28e250fb15f7cb360d36ebb7dafa6da4f74543ce593baa96c27891ccac83a0cb9f9e60fb971068b76a8dece4202dde6b4075ebd90e7b2cd21c7fd8e121bba1b9010082e01d13f40116b1e1a0244090289b6920c51418685a0855031b988aef1b494313054c4002584928380267bc11cec18b0b30c456ca30651d9b06c931ea78aa0c40849859c7e0432df944341b489322b0450ce12026cafa1ba590f20af8051024fb8722a43610800381a531aa92042dd02448b1549052d6f06e4005b1000e063035c0220402a09c0124daab9028836209c446240d652c927bc7e4004b849256db5ba8d08b4a2321fd1e25c4d1dc480d18465d8600a41e864001cae44f38609d1c7414a8d62b5869d5a8001180d87228d788e852119c8a03df162471a317832622153da12fc21d828710062c7103534eb119714280201341ce6889ae926e025067872b68048d94e1ed83d6326b8401caa84183b062808461e859a88c617369612d65617374322d32a03472320df4ea70d29b89afdf195c3aa2289560a453957eea5058b57b80b908bf88d6450793e6dcec1c8532ff3f048d").unwrap();

        let header: Header = rlp::decode(&header_rlp).unwrap();

        assert_eq!(header.number, U64::from(14037611));
        assert_eq!(
            header.hash(),
            keccak_hash::H256::from_slice(
                // https://etherscan.io/block/14037611
                &hex::decode("a8227474afb7372058aceb724e44fd32bcebf3d39bc2e5e00dcdda2e442eebde")
                    .unwrap()
            )
        );
        let encoded_header = rlp::encode(&header);
        assert_eq!(header_rlp, encoded_header);
    }

    #[test]
    fn block_header_ser_de() {
        let block_header_json = "\"0xf90214a02320c9ca606618919c2a4cf5c6012cfac99399446c60a07f084334dea25f69eca01dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d4934794ea674fdde714fd979de3edf0f56aa9716b898ec8a0604a0ab7fe0d434943fbf2c525c4086818b8305349d91d6f4b205aca0759a2b8a0fdfe28e250fb15f7cb360d36ebb7dafa6da4f74543ce593baa96c27891ccac83a0cb9f9e60fb971068b76a8dece4202dde6b4075ebd90e7b2cd21c7fd8e121bba1b9010082e01d13f40116b1e1a0244090289b6920c51418685a0855031b988aef1b494313054c4002584928380267bc11cec18b0b30c456ca30651d9b06c931ea78aa0c40849859c7e0432df944341b489322b0450ce12026cafa1ba590f20af8051024fb8722a43610800381a531aa92042dd02448b1549052d6f06e4005b1000e063035c0220402a09c0124daab9028836209c446240d652c927bc7e4004b849256db5ba8d08b4a2321fd1e25c4d1dc480d18465d8600a41e864001cae44f38609d1c7414a8d62b5869d5a8001180d87228d788e852119c8a03df162471a317832622153da12fc21d828710062c7103534eb119714280201341ce6889ae926e025067872b68048d94e1ed83d6326b8401caa84183b062808461e859a88c617369612d65617374322d32a03472320df4ea70d29b89afdf195c3aa2289560a453957eea5058b57b80b908bf88d6450793e6dcec1c8532ff3f048d\"";
        let block_header: BlockHeader = serde_json::from_str(block_header_json).unwrap();

        assert_eq!(
            block_header_json,
            serde_json::to_string(&block_header).unwrap()
        )
    }

    #[test]
    fn decode_encode_fluffy_header_with_proofs() {
        let file = fs::read_to_string("./src/assets/test/fluffy_header_with_proofs.json").unwrap();
        let json: Value = serde_json::from_str(&file).unwrap();
        let hwps = json.as_object().unwrap();
        for (block_number, obj) in hwps {
            let _content_key = obj.get("content_key").unwrap();
            let block_number: u64 = block_number.parse().unwrap();
            let proof = obj.get("value").unwrap().as_str().unwrap();
            let proof_ssz = &hex::decode(proof.strip_prefix("0x").unwrap()).unwrap();
            let header = BlockHeaderWithProof::from_ssz_bytes(proof_ssz).unwrap();
            // Test ssz encode/decode
            assert_eq!(U64::from(block_number), header.header.number);
            assert_eq!(proof_ssz, &header.as_ssz_bytes());
            // Test serde
            assert_eq!(
                serde_json::from_value::<BlockHeaderWithProof>(json!(proof)).unwrap(),
                header
            );
            assert_eq!(json!(serde_json::to_value(&header).unwrap()), json!(proof));
        }
    }
}