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:
- The pre-computed
payload_digest(32 bytes) — the guest does not need the full payload - The
rest_preimage— the guest hashes this to getrest_digestand parses it for output data
Why the split matters
The V1 split design benefits the ZK guest:
-
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. -
Output and input extraction: The
rest_preimagecontains both outputs and inputs. The guest extracts output SPKs for deposit verification and first-input outpoints for source authorization. Since the guest reads the fullrest_preimageand computesrest_digestitself (never trusting a host-provided digest), all parsed data is tamper-proof. -
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:
| Version | Witness type | Host provides | Guest computes |
|---|---|---|---|
| V0 | PrevTxV0Witness | Full preimage | blake2b(preimage) |
| V1 | PrevTxV1Witness | rest_preimage + payload_digest | blake3(payload_digest || blake3(rest)) |
Kaspa compatibility
Both implementations are tested against Kaspa’s canonical hashers:
tx_id_v0matcheskaspa_hashes::TransactionIDpayload_digestmatcheskaspa_hashes::PayloadDigestrest_digestmatcheskaspa_hashes::TransactionResttx_id_v1matcheskaspa_hashes::TransactionV1Id
See core/src/lib.rs tests for the compatibility assertions.