zebra-chain 8.0.0

Core Zcash data structures
Documentation
use std::sync::Arc;

use crate::{
    block::{Block, Height},
    parameters::Network,
    serialization::{SerializationError, ZcashDeserialize, ZcashDeserializeInto},
    transaction,
    transparent::Input,
};
use hex::FromHex;
use zcash_script::{opcode::Evaluable, pattern};

use zebra_test::prelude::*;

/// Builds a serialized transparent input wire-format with a null prevout (coinbase) and the given
/// `script_sig` bytes. Used to exercise [`Input::zcash_deserialize`] directly.
fn build_coinbase_wire(script_sig: &[u8]) -> Vec<u8> {
    let mut buf = vec![0u8; 32];
    buf.extend_from_slice(&0xffff_ffffu32.to_le_bytes());
    let len = script_sig.len();
    if len < 0xfd {
        buf.push(len as u8);
    } else if len <= 0xffff {
        buf.push(0xfd);
        buf.extend_from_slice(&(len as u16).to_le_bytes());
    } else {
        buf.push(0xfe);
        buf.extend_from_slice(&(len as u32).to_le_bytes());
    }
    buf.extend_from_slice(script_sig);
    buf.extend_from_slice(&0u32.to_le_bytes());
    buf
}

/// Returns the canonical BIP-34 height-prefix bytes for the given height, computed via the
/// upstream encoder used by `zcash_transparent::bundle::TxIn::coinbase`.
fn canonical_height_prefix(height: Height) -> Vec<u8> {
    pattern::push_num(i64::from(height.0)).to_bytes()
}

#[test]
fn coinbase_deserialize_rejects_non_minimal_heights() {
    let _init_guard = zebra_test::init();

    // Each of these is a non-canonical encoding of a valid height. They must all be rejected.
    // The trailing byte makes the script ≥ 2 bytes (the consensus minimum); without it the length
    // check would reject before the height check, masking the bug we are guarding against.
    let cases: &[(&str, &[u8])] = &[
        // Height 1: minimal is OP_1 (0x51). Length-prefixed forms are non-canonical.
        ("h1_via_01_01", &[0x01, 0x01, 0x00]),
        ("h1_via_02", &[0x02, 0x01, 0x00, 0x00]),
        ("h1_via_03", &[0x03, 0x01, 0x00, 0x00, 0x00]),
        ("h1_via_04", &[0x04, 0x01, 0x00, 0x00, 0x00, 0x00]),
        // Height 16: minimal is OP_16 (0x60). 1-byte push form is non-canonical.
        ("h16_via_01_10", &[0x01, 0x10, 0x00]),
        // Height 17: minimal is `[0x01, 0x11]`. Wider forms are non-canonical.
        ("h17_via_02", &[0x02, 0x11, 0x00, 0x00]),
        ("h17_via_03", &[0x03, 0x11, 0x00, 0x00, 0x00]),
    ];

    for (name, sig) in cases {
        let wire = build_coinbase_wire(sig);
        let result = Input::zcash_deserialize(&wire[..]);
        assert!(
            result.is_err(),
            "{name}: must reject non-canonical encoding {sig:02x?}, got {result:?}",
        );
    }
}

#[test]
fn coinbase_deserialize_accepts_canonical_heights() {
    let _init_guard = zebra_test::init();

    // Boundary heights spanning every valid prefix length, plus Height::MAX.
    let heights = [
        1,
        16,
        17,
        127,
        128,
        32_767,
        32_768,
        8_388_607,
        8_388_608,
        Height::MAX.0,
    ];

    for h in heights {
        let height = Height(h);
        let mut script_sig = canonical_height_prefix(height);
        // Pad to ≥ MIN_COINBASE_SCRIPT_LEN (= 2 bytes) so the length check passes.
        if script_sig.len() < 2 {
            script_sig.push(0x00);
        }
        let wire = build_coinbase_wire(&script_sig);
        let parsed = Input::zcash_deserialize(&wire[..])
            .unwrap_or_else(|e| panic!("must accept canonical height {h}: {e:?}"));
        match parsed {
            Input::Coinbase {
                height: parsed_h, ..
            } => assert_eq!(parsed_h, height, "round-trip mismatch for height {h}"),
            _ => panic!("expected Coinbase input for height {h}"),
        }
    }
}

/// Build a coinbase `Input` byte sequence: 32 zero bytes for the null outpoint
/// hash, the coinbase index `0xffffffff` little-endian, and a coinbase-script
/// CompactSize length followed by `payload`.
///
/// Used by the regression test below to confirm that `Input::zcash_deserialize`
/// rejects attacker-controlled coinbase script lengths *before* allocating or
/// reading the script bytes.
fn coinbase_input_bytes(compactsize_len: &[u8], payload: &[u8]) -> Vec<u8> {
    let mut bytes = Vec::with_capacity(32 + 4 + compactsize_len.len() + payload.len());
    bytes.extend_from_slice(&[0u8; 32]);
    bytes.extend_from_slice(&0xffff_ffffu32.to_le_bytes());
    bytes.extend_from_slice(compactsize_len);
    bytes.extend_from_slice(payload);
    bytes
}

/// Coinbase scripts longer than the consensus maximum (100 bytes) must be
/// rejected at length-decode time, not after allocating the bytes.
///
/// Regression test: encoding a CompactSize length of 101 with no payload would
/// have made the previous implementation try to `read_exact(101)` and surface
/// an `io::Error`; the fix returns the consensus error string before the read.
#[test]
fn coinbase_script_oversize_rejected_before_allocation() {
    let _init_guard = zebra_test::init();

    let bytes = coinbase_input_bytes(&[101u8], &[]);
    let result = Input::zcash_deserialize(std::io::Cursor::new(&bytes));
    assert!(
        matches!(
            result,
            Err(SerializationError::Parse("Coinbase script is too long"))
        ),
        "expected `Coinbase script is too long`, got {result:?}",
    );
}

#[test]
fn coinbase_deserialize_rejects_out_of_range_heights() {
    let _init_guard = zebra_test::init();

    // Sign-bit set on the top byte: would decode as negative under signed-LE.
    let sig = [0x01, 0x80, 0x00];
    Input::zcash_deserialize(&build_coinbase_wire(&sig)[..])
        .expect_err("must reject negative-signed height encoding");

    // 6-byte push (n = 6 invalid; spec restricts heightBytes length to {1..=5}).
    let sig = [0x06, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00];
    Input::zcash_deserialize(&build_coinbase_wire(&sig)[..])
        .expect_err("must reject 6-byte height push");

    // Height > Height::MAX. Height::MAX = 0x7FFF_FFFF, so 0x8000_0000 encoded with sign-bit clear
    // would need 5 bytes: [0x05, 0x00, 0x00, 0x00, 0x80, 0x00].
    let sig = [0x05, 0x00, 0x00, 0x00, 0x80, 0x00];
    Input::zcash_deserialize(&build_coinbase_wire(&sig)[..])
        .expect_err("must reject height > Height::MAX");
}

proptest! {
    #[test]
    fn coinbase_canonical_round_trip(h in 1u32..=Height::MAX.0) {
        let height = Height(h);
        let mut script_sig = canonical_height_prefix(height);
        if script_sig.len() < 2 {
            script_sig.push(0x00);
        }
        let wire = build_coinbase_wire(&script_sig);
        let parsed = Input::zcash_deserialize(&wire[..]).expect("canonical encoding must parse");
        match parsed {
            Input::Coinbase { height: parsed_h, .. } => prop_assert_eq!(parsed_h, height),
            _ => prop_assert!(false, "expected Coinbase input"),
        }
    }
}

#[test]
fn get_transparent_output_address() -> Result<()> {
    let _init_guard = zebra_test::init();

    let script_tx: Vec<u8> = <Vec<u8>>::from_hex("0400008085202f8901fcaf44919d4a17f6181a02a7ebe0420be6f7dad1ef86755b81d5a9567456653c010000006a473044022035224ed7276e61affd53315eca059c92876bc2df61d84277cafd7af61d4dbf4002203ed72ea497a9f6b38eb29df08e830d99e32377edb8a574b8a289024f0241d7c40121031f54b095eae066d96b2557c1f99e40e967978a5fd117465dbec0986ca74201a6feffffff020050d6dc0100000017a9141b8a9bda4b62cd0d0582b55455d0778c86f8628f870d03c812030000001976a914e4ff5512ffafe9287992a1cd177ca6e408e0300388ac62070d0095070d000000000000000000000000")
    .expect("Block bytes are in valid hex representation");

    let transaction = script_tx.zcash_deserialize_into::<Arc<transaction::Transaction>>()?;

    // Hashes were extracted from the transaction (parsed with zebra-chain,
    // then manually extracted from lock_script).
    // Final expected values were generated with https://secretscan.org/PrivateKeyHex,
    // by filling field 4 with the prefix followed by the address hash.
    // Refer to <https://zips.z.cash/protocol/protocol.pdf#transparentaddrencoding>
    // for the prefixes.

    // Script hash 1b8a9bda4b62cd0d0582b55455d0778c86f8628f
    let addr = transaction.outputs()[0]
        .address(&Network::Mainnet)
        .expect("should return address");
    assert_eq!(addr.to_string(), "t3M5FDmPfWNRG3HRLddbicsuSCvKuk9hxzZ");
    let addr = transaction.outputs()[0]
        .address(&Network::new_default_testnet())
        .expect("should return address");
    assert_eq!(addr.to_string(), "t294SGSVoNq2daz15ZNbmAW65KQZ5e3nN5G");
    // Public key hash e4ff5512ffafe9287992a1cd177ca6e408e03003
    let addr = transaction.outputs()[1]
        .address(&Network::Mainnet)
        .expect("should return address");
    assert_eq!(addr.to_string(), "t1ekRwsd4LaSsd6NXgsx66q2HxQWTLCF44y");
    let addr = transaction.outputs()[1]
        .address(&Network::new_default_testnet())
        .expect("should return address");
    assert_eq!(addr.to_string(), "tmWbBGi7TjExNmLZyMcFpxVh3ZPbGrpbX3H");

    Ok(())
}

#[test]
fn get_transparent_output_address_with_blocks() {
    let _init_guard = zebra_test::init();
    for network in Network::iter() {
        get_transparent_output_address_with_blocks_for_network(network);
    }
}

/// Test that the block test vector indexes match the heights in the block data,
/// and that each post-sapling block has a corresponding final sapling root.
fn get_transparent_output_address_with_blocks_for_network(network: Network) {
    let block_iter = network.block_iter();

    let mut valid_addresses = 0;

    for (&height, block_bytes) in block_iter.skip(1) {
        let block = block_bytes
            .zcash_deserialize_into::<Block>()
            .expect("block is structurally valid");

        for (idx, tx) in block.transactions.iter().enumerate() {
            for output in tx.outputs() {
                let addr = output.address(&network);
                if addr.is_none() && idx == 0 && output.lock_script.as_raw_bytes()[0] == 0x21 {
                    // There are a bunch of coinbase transactions with pay-to-pubkey scripts
                    // which we don't support; skip them
                    continue;
                }
                assert!(
                    addr.is_some(),
                    "address of {output:?}; block #{height}; tx #{idx}; must not be None",
                );
                valid_addresses += 1;
            }
        }
    }
    // Make sure we didn't accidentally skip all vectors
    assert!(valid_addresses > 0);
}