Skip to content

Commit

Permalink
feat: add range proof batch verification to validators (#4260)
Browse files Browse the repository at this point in the history
Description
---
Added batch verification of range proofs where ever more than one range proof needed to be verified.

Motivation and Context
---
Batch verification range proofs is much faster than running linear proofs

How Has This Been Tested?
---
Unit tests
Integration tests
_System-level tests pending_
  • Loading branch information
hansieodendaal authored Jul 7, 2022
1 parent f910cce commit 02d3121
Show file tree
Hide file tree
Showing 11 changed files with 188 additions and 25 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ use tari_comms::{
peer_manager::NodeId,
protocol::rpc::{RpcError, RpcStatus},
};
use tari_crypto::errors::RangeProofError;
use tari_mmr::error::MerkleMountainRangeError;
use thiserror::Error;
use tokio::task;
Expand All @@ -50,6 +51,8 @@ pub enum HorizonSyncError {
FinalStateValidationFailed(ValidationError),
#[error("Join error: {0}")]
JoinError(#[from] task::JoinError),
#[error("A range proof verification has produced an error: {0}")]
RangeProofError(#[from] RangeProofError),
#[error("Invalid kernel signature: {0}")]
InvalidKernelSignature(TransactionError),
#[error("MMR did not match for {mmr_tree} at height {at_height}. Expected {actual_hex} to equal {expected_hex}")]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,11 @@ use crate::{
SyncUtxosRequest,
SyncUtxosResponse,
},
transactions::transaction_components::{TransactionKernel, TransactionOutput},
transactions::transaction_components::{
transaction_output::batch_verify_range_proofs,
TransactionKernel,
TransactionOutput,
},
validation::{helpers, FinalHorizonStateValidation},
};

Expand Down Expand Up @@ -713,10 +717,8 @@ impl<'a, B: BlockchainBackend + 'static> HorizonStateSynchronization<'a, B> {
.map(|chunk| {
let prover = self.prover.clone();
task::spawn_blocking(move || -> Result<(), HorizonSyncError> {
for o in chunk {
o.verify_range_proof(&prover)
.map_err(|err| HorizonSyncError::InvalidRangeProof(o.hash().to_hex(), err.to_string()))?;
}
let outputs = chunk.iter().collect::<Vec<_>>();
batch_verify_range_proofs(&prover, &outputs)?;
Ok(())
})
})
Expand Down
7 changes: 3 additions & 4 deletions base_layer/core/src/blocks/genesis_block.rs
Original file line number Diff line number Diff line change
Expand Up @@ -318,7 +318,7 @@ mod test {
use crate::{
consensus::ConsensusManager,
test_helpers::blockchain::create_new_blockchain_with_network,
transactions::CryptoFactories,
transactions::{transaction_components::transaction_output::batch_verify_range_proofs, CryptoFactories},
validation::{ChainBalanceValidator, FinalHorizonStateValidation},
};

Expand All @@ -331,9 +331,8 @@ mod test {

let factories = CryptoFactories::default();
assert!(block.block().body.outputs().iter().any(|o| o.is_coinbase()));
for o in block.block().body.outputs() {
o.verify_range_proof(&factories.range_proof).unwrap();
}
let outputs = block.block().body.outputs().iter().collect::<Vec<_>>();
batch_verify_range_proofs(&factories.range_proof, &outputs).unwrap();
// Coinbase and faucet kernel
assert_eq!(
block.block().body.kernels().len() as u64,
Expand Down
6 changes: 3 additions & 3 deletions base_layer/core/src/transactions/aggregated_body.rs
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ use crate::{
crypto_factories::CryptoFactories,
tari_amount::MicroTari,
transaction_components::{
transaction_output::batch_verify_range_proofs,
KernelFeatures,
KernelSum,
OutputType,
Expand Down Expand Up @@ -478,9 +479,8 @@ impl AggregateBody {

fn validate_range_proofs(&self, range_proof_service: &RangeProofService) -> Result<(), TransactionError> {
trace!(target: LOG_TARGET, "Checking range proofs");
for o in &self.outputs {
o.verify_range_proof(range_proof_service)?;
}
let outputs = self.outputs.iter().collect::<Vec<_>>();
batch_verify_range_proofs(range_proof_service, &outputs)?;
Ok(())
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@ mod transaction_input;
mod transaction_input_version;
mod transaction_kernel;
mod transaction_kernel_version;
mod transaction_output;
pub mod transaction_output;
mod transaction_output_version;
mod unblinded_output;
mod unblinded_output_builder;
Expand Down
59 changes: 58 additions & 1 deletion base_layer/core/src/transactions/transaction_components/test.rs
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ use crate::{
tari_amount::{uT, MicroTari, T},
test_helpers,
test_helpers::{create_sender_transaction_protocol_with, create_unblinded_txos, TestParams, UtxoTestParams},
transaction_components::{EncryptedValue, OutputFeatures},
transaction_components::{transaction_output::batch_verify_range_proofs, EncryptedValue, OutputFeatures},
transaction_protocol::TransactionProtocolError,
CryptoFactories,
},
Expand Down Expand Up @@ -151,6 +151,63 @@ fn range_proof_verification() {
};
}

#[test]
fn range_proof_verification_batch() {
let factories = CryptoFactories::new(64);

let unblinded_output1 = TestParams::new().create_unblinded_output(UtxoTestParams {
value: (1u64).into(),
..Default::default()
});
let tx_output1 = unblinded_output1.as_transaction_output(&factories).unwrap();
assert!(tx_output1.verify_range_proof(&factories.range_proof).is_ok());

let unblinded_output2 = TestParams::new().create_unblinded_output(UtxoTestParams {
value: (2u64).into(),
..Default::default()
});
let tx_output2 = unblinded_output2.as_transaction_output(&factories).unwrap();
assert!(tx_output2.verify_range_proof(&factories.range_proof).is_ok());

let unblinded_output3 = TestParams::new().create_unblinded_output(UtxoTestParams {
value: (3u64).into(),
..Default::default()
});
let tx_output3 = unblinded_output3.as_transaction_output(&factories).unwrap();
assert!(tx_output3.verify_range_proof(&factories.range_proof).is_ok());

let unblinded_output4 = TestParams::new().create_unblinded_output(UtxoTestParams {
value: (4u64).into(),
..Default::default()
});
let tx_output4 = unblinded_output4.as_transaction_output(&factories).unwrap();
assert!(tx_output4.verify_range_proof(&factories.range_proof).is_ok());

let unblinded_output5 = TestParams::new().create_unblinded_output(UtxoTestParams {
value: (5u64).into(),
..Default::default()
});
let mut tx_output5 = unblinded_output5.as_transaction_output(&factories).unwrap();
assert!(tx_output5.verify_range_proof(&factories.range_proof).is_ok());

// The batch should pass
let outputs = vec![
tx_output1.clone(),
tx_output2.clone(),
tx_output3.clone(),
tx_output4.clone(),
tx_output5.clone(),
];
let outputs = outputs.iter().collect::<Vec<_>>();
assert!(batch_verify_range_proofs(&factories.range_proof, &outputs).is_ok());

// The batch should fail after tampering with a single proof
tx_output5.proof = tx_output4.proof.clone();
let outputs = vec![tx_output1, tx_output2, tx_output3, tx_output4, tx_output5];
let outputs = outputs.iter().collect::<Vec<_>>();
assert!(batch_verify_range_proofs(&factories.range_proof, &outputs).is_err());
}

#[test]
fn sender_signature_verification() {
let test_params = TestParams::new();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,13 +24,14 @@
// Version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0.

use std::{
cmp::Ordering,
cmp::{min, Ordering},
fmt::{Display, Formatter},
io,
io::{Read, Write},
};

use digest::FixedOutput;
use log::*;
use rand::rngs::OsRng;
use serde::{Deserialize, Serialize};
use tari_common_types::types::{
Expand All @@ -46,9 +47,11 @@ use tari_common_types::types::{
};
use tari_crypto::{
commitment::HomomorphicCommitmentFactory,
extended_range_proof::ExtendedRangeProofService,
errors::RangeProofError,
extended_range_proof::{ExtendedRangeProofService, Statement},
keys::{PublicKey as PublicKeyTrait, SecretKey},
range_proof::RangeProofService as RangeProofServiceTrait,
ristretto::bulletproofs_plus::RistrettoAggregatedPublicStatement,
tari_utilities::{hex::Hex, ByteArray, Hashable},
};
use tari_script::TariScript;
Expand All @@ -64,6 +67,8 @@ use crate::{
},
};

pub const LOG_TARGET: &str = "c::transactions::transaction_output";

/// Output for a transaction, defining the new ownership of coins that are being transferred. The commitment is a
/// blinded value for the output while the range proof guarantees the commitment includes a positive value without
/// overflow and the ownership of the private key.
Expand Down Expand Up @@ -476,3 +481,98 @@ impl ConsensusDecoding for TransactionOutput {
Ok(output)
}
}

/// Performs batched range proof verification for an arbitrary number of outputs. Batched range proof verification gains
/// above batch sizes of 2^8 = 256 gives diminishing returns, see <https://github.com/tari-project/bulletproofs-plus>,
/// so the batch sizes are limited to 2^8.
pub fn batch_verify_range_proofs(
prover: &RangeProofService,
outputs: &[&TransactionOutput],
) -> Result<(), RangeProofError> {
// We need optimized power of two chunks, for example if we have 15 outputs, then we need chunks of 8, 4, 2, 1.
let power_of_two_vec = power_of_two_chunk_sizes(outputs.len(), 8);
debug!(
target: LOG_TARGET,
"Queueing range proof batch verify output(s): {:?}", &power_of_two_vec
);
let mut index = 0;
for power_of_two in power_of_two_vec {
let mut statements = Vec::with_capacity(power_of_two);
let mut proofs = Vec::with_capacity(power_of_two);
for output in outputs.iter().skip(index).take(power_of_two) {
statements.push(RistrettoAggregatedPublicStatement {
statements: vec![Statement {
commitment: output.commitment.clone(),
minimum_value_promise: 0,
}],
});
proofs.push(output.proof.to_vec().clone());
}
index += power_of_two;
prover.verify_batch(proofs.iter().collect(), statements.iter().collect())?;
}
Ok(())
}

// This function will create a vector of integers whose contents will all be powers of two; the entries will sum to the
// given length and each entry will be limited to the maximum power of two provided.
// Examples: A length of 15 without restrictions will produce chunks of [8, 4, 2, 1]; a length of 32 limited to 2^3 will
// produce chunks of [8, 8, 8, 8].
fn power_of_two_chunk_sizes(len: usize, max_power: u8) -> Vec<usize> {
// This function will search for the highest power of two contained within an integer number
fn highest_power_of_two(n: usize) -> usize {
let mut res = 0;
for i in (1..=n).rev() {
if i.is_power_of_two() {
res = i;
break;
}
}
res
}

if len == 0 {
Vec::new()
} else {
let mut res_vec = Vec::new();
let mut n = len;
loop {
let chunk = min(2usize.pow(u32::from(max_power)), highest_power_of_two(n));
res_vec.push(chunk);
n = n.saturating_sub(chunk);
if n == 0 {
break;
}
}
res_vec
}
}

#[cfg(test)]
mod test {
use crate::transactions::transaction_components::transaction_output::power_of_two_chunk_sizes;

#[test]
fn it_creates_power_of_two_chunks() {
let p2vec = power_of_two_chunk_sizes(0, 7);
assert!(p2vec.is_empty());
let p2vec = power_of_two_chunk_sizes(1, 7);
assert_eq!(p2vec, vec![1]);
let p2vec = power_of_two_chunk_sizes(2, 7);
assert_eq!(p2vec, vec![2]);
let p2vec = power_of_two_chunk_sizes(3, 0);
assert_eq!(p2vec, vec![1, 1, 1]);
let p2vec = power_of_two_chunk_sizes(4, 2);
assert_eq!(p2vec, vec![4]);
let p2vec = power_of_two_chunk_sizes(15, 7);
assert_eq!(p2vec, vec![8, 4, 2, 1]);
let p2vec = power_of_two_chunk_sizes(32, 3);
assert_eq!(p2vec, vec![8, 8, 8, 8]);
let p2vec = power_of_two_chunk_sizes(1007, 8);
assert_eq!(p2vec, vec![256, 256, 256, 128, 64, 32, 8, 4, 2, 1]);
let p2vec = power_of_two_chunk_sizes(10307, 10);
assert_eq!(p2vec, vec![
1024, 1024, 1024, 1024, 1024, 1024, 1024, 1024, 1024, 1024, 64, 2, 1
]);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ use crate::{
transactions::{
aggregated_body::AggregateBody,
transaction_components::{
transaction_output::batch_verify_range_proofs,
KernelSum,
OutputType,
TransactionError,
Expand Down Expand Up @@ -400,15 +401,14 @@ impl<B: BlockchainBackend + 'static> BlockValidator<B> {
}

helpers::check_tari_script_byte_size(&output.script, max_script_size)?;

output.verify_metadata_signature()?;
if !bypass_range_proof_verification {
output.verify_range_proof(&range_proof_prover)?;
}

helpers::check_not_duplicate_txo(&*db, output)?;
commitment_sum = &commitment_sum + &output.commitment;
}
if !bypass_range_proof_verification {
let this_outputs = outputs.iter().map(|o| &o.1).collect::<Vec<_>>();
batch_verify_range_proofs(&range_proof_prover, &this_outputs)?;
}

Ok((outputs, aggregate_sender_offset, commitment_sum, coinbase_index))
})
Expand Down
3 changes: 3 additions & 0 deletions base_layer/core/src/validation/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
// USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

use tari_common_types::types::{FixedHash, HashOutput};
use tari_crypto::errors::RangeProofError;
use thiserror::Error;
use tokio::task;

Expand All @@ -47,6 +48,8 @@ pub enum ValidationError {
UnknownInput,
#[error("The transaction is invalid: {0}")]
TransactionError(#[from] TransactionError),
#[error("A range proof verification has produced an error: {0}")]
RangeProofError(#[from] RangeProofError),
#[error("Error: {0}")]
CustomError(String),
#[error("Fatal storage error during validation: {0}")]
Expand Down
1 change: 0 additions & 1 deletion base_layer/core/src/validation/helpers.rs
Original file line number Diff line number Diff line change
Expand Up @@ -793,7 +793,6 @@ pub fn check_blockchain_version(constants: &ConsensusConstants, version: u16) ->

#[cfg(test)]
mod test {

use tari_test_utils::unpack_enum;

use super::*;
Expand Down
6 changes: 3 additions & 3 deletions base_layer/core/tests/helpers/block_builders.rs
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ use tari_core::{
TransactionSchema,
},
transaction_components::{
transaction_output::batch_verify_range_proofs,
KernelBuilder,
KernelFeatures,
OutputFeatures,
Expand Down Expand Up @@ -160,10 +161,9 @@ fn print_new_genesis_block(network: Network) {
}
for output in block.body.outputs() {
output.verify_metadata_signature().unwrap();
output
.verify_range_proof(&CryptoFactories::default().range_proof)
.unwrap();
}
let outputs = block.body.outputs().iter().collect::<Vec<_>>();
batch_verify_range_proofs(&CryptoFactories::default().range_proof, &outputs).unwrap();

// Note: This is printed in the same order as needed for 'fn get_dibbler_genesis_block_raw()'
println!();
Expand Down

0 comments on commit 02d3121

Please sign in to comment.