eth-valkyoth-protocol 0.25.2

Fork-aware no_std Ethereum protocol validation state.
Documentation
use eth_valkyoth_codec::DecodeLimits;
use eth_valkyoth_primitives::Address;
extern crate std;
use std::{vec, vec::Vec};

use super::*;

const TEST_LIMITS: DecodeLimits = DecodeLimits {
    max_input_bytes: 512,
    max_list_items: 16,
    max_nesting_depth: 4,
    max_total_allocation: 512,
    max_proof_nodes: 4,
    max_total_items: 32,
};

#[test]
fn decodes_withdrawals_list() -> Result<(), &'static str> {
    let input = list(&[withdrawal(7, 11, address_bytes(), 13)]);
    let withdrawals = decode_ok(&input)?;
    let mut entries = withdrawals.entries();
    let Some(entry) = entries.next() else {
        return Err("withdrawal fixture must include one entry");
    };
    let entry = entry.map_err(|_| "withdrawal entry must decode")?;

    assert_eq!(withdrawals.encoded_rlp(), input.as_slice());
    assert_eq!(withdrawals.len(), 1);
    assert_eq!(entry.index.get(), 7);
    assert_eq!(entry.validator_index.get(), 11);
    assert_eq!(entry.address, Address::from_bytes(address_bytes()));
    assert_eq!(entry.amount.get(), 13);
    assert!(entries.next().is_none());
    Ok(())
}

#[test]
fn decodes_empty_withdrawals_list() -> Result<(), &'static str> {
    let input = list(&[]);
    let withdrawals = decode_ok(&input)?;

    assert!(withdrawals.is_empty());
    assert_eq!(withdrawals.entries().count(), 0);
    Ok(())
}

#[test]
fn rejects_wrong_withdrawal_field_count() -> Result<(), &'static str> {
    let input = list(&[list(&[uint(1), uint(2), scalar(&address_bytes())])]);
    let error = decode_err(&input)?;

    assert_eq!(
        error,
        WithdrawalDecodeError::WrongFieldCount {
            expected: WITHDRAWAL_FIELD_COUNT,
            found: 3
        }
    );
    assert_eq!(error.code(), "ETH_WITHDRAWAL_WRONG_FIELD_COUNT");
    assert_eq!(
        error.category(),
        WithdrawalDecodeErrorCategory::MalformedInput
    );
    Ok(())
}

#[test]
fn rejects_invalid_address_length() -> Result<(), &'static str> {
    let input = list(&[withdrawal(1, 2, [0x11; 19], 3)]);
    let error = decode_err(&input)?;

    assert_eq!(
        error,
        WithdrawalDecodeError::InvalidAddressLength { found: 19 }
    );
    Ok(())
}

#[test]
fn rejects_zero_amount() -> Result<(), &'static str> {
    let input = list(&[withdrawal(1, 2, address_bytes(), 0)]);
    let error = decode_err(&input)?;

    assert_eq!(error, WithdrawalDecodeError::ZeroAmount);
    assert_eq!(error.code(), "ETH_WITHDRAWAL_ZERO_AMOUNT");
    Ok(())
}

#[test]
fn rejects_noncanonical_integer_field() -> Result<(), &'static str> {
    let bad_index = scalar(&[0, 1]);
    let input = list(&[list(&[
        bad_index,
        uint(2),
        scalar(&address_bytes()),
        uint(3),
    ])]);
    let error = decode_err(&input)?;

    assert!(matches!(
        error,
        WithdrawalDecodeError::FieldDecode {
            field: WithdrawalField::Index,
            ..
        }
    ));
    Ok(())
}

#[test]
fn rejects_oversized_withdrawal_input_as_resource_exhaustion() -> Result<(), &'static str> {
    let input = list(&[withdrawal(1, 2, address_bytes(), 3)]);
    let limits = DecodeLimits {
        max_input_bytes: 4,
        ..TEST_LIMITS
    };
    let error = decode_err_with_limits(&input, limits)?;

    assert_eq!(
        error.category(),
        WithdrawalDecodeErrorCategory::ResourceExhaustion
    );
    Ok(())
}

fn withdrawal<const N: usize>(
    index: u64,
    validator_index: u64,
    address: [u8; N],
    amount: u64,
) -> Vec<u8> {
    list(&[
        uint(index),
        uint(validator_index),
        scalar(&address),
        uint(amount),
    ])
}

fn address_bytes() -> [u8; 20] {
    core::array::from_fn(|index| 0x30_u8.wrapping_add(usize_to_u8(index)))
}

fn uint(value: u64) -> Vec<u8> {
    if value == 0 {
        return scalar(&[]);
    }
    let bytes = value.to_be_bytes();
    let Some(first) = bytes.iter().position(|byte| *byte != 0) else {
        return scalar(&[]);
    };
    let Some(payload) = bytes.get(first..) else {
        return scalar(&[]);
    };
    scalar(payload)
}

fn scalar(payload: &[u8]) -> Vec<u8> {
    if let [byte] = payload
        && *byte < 0x80
    {
        return vec![*byte];
    }
    let mut output = Vec::new();
    append_header(0x80, payload.len(), &mut output);
    output.extend_from_slice(payload);
    output
}

fn list(items: &[Vec<u8>]) -> Vec<u8> {
    let payload_len = items.iter().map(Vec::len).sum();
    let mut output = Vec::new();
    append_header(0xc0, payload_len, &mut output);
    for item in items {
        output.extend_from_slice(item);
    }
    output
}

fn append_header(offset: u8, payload_len: usize, output: &mut Vec<u8>) {
    if payload_len < 56 {
        output.push(offset.saturating_add(usize_to_u8(payload_len)));
        return;
    }
    let len_bytes = payload_len.to_be_bytes();
    let Some(first) = len_bytes.iter().position(|byte| *byte != 0) else {
        return;
    };
    let len_of_len = usize_to_u8(len_bytes.len().saturating_sub(first));
    output.push(offset.saturating_add(55).saturating_add(len_of_len));
    let Some(encoded_len) = len_bytes.get(first..) else {
        return;
    };
    output.extend_from_slice(encoded_len);
}

fn decode_ok(input: &[u8]) -> Result<UnvalidatedWithdrawals<'_>, &'static str> {
    match decode_withdrawals(input, TEST_LIMITS) {
        Ok(withdrawals) => Ok(withdrawals),
        Err(_) => Err("withdrawal fixture should decode"),
    }
}

fn decode_err(input: &[u8]) -> Result<WithdrawalDecodeError, &'static str> {
    decode_err_with_limits(input, TEST_LIMITS)
}

fn decode_err_with_limits(
    input: &[u8],
    limits: DecodeLimits,
) -> Result<WithdrawalDecodeError, &'static str> {
    match decode_withdrawals(input, limits) {
        Ok(_) => Err("withdrawal fixture should fail"),
        Err(error) => Ok(error),
    }
}

fn usize_to_u8(value: usize) -> u8 {
    u8::try_from(value).unwrap_or(u8::MAX)
}