taproot-assets-core 0.0.2

Core no-std logic and verification primitives for Taproot Assets
Documentation
use std::path::PathBuf;

use bitcoin::TapNodeHash;
use bitcoin::hashes::Hash;
use bitcoin::key::TapTweak;
use bitcoin::secp256k1::{self, PublicKey as SecpPublicKey, Scalar, Secp256k1};
use serde::Deserialize;
use taproot_assets_core::verify::proof::{verify_genesis_reveal, verify_proofs};
use taproot_assets_core::{OpsError, TaprootOps};
use taproot_assets_types::asset::SerializedKey;
use taproot_assets_types::proof::{File, Proof};

const VECTOR_JSON_FILES: &[&str] =
    &["../external/taproot-assets-upstream/proof/testdata/proof_tlv_encoding_regtest.json"];

#[test]
fn verify_vectors_in_core() {
    let ops = BitcoinTaprootOps::new();
    let mut saw_vector_file = false;
    let mut decoded_any = false;
    let mut verified_any = false;

    for path in VECTOR_JSON_FILES {
        let full_path = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join(path);
        if !full_path.exists() {
            eprintln!("skipping {path}: vector file not found");
            continue;
        }
        saw_vector_file = true;
        let proofs = load_proofs_from_json(path);
        if !proofs.is_empty() {
            decoded_any = true;
        }
        for (index, proof) in proofs.into_iter().enumerate() {
            if verify_proofs(&ops, &proof).is_ok() && verify_genesis_reveal(&proof).is_ok() {
                verified_any = true;
            } else {
                eprintln!("skipping {path}#{index}: core verification failed");
            }
        }
    }

    if !saw_vector_file {
        eprintln!("skipping proof vector test: no vector files found");
        return;
    }

    assert!(decoded_any, "no vectors decoded from test data");
    if !verified_any {
        eprintln!("no vectors passed core verification");
    }
}

#[derive(Deserialize)]
struct VectorFile {
    valid_test_cases: Vec<TestCase>,
}

#[derive(Deserialize)]
struct TestCase {
    expected: String,
}

fn load_proofs_from_json(relative_path: &str) -> Vec<Proof> {
    let path = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join(relative_path);
    let contents = std::fs::read_to_string(&path)
        .unwrap_or_else(|err| panic!("failed to read {}: {err}", path.display()));
    let vectors: VectorFile = serde_json::from_str(&contents)
        .unwrap_or_else(|err| panic!("failed to parse {}: {err}", path.display()));

    let mut proofs = Vec::new();
    for (index, case) in vectors.valid_test_cases.into_iter().enumerate() {
        let stripped: String = case.expected.split_whitespace().collect();
        let bytes = match hex::decode(stripped) {
            Ok(bytes) => bytes,
            Err(err) => {
                eprintln!("skipping {relative_path}#{index}: hex decode failed ({err})");
                continue;
            }
        };
        match decode_proofs(&bytes, relative_path, index) {
            Ok(mut decoded) => proofs.append(&mut decoded),
            Err(err) => {
                eprintln!("skipping {relative_path}#{index}: {err}");
            }
        }
    }
    proofs
}

fn decode_proofs(bytes: &[u8], relative_path: &str, index: usize) -> Result<Vec<Proof>, String> {
    if let Ok(file) = File::from_bytes(&bytes) {
        let mut proofs = Vec::new();
        for (proof_index, hashed) in file.proofs.iter().enumerate() {
            let proof = Proof::from_bytes(&hashed.proof_bytes).map_err(|err| {
                format!("failed to decode proof {relative_path}#{index}.{proof_index}: {err}")
            })?;
            proofs.push(proof);
        }
        return Ok(proofs);
    }

    let proof = Proof::from_bytes(&bytes)
        .map_err(|err| format!("failed to decode proof {relative_path}#{index}: {err}"))?;
    Ok(vec![proof])
}

#[derive(Debug)]
struct BitcoinTaprootOps {
    secp: Secp256k1<secp256k1::VerifyOnly>,
}

impl BitcoinTaprootOps {
    fn new() -> Self {
        Self {
            secp: Secp256k1::verification_only(),
        }
    }
}

impl TaprootOps for BitcoinTaprootOps {
    type PubKey = SecpPublicKey;

    fn parse_group_key(&self, key: &SerializedKey) -> Result<Self::PubKey, OpsError> {
        SecpPublicKey::from_slice(&key.bytes).map_err(|_| OpsError::InvalidRawGroupKey)
    }

    fn parse_internal_key(&self, key: &SerializedKey) -> Result<Self::PubKey, OpsError> {
        SecpPublicKey::from_slice(&key.bytes).map_err(|_| OpsError::InvalidInternalKey)
    }

    fn add_tweak(&self, pubkey: &Self::PubKey, tweak: [u8; 32]) -> Result<Self::PubKey, OpsError> {
        let tweak = Scalar::from_be_bytes(tweak).map_err(|_| OpsError::AssetIdTweakOutOfRange)?;
        pubkey
            .add_exp_tweak(&self.secp, &tweak)
            .map_err(|_| OpsError::InvalidGroupKeyTweak)
    }

    fn taproot_output_key(
        &self,
        internal_key: &Self::PubKey,
        tapscript_root: Option<[u8; 32]>,
    ) -> Result<SerializedKey, OpsError> {
        let merkle_root = tapscript_root.map(TapNodeHash::from_byte_array);
        let (xonly_key, _) = internal_key.x_only_public_key();
        let (tweaked, parity) = xonly_key.tap_tweak(&self.secp, merkle_root);
        let output_key =
            SecpPublicKey::from_x_only_public_key(tweaked.to_x_only_public_key(), parity);

        Ok(SerializedKey {
            bytes: output_key.serialize(),
        })
    }
}