kaspa-txscript 2.1.0

Kaspa txscript engine library
Documentation
// Copyright 2025 RISC Zero, Inc.
//
// Licensed under the Apache License, Version 2.0, <LICENSE-APACHE or
// http://apache.org/licenses/LICENSE-2.0> or the MIT license <LICENSE-MIT or
// http://opensource.org/licenses/MIT>, at your option. This file may not be
// copied, modified, or distributed except according to those terms.
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
// SPDX-License-Identifier: Apache-2.0 OR MIT

use alloc::{collections::VecDeque, vec::Vec};
use risc0_binfmt::read_sha_halfs;
use risc0_circuit_recursion::{CIRCUIT, CircuitImpl, control_id::ALLOWED_CONTROL_ROOT};
use risc0_core::field::baby_bear::BabyBearElem;
use risc0_zkp::core::hash::{HashSuite, blake2b::Blake2bCpuHashSuite, poseidon2::Poseidon2HashSuite, sha::Sha256HashSuite};
use risc0_zkp::{adapter::CircuitInfo, core::digest::Digest, verify::VerificationError};

use crate::zk_precompiles::risc0::R0Error;
use crate::zk_precompiles::risc0::merkle::MerkleProof;

#[repr(u8)]
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum HashFnId {
    Blake2b = 0,
    Poseidon2 = 1,
    Sha256 = 2,
}

impl TryFrom<u8> for HashFnId {
    type Error = R0Error;

    fn try_from(value: u8) -> Result<Self, Self::Error> {
        match value {
            0 => Ok(HashFnId::Blake2b),
            1 => Ok(HashFnId::Poseidon2),
            2 => Ok(HashFnId::Sha256),
            _ => Err(R0Error::InvalidHashFnId(value)),
        }
    }
}

impl From<HashFnId> for u8 {
    fn from(value: HashFnId) -> Self {
        value as u8
    }
}
/// A succinct receipt, produced via recursion, proving the execution of the zkVM with a [STARK].
///
/// Using recursion, a [CompositeReceipt][crate::CompositeReceipt] can be compressed to form a
/// [SuccinctReceipt]. In this way, a constant sized proof can be generated for arbitrarily long
/// computations, and with an arbitrary number of segments linked via composition.
///
/// [STARK]: https://dev.risczero.com/terminology#stark
///
/// This is a modified version of the SuccinctReceipt defined in risc0. The reason for this is to
/// simplify it, as we are certain to only receive digests for the claim and verifier parameters.
#[derive(Debug)]
#[cfg_attr(test, derive(PartialEq))]
pub struct SuccinctReceipt {
    /// The cryptographic seal of this receipt. This seal is a STARK proving an execution of the
    /// recursion circuit.
    seal: Vec<u32>,

    /// The control ID of this receipt, identifying the recursion program that was run (e.g. lift,
    /// join, or resolve).
    control_id: Digest,
    /// Claim containing information about the computation that this receipt proves.
    ///
    /// The standard claim type is [ReceiptClaim][crate::ReceiptClaim], which represents a RISC-V
    /// zkVM execution.
    claim: Digest,

    /// Hash function id used to create this receipt.
    hashfn: HashFnId,

    /// Merkle inclusion proof for control_id against the control root for this receipt.
    control_inclusion_proof: MerkleProof,
}

impl SuccinctReceipt {
    pub fn new(seal: Vec<u32>, control_id: Digest, claim: Digest, hashfn: HashFnId, control_inclusion_proof: MerkleProof) -> Self {
        Self { seal, control_id, claim, hashfn, control_inclusion_proof }
    }

    pub fn claim(&self) -> &Digest {
        &self.claim
    }
}

impl SuccinctReceipt {
    /// Verify the integrity of this receipt, ensuring the claim is attested
    /// to by the seal.
    pub fn verify_integrity(&self) -> Result<(), R0Error> {
        // Prepare the hash suite for this receipt
        let suite: HashSuite<risc0_zkp::field::baby_bear::BabyBear> = match self.hashfn {
            HashFnId::Blake2b => Blake2bCpuHashSuite::new_suite(),
            HashFnId::Poseidon2 => Poseidon2HashSuite::new_suite(),
            HashFnId::Sha256 => Sha256HashSuite::new_suite(),
        };

        // There are only some control roots allowed, specifying which circuits are allowed
        // to be verified with this proof. We verify that the control id of the receipt verifies
        // as a valid merkle proof.
        let check_code = |_, control_id: &Digest| -> Result<(), VerificationError> {
            // Ensure that the control_id decoded from the seal matches the one in the
            // SuccinctReceipt metadata.
            if *control_id != self.control_id {
                return Err(VerificationError::ControlVerificationError { control_id: *control_id });
            }
            self.control_inclusion_proof
                .verify(control_id, &ALLOWED_CONTROL_ROOT, suite.hashfn.as_ref())
                .map_err(|_| VerificationError::ControlVerificationError { control_id: *control_id })
        };

        // Verify the receipt itself is correct, and therefore the encoded globals are
        // reliable.
        risc0_zkp::verify::verify(&CIRCUIT, &suite, &self.seal, check_code)?;

        // Extract the globals from the seal. The verifier call above has already read
        // seal[..CircuitImpl::OUTPUT_SIZE + 1] via read_field_elem_slice, so these
        // output words are valid BabyBear elements.
        let output_elems = self.seal[..CircuitImpl::OUTPUT_SIZE].iter().copied().map(BabyBearElem::new_raw);
        let mut seal_claim = VecDeque::from_iter(output_elems.map(|elem| elem.as_u32()));

        // Read the Poseidon2 control root digest from the first 16 words of the output.
        // NOTE: Implemented recursion programs have two output slots, each of size 16 elems.
        // A SHA2 digest is encoded as 16 half words. Poseidon digests are encoded in 8 elems,
        // but are interspersed with padding to fill out the whole 16 elems.
        let control_root: Digest = seal_claim
            .drain(0..16)
            .enumerate()
            .filter_map(|(i, word)| (i & 1 == 0).then_some(word))
            .collect::<Vec<_>>()
            .try_into()
            .map_err(|_| VerificationError::ReceiptFormatError)?;

        // Verify that the globals also only contain allowed control roots.
        if control_root != ALLOWED_CONTROL_ROOT {
            return Err(VerificationError::ControlVerificationError { control_id: control_root })?;
        }

        // Verify the output hash matches that data
        // The seal claim is now reliable so we need to ensure that the computed claim is equal
        // to what has been claimed in the receipt.
        let output_hash = read_sha_halfs(&mut seal_claim).map_err(|_| VerificationError::ReceiptFormatError)?;
        if output_hash != self.claim {
            return Err(VerificationError::JournalDigestMismatch)?;
        }

        // Everything passed
        Ok(())
    }
}

#[cfg(test)]
mod tests {
    use risc0_binfmt::tagged_struct;
    use risc0_circuit_recursion::{CircuitImpl, control_id::ALLOWED_CONTROL_ROOT};
    use risc0_zkp::adapter::{CircuitInfo, PROOF_SYSTEM_INFO};
    use risc0_zkp::core::{
        digest::digest,
        hash::sha::{Impl, Sha256},
    };
    // Check that the verifier parameters has a stable digest (and therefore a stable value). This
    // struct encodes parameters used in verification, and so this value should be updated if and
    // only if a change to the verifier parameters is expected. Updating the verifier parameters
    // will result in incompatibility with previous versions.
    #[test]
    fn succinct_receipt_verifier_parameters_is_stable() {
        assert_eq!(
            tagged_struct::<Impl>(
                "risc0.SuccinctReceiptVerifierParameters",
                &[
                    ALLOWED_CONTROL_ROOT,
                    ALLOWED_CONTROL_ROOT,
                    *Impl::hash_bytes(&PROOF_SYSTEM_INFO.0),
                    *Impl::hash_bytes(&CircuitImpl::CIRCUIT_INFO.0),
                ],
                &[],
            ),
            digest!("ece5e9b8ae2cd6ea6b1827b464ff0348f9a7f4decd269c0087fdfd75098da013")
        );
    }
}