Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Appendix B: Transaction ID Schemes

Kaspa supports two transaction ID formats. The rollup guest must handle both to verify previous transaction outputs.

V0: BLAKE2b full preimage

V0 transactions compute their ID by hashing the entire serialized transaction:

#![allow(unused)]
fn main() {
/// Compute V0 transaction ID using blake2b.
/// This uses the same domain separator as Kaspa's TransactionID hasher.
pub fn tx_id_v0(preimage: &[u8]) -> [u32; 8] {
    const DOMAIN_SEP: &[u8] = b"TransactionID";

    let hash = blake2b_simd::Params::new().hash_length(32).key(DOMAIN_SEP).hash(preimage);

    let mut out = [0u32; 8];
    bytemuck::bytes_of_mut(&mut out).copy_from_slice(hash.as_bytes());
    out
}
}

The domain key "TransactionID" matches Kaspa’s kaspa_hashes::TransactionID hasher exactly. The full transaction bytes (version, inputs, outputs, locktime, subnetwork, gas, payload) are hashed in one pass.

Verification in the guest:

#![allow(unused)]
fn main() {
    /// Compute the tx_id from the preimage
    pub fn compute_tx_id(&self) -> [u32; 8] {
        crate::tx_id_v0(&self.preimage)
    }
}

The host provides the full transaction preimage. The guest hashes it, compares against the claimed tx_id, then parses the preimage to extract the output SPK at the claimed index.

V1: BLAKE3 split digest

V1 transactions split the hash into two components:

tx_id = blake3_keyed("TransactionV1Id", payload_digest || rest_digest)

where:

  • payload_digest = blake3_keyed("PayloadDigest", payload_bytes)
  • rest_digest = blake3_keyed("TransactionRest", rest_preimage)
#![allow(unused)]
fn main() {
pub fn payload_digest(payload: &[u32]) -> [u32; 8] {
    payload_digest_bytes(bytemuck::cast_slice(payload))
}

pub fn payload_digest_bytes(payload: &[u8]) -> [u32; 8] {
    const DOMAIN_SEP: &[u8] = b"PayloadDigest";
    const KEY: [u8; blake3::KEY_LEN] = domain_to_key(DOMAIN_SEP);

    let mut out = [0u32; 8];
    bytemuck::bytes_of_mut(&mut out).copy_from_slice(blake3::keyed_hash(&KEY, payload).as_bytes());
    out
}
}
#![allow(unused)]
fn main() {
pub fn tx_id_v1(payload_digest: &[u32; 8], rest_digest: &[u32; 8]) -> [u32; 8] {
    const DOMAIN_SEP: &[u8] = b"TransactionV1Id";
    const KEY: [u8; blake3::KEY_LEN] = domain_to_key(DOMAIN_SEP);

    let mut hasher = blake3::Hasher::new_keyed(&KEY);
    hasher.update(bytemuck::cast_slice(payload_digest));
    hasher.update(bytemuck::cast_slice(rest_digest));
    let mut out = [0u32; 8];
    bytemuck::bytes_of_mut(&mut out).copy_from_slice(hasher.finalize().as_bytes());
    out
}
}

Verification in the guest:

#![allow(unused)]
fn main() {
    /// Compute the tx_id from rest_preimage and payload_digest
    pub fn compute_tx_id(&self) -> [u32; 8] {
        let rest_digest = crate::rest_digest_bytes(self.rest_preimage.as_bytes());
        crate::tx_id_v1(&self.payload_digest, &rest_digest)
    }
}

The host provides:

  1. The pre-computed payload_digest (32 bytes) — the guest does not need the full payload
  2. The rest_preimage — the guest hashes this to get rest_digest and parses it for output data

Why the split matters

The V1 split design benefits the ZK guest:

  1. Smaller witness: For previous transaction verification, the guest only needs payload_digest (32 bytes) instead of the full payload (variable, potentially large). The payload contains the action data which the guest already has from block processing.

  2. Output and input extraction: The rest_preimage contains both outputs and inputs. The guest extracts output SPKs for deposit verification and first-input outpoints for source authorization. Since the guest reads the full rest_preimage and computes rest_digest itself (never trusting a host-provided digest), all parsed data is tamper-proof.

  3. Efficient hashing: BLAKE3 is faster than BLAKE2b in the RISC-V guest, and the split allows partial preimage reuse.

Witness structures

The PrevTxWitness enum handles both versions:

VersionWitness typeHost providesGuest computes
V0PrevTxV0WitnessFull preimageblake2b(preimage)
V1PrevTxV1Witnessrest_preimage + payload_digestblake3(payload_digest || blake3(rest))

Kaspa compatibility

Both implementations are tested against Kaspa’s canonical hashers:

  • tx_id_v0 matches kaspa_hashes::TransactionID
  • payload_digest matches kaspa_hashes::PayloadDigest
  • rest_digest matches kaspa_hashes::TransactionRest
  • tx_id_v1 matches kaspa_hashes::TransactionV1Id

See core/src/lib.rs tests for the compatibility assertions.