Use batch validation for Sapling proofs and signatures

This commit is contained in:
Jack Grigg 2022-07-04 17:33:07 +00:00
parent 5d9ae0ba63
commit 90f13641b9
11 changed files with 334 additions and 57 deletions

26
Cargo.lock generated
View File

@ -120,9 +120,9 @@ checksum = "cf9ff0bbfd639f15c74af777d81383cf53efb7c93613f6cab67c6c11e05bbf8b"
[[package]]
name = "bellman"
version = "0.13.0"
version = "0.13.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0d96d7f4f3dc9a699bdef1d19648f6f20ef966b51892d224582a4475be669cb5"
checksum = "a4dd656ef4fdf7debb6d87d4dd92642fcbcdb78cbf6600c13e25c87e4d1a3807"
dependencies = [
"bitvec",
"blake2s_simd",
@ -1641,6 +1641,22 @@ dependencies = [
"zeroize",
]
[[package]]
name = "redjubjub"
version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6039ff156887caf92df308cbaccdc058c9d3155a913da046add6e48c4cdbd91d"
dependencies = [
"blake2b_simd",
"byteorder",
"digest 0.9.0",
"jubjub",
"rand_core 0.6.3",
"serde",
"thiserror",
"zeroize",
]
[[package]]
name = "redox_syscall"
version = "0.2.13"
@ -2320,9 +2336,9 @@ dependencies = [
[[package]]
name = "zcash_proofs"
version = "0.7.0"
version = "0.7.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "92c0928a33ab146a4e93e2a1141ce589de1bbe93c90219965dcccadc15f4e4e8"
checksum = "98bf5f6af051dd929263f279b21b9c04c1f30116c70f3c190de2566677f245ef"
dependencies = [
"bellman",
"blake2b_simd",
@ -2334,6 +2350,8 @@ dependencies = [
"jubjub",
"lazy_static",
"rand_core 0.6.3",
"redjubjub",
"tracing",
"zcash_primitives",
]

View File

@ -56,7 +56,7 @@ zcash_encoding = "0.1"
zcash_history = "0.3"
zcash_note_encryption = "0.1"
zcash_primitives = { version = "0.7", features = ["transparent-inputs"] }
zcash_proofs = "0.7"
zcash_proofs = "0.7.1"
ed25519-zebra = "3"
zeroize = "1.4.2"

View File

@ -7,6 +7,12 @@ description = "The cryptographic code in this crate has been reviewed for correc
[criteria.license-reviewed]
description = "The license of this crate has been reviewed for compatibility with its usage in this repository. If the crate is not available under the MIT license, `contrib/debian/copyright` has been updated with a corresponding copyright notice for files under `depends/*/vendored-sources/CRATE_NAME`."
[[audits.bellman]]
who = "Jack Grigg <jack@z.cash>"
criteria = ["crypto-reviewed", "safe-to-deploy"]
delta = "0.13.0 -> 0.13.1"
notes = "Adds multi-threaded batch validation, which I checked against the existing single-threaded batch validation."
[[audits.equihash]]
who = "Jack Grigg <jack@z.cash>"
criteria = "safe-to-deploy"
@ -151,3 +157,9 @@ criteria = ["crypto-reviewed", "safe-to-deploy"]
delta = "0.6.0 -> 0.7.0"
notes = "The ECC core team maintains this crate, and we have reviewed every line."
[[audits.zcash_proofs]]
who = "Jack Grigg <jack@z.cash>"
criteria = ["crypto-reviewed", "safe-to-deploy"]
delta = "0.7.0 -> 0.7.1"
notes = "The ECC core team maintains this crate, and we have reviewed every line."

View File

@ -632,6 +632,10 @@ criteria = "safe-to-deploy"
version = "0.3.0"
criteria = "safe-to-deploy"
[[unaudited.redjubjub]]
version = "0.5.0"
criteria = "safe-to-deploy"
[[unaudited.redox_syscall]]
version = "0.2.13"
criteria = "safe-to-deploy"

View File

@ -11,6 +11,7 @@
#include <librustzcash.h>
#include <rust/ed25519.h>
#include <rust/sapling.h>
#include <rust/orchard.h>
// Subclass of CTransaction which doesn't call UpdateHash when constructing
@ -534,6 +535,7 @@ TEST(ChecktransactionTests, BadTxnsPrevoutNull) {
TEST(ContextualCheckShieldedInputsTest, BadTxnsInvalidJoinsplitSignature) {
SelectParams(CBaseChainParams::REGTEST);
auto consensus = Params().GetConsensus();
std::optional<rust::Box<sapling::BatchValidator>> saplingAuth = std::nullopt;
auto orchardAuth = orchard::AuthValidator::Disabled();
CMutableTransaction mtx = GetValidTransaction();
@ -551,20 +553,21 @@ TEST(ContextualCheckShieldedInputsTest, BadTxnsInvalidJoinsplitSignature) {
// during initial block download, for transactions being accepted into the
// mempool (and thus not mined), DoS ban score should be zero, else 10
EXPECT_CALL(state, DoS(0, false, REJECT_INVALID, "bad-txns-invalid-joinsplit-signature", false, "")).Times(1);
ContextualCheckShieldedInputs(tx, txdata, state, view, orchardAuth, consensus, 0, false, false, [](const Consensus::Params&) { return true; });
ContextualCheckShieldedInputs(tx, txdata, state, view, saplingAuth, orchardAuth, consensus, 0, false, false, [](const Consensus::Params&) { return true; });
EXPECT_CALL(state, DoS(10, false, REJECT_INVALID, "bad-txns-invalid-joinsplit-signature", false, "")).Times(1);
ContextualCheckShieldedInputs(tx, txdata, state, view, orchardAuth, consensus, 0, false, false, [](const Consensus::Params&) { return false; });
ContextualCheckShieldedInputs(tx, txdata, state, view, saplingAuth, orchardAuth, consensus, 0, false, false, [](const Consensus::Params&) { return false; });
// for transactions that have been mined in a block, DoS ban score should
// always be 100.
EXPECT_CALL(state, DoS(100, false, REJECT_INVALID, "bad-txns-invalid-joinsplit-signature", false, "")).Times(1);
ContextualCheckShieldedInputs(tx, txdata, state, view, orchardAuth, consensus, 0, false, true, [](const Consensus::Params&) { return true; });
ContextualCheckShieldedInputs(tx, txdata, state, view, saplingAuth, orchardAuth, consensus, 0, false, true, [](const Consensus::Params&) { return true; });
EXPECT_CALL(state, DoS(100, false, REJECT_INVALID, "bad-txns-invalid-joinsplit-signature", false, "")).Times(1);
ContextualCheckShieldedInputs(tx, txdata, state, view, orchardAuth, consensus, 0, false, true, [](const Consensus::Params&) { return false; });
ContextualCheckShieldedInputs(tx, txdata, state, view, saplingAuth, orchardAuth, consensus, 0, false, true, [](const Consensus::Params&) { return false; });
}
TEST(ContextualCheckShieldedInputsTest, JoinsplitSignatureDetectsOldBranchId) {
SelectParams(CBaseChainParams::REGTEST);
auto consensus = Params().GetConsensus();
std::optional<rust::Box<sapling::BatchValidator>> saplingAuth = std::nullopt;
auto orchardAuth = orchard::AuthValidator::Disabled();
auto saplingBranchId = NetworkUpgradeInfo[Consensus::UPGRADE_SAPLING].nBranchId;
@ -585,7 +588,7 @@ TEST(ContextualCheckShieldedInputsTest, JoinsplitSignatureDetectsOldBranchId) {
CCoinsViewCache view(&baseView);
// Ensure that the transaction validates against Sapling.
EXPECT_TRUE(ContextualCheckShieldedInputs(
tx, txdata, state, view, orchardAuth, consensus, saplingBranchId, false, false,
tx, txdata, state, view, saplingAuth, orchardAuth, consensus, saplingBranchId, false, false,
[](const Consensus::Params&) { return false; }));
// Attempt to validate the inputs against Blossom. We should be notified
@ -597,7 +600,7 @@ TEST(ContextualCheckShieldedInputsTest, JoinsplitSignatureDetectsOldBranchId) {
HexInt(saplingBranchId)),
false, "")).Times(1);
EXPECT_FALSE(ContextualCheckShieldedInputs(
tx, txdata, state, view, orchardAuth, consensus, blossomBranchId, false, false,
tx, txdata, state, view, saplingAuth, orchardAuth, consensus, blossomBranchId, false, false,
[](const Consensus::Params&) { return false; }));
// Attempt to validate the inputs against Heartwood. All we should learn is
@ -607,13 +610,14 @@ TEST(ContextualCheckShieldedInputsTest, JoinsplitSignatureDetectsOldBranchId) {
10, false, REJECT_INVALID,
"bad-txns-invalid-joinsplit-signature", false, "")).Times(1);
EXPECT_FALSE(ContextualCheckShieldedInputs(
tx, txdata, state, view, orchardAuth, consensus, heartwoodBranchId, false, false,
tx, txdata, state, view, saplingAuth, orchardAuth, consensus, heartwoodBranchId, false, false,
[](const Consensus::Params&) { return false; }));
}
TEST(ContextualCheckShieldedInputsTest, NonCanonicalEd25519Signature) {
SelectParams(CBaseChainParams::REGTEST);
auto consensus = Params().GetConsensus();
std::optional<rust::Box<sapling::BatchValidator>> saplingAuth = std::nullopt;
auto orchardAuth = orchard::AuthValidator::Disabled();
AssumeShieldedInputsExistAndAreSpendable baseView;
@ -631,7 +635,7 @@ TEST(ContextualCheckShieldedInputsTest, NonCanonicalEd25519Signature) {
CTransaction tx(mtx);
const PrecomputedTransactionData txdata(tx, allPrevOutputs);
MockCValidationState state;
EXPECT_TRUE(ContextualCheckShieldedInputs(tx, txdata, state, view, orchardAuth, consensus, saplingBranchId, false, true));
EXPECT_TRUE(ContextualCheckShieldedInputs(tx, txdata, state, view, saplingAuth, orchardAuth, consensus, saplingBranchId, false, true));
}
// Copied from libsodium/crypto_sign/ed25519/ref10/open.c
@ -655,15 +659,15 @@ TEST(ContextualCheckShieldedInputsTest, NonCanonicalEd25519Signature) {
// during initial block download, for transactions being accepted into the
// mempool (and thus not mined), DoS ban score should be zero, else 10
EXPECT_CALL(state, DoS(0, false, REJECT_INVALID, "bad-txns-invalid-joinsplit-signature", false, "")).Times(1);
ContextualCheckShieldedInputs(tx, txdata, state, view, orchardAuth, consensus, saplingBranchId, false, false, [](const Consensus::Params&) { return true; });
ContextualCheckShieldedInputs(tx, txdata, state, view, saplingAuth, orchardAuth, consensus, saplingBranchId, false, false, [](const Consensus::Params&) { return true; });
EXPECT_CALL(state, DoS(10, false, REJECT_INVALID, "bad-txns-invalid-joinsplit-signature", false, "")).Times(1);
ContextualCheckShieldedInputs(tx, txdata, state, view, orchardAuth, consensus, saplingBranchId, false, false, [](const Consensus::Params&) { return false; });
ContextualCheckShieldedInputs(tx, txdata, state, view, saplingAuth, orchardAuth, consensus, saplingBranchId, false, false, [](const Consensus::Params&) { return false; });
// for transactions that have been mined in a block, DoS ban score should
// always be 100.
EXPECT_CALL(state, DoS(100, false, REJECT_INVALID, "bad-txns-invalid-joinsplit-signature", false, "")).Times(1);
ContextualCheckShieldedInputs(tx, txdata, state, view, orchardAuth, consensus, saplingBranchId, false, true, [](const Consensus::Params&) { return true; });
ContextualCheckShieldedInputs(tx, txdata, state, view, saplingAuth, orchardAuth, consensus, saplingBranchId, false, true, [](const Consensus::Params&) { return true; });
EXPECT_CALL(state, DoS(100, false, REJECT_INVALID, "bad-txns-invalid-joinsplit-signature", false, "")).Times(1);
ContextualCheckShieldedInputs(tx, txdata, state, view, orchardAuth, consensus, saplingBranchId, false, true, [](const Consensus::Params&) { return false; });
ContextualCheckShieldedInputs(tx, txdata, state, view, saplingAuth, orchardAuth, consensus, saplingBranchId, false, true, [](const Consensus::Params&) { return false; });
}
TEST(ChecktransactionTests, OverwinterConstructors) {
@ -1321,20 +1325,23 @@ TEST(ChecktransactionTests, HeartwoodEnforcesSaplingRulesOnShieldedCoinbase) {
// Coinbase transaction should pass contextual checks.
EXPECT_TRUE(ContextualCheckTransaction(tx, state, chainparams, 10, 57));
std::optional<rust::Box<sapling::BatchValidator>> saplingAuth = sapling::init_batch_validator();
auto orchardAuth = orchard::AuthValidator::Disabled();
auto heartwoodBranchId = NetworkUpgradeInfo[Consensus::UPGRADE_HEARTWOOD].nBranchId;
// Coinbase transaction does not pass shielded input checks, as bindingSig
// consensus rule is enforced.
// consensus rule is enforced. ContextualCheckShieldedInputs passes because
// the rest of the input checks pass, but saplingAuth fails when it attempts
// to validate the batch of signatures that includes bindingSig.
// - Note that coinbase txs don't have a previous output corresponding to
// their transparent input; ZIP 244 handles this by making the coinbase
// sighash the txid.
PrecomputedTransactionData txdata(tx, {});
AssumeShieldedInputsExistAndAreSpendable baseView;
CCoinsViewCache view(&baseView);
EXPECT_CALL(state, DoS(100, false, REJECT_INVALID, "bad-txns-sapling-binding-signature-invalid", false, "")).Times(1);
EXPECT_FALSE(ContextualCheckShieldedInputs(
tx, txdata, state, view, orchardAuth, chainparams.GetConsensus(), heartwoodBranchId, false, true));
EXPECT_TRUE(ContextualCheckShieldedInputs(
tx, txdata, state, view, saplingAuth, orchardAuth, chainparams.GetConsensus(), heartwoodBranchId, false, true));
EXPECT_FALSE(saplingAuth.value()->validate());
RegtestDeactivateHeartwood();
}

View File

@ -47,7 +47,6 @@
#include <rust/ed25519.h>
#include <rust/metrics.h>
#include <rust/sapling.h>
using namespace std;
@ -1246,6 +1245,7 @@ bool ContextualCheckShieldedInputs(
const PrecomputedTransactionData& txdata,
CValidationState &state,
const CCoinsViewCache &view,
std::optional<rust::Box<sapling::BatchValidator>>& saplingAuth,
std::optional<orchard::AuthValidator>& orchardAuth,
const Consensus::Params& consensus,
uint32_t consensusBranchId,
@ -1318,17 +1318,16 @@ bool ContextualCheckShieldedInputs(
if (!tx.vShieldedSpend.empty() ||
!tx.vShieldedOutput.empty())
{
auto ctx = sapling::init_verifier();
auto assembler = sapling::new_bundle_assembler();
for (const SpendDescription &spend : tx.vShieldedSpend) {
if (!ctx->check_spend(
if (!assembler->add_spend(
spend.cv.GetRawBytes(),
spend.anchor.GetRawBytes(),
spend.nullifier.GetRawBytes(),
spend.rk.GetRawBytes(),
spend.zkproof,
spend.spendAuthSig,
dataToBeSigned.GetRawBytes()
spend.spendAuthSig
)) {
return state.DoS(
dosLevelPotentiallyRelaxing,
@ -1338,10 +1337,12 @@ bool ContextualCheckShieldedInputs(
}
for (const OutputDescription &output : tx.vShieldedOutput) {
if (!ctx->check_output(
if (!assembler->add_output(
output.cv.GetRawBytes(),
output.cmu.GetRawBytes(),
output.ephemeralKey.GetRawBytes(),
output.encCiphertext,
output.outCiphertext,
output.zkproof
)) {
// This should be a non-contextual check, but we check it here
@ -1352,15 +1353,19 @@ bool ContextualCheckShieldedInputs(
}
}
if (!ctx->final_check(
auto bundle = sapling::finish_bundle_assembly(
std::move(assembler),
tx.GetValueBalanceSapling(),
tx.bindingSig,
dataToBeSigned.GetRawBytes()
)) {
return state.DoS(
dosLevelPotentiallyRelaxing,
error("ContextualCheckShieldedInputs(): Sapling binding signature invalid"),
REJECT_INVALID, "bad-txns-sapling-binding-signature-invalid");
tx.bindingSig);
// Queue Sapling bundle to be batch-validated. This also checks some consensus rules.
if (saplingAuth.has_value()) {
if (!saplingAuth.value()->check_bundle(std::move(bundle), dataToBeSigned.GetRawBytes())) {
return state.DoS(
dosLevelPotentiallyRelaxing,
error("ContextualCheckShieldedInputs(): Sapling bundle invalid"),
REJECT_INVALID, "bad-txns-sapling-bundle-invalid");
}
}
}
@ -1986,6 +1991,11 @@ bool AcceptToMemoryPool(
__func__, hash.ToString(), FormatStateMessage(state));
}
// This will be a single-transaction batch, which will be more efficient
// than unbatched if the transaction contains at least one Sapling Spend
// or at least two Sapling Outputs.
std::optional<rust::Box<sapling::BatchValidator>> saplingAuth = sapling::init_batch_validator();
// This will be a single-transaction batch, which is still more efficient as every
// Orchard bundle contains at least two signatures.
std::optional<orchard::AuthValidator> orchardAuth = orchard::AuthValidator::Batch();
@ -1996,6 +2006,7 @@ bool AcceptToMemoryPool(
txdata,
state,
view,
saplingAuth,
orchardAuth,
chainparams.GetConsensus(),
consensusBranchId,
@ -2005,7 +2016,11 @@ bool AcceptToMemoryPool(
return false;
}
// Check Orchard bundle authorizations. `orchardAuth` here is known to be non-null
// Check Sapling and Orchard bundle authorizations.
// `saplingAuth` and `orchardAuth` are known here to be non-null.
if (!saplingAuth.value()->validate()) {
return state.DoS(100, false, REJECT_INVALID, "bad-sapling-bundle-authorization");
}
if (!orchardAuth.value().Validate()) {
return state.DoS(100, false, REJECT_INVALID, "bad-orchard-bundle-authorization");
}
@ -3059,7 +3074,9 @@ bool ConnectBlock(const CBlock& block, CValidationState& state, CBlockIndex* pin
// proof verification is expensive, disable if possible
auto verifier = fExpensiveChecks ? ProofVerifier::Strict() : ProofVerifier::Disabled();
// Disable Orchard batch signature validation if possible.
// Disable Sapling and Orchard batch validation if possible.
std::optional<rust::Box<sapling::BatchValidator>> saplingAuth = fExpensiveChecks ?
std::optional(sapling::init_batch_validator()) : std::nullopt;
std::optional<orchard::AuthValidator> orchardAuth = fExpensiveChecks ?
orchard::AuthValidator::Batch() : orchard::AuthValidator::Disabled();
@ -3302,6 +3319,7 @@ bool ConnectBlock(const CBlock& block, CValidationState& state, CBlockIndex* pin
txdata.back(),
state,
view,
saplingAuth,
orchardAuth,
chainparams.GetConsensus(),
consensusBranchId,
@ -3502,6 +3520,13 @@ bool ConnectBlock(const CBlock& block, CValidationState& state, CBlockIndex* pin
block.vtx[0].GetValueOut(), blockReward),
REJECT_INVALID, "bad-cb-amount");
// Ensure Sapling authorizations are valid (if we are checking them)
if (saplingAuth.has_value() && !saplingAuth.value()->validate()) {
return state.DoS(100,
error("ConnectBlock(): a Sapling bundle within the block is invalid"),
REJECT_INVALID, "bad-sapling-bundle-authorization");
}
// Ensure Orchard signatures are valid (if we are checking them)
if (orchardAuth.has_value() && !orchardAuth.value().Validate()) {
return state.DoS(100,

View File

@ -43,6 +43,7 @@
#include <utility>
#include <vector>
#include <rust/sapling.h>
#include <rust/orchard.h>
#include <boost/unordered_map.hpp>
@ -402,6 +403,7 @@ bool ContextualCheckShieldedInputs(
const PrecomputedTransactionData& txdata,
CValidationState &state,
const CCoinsViewCache &view,
std::optional<rust::Box<sapling::BatchValidator>>& saplingAuth,
std::optional<orchard::AuthValidator>& orchardAuth,
const Consensus::Params& consensus,
uint32_t consensusBranchId,

View File

@ -19,7 +19,7 @@
// See https://github.com/rust-lang/rfcs/pull/2585 for more background.
#![allow(clippy::not_unsafe_ptr_arg_deref)]
use bellman::groth16::{Parameters, PreparedVerifyingKey};
use bellman::groth16::{self, prepare_verifying_key, Parameters, PreparedVerifyingKey};
use blake2s_simd::Params as Blake2sParams;
use bls12_381::Bls12;
use group::{cofactor::CofactorGroup, GroupEncoding};
@ -88,8 +88,8 @@ mod test_harness_ffi;
mod tests;
static PROOF_PARAMETERS_LOADED: Once = Once::new();
static mut SAPLING_SPEND_VK: Option<PreparedVerifyingKey<Bls12>> = None;
static mut SAPLING_OUTPUT_VK: Option<PreparedVerifyingKey<Bls12>> = None;
static mut SAPLING_SPEND_VK: Option<groth16::VerifyingKey<Bls12>> = None;
static mut SAPLING_OUTPUT_VK: Option<groth16::VerifyingKey<Bls12>> = None;
static mut SPROUT_GROTH16_VK: Option<PreparedVerifyingKey<Bls12>> = None;
static mut SAPLING_SPEND_PARAMS: Option<Parameters<Bls12>> = None;
@ -179,6 +179,11 @@ pub extern "C" fn librustzcash_init_zksnark_params(
let sapling_spend_params = params.spend_params;
let sapling_output_params = params.output_params;
// We need to clone these because we aren't necessarily storing the proving
// parameters in memory.
let sapling_spend_vk = sapling_spend_params.vk.clone();
let sapling_output_vk = sapling_output_params.vk.clone();
// Generate Orchard parameters.
info!(target: "main", "Loading Orchard parameters");
let orchard_pk = load_proving_keys.then(orchard::circuit::ProvingKey::build);
@ -191,8 +196,8 @@ pub extern "C" fn librustzcash_init_zksnark_params(
SAPLING_OUTPUT_PARAMS = load_proving_keys.then(|| sapling_output_params);
SPROUT_GROTH16_PARAMS_PATH = sprout_path.map(|p| p.to_owned());
SAPLING_SPEND_VK = Some(params.spend_vk);
SAPLING_OUTPUT_VK = Some(params.output_vk);
SAPLING_SPEND_VK = Some(sapling_spend_vk);
SAPLING_OUTPUT_VK = Some(sapling_output_vk);
SPROUT_GROTH16_VK = params.sprout_vk;
ORCHARD_PK = orchard_pk;
@ -845,7 +850,7 @@ pub extern "C" fn librustzcash_sapling_spend_proof(
anchor,
merkle_path,
unsafe { SAPLING_SPEND_PARAMS.as_ref() }.unwrap(),
unsafe { SAPLING_SPEND_VK.as_ref() }.unwrap(),
&prepare_verifying_key(unsafe { SAPLING_SPEND_VK.as_ref() }.unwrap()),
)
.expect("proving should not fail");

View File

@ -7,14 +7,19 @@
// on the entire module.
#![allow(clippy::too_many_arguments)]
use bellman::groth16::Proof;
use bellman::groth16::{prepare_verifying_key, Proof};
use group::GroupEncoding;
use rand_core::OsRng;
use zcash_note_encryption::EphemeralKeyBytes;
use zcash_primitives::{
sapling::redjubjub::{self, Signature},
transaction::components::Amount,
sapling::{
redjubjub::{self, Signature},
Nullifier,
},
transaction::components::{sapling, Amount},
};
use zcash_proofs::sapling::SaplingVerificationContext;
use zcash_proofs::sapling::{self as sapling_proofs, SaplingVerificationContext};
use super::GROTH_PROOF_SIZE;
use super::{de_ct, SAPLING_OUTPUT_VK, SAPLING_SPEND_VK};
@ -22,11 +27,38 @@ use super::{de_ct, SAPLING_OUTPUT_VK, SAPLING_SPEND_VK};
#[cxx::bridge(namespace = "sapling")]
mod ffi {
extern "Rust" {
type Bundle;
type BundleAssembler;
fn new_bundle_assembler() -> Box<BundleAssembler>;
fn add_spend(
self: &mut BundleAssembler,
cv: &[u8; 32],
anchor: &[u8; 32],
nullifier: [u8; 32],
rk: &[u8; 32],
zkproof: [u8; 192], // GROTH_PROOF_SIZE
spend_auth_sig: &[u8; 64],
) -> bool;
fn add_output(
self: &mut BundleAssembler,
cv: &[u8; 32],
cmu: &[u8; 32],
ephemeral_key: [u8; 32],
enc_ciphertext: [u8; 580],
out_ciphertext: [u8; 80],
zkproof: [u8; 192], // GROTH_PROOF_SIZE
) -> bool;
fn finish_bundle_assembly(
assembler: Box<BundleAssembler>,
value_balance: i64,
binding_sig: [u8; 64],
) -> Box<Bundle>;
type Verifier;
fn init_verifier() -> Box<Verifier>;
fn check_spend(
&mut self,
self: &mut Verifier,
cv: &[u8; 32],
anchor: &[u8; 32],
nullifier: &[u8; 32],
@ -36,21 +68,139 @@ mod ffi {
sighash_value: &[u8; 32],
) -> bool;
fn check_output(
&mut self,
self: &mut Verifier,
cv: &[u8; 32],
cm: &[u8; 32],
ephemeral_key: &[u8; 32],
zkproof: &[u8; 192], // GROTH_PROOF_SIZE
) -> bool;
fn final_check(
&self,
self: &Verifier,
value_balance: i64,
binding_sig: &[u8; 64],
sighash_value: &[u8; 32],
) -> bool;
type BatchValidator;
fn init_batch_validator() -> Box<BatchValidator>;
fn check_bundle(self: &mut BatchValidator, bundle: Box<Bundle>, sighash: [u8; 32]) -> bool;
fn validate(self: &mut BatchValidator) -> bool;
}
}
struct Bundle(sapling::Bundle<sapling::Authorized>);
struct BundleAssembler {
shielded_spends: Vec<sapling::SpendDescription<sapling::Authorized>>,
shielded_outputs: Vec<sapling::OutputDescription<[u8; 192]>>, // GROTH_PROOF_SIZE
}
fn new_bundle_assembler() -> Box<BundleAssembler> {
Box::new(BundleAssembler {
shielded_spends: vec![],
shielded_outputs: vec![],
})
}
impl BundleAssembler {
fn add_spend(
self: &mut BundleAssembler,
cv: &[u8; 32],
anchor: &[u8; 32],
nullifier: [u8; 32],
rk: &[u8; 32],
zkproof: [u8; 192], // GROTH_PROOF_SIZE
spend_auth_sig: &[u8; 64],
) -> bool {
// Deserialize the value commitment
let cv = match de_ct(jubjub::ExtendedPoint::from_bytes(cv)) {
Some(p) => p,
None => return false,
};
// Deserialize the anchor, which should be an element
// of Fr.
let anchor = match de_ct(bls12_381::Scalar::from_bytes(anchor)) {
Some(a) => a,
None => return false,
};
// Deserialize rk
let rk = match redjubjub::PublicKey::read(&rk[..]) {
Ok(p) => p,
Err(_) => return false,
};
// Deserialize the signature
let spend_auth_sig = match Signature::read(&spend_auth_sig[..]) {
Ok(sig) => sig,
Err(_) => return false,
};
self.shielded_spends.push(sapling::SpendDescription {
cv,
anchor,
nullifier: Nullifier(nullifier),
rk,
zkproof,
spend_auth_sig,
});
true
}
fn add_output(
self: &mut BundleAssembler,
cv: &[u8; 32],
cm: &[u8; 32],
ephemeral_key: [u8; 32],
enc_ciphertext: [u8; 580],
out_ciphertext: [u8; 80],
zkproof: [u8; 192], // GROTH_PROOF_SIZE
) -> bool {
// Deserialize the value commitment
let cv = match de_ct(jubjub::ExtendedPoint::from_bytes(cv)) {
Some(p) => p,
None => return false,
};
// Deserialize the commitment, which should be an element
// of Fr.
let cmu = match de_ct(bls12_381::Scalar::from_bytes(cm)) {
Some(a) => a,
None => return false,
};
self.shielded_outputs.push(sapling::OutputDescription {
cv,
cmu,
ephemeral_key: EphemeralKeyBytes(ephemeral_key),
enc_ciphertext,
out_ciphertext,
zkproof,
});
true
}
}
#[allow(clippy::boxed_local)]
fn finish_bundle_assembly(
assembler: Box<BundleAssembler>,
value_balance: i64,
binding_sig: [u8; 64],
) -> Box<Bundle> {
let value_balance = Amount::from_i64(value_balance).expect("parsed elsewhere");
let binding_sig = redjubjub::Signature::read(&binding_sig[..]).expect("parsed elsewhere");
Box::new(Bundle(sapling::Bundle {
shielded_spends: assembler.shielded_spends,
shielded_outputs: assembler.shielded_outputs,
value_balance,
authorization: sapling::Authorized { binding_sig },
}))
}
struct Verifier(SaplingVerificationContext);
fn init_verifier() -> Box<Verifier> {
@ -110,7 +260,7 @@ impl Verifier {
sighash_value,
spend_auth_sig,
zkproof,
unsafe { SAPLING_SPEND_VK.as_ref() }.unwrap(),
&prepare_verifying_key(unsafe { SAPLING_SPEND_VK.as_ref() }.unwrap()),
)
}
fn check_output(
@ -134,7 +284,7 @@ impl Verifier {
};
// Deserialize the ephemeral key
let ephemeral_key = match de_ct(jubjub::ExtendedPoint::from_bytes(ephemeral_key)) {
let epk = match de_ct(jubjub::ExtendedPoint::from_bytes(ephemeral_key)) {
Some(p) => p,
None => return false,
};
@ -148,9 +298,9 @@ impl Verifier {
self.0.check_output(
cv,
cm,
ephemeral_key,
epk,
zkproof,
unsafe { SAPLING_OUTPUT_VK.as_ref() }.unwrap(),
&prepare_verifying_key(unsafe { SAPLING_OUTPUT_VK.as_ref() }.unwrap()),
)
}
fn final_check(
@ -174,3 +324,43 @@ impl Verifier {
.final_check(value_balance, sighash_value, binding_sig)
}
}
struct BatchValidator(Option<sapling_proofs::BatchValidator>);
fn init_batch_validator() -> Box<BatchValidator> {
Box::new(BatchValidator(Some(sapling_proofs::BatchValidator::new())))
}
impl BatchValidator {
/// Checks the bundle against Sapling-specific consensus rules, and queues its
/// authorization for validation.
///
/// Returns `false` if the bundle doesn't satisfy the checked consensus rules. This
/// `BatchValidator` can continue to be used regardless, but some or all of the proofs
/// and signatures from this bundle may have already been added to the batch even if
/// it fails other consensus rules.
///
/// `sighash` must be for the transaction this bundle is within.
#[allow(clippy::boxed_local)]
fn check_bundle(&mut self, bundle: Box<Bundle>, sighash: [u8; 32]) -> bool {
if let Some(validator) = &mut self.0 {
validator.check_bundle(bundle.0, sighash)
} else {
tracing::error!("sapling::BatchValidator has already been used");
false
}
}
fn validate(&mut self) -> bool {
if let Some(validator) = self.0.take() {
validator.validate(
unsafe { SAPLING_SPEND_VK.as_ref() }.unwrap(),
unsafe { SAPLING_OUTPUT_VK.as_ref() }.unwrap(),
OsRng,
)
} else {
tracing::error!("sapling::BatchValidator has already been used");
false
}
}
}

View File

@ -36,6 +36,7 @@
#include <boost/test/data/test_case.hpp>
#include <rust/ed25519.h>
#include <rust/sapling.h>
#include <rust/orchard.h>
#include <univalue.h>
@ -356,6 +357,7 @@ void test_simple_sapling_invalidity(uint32_t consensusBranchId, CMutableTransact
void test_simple_joinsplit_invalidity(uint32_t consensusBranchId, CMutableTransaction tx)
{
auto verifier = ProofVerifier::Strict();
std::optional<rust::Box<sapling::BatchValidator>> saplingAuth = std::nullopt;
auto orchardAuth = orchard::AuthValidator::Disabled();
{
// Ensure that empty vin/vout remain invalid without
@ -390,7 +392,13 @@ void test_simple_joinsplit_invalidity(uint32_t consensusBranchId, CMutableTransa
BOOST_CHECK(CheckTransactionWithoutProofVerification(newTx, state));
BOOST_CHECK(ContextualCheckTransaction(newTx, state, Params(), 0, true));
BOOST_CHECK(!ContextualCheckShieldedInputs(newTx, txdata, state, view, orchardAuth, Params().GetConsensus(), consensusBranchId, false, true));
BOOST_CHECK(!ContextualCheckShieldedInputs(
newTx, txdata,
state, view,
saplingAuth, orchardAuth,
Params().GetConsensus(),
consensusBranchId,
false, true));
BOOST_CHECK(state.GetRejectReason() == "bad-txns-invalid-joinsplit-signature");
// Empty output script.
@ -406,7 +414,13 @@ void test_simple_joinsplit_invalidity(uint32_t consensusBranchId, CMutableTransa
state = CValidationState();
BOOST_CHECK(CheckTransactionWithoutProofVerification(newTx, state));
BOOST_CHECK(ContextualCheckTransaction(newTx, state, Params(), 0, true));
BOOST_CHECK(ContextualCheckShieldedInputs(newTx, txdata, state, view, orchardAuth, Params().GetConsensus(), consensusBranchId, false, true));
BOOST_CHECK(ContextualCheckShieldedInputs(
newTx, txdata,
state, view,
saplingAuth, orchardAuth,
Params().GetConsensus(),
consensusBranchId,
false, true));
BOOST_CHECK_EQUAL(state.GetRejectReason(), "");
}
{

View File

@ -92,7 +92,7 @@ public:
std::array<uint8_t, WIDTH> GetRawBytes() const
{
std::array<uint8_t, WIDTH> buf = {};
memcpy(buf.data(), this->begin(), WIDTH);
std::memcpy(buf.data(), this->begin(), WIDTH);
return buf;
}