From 0b8a348c2b45cdda91a11350a8c131f935891370 Mon Sep 17 00:00:00 2001 From: Jack Grigg Date: Sat, 12 Jun 2021 16:40:16 +0100 Subject: [PATCH] ZIP 244 signature digests --- src/rust/include/rust/transaction.h | 30 ++++++++ src/rust/src/transaction_ffi.rs | 110 +++++++++++++++++++++++++++- src/script/interpreter.cpp | 75 ++++++++++++++++--- src/script/interpreter.h | 5 ++ src/test/transaction_tests.cpp | 75 +++++++++++++++++++ 5 files changed, 284 insertions(+), 11 deletions(-) diff --git a/src/rust/include/rust/transaction.h b/src/rust/include/rust/transaction.h index 84ba51965..b36d1d32e 100644 --- a/src/rust/include/rust/transaction.h +++ b/src/rust/include/rust/transaction.h @@ -12,6 +12,9 @@ extern "C" { #endif +struct PrecomputedTxParts; +typedef struct PrecomputedTxParts PrecomputedTxParts; + /// Calculates identifying and authorizing digests for the given transaction. /// /// If either `txid_ret` or `authDigest_ret` is `nullptr`, the corresponding @@ -24,6 +27,33 @@ bool zcash_transaction_digests( unsigned char* txid_ret, unsigned char* authDigest_ret); +/// Precomputes data for calculating signature digests from the given +/// transaction. +/// +/// Please free this with `zcash_transaction_precomputed_free` when you are +/// done. +/// +/// Returns `nullptr` if the transaction is invalid, or a v1-v4 transaction format. +PrecomputedTxParts* zcash_transaction_precomputed_init( + const unsigned char* txBytes, + size_t txBytes_len); + +/// Frees a precomputed transaction from `zcash_transaction_precomputed_init`. +void zcash_transaction_precomputed_free(PrecomputedTxParts* preTx); + +/// Calculates a signature digest for the given transparent input. +/// +/// Returns `false` if any of the parameters are invalid; in this case, +/// `sighash_ret` will be unaltered. +bool zcash_transaction_transparent_signature_digest( + const PrecomputedTxParts* preTx, + uint32_t sighashType, + size_t index, + const unsigned char* scriptCode, + size_t scriptCode_len, + int64_t value, + unsigned char* sighash_ret); + #ifdef __cplusplus } #endif diff --git a/src/rust/src/transaction_ffi.rs b/src/rust/src/transaction_ffi.rs index e33211808..1794c887c 100644 --- a/src/rust/src/transaction_ffi.rs +++ b/src/rust/src/transaction_ffi.rs @@ -1,10 +1,17 @@ -use std::slice; +use std::{ptr, slice}; +use blake2b_simd::Hash; use libc::{c_uchar, size_t}; use tracing::error; use zcash_primitives::{ consensus::BranchId, - transaction::{Transaction, TxVersion}, + legacy::Script, + transaction::{ + components::Amount, + sighash::{signature_hash, SignableInput}, + txid::TxIdDigester, + Transaction, TxDigests, TxVersion, + }, }; /// Calculates identifying and authorizing digests for the given transaction. @@ -45,3 +52,102 @@ pub extern "C" fn zcash_transaction_digests( true } + +pub struct PrecomputedTxParts { + tx: Transaction, + txid_parts: TxDigests, +} + +/// Precomputes the `TxDigest` struct for the given transaction. +/// +/// Returns `nullptr` if the transaction is invalid, or a v1-v4 transaction format. +#[no_mangle] +pub extern "C" fn zcash_transaction_precomputed_init( + tx_bytes: *const c_uchar, + tx_bytes_len: size_t, +) -> *mut PrecomputedTxParts { + let tx_bytes = unsafe { slice::from_raw_parts(tx_bytes, tx_bytes_len) }; + + // We use a placeholder branch ID here, since it is not used for anything. + let tx = match Transaction::read(tx_bytes, BranchId::Canopy) { + Ok(tx) => tx, + Err(e) => { + error!("Failed to parse transaction: {}", e); + return ptr::null_mut(); + } + }; + + match tx.version() { + TxVersion::Sprout(_) | TxVersion::Overwinter | TxVersion::Sapling => { + // We don't support these legacy transaction formats in this API. + ptr::null_mut() + } + _ => { + let txid_parts = tx.digest(TxIdDigester); + Box::into_raw(Box::new(PrecomputedTxParts { tx, txid_parts })) + } + } +} + +/// Frees a precomputed transaction from `zcash_transaction_precomputed_init`. +#[no_mangle] +pub extern "C" fn zcash_transaction_precomputed_free(precomputed_tx: *mut PrecomputedTxParts) { + if !precomputed_tx.is_null() { + drop(unsafe { Box::from_raw(precomputed_tx) }); + } +} + +/// Calculates a signature digest for the given transaction. +/// +/// Returns `false` if any of the parameters are invalid; in this case, `sighash_ret` +/// will be unaltered. +#[no_mangle] +pub extern "C" fn zcash_transaction_transparent_signature_digest( + precomputed_tx: *const PrecomputedTxParts, + hash_type: u32, + index: size_t, + script_code: *const c_uchar, + script_code_len: size_t, + value: i64, + sighash_ret: *mut [u8; 32], +) -> bool { + let precomputed_tx = if let Some(res) = unsafe { precomputed_tx.as_ref() } { + res + } else { + error!("Invalid precomputed transaction"); + return false; + }; + let script_code = + match Script::read(unsafe { slice::from_raw_parts(script_code, script_code_len) }) { + Ok(res) => res, + Err(e) => { + error!("Invalid scriptCode: {}", e); + return false; + } + }; + let value = match Amount::from_i64(value) { + Ok(res) => res, + Err(()) => { + error!("Invalid amount"); + return false; + } + }; + + let signable_input = SignableInput::transparent(index, &script_code, value); + + let sighash = match precomputed_tx.tx.version() { + TxVersion::Sprout(_) | TxVersion::Overwinter | TxVersion::Sapling => { + // We don't support these legacy transaction formats in this API. + return false; + } + _ => signature_hash( + &precomputed_tx.tx, + hash_type, + &signable_input, + &precomputed_tx.txid_parts, + ), + }; + + *unsafe { &mut *sighash_ret } = *sighash.as_ref(); + true +} diff --git a/src/script/interpreter.cpp b/src/script/interpreter.cpp index 6f5cb12af..27d073b03 100644 --- a/src/script/interpreter.cpp +++ b/src/script/interpreter.cpp @@ -15,6 +15,7 @@ #include "uint256.h" #include +#include using namespace std; @@ -1127,20 +1128,41 @@ uint256 GetShieldedOutputsHash(const CTransaction& txTo) { } // anon namespace -PrecomputedTransactionData::PrecomputedTransactionData(const CTransaction& txTo) +PrecomputedTransactionData::PrecomputedTransactionData(const CTransaction& txTo) : + preTx(nullptr, zcash_transaction_precomputed_free) { - hashPrevouts = GetPrevoutHash(txTo); - hashSequence = GetSequenceHash(txTo); - hashOutputs = GetOutputsHash(txTo); - hashJoinSplits = GetJoinSplitsHash(txTo); - hashShieldedSpends = GetShieldedSpendsHash(txTo); - hashShieldedOutputs = GetShieldedOutputsHash(txTo); + bool isOverwinterV3 = + txTo.fOverwintered && + txTo.nVersionGroupId == OVERWINTER_VERSION_GROUP_ID && + txTo.nVersion == OVERWINTER_TX_VERSION; + bool isSaplingV4 = + txTo.fOverwintered && + txTo.nVersionGroupId == SAPLING_VERSION_GROUP_ID && + txTo.nVersion == SAPLING_TX_VERSION; + + if (!txTo.fOverwintered || isOverwinterV3 || isSaplingV4) { + hashPrevouts = GetPrevoutHash(txTo); + hashSequence = GetSequenceHash(txTo); + hashOutputs = GetOutputsHash(txTo); + hashJoinSplits = GetJoinSplitsHash(txTo); + hashShieldedSpends = GetShieldedSpendsHash(txTo); + hashShieldedOutputs = GetShieldedOutputsHash(txTo); + } else { + // TODO: If we already have this serialized, use it. + CDataStream ss(SER_NETWORK, PROTOCOL_VERSION); + ss << txTo; + preTx.reset(zcash_transaction_precomputed_init( + reinterpret_cast(ss.data()), + ss.size())); + } } SigVersion SignatureHashVersion(const CTransaction& txTo) { if (txTo.fOverwintered) { - if (txTo.nVersionGroupId == SAPLING_VERSION_GROUP_ID) { + if (txTo.nVersionGroupId == ZIP225_VERSION_GROUP_ID) { + return SIGVERSION_ZIP244; + } else if (txTo.nVersionGroupId == SAPLING_VERSION_GROUP_ID) { return SIGVERSION_SAPLING; } else { return SIGVERSION_OVERWINTER; @@ -1166,7 +1188,42 @@ uint256 SignatureHash( auto sigversion = SignatureHashVersion(txTo); - if (sigversion == SIGVERSION_OVERWINTER || sigversion == SIGVERSION_SAPLING) { + if (sigversion == SIGVERSION_ZIP244) { + // The consensusBranchId parameter is ignored; we use the value stored + // in the transaction itself. + if (nIn == NOT_AN_INPUT) { + // The signature digest is just the txid! No need to cross the FFI. + return txTo.GetHash(); + } else { + CDataStream sScriptCode(SER_NETWORK, PROTOCOL_VERSION); + sScriptCode << *(CScriptBase*)(&scriptCode); + + if (cache) { + uint256 hash; + zcash_transaction_transparent_signature_digest( + cache->preTx.get(), + nHashType, + nIn, + reinterpret_cast(sScriptCode.data()), + sScriptCode.size(), + amount, + hash.begin()); + return hash; + } else { + PrecomputedTransactionData local(txTo); + uint256 hash; + zcash_transaction_transparent_signature_digest( + local.preTx.get(), + nHashType, + nIn, + reinterpret_cast(sScriptCode.data()), + sScriptCode.size(), + amount, + hash.begin()); + return hash; + } + } + } else if (sigversion == SIGVERSION_OVERWINTER || sigversion == SIGVERSION_SAPLING) { uint256 hashPrevouts; uint256 hashSequence; uint256 hashOutputs; diff --git a/src/script/interpreter.h b/src/script/interpreter.h index e88528cf5..c9c21d8c8 100644 --- a/src/script/interpreter.h +++ b/src/script/interpreter.h @@ -9,6 +9,8 @@ #include "script_error.h" #include "primitives/transaction.h" +#include + #include #include #include @@ -93,6 +95,8 @@ bool CheckSignatureEncoding(const std::vector &vchSig, unsigned i struct PrecomputedTransactionData { uint256 hashPrevouts, hashSequence, hashOutputs, hashJoinSplits, hashShieldedSpends, hashShieldedOutputs; + /** Precomputed transaction parts. */ + std::unique_ptr preTx; PrecomputedTransactionData(const CTransaction& tx); }; @@ -102,6 +106,7 @@ enum SigVersion SIGVERSION_SPROUT = 0, SIGVERSION_OVERWINTER = 1, SIGVERSION_SAPLING = 2, + SIGVERSION_ZIP244 = 3, }; uint256 SignatureHash( diff --git a/src/test/transaction_tests.cpp b/src/test/transaction_tests.cpp index e4d6244b0..c8fc29849 100644 --- a/src/test/transaction_tests.cpp +++ b/src/test/transaction_tests.cpp @@ -872,6 +872,81 @@ BOOST_AUTO_TEST_CASE(TxV5) // ZIP 244: Check the transaction digests. BOOST_CHECK_EQUAL(tx.GetHash().GetHex(), test[1].getValStr()); BOOST_CHECK_EQUAL(tx.GetAuthDigest().GetHex(), test[2].getValStr()); + + // ZIP 244: Check the signature digests. + unsigned int nIn = NOT_AN_INPUT; + if (!test[3].isNull()) { + nIn = test[3].get_int(); + } + + CScript scriptCode; + if (!test[4].isNull()) { + auto scriptCodeBytes = ParseHex(test[4].get_str()); + scriptCode = CScript(scriptCodeBytes.begin(), scriptCodeBytes.end()); + } + + CAmount amount; + if (!test[5].isNull()) { + amount = test[5].get_int64(); + } + + BOOST_CHECK_EQUAL( + SignatureHash( + scriptCode, tx, nIn, + SIGHASH_ALL, + amount, tx.GetConsensusBranchId() + ).GetHex(), + test[6].getValStr()); + + if (!test[7].isNull()) { + BOOST_CHECK_EQUAL( + SignatureHash( + scriptCode, tx, nIn, + SIGHASH_NONE, + amount, tx.GetConsensusBranchId() + ).GetHex(), + test[7].getValStr()); + } + + if (!test[8].isNull()) { + BOOST_CHECK_EQUAL( + SignatureHash( + scriptCode, tx, nIn, + SIGHASH_SINGLE, + amount, tx.GetConsensusBranchId() + ).GetHex(), + test[8].getValStr()); + } + + if (!test[9].isNull()) { + BOOST_CHECK_EQUAL( + SignatureHash( + scriptCode, tx, nIn, + SIGHASH_ALL | SIGHASH_ANYONECANPAY, + amount, tx.GetConsensusBranchId() + ).GetHex(), + test[9].getValStr()); + } + + if (!test[10].isNull()) { + BOOST_CHECK_EQUAL( + SignatureHash( + scriptCode, tx, nIn, + SIGHASH_NONE | SIGHASH_ANYONECANPAY, + amount, tx.GetConsensusBranchId() + ).GetHex(), + test[10].getValStr()); + } + + if (!test[11].isNull()) { + BOOST_CHECK_EQUAL( + SignatureHash( + scriptCode, tx, nIn, + SIGHASH_SINGLE | SIGHASH_ANYONECANPAY, + amount, tx.GetConsensusBranchId() + ).GetHex(), + test[11].getValStr()); + } } }