Auto merge of #5215 - str4d:4983-zip-244, r=str4d

ZIP 244 transaction and signature digests

Closes zcash/zcash#4983.
This commit is contained in:
Homu 2021-06-14 16:53:22 +00:00
commit 3f50ffc3f8
21 changed files with 1142 additions and 544 deletions

View File

@ -1,3 +1,10 @@
# "Coins" view
TBD
## Notes
- This is the main context in which `CTxOut::IsNull()` is used. The other is a
single spot in the mempool code. Once we've backported the
[per-txout CoinsDB](https://github.com/bitcoin/bitcoin/pull/10195) we can
hopefully eliminate this method.

View File

@ -36,6 +36,14 @@ TEST(CheckBlock, VersionTooLow) {
}
// Subclass of CTransaction which doesn't call UpdateHash when constructing
// from a CMutableTransaction. This enables us to create a CTransaction
// with bad values which normally trigger an exception during construction.
class UNSAFE_CTransaction : public CTransaction {
public:
UNSAFE_CTransaction(const CMutableTransaction &tx) : CTransaction(tx, true) {}
};
// Test that a Sprout tx with negative version is still rejected
// by CheckBlock under Sprout consensus rules.
TEST(CheckBlock, BlockSproutRejectsBadVersion) {
@ -55,7 +63,8 @@ TEST(CheckBlock, BlockSproutRejectsBadVersion) {
mtx.nVersion = -1;
mtx.nVersionGroupId = 0;
CTransaction tx {mtx};
EXPECT_THROW((CTransaction(mtx)), std::ios_base::failure);
UNSAFE_CTransaction tx {mtx};
CBlock block;
block.vtx.push_back(tx);

View File

@ -11,6 +11,14 @@
#include <librustzcash.h>
#include <rust/ed25519.h>
// Subclass of CTransaction which doesn't call UpdateHash when constructing
// from a CMutableTransaction. This enables us to create a CTransaction
// with bad values which normally trigger an exception during construction.
class UNSAFE_CTransaction : public CTransaction {
public:
UNSAFE_CTransaction(const CMutableTransaction &tx) : CTransaction(tx, true) {}
};
TEST(ChecktransactionTests, CheckVpubNotBothNonzero) {
CMutableTransaction tx;
tx.nVersion = 2;
@ -124,7 +132,8 @@ TEST(ChecktransactionTests, BadVersionTooLow) {
CMutableTransaction mtx = GetValidTransaction();
mtx.nVersion = 0;
CTransaction tx(mtx);
EXPECT_THROW((CTransaction(mtx)), std::ios_base::failure);
UNSAFE_CTransaction tx(mtx);
MockCValidationState state;
EXPECT_CALL(state, DoS(100, false, REJECT_INVALID, "bad-txns-version-too-low", false)).Times(1);
CheckTransactionWithoutProofVerification(tx, state);
@ -273,7 +282,8 @@ TEST(ChecktransactionTests, BadTxnsVoutNegative) {
CMutableTransaction mtx = GetValidTransaction();
mtx.vout[0].nValue = -1;
CTransaction tx(mtx);
EXPECT_THROW((CTransaction(mtx)), std::ios_base::failure);
UNSAFE_CTransaction tx(mtx);
MockCValidationState state;
EXPECT_CALL(state, DoS(100, false, REJECT_INVALID, "bad-txns-vout-negative", false)).Times(1);
@ -284,7 +294,8 @@ TEST(ChecktransactionTests, BadTxnsVoutToolarge) {
CMutableTransaction mtx = GetValidTransaction();
mtx.vout[0].nValue = MAX_MONEY + 1;
CTransaction tx(mtx);
EXPECT_THROW((CTransaction(mtx)), std::ios_base::failure);
UNSAFE_CTransaction tx(mtx);
MockCValidationState state;
EXPECT_CALL(state, DoS(100, false, REJECT_INVALID, "bad-txns-vout-toolarge", false)).Times(1);
@ -787,7 +798,8 @@ TEST(ChecktransactionTests, SproutTxVersionTooLow) {
mtx.fOverwintered = false;
mtx.nVersion = -1;
CTransaction tx(mtx);
EXPECT_THROW((CTransaction(mtx)), std::ios_base::failure);
UNSAFE_CTransaction tx(mtx);
MockCValidationState state;
EXPECT_CALL(state, DoS(100, false, REJECT_INVALID, "bad-txns-version-too-low", false)).Times(1);
CheckTransactionWithoutProofVerification(tx, state);
@ -795,14 +807,6 @@ TEST(ChecktransactionTests, SproutTxVersionTooLow) {
// Subclass of CTransaction which doesn't call UpdateHash when constructing
// from a CMutableTransaction. This enables us to create a CTransaction
// with bad values which normally trigger an exception during construction.
class UNSAFE_CTransaction : public CTransaction {
public:
UNSAFE_CTransaction(const CMutableTransaction &tx) : CTransaction(tx, true) {}
};
TEST(ChecktransactionTests, SaplingSproutInputSumsTooLarge) {
CMutableTransaction mtx = GetValidTransaction();
mtx.vJoinSplit.resize(0);
@ -866,6 +870,7 @@ TEST(ChecktransactionTests, OverwinterVersionNumberLow) {
mtx.nVersionGroupId = OVERWINTER_VERSION_GROUP_ID;
mtx.nExpiryHeight = 0;
EXPECT_THROW((CTransaction(mtx)), std::ios_base::failure);
UNSAFE_CTransaction tx(mtx);
MockCValidationState state;
EXPECT_CALL(state, DoS(100, false, REJECT_INVALID, "bad-tx-overwinter-version-too-low", false)).Times(1);
@ -884,6 +889,7 @@ TEST(ChecktransactionTests, OverwinterVersionNumberHigh) {
mtx.nVersionGroupId = OVERWINTER_VERSION_GROUP_ID;
mtx.nExpiryHeight = 0;
EXPECT_THROW((CTransaction(mtx)), std::ios_base::failure);
UNSAFE_CTransaction tx(mtx);
MockCValidationState state;
EXPECT_CALL(state, DoS(100, false, REJECT_INVALID, "bad-tx-overwinter-version-too-high", false)).Times(1);
@ -903,6 +909,7 @@ TEST(ChecktransactionTests, OverwinterBadVersionGroupId) {
mtx.nExpiryHeight = 0;
mtx.nVersionGroupId = 0x12345678;
EXPECT_THROW((CTransaction(mtx)), std::ios_base::failure);
UNSAFE_CTransaction tx(mtx);
MockCValidationState state;
EXPECT_CALL(state, DoS(100, false, REJECT_INVALID, "bad-tx-version-group-id", false)).Times(1);
@ -1250,7 +1257,8 @@ TEST(ChecktransactionTests, HeartwoodEnforcesSaplingRulesOnShieldedCoinbase) {
// out of range.
{
mtx.valueBalance = MAX_MONEY + 1;
CTransaction tx(mtx);
EXPECT_THROW((CTransaction(mtx)), std::ios_base::failure);
UNSAFE_CTransaction tx(mtx);
EXPECT_TRUE(tx.IsCoinBase());
MockCValidationState state;
@ -1259,7 +1267,8 @@ TEST(ChecktransactionTests, HeartwoodEnforcesSaplingRulesOnShieldedCoinbase) {
}
{
mtx.valueBalance = -MAX_MONEY - 1;
CTransaction tx(mtx);
EXPECT_THROW((CTransaction(mtx)), std::ios_base::failure);
UNSAFE_CTransaction tx(mtx);
EXPECT_TRUE(tx.IsCoinBase());
MockCValidationState state;

View File

@ -175,6 +175,14 @@ TEST(Mempool, SproutV3TxWhenOverwinterActive) {
}
// Subclass of CTransaction which doesn't call UpdateHash when constructing
// from a CMutableTransaction. This enables us to create a CTransaction
// with bad values which normally trigger an exception during construction.
class UNSAFE_CTransaction : public CTransaction {
public:
UNSAFE_CTransaction(const CMutableTransaction &tx) : CTransaction(tx, true) {}
};
// Sprout transaction with negative version, rejected by the mempool in CheckTransaction
// under Sprout consensus rules, should still be rejected under Overwinter consensus rules.
// 1. fails CheckTransaction (specifically CheckTransactionWithoutProofVerification)
@ -198,7 +206,8 @@ TEST(Mempool, SproutNegativeVersionTxWhenOverwinterActive) {
mtx.nVersion = -3;
EXPECT_EQ(mtx.nVersion, static_cast<int32_t>(0xfffffffd));
CTransaction tx1(mtx);
EXPECT_THROW((CTransaction(mtx)), std::ios_base::failure);
UNSAFE_CTransaction tx1(mtx);
EXPECT_EQ(tx1.nVersion, -3);
CValidationState state1;
@ -215,7 +224,8 @@ TEST(Mempool, SproutNegativeVersionTxWhenOverwinterActive) {
mtx.nVersion = static_cast<int32_t>((1 << 31) | 3);
EXPECT_EQ(mtx.nVersion, static_cast<int32_t>(0x80000003));
CTransaction tx1(mtx);
EXPECT_THROW((CTransaction(mtx)), std::ios_base::failure);
UNSAFE_CTransaction tx1(mtx);
EXPECT_EQ(tx1.nVersion, -2147483645);
CValidationState state1;

View File

@ -10,6 +10,11 @@
#include "utilstrencodings.h"
#include "crypto/common.h"
#include <algorithm>
const unsigned char ZCASH_AUTH_DATA_HASH_PERSONALIZATION[BLAKE2bPersonalBytes] =
{'Z','c','a','s','h','A','u','t','h','D','a','t','H','a','s','h'};
uint256 CBlockHeader::GetHash() const
{
return SerializeHash(*this);
@ -109,6 +114,53 @@ uint256 CBlock::CheckMerkleBranch(uint256 hash, const std::vector<uint256>& vMer
return hash;
}
// Return 0 if x == 0, otherwise the smallest power of 2 greater than or equal to x.
// Algorithm based on <https://graphics.stanford.edu/~seander/bithacks.html#RoundUpPowerOf2>.
static uint64_t next_pow2(uint64_t x)
{
x -= 1;
x |= (x >> 1);
x |= (x >> 2);
x |= (x >> 4);
x |= (x >> 8);
x |= (x >> 16);
x |= (x >> 32);
return x + 1;
}
uint256 CBlock::BuildAuthDataMerkleTree() const
{
std::vector<uint256> tree;
auto perfectSize = next_pow2(vtx.size());
assert((perfectSize & (perfectSize - 1)) == 0);
size_t expectedSize = std::max(perfectSize*2, (uint64_t)1) - 1; // The total number of nodes.
tree.reserve(expectedSize);
// Add the leaves to the tree. v1-v4 transactions will append empty leaves.
for (auto &tx : vtx) {
tree.push_back(tx.GetAuthDigest());
}
// Append empty leaves until we get a perfect tree.
tree.insert(tree.end(), perfectSize - vtx.size(), uint256());
assert(tree.size() == perfectSize);
int j = 0;
for (int layerWidth = perfectSize; layerWidth > 1; layerWidth = layerWidth / 2) {
for (int i = 0; i < layerWidth; i += 2) {
CBLAKE2bWriter ss(SER_GETHASH, 0, ZCASH_AUTH_DATA_HASH_PERSONALIZATION);
ss << tree[j + i];
ss << tree[j + i + 1];
tree.push_back(ss.GetHash());
}
// Move to the next layer.
j += layerWidth;
}
assert(tree.size() == expectedSize);
return (tree.empty() ? uint256() : tree.back());
}
std::string CBlock::ToString() const
{
std::stringstream s;

View File

@ -134,6 +134,11 @@ public:
std::vector<uint256> GetMerkleBranch(int nIndex) const;
static uint256 CheckMerkleBranch(uint256 hash, const std::vector<uint256>& vMerkleBranch, int nIndex);
// Build the authorizing data Merkle tree for this block and return its
// root.
uint256 BuildAuthDataMerkleTree() const;
std::string ToString() const;
};

View File

@ -9,6 +9,8 @@
#include "tinyformat.h"
#include "utilstrencodings.h"
#include <rust/transaction.h>
SaplingBundle::SaplingBundle(
const std::vector<SpendDescription>& vShieldedSpend,
const std::vector<OutputDescription>& vShieldedOutput,
@ -138,12 +140,48 @@ CMutableTransaction::CMutableTransaction(const CTransaction& tx) : nVersion(tx.n
uint256 CMutableTransaction::GetHash() const
{
return SerializeHash(*this);
CDataStream ss(SER_NETWORK, PROTOCOL_VERSION);
ss << *this;
uint256 hash;
if (!zcash_transaction_digests(
reinterpret_cast<const unsigned char*>(ss.data()),
ss.size(),
hash.begin(),
nullptr))
{
throw std::ios_base::failure("CMutableTransaction::GetHash: Invalid transaction format");
}
return hash;
}
uint256 CMutableTransaction::GetAuthDigest() const
{
CDataStream ss(SER_NETWORK, PROTOCOL_VERSION);
ss << *this;
uint256 authDigest;
if (!zcash_transaction_digests(
reinterpret_cast<const unsigned char*>(ss.data()),
ss.size(),
nullptr,
authDigest.begin()))
{
throw std::ios_base::failure("CMutableTransaction::GetAuthDigest: Invalid transaction format");
}
return authDigest;
}
void CTransaction::UpdateHash() const
{
*const_cast<uint256*>(&hash) = SerializeHash(*this);
CDataStream ss(SER_NETWORK, PROTOCOL_VERSION);
ss << *this;
if (!zcash_transaction_digests(
reinterpret_cast<const unsigned char*>(ss.data()),
ss.size(),
const_cast<uint256*>(&hash)->begin(),
const_cast<uint256*>(&authDigest)->begin()))
{
throw std::ios_base::failure("CTransaction::UpdateHash: Invalid transaction format");
}
}
CTransaction::CTransaction() : nVersion(CTransaction::SPROUT_MIN_CURRENT_VERSION),

View File

@ -714,6 +714,8 @@ private:
/** Memory only. */
const uint256 hash;
/** Memory only. */
const uint256 authDigest;
void UpdateHash() const;
protected:
@ -899,6 +901,15 @@ public:
return hash;
}
/**
* Returns the authorizing data commitment for this transaction.
*
* For v1-v4 transactions, this returns the null hash (i.e. all-zeroes).
*/
const uint256& GetAuthDigest() const {
return authDigest;
}
uint32_t GetHeader() const {
// When serializing v1 and v2, the 4 byte header is nVersion
uint32_t header = this->nVersion;
@ -1093,6 +1104,14 @@ struct CMutableTransaction
* fly, as opposed to GetHash() in CTransaction, which uses a cached result.
*/
uint256 GetHash() const;
/** Compute the authentication digest of this CMutableTransaction. This is
* computed on the fly, as opposed to GetAuthDigest() in CTransaction, which
* uses a cached result.
*
* For v1-v4 transactions, this returns the null hash (i.e. all-zeroes).
*/
uint256 GetAuthDigest() const;
};
#endif // BITCOIN_PRIMITIVES_TRANSACTION_H

View File

@ -0,0 +1,28 @@
// Copyright (c) 2020 The Zcash developers
// Distributed under the MIT software license, see the accompanying
// file COPYING or https://www.opensource.org/licenses/mit-license.php .
#ifndef ZCASH_RUST_INCLUDE_RUST_TEST_HARNESS_H
#define ZCASH_RUST_INCLUDE_RUST_TEST_HARNESS_H
#ifdef __cplusplus
extern "C" {
#endif
/// Generates a random Jubjub base field element.
///
/// `ret` must point to a 32-byte array.
void zcash_test_harness_random_jubjub_base(
unsigned char* ret);
/// Generates a random Jubjub point.
///
/// `ret` must point to a 32-byte array.
void zcash_test_harness_random_jubjub_point(
unsigned char* ret);
#ifdef __cplusplus
}
#endif
#endif // ZCASH_RUST_INCLUDE_RUST_TEST_HARNESS_H

View File

@ -0,0 +1,66 @@
// Copyright (c) 2020 The Zcash developers
// Distributed under the MIT software license, see the accompanying
// file COPYING or https://www.opensource.org/licenses/mit-license.php .
#ifndef ZCASH_RUST_INCLUDE_RUST_TRANSACTION_H
#define ZCASH_RUST_INCLUDE_RUST_TRANSACTION_H
#include <stddef.h>
#include <stdint.h>
#ifdef __cplusplus
extern "C" {
#endif
struct PrecomputedTxParts;
typedef struct PrecomputedTxParts PrecomputedTxParts;
/// Calculates identifying and authorizing digests for the given transaction.
///
/// `txid_ret` and `authDigest_ret` must either be `nullptr` or point to a
/// 32-byte array.
///
/// If either `txid_ret` or `authDigest_ret` is `nullptr`, the corresponding
/// digest will not be calculated.
///
/// Returns `false` if the transaction is invalid.
bool zcash_transaction_digests(
const unsigned char* txBytes,
size_t txBytes_len,
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.
///
/// `sighash_ret` must point to a 32-byte array.
///
/// 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
#endif // ZCASH_RUST_INCLUDE_RUST_TRANSACTION_H

View File

@ -70,6 +70,9 @@ mod streams_ffi;
mod tracing_ffi;
mod orchard_ffi;
mod transaction_ffi;
mod test_harness_ffi;
#[cfg(test)]
mod tests;

View File

@ -0,0 +1,13 @@
use group::{ff::Field, Group, GroupEncoding};
use jubjub::{ExtendedPoint, Fq};
use rand_core::OsRng;
#[no_mangle]
pub extern "C" fn zcash_test_harness_random_jubjub_base(ret: *mut [u8; 32]) {
*unsafe { &mut *ret } = Fq::random(OsRng).to_bytes();
}
#[no_mangle]
pub extern "C" fn zcash_test_harness_random_jubjub_point(ret: *mut [u8; 32]) {
*unsafe { &mut *ret } = ExtendedPoint::random(OsRng).to_bytes();
}

View File

@ -0,0 +1,155 @@
use std::{ptr, slice};
use blake2b_simd::Hash;
use libc::{c_uchar, size_t};
use tracing::error;
use zcash_primitives::{
consensus::BranchId,
legacy::Script,
transaction::{
components::Amount,
sighash::{signature_hash, SignableInput},
txid::TxIdDigester,
Transaction, TxDigests, TxVersion,
},
};
/// Calculates identifying and authorizing digests for the given transaction.
///
/// If either `txid_ret` or `auth_digest_ret` is `nullptr`, the corresponding digest will
/// not be calculated.
///
/// Returns `false` if the transaction is invalid.
#[no_mangle]
pub extern "C" fn zcash_transaction_digests(
tx_bytes: *const c_uchar,
tx_bytes_len: size_t,
txid_ret: *mut [u8; 32],
auth_digest_ret: *mut [u8; 32],
) -> bool {
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 false;
}
};
if let Some(txid_ret) = unsafe { txid_ret.as_mut() } {
*txid_ret = *tx.txid().as_ref();
}
if let Some(auth_digest_ret) = unsafe { auth_digest_ret.as_mut() } {
match tx.version() {
// Pre-NU5 transaction formats don't have authorizing data commitments; when
// included in the authDataCommitment tree, they use the null hash.
TxVersion::Sprout(_) | TxVersion::Overwinter | TxVersion::Sapling => {
*auth_digest_ret = [0; 32]
}
_ => auth_digest_ret.copy_from_slice(tx.auth_commitment().as_bytes()),
}
}
true
}
pub struct PrecomputedTxParts {
tx: Transaction,
txid_parts: TxDigests<Hash>,
}
/// 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
}

View File

@ -15,6 +15,7 @@
#include "uint256.h"
#include <librustzcash.h>
#include <rust/transaction.h>
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<const unsigned char*>(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<const unsigned char*>(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<const unsigned char*>(sScriptCode.data()),
sScriptCode.size(),
amount,
hash.begin());
return hash;
}
}
} else if (sigversion == SIGVERSION_OVERWINTER || sigversion == SIGVERSION_SAPLING) {
uint256 hashPrevouts;
uint256 hashSequence;
uint256 hashOutputs;

View File

@ -9,6 +9,8 @@
#include "script_error.h"
#include "primitives/transaction.h"
#include <rust/transaction.h>
#include <vector>
#include <stdint.h>
#include <string>
@ -93,6 +95,8 @@ bool CheckSignatureEncoding(const std::vector<unsigned char> &vchSig, unsigned i
struct PrecomputedTransactionData
{
uint256 hashPrevouts, hashSequence, hashOutputs, hashJoinSplits, hashShieldedSpends, hashShieldedOutputs;
/** Precomputed transaction parts. */
std::unique_ptr<PrecomputedTxParts, decltype(&zcash_transaction_precomputed_free)> 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(

File diff suppressed because one or more lines are too long

View File

@ -65,6 +65,10 @@ BOOST_DATA_TEST_CASE(multisig_verify, boost::unit_test::data::xrange(static_cast
txFrom.vout[0].scriptPubKey = a_and_b;
txFrom.vout[1].scriptPubKey = a_or_b;
txFrom.vout[2].scriptPubKey = escrow;
// Meaningless values, but we need them for the Rust code to parse this.
txFrom.vout[0].nValue = 10;
txFrom.vout[1].nValue = 10;
txFrom.vout[2].nValue = 10;
CMutableTransaction txTo[3]; // Spending transaction
for (int i = 0; i < 3; i++)
@ -205,6 +209,10 @@ BOOST_DATA_TEST_CASE(multisig_Sign, boost::unit_test::data::xrange(static_cast<i
txFrom.vout[0].scriptPubKey = a_and_b;
txFrom.vout[1].scriptPubKey = a_or_b;
txFrom.vout[2].scriptPubKey = escrow;
// Meaningless values, but we need them for the Rust code to parse this.
txFrom.vout[0].nValue = 10;
txFrom.vout[1].nValue = 10;
txFrom.vout[2].nValue = 10;
CMutableTransaction txTo[3]; // Spending transaction
for (int i = 0; i < 3; i++)

View File

@ -36,6 +36,8 @@ Verify(const CScript& scriptSig, const CScript& scriptPubKey, bool fStrict, Scri
CMutableTransaction txFrom;
txFrom.vout.resize(1);
txFrom.vout[0].scriptPubKey = scriptPubKey;
// Meaningless value, but we need it for the Rust code to parse this.
txFrom.vout[0].nValue = 10;
CMutableTransaction txTo;
txTo.vin.resize(1);
@ -336,6 +338,8 @@ BOOST_DATA_TEST_CASE(AreInputsStandard, boost::unit_test::data::xrange(static_ca
CMutableTransaction txTo;
txTo.vout.resize(1);
txTo.vout[0].scriptPubKey = GetScriptForDestination(key[1].GetPubKey().GetID());
// Meaningless value, but we need it for the Rust code to parse this.
txTo.vout[0].nValue = 10;
txTo.vin.resize(5);
for (int i = 0; i < 5; i++)

View File

@ -21,6 +21,7 @@
#include <boost/test/unit_test.hpp>
#include <rust/ed25519.h>
#include <rust/test_harness.h>
#include <univalue.h>
@ -116,7 +117,7 @@ void static RandomTransaction(CMutableTransaction &tx, bool fSingle, uint32_t co
tx.nVersionGroupId = OVERWINTER_VERSION_GROUP_ID;
tx.nVersion = overwinter_version_dist(rng);
}
tx.nExpiryHeight = (insecure_rand() % 2) ? insecure_rand() : 0;
tx.nExpiryHeight = (insecure_rand() % 2) ? insecure_rand() % TX_EXPIRY_HEIGHT_THRESHOLD : 0;
} else {
tx.nVersion = insecure_rand() & 0x7FFFFFFF;
}
@ -149,18 +150,18 @@ void static RandomTransaction(CMutableTransaction &tx, bool fSingle, uint32_t co
tx.valueBalance = insecure_rand() % 100000000;
for (int spend = 0; spend < shielded_spends; spend++) {
SpendDescription sdesc;
sdesc.cv = GetRandHash();
sdesc.anchor = GetRandHash();
zcash_test_harness_random_jubjub_point(sdesc.cv.begin());
zcash_test_harness_random_jubjub_base(sdesc.anchor.begin());
sdesc.nullifier = GetRandHash();
sdesc.rk = GetRandHash();
zcash_test_harness_random_jubjub_point(sdesc.rk.begin());
GetRandBytes(sdesc.zkproof.begin(), sdesc.zkproof.size());
tx.vShieldedSpend.push_back(sdesc);
}
for (int out = 0; out < shielded_outs; out++) {
OutputDescription odesc;
odesc.cv = GetRandHash();
odesc.cmu = GetRandHash();
odesc.ephemeralKey = GetRandHash();
zcash_test_harness_random_jubjub_point(odesc.cv.begin());
zcash_test_harness_random_jubjub_base(odesc.cmu.begin());
zcash_test_harness_random_jubjub_point(odesc.ephemeralKey.begin());
GetRandBytes(odesc.encCiphertext.begin(), odesc.encCiphertext.size());
GetRandBytes(odesc.outCiphertext.begin(), odesc.outCiphertext.size());
GetRandBytes(odesc.zkproof.begin(), odesc.zkproof.size());
@ -228,7 +229,8 @@ BOOST_AUTO_TEST_CASE(sighash_test)
#endif
for (int i=0; i<nRandomTests; i++) {
int nHashType = insecure_rand();
uint32_t consensusBranchId = NetworkUpgradeInfo[insecure_rand() % Consensus::MAX_NETWORK_UPGRADES].nBranchId;
// Exclude ZFUTURE as its branch ID can't be represented as a JSON int32
uint32_t consensusBranchId = NetworkUpgradeInfo[insecure_rand() % (Consensus::MAX_NETWORK_UPGRADES - 1)].nBranchId;
CMutableTransaction txTo;
RandomTransaction(txTo, (nHashType & 0x1f) == SIGHASH_SINGLE, consensusBranchId);
CScript scriptCode;
@ -291,7 +293,8 @@ BOOST_AUTO_TEST_CASE(sighash_from_data)
raw_script = test[1].get_str();
nIn = test[2].get_int();
nHashType = test[3].get_int();
consensusBranchId = test[4].get_int();
// JSON integers are signed, so parse uint32 as int64
consensusBranchId = test[4].get_int64();
sigHashHex = test[5].get_str();
uint256 sh;
@ -323,6 +326,9 @@ BOOST_AUTO_TEST_CASE(sighash_from_data)
std::vector<unsigned char> raw = ParseHex(raw_script);
scriptCode.insert(scriptCode.end(), raw.begin(), raw.end());
} catch (const std::exception& ex) {
BOOST_ERROR("Bad test, couldn't deserialize data: " << strTest << ": " << ex.what());
continue;
} catch (...) {
BOOST_ERROR("Bad test, couldn't deserialize data: " << strTest);
continue;

View File

@ -43,6 +43,14 @@
using namespace std;
// Subclass of CTransaction which doesn't call UpdateHash when constructing
// from a CMutableTransaction. This enables us to create a CTransaction
// with bad values which normally trigger an exception during construction.
class UNSAFE_CTransaction : public CTransaction {
public:
UNSAFE_CTransaction(const CMutableTransaction &tx) : CTransaction(tx, true) {}
};
BOOST_FIXTURE_TEST_SUITE(transaction_tests, JoinSplitTestingSetup)
BOOST_AUTO_TEST_CASE(tx_valid)
@ -188,7 +196,12 @@ BOOST_AUTO_TEST_CASE(tx_invalid)
string transaction = test[1].get_str();
CDataStream stream(ParseHex(transaction), SER_NETWORK, PROTOCOL_VERSION);
CTransaction tx;
stream >> tx;
try {
stream >> tx;
} catch (std::ios_base::failure) {
// Invalid transaction was caught at parse time by the Rust logic.
continue;
}
CValidationState state;
fValid = CheckTransaction(tx, state, verifier) && state.IsValid();
@ -463,23 +476,27 @@ void test_simple_joinsplit_invalidity(uint32_t consensusBranchId, CMutableTransa
JSDescription *jsdesc = &newTx.vJoinSplit[0];
jsdesc->vpub_old = -1;
BOOST_CHECK(!CheckTransaction(newTx, state, verifier));
BOOST_CHECK_THROW((CTransaction(newTx)), std::ios_base::failure);
BOOST_CHECK(!CheckTransaction(UNSAFE_CTransaction(newTx), state, verifier));
BOOST_CHECK(state.GetRejectReason() == "bad-txns-vpub_old-negative");
jsdesc->vpub_old = MAX_MONEY + 1;
BOOST_CHECK(!CheckTransaction(newTx, state, verifier));
BOOST_CHECK_THROW((CTransaction(newTx)), std::ios_base::failure);
BOOST_CHECK(!CheckTransaction(UNSAFE_CTransaction(newTx), state, verifier));
BOOST_CHECK(state.GetRejectReason() == "bad-txns-vpub_old-toolarge");
jsdesc->vpub_old = 0;
jsdesc->vpub_new = -1;
BOOST_CHECK(!CheckTransaction(newTx, state, verifier));
BOOST_CHECK_THROW((CTransaction(newTx)), std::ios_base::failure);
BOOST_CHECK(!CheckTransaction(UNSAFE_CTransaction(newTx), state, verifier));
BOOST_CHECK(state.GetRejectReason() == "bad-txns-vpub_new-negative");
jsdesc->vpub_new = MAX_MONEY + 1;
BOOST_CHECK(!CheckTransaction(newTx, state, verifier));
BOOST_CHECK_THROW((CTransaction(newTx)), std::ios_base::failure);
BOOST_CHECK(!CheckTransaction(UNSAFE_CTransaction(newTx), state, verifier));
BOOST_CHECK(state.GetRejectReason() == "bad-txns-vpub_new-toolarge");
jsdesc->vpub_new = (MAX_MONEY / 2) + 10;
@ -595,6 +612,8 @@ BOOST_DATA_TEST_CASE(test_Get, boost::unit_test::data::xrange(static_cast<int>(C
t1.vout.resize(2);
t1.vout[0].nValue = 90*CENT;
t1.vout[0].scriptPubKey << OP_1;
// Meaningless value, but we need it for the Rust code to parse this.
t1.vout[1].nValue = CENT;
BOOST_CHECK(AreInputsStandard(t1, coins, consensusBranchId));
BOOST_CHECK_EQUAL(coins.GetValueIn(t1), (50+21+22)*CENT);
@ -765,11 +784,14 @@ BOOST_AUTO_TEST_CASE(test_IsStandard)
// TX_NULL_DATA w/o PUSHDATA
t.vout.resize(1);
t.vout[0].nValue = 0; // Needed for Rust parser
t.vout[0].scriptPubKey = CScript() << OP_RETURN;
BOOST_CHECK(IsStandardTx(t, reason, chainparams));
// Only one TX_NULL_DATA permitted in all cases
t.vout.resize(2);
t.vout[0].nValue = 0; // Needed for Rust parser
t.vout[1].nValue = 0; // Needed for Rust parser
t.vout[0].scriptPubKey = CScript() << OP_RETURN << ParseHex("04678afdb0fe5548271967f1a67130b7105cd6a828e03909a67962e0ea1f61deb649f6bc3f4cef38");
t.vout[1].scriptPubKey = CScript() << OP_RETURN << ParseHex("04678afdb0fe5548271967f1a67130b7105cd6a828e03909a67962e0ea1f61deb649f6bc3f4cef38");
BOOST_CHECK(!IsStandardTx(t, reason, chainparams));
@ -868,6 +890,85 @@ BOOST_AUTO_TEST_CASE(TxV5)
CDataStream ss(SER_NETWORK, PROTOCOL_VERSION);
ss << tx;
BOOST_CHECK_EQUAL(HexStr(ss.begin(), ss.end()), transaction);
// 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());
}
}
}

View File

@ -15,6 +15,8 @@
#include "zcash/Note.hpp"
#include "zcash/NoteEncryption.hpp"
#include <rust/test_harness.h>
#include <optional>
using ::testing::Return;
@ -1665,6 +1667,7 @@ TEST(WalletTests, SetBestChainIgnoresTxsWithoutShieldedData) {
auto wtxTmp = GetValidSproutSpend(sk2, note, 5);
CMutableTransaction mtx {wtxTmp};
mtx.vout[0].scriptPubKey = scriptPubKey;
mtx.vout[0].nValue = CENT;
CWalletTx wtxSproutTransparent {nullptr, mtx};
wallet.AddToWallet(wtxSproutTransparent, true, nullptr);
@ -1674,7 +1677,7 @@ TEST(WalletTests, SetBestChainIgnoresTxsWithoutShieldedData) {
mtxSapling.nVersion = SAPLING_TX_VERSION;
mtxSapling.nVersionGroupId = SAPLING_VERSION_GROUP_ID;
mtxSapling.vShieldedOutput.resize(1);
mtxSapling.vShieldedOutput[0].cv = libzcash::random_uint256();
zcash_test_harness_random_jubjub_point(mtxSapling.vShieldedOutput[0].cv.begin());
CWalletTx wtxSapling {nullptr, mtxSapling};
SetSaplingNoteData(wtxSapling);
wallet.AddToWallet(wtxSapling, true, nullptr);
@ -1685,7 +1688,7 @@ TEST(WalletTests, SetBestChainIgnoresTxsWithoutShieldedData) {
mtxSaplingTransparent.nVersion = SAPLING_TX_VERSION;
mtxSaplingTransparent.nVersionGroupId = SAPLING_VERSION_GROUP_ID;
mtxSaplingTransparent.vShieldedOutput.resize(1);
mtxSaplingTransparent.vShieldedOutput[0].cv = libzcash::random_uint256();
zcash_test_harness_random_jubjub_point(mtxSaplingTransparent.vShieldedOutput[0].cv.begin());
CWalletTx wtxSaplingTransparent {nullptr, mtxSaplingTransparent};
wallet.AddToWallet(wtxSaplingTransparent, true, nullptr);