eth-valkyoth-verify 0.13.0

no_std Ethereum verification boundaries.
Documentation
use eth_valkyoth_codec::DecodeLimits;
use eth_valkyoth_hash::Keccak256Digest;
use eth_valkyoth_primitives::{Address, B256, ChainId};
use eth_valkyoth_protocol::{
    SignatureYParity, UnvalidatedAccessListTransaction, UnvalidatedBlobTransaction,
    UnvalidatedDynamicFeeTransaction, UnvalidatedLegacyTransaction, UnvalidatedTransaction,
    decode_access_list_transaction, decode_blob_transaction, decode_dynamic_fee_transaction,
    decode_legacy_transaction, decode_set_code_transaction,
};
use k256::ecdsa::SigningKey;
use sha3::Digest;

use super::*;

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

const LEGACY_TX: [u8; 45] = [
    0xec, 0x09, 0x85, 0x04, 0xa8, 0x17, 0xc8, 0x00, 0x82, 0x52, 0x08, 0x94, 0x35, 0x35, 0x35, 0x35,
    0x35, 0x35, 0x35, 0x35, 0x35, 0x35, 0x35, 0x35, 0x35, 0x35, 0x35, 0x35, 0x35, 0x35, 0x35, 0x35,
    0x88, 0x0d, 0xe0, 0xb6, 0xb3, 0xa7, 0x64, 0x00, 0x00, 0x80, 0x25, 0x01, 0x02,
];
const ACCESS_LIST_TX: [u8; 15] = [
    0x01, 0xcd, 0x01, 0x02, 0x03, 0x82, 0x52, 0x08, 0x80, 0x05, 0x80, 0xc0, 0x01, 0x01, 0x02,
];
const DYNAMIC_FEE_TX: [u8; 16] = [
    0x02, 0xce, 0x01, 0x02, 0x03, 0x04, 0x82, 0x52, 0x08, 0x80, 0x05, 0x80, 0xc0, 0x01, 0x01, 0x02,
];
const BLOB_TX: [u8; 72] = [
    0x03, 0xf8, 0x45, 0x01, 0x02, 0x03, 0x04, 0x82, 0x52, 0x08, 0x94, 0x11, 0x11, 0x11, 0x11, 0x11,
    0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x05,
    0x80, 0xc0, 0x06, 0xe1, 0xa0, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01,
    0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01,
    0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x02,
];
const SET_CODE_TX: [u8; 37] = [
    0x04, 0xe3, 0x01, 0x02, 0x03, 0x04, 0x82, 0x52, 0x08, 0x94, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11,
    0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x05, 0x80,
    0xc0, 0xc0, 0x01, 0x01, 0x02,
];

struct RealKeccak {
    inner: sha3::Keccak256,
}

impl RealKeccak {
    fn new() -> Self {
        Self {
            inner: sha3::Keccak256::new(),
        }
    }
}

impl Keccak256 for RealKeccak {
    fn update(&mut self, input: &[u8]) {
        Digest::update(&mut self.inner, input);
    }

    fn finalize(self) -> Keccak256Digest {
        B256::from_bytes(self.inner.finalize().into())
    }
}

fn signing_key() -> Result<SigningKey, TransactionSignatureValidationError> {
    SigningKey::from_bytes(
        (&[
            0x4c, 0x08, 0x83, 0xa6, 0x91, 0x02, 0x93, 0x7d, 0x62, 0x31, 0x47, 0x1b, 0x5d, 0xbb,
            0x62, 0x04, 0xfe, 0x51, 0x29, 0x61, 0x70, 0x82, 0x79, 0x2a, 0xe4, 0x68, 0xd0, 0x1a,
            0x3f, 0x36, 0x23, 0x18,
        ])
            .into(),
    )
    .map_err(|_| TransactionSignatureValidationError::InvalidSignature)
}

fn expected_sender() -> Result<Address, TransactionSignatureValidationError> {
    let key = signing_key()?;
    let encoded = key.verifying_key().to_encoded_point(false);
    let public_key = encoded
        .as_bytes()
        .get(1..)
        .ok_or(TransactionSignatureValidationError::InvalidSignature)?;
    let digest = eth_valkyoth_hash::hash_one(RealKeccak::new(), public_key);
    let bytes = <[u8; 32]>::from(digest);
    let address = bytes
        .get(12..)
        .and_then(|value| <[u8; 20]>::try_from(value).ok())
        .ok_or(TransactionSignatureValidationError::InvalidSignature)?;
    Ok(Address::from_bytes(address))
}

fn sign_hash(
    signing_hash: TransactionSigningHash,
) -> Result<([u8; 32], [u8; 32], SignatureYParity), TransactionSignatureValidationError> {
    let key = signing_key()?;
    let (signature, recovery_id) = key
        .sign_prehash_recoverable(&<[u8; 32]>::from(signing_hash.to_b256()))
        .map_err(|_| TransactionSignatureValidationError::InvalidSignature)?;
    let bytes = signature.to_bytes();
    let r = bytes
        .get(..32)
        .and_then(|value| <[u8; 32]>::try_from(value).ok())
        .ok_or(TransactionSignatureValidationError::InvalidSignature)?;
    let s = bytes
        .get(32..)
        .and_then(|value| <[u8; 32]>::try_from(value).ok())
        .ok_or(TransactionSignatureValidationError::InvalidSignature)?;
    let y_parity = SignatureYParity::try_new(u64::from(recovery_id.to_byte()))
        .map_err(|_| TransactionSignatureValidationError::InvalidSignature)?;
    Ok((r, s, y_parity))
}

fn legacy_fixture()
-> Result<UnvalidatedLegacyTransaction<'static>, TransactionSignatureValidationError> {
    decode_legacy_transaction(&LEGACY_TX, TEST_LIMITS)
        .map_err(|_| TransactionSignatureValidationError::InvalidSignature)
}

fn access_list_fixture()
-> Result<UnvalidatedAccessListTransaction<'static>, TransactionSignatureValidationError> {
    decode_access_list_transaction(&ACCESS_LIST_TX, TEST_LIMITS)
        .map_err(|_| TransactionSignatureValidationError::InvalidSignature)
}

fn dynamic_fee_fixture()
-> Result<UnvalidatedDynamicFeeTransaction<'static>, TransactionSignatureValidationError> {
    decode_dynamic_fee_transaction(&DYNAMIC_FEE_TX, TEST_LIMITS)
        .map_err(|_| TransactionSignatureValidationError::InvalidSignature)
}

fn blob_fixture() -> Result<UnvalidatedBlobTransaction<'static>, TransactionSignatureValidationError>
{
    decode_blob_transaction(&BLOB_TX, TEST_LIMITS)
        .map_err(|_| TransactionSignatureValidationError::InvalidSignature)
}

fn signed_legacy()
-> Result<UnvalidatedLegacyTransaction<'static>, TransactionSignatureValidationError> {
    let tx = legacy_fixture()?;
    let mut scratch = [0_u8; 128];
    let signing_hash = legacy_eip155_transaction_signing_hash(&tx, &mut scratch, RealKeccak::new())
        .map_err(TransactionSignatureValidationError::SigningHash)?;
    let (r, s, y_parity) = sign_hash(signing_hash)?;
    let mut v = [0_u8; 32];
    let v_value = 35_u64
        .checked_add(2)
        .and_then(|value| value.checked_add(u64::from(y_parity.get())))
        .ok_or(TransactionSignatureValidationError::InvalidSignature)?;
    if let Some(target) = v.get_mut(24..) {
        target.copy_from_slice(&v_value.to_be_bytes());
    } else {
        return Err(TransactionSignatureValidationError::InvalidSignature);
    }
    Ok(UnvalidatedLegacyTransaction { v, r, s, ..tx })
}

fn signed_access_list()
-> Result<UnvalidatedAccessListTransaction<'static>, TransactionSignatureValidationError> {
    let tx = access_list_fixture()?;
    let mut scratch = [0_u8; 128];
    let signing_hash = access_list_transaction_signing_hash(&tx, &mut scratch, RealKeccak::new())
        .map_err(TransactionSignatureValidationError::SigningHash)?;
    let (r, s, y_parity) = sign_hash(signing_hash)?;
    Ok(UnvalidatedAccessListTransaction {
        y_parity,
        r,
        s,
        ..tx
    })
}

fn signed_dynamic_fee()
-> Result<UnvalidatedDynamicFeeTransaction<'static>, TransactionSignatureValidationError> {
    let tx = dynamic_fee_fixture()?;
    let mut scratch = [0_u8; 128];
    let signing_hash = dynamic_fee_transaction_signing_hash(&tx, &mut scratch, RealKeccak::new())
        .map_err(TransactionSignatureValidationError::SigningHash)?;
    let (r, s, y_parity) = sign_hash(signing_hash)?;
    Ok(UnvalidatedDynamicFeeTransaction {
        y_parity,
        r,
        s,
        ..tx
    })
}

fn signed_blob() -> Result<UnvalidatedBlobTransaction<'static>, TransactionSignatureValidationError>
{
    let tx = blob_fixture()?;
    let mut scratch = [0_u8; 128];
    let signing_hash = blob_transaction_signing_hash(&tx, &mut scratch, RealKeccak::new())
        .map_err(TransactionSignatureValidationError::SigningHash)?;
    let (r, s, y_parity) = sign_hash(signing_hash)?;
    Ok(UnvalidatedBlobTransaction {
        y_parity,
        r,
        s,
        ..tx
    })
}

#[test]
fn validates_supported_transaction_signatures() {
    let expected = expected_sender();
    let legacy = signed_legacy();
    let access_list = signed_access_list();
    let dynamic_fee = signed_dynamic_fee();
    let blob = signed_blob();
    assert!(expected.is_ok());
    assert!(legacy.is_ok());
    assert!(access_list.is_ok());
    assert!(dynamic_fee.is_ok());
    assert!(blob.is_ok());

    if let (Ok(expected), Ok(legacy), Ok(access_list), Ok(dynamic_fee), Ok(blob)) =
        (expected, legacy, access_list, dynamic_fee, blob)
    {
        let mut scratch = [0_u8; 128];

        assert_eq!(
            validate_legacy_transaction_signature(
                ChainId::new(1),
                &legacy,
                Some(expected),
                &mut scratch,
                RealKeccak::new(),
                RealKeccak::new(),
            )
            .map(ValidatedTransactionSignature::sender),
            Ok(expected)
        );
        assert_eq!(
            validate_transaction_signature(
                ChainId::new(1),
                UnvalidatedTransaction::AccessList(access_list),
                Some(expected),
                &mut scratch,
                RealKeccak::new(),
                RealKeccak::new(),
            )
            .map(ValidatedTransactionSignature::sender),
            Ok(expected)
        );
        assert_eq!(
            validate_dynamic_fee_transaction_signature(
                ChainId::new(1),
                &dynamic_fee,
                Some(expected),
                &mut scratch,
                RealKeccak::new(),
                RealKeccak::new(),
            )
            .map(ValidatedTransactionSignature::sender),
            Ok(expected)
        );
        assert_eq!(
            validate_blob_transaction_signature(
                ChainId::new(1),
                &blob,
                Some(expected),
                &mut scratch,
                RealKeccak::new(),
                RealKeccak::new(),
            )
            .map(ValidatedTransactionSignature::sender),
            Ok(expected)
        );
    }
}

#[test]
fn rejects_wrong_chain_and_wrong_sender() {
    let tx = signed_access_list();
    assert!(tx.is_ok());
    if let Ok(tx) = tx {
        let mut scratch = [0_u8; 128];
        assert_eq!(
            validate_access_list_transaction_signature(
                ChainId::new(5),
                &tx,
                None,
                &mut scratch,
                RealKeccak::new(),
                RealKeccak::new(),
            ),
            Err(TransactionSignatureValidationError::ReplayDomain(
                VerifyError::WrongChain
            ))
        );
        assert_eq!(
            validate_access_list_transaction_signature(
                ChainId::new(1),
                &tx,
                Some(Address::from_bytes([0x44; 20])),
                &mut scratch,
                RealKeccak::new(),
                RealKeccak::new(),
            ),
            Err(TransactionSignatureValidationError::WrongSender)
        );
    }
}

#[test]
fn rejects_high_s_and_malformed_scalars() {
    let tx = signed_dynamic_fee();
    assert!(tx.is_ok());
    if let Ok(tx) = tx {
        let mut scratch = [0_u8; 128];
        let high_s = UnvalidatedDynamicFeeTransaction {
            s: [
                0xee, 0x2f, 0x11, 0xef, 0x8c, 0xb0, 0x0a, 0x49, 0x61, 0x7d, 0x13, 0x57, 0xf4, 0xd5,
                0x56, 0x41, 0x09, 0x0a, 0x48, 0xf2, 0x01, 0xe9, 0xb9, 0x59, 0xc4, 0x8f, 0x6f, 0x6b,
                0xec, 0x6f, 0x93, 0x8f,
            ],
            ..tx
        };
        assert_eq!(
            validate_dynamic_fee_transaction_signature(
                ChainId::new(1),
                &high_s,
                None,
                &mut scratch,
                RealKeccak::new(),
                RealKeccak::new(),
            ),
            Err(TransactionSignatureValidationError::InvalidSignature)
        );

        let malformed = UnvalidatedDynamicFeeTransaction {
            r: [0_u8; 32],
            ..tx
        };
        assert_eq!(
            validate_dynamic_fee_transaction_signature(
                ChainId::new(1),
                &malformed,
                None,
                &mut scratch,
                RealKeccak::new(),
                RealKeccak::new(),
            ),
            Err(TransactionSignatureValidationError::InvalidSignature)
        );
    }
}

#[test]
fn reports_signing_hash_construction_failures() {
    let tx = signed_blob();
    assert!(tx.is_ok());
    if let Ok(tx) = tx {
        let mut scratch = [0_u8; 8];
        let result = validate_blob_transaction_signature(
            ChainId::new(1),
            &tx,
            None,
            &mut scratch,
            RealKeccak::new(),
            RealKeccak::new(),
        );
        assert!(matches!(
            result,
            Err(TransactionSignatureValidationError::SigningHash(_))
        ));
    }
}

#[test]
fn rejects_set_code_signature_validation_until_supported() {
    let tx = decode_set_code_transaction(&SET_CODE_TX, TEST_LIMITS);
    assert!(tx.is_ok());
    if let Ok(tx) = tx {
        let mut scratch = [0_u8; 128];
        assert_eq!(
            validate_transaction_signature(
                ChainId::new(1),
                UnvalidatedTransaction::SetCode(tx),
                None,
                &mut scratch,
                RealKeccak::new(),
                RealKeccak::new(),
            ),
            Err(TransactionSignatureValidationError::UnsupportedTransactionType)
        );
    }
}