Skip to content

Commit

Permalink
feat(base_layer/core): add domain hashing wrapper for consensus encod…
Browse files Browse the repository at this point in the history
…ing (#4381)

Description
---
- adds `DomainSeparatedConsensusHasher` 
- renames `ConsensusHashWriter` to `ConsensusHasher` 
- removes `Write` implementation from `ConsensusHasher` 
- adds `TransactionHashDomain` hash domain 
- uses domain hashing for transaction metadata signature and script challenges

Motivation and Context
---
Creates an ergonomic and 0-alloc hasher for consensus encoded values.

Usage:
```rust
  DomainSeparatedConsensusHasher::<TransactionHashDomain>::new("metadata_signature")
            .chain(public_commitment_nonce)
            .chain(script)
            .chain(features)
            .chain(sender_offset_public_key)
            .chain(commitment)
            .chain(covenant)
            .chain(encrypted_value)
            .finalize()
```

How Has This Been Tested?
---
New unit test, existing tests use hashing
  • Loading branch information
sdbondi committed Aug 4, 2022
1 parent 4ca7756 commit ad11ec5
Show file tree
Hide file tree
Showing 23 changed files with 197 additions and 126 deletions.
6 changes: 4 additions & 2 deletions base_layer/core/src/base_node/sync/rpc/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -157,8 +157,10 @@ mod sync_utxos {
let (_, chain) = create_main_chain(&db, block_specs!(["A->GB"], ["B->A"]));

let block = chain.get("B").unwrap();
let total_outputs = block.block().header.output_mmr_size;
let start = total_outputs - 2;
let msg = SyncUtxosRequest {
start: 3500,
start,
end_header_hash: block.hash().clone(),
include_pruned_utxos: true,
include_deleted_bitmaps: false,
Expand All @@ -170,6 +172,6 @@ mod sync_utxos {
.collect::<Vec<_>>()
.await;

assert!(utxo_indexes.iter().all(|index| (3500..=4002).contains(index)));
assert!(utxo_indexes.iter().all(|index| (start..=start + 2).contains(index)));
}
}
6 changes: 3 additions & 3 deletions base_layer/core/src/blocks/block_header.rs
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ use thiserror::Error;
#[cfg(feature = "base_node")]
use crate::blocks::{BlockBuilder, NewBlockHeaderTemplate};
use crate::{
consensus::{ConsensusDecoding, ConsensusEncoding, ConsensusEncodingSized, ConsensusHashWriter},
consensus::{ConsensusDecoding, ConsensusEncoding, ConsensusEncodingSized, ConsensusHasher},
proof_of_work::{PowAlgorithm, PowError, ProofOfWork},
};

Expand Down Expand Up @@ -235,7 +235,7 @@ impl BlockHeader {
.finalize()
.to_vec()
} else {
ConsensusHashWriter::default()
ConsensusHasher::default()
.chain(&self.version)
.chain(&self.height)
.chain(&self.prev_hash)
Expand Down Expand Up @@ -302,7 +302,7 @@ impl Hashable for BlockHeader {
.finalize()
.to_vec()
} else {
ConsensusHashWriter::default()
ConsensusHasher::default()
// TODO: this excludes extraneous length varint used for Vec<u8> since a hash is always 32-bytes. Clean this
// up if we decide to migrate to a fixed 32-byte type
.chain(&copy_into_fixed_array::<_, 32>(&self.merged_mining_hash()).unwrap())
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -526,11 +526,12 @@ mod fetch_total_size_stats {
#[test]
fn it_measures_the_number_of_entries() {
let db = setup();
let genesis_output_count = db.fetch_header(0).unwrap().unwrap().output_mmr_size;
let _block_and_outputs = add_many_chained_blocks(2, &db);
let stats = db.fetch_total_size_stats().unwrap();
assert_eq!(
stats.sizes().iter().find(|s| s.name == "utxos_db").unwrap().num_entries,
4003
genesis_output_count + 2
);
}
}
Expand Down Expand Up @@ -580,7 +581,7 @@ mod fetch_header_containing_utxo_mmr {
fn it_returns_genesis() {
let db = setup();
let genesis = db.fetch_block(0).unwrap();
assert_eq!(genesis.block().body.outputs().len(), 4001);
assert!(!genesis.block().body.outputs().is_empty());
let mut mmr_position = 0;
genesis.block().body.outputs().iter().for_each(|_| {
let header = db.fetch_header_containing_utxo_mmr(mmr_position).unwrap();
Expand Down
2 changes: 1 addition & 1 deletion base_layer/core/src/consensus/consensus_constants.rs
Original file line number Diff line number Diff line change
Expand Up @@ -296,7 +296,7 @@ impl ConsensusConstants {
vec![ConsensusConstants {
effective_from_height: 0,
coinbase_lock_height: 2,
blockchain_version: 1,
blockchain_version: 2,
valid_blockchain_version_range: 0..=3,
future_time_limit: 540,
difficulty_block_window,
Expand Down
4 changes: 2 additions & 2 deletions base_layer/core/src/consensus/consensus_encoding.rs
Original file line number Diff line number Diff line change
Expand Up @@ -26,14 +26,14 @@ mod crypto;
mod epoch_time;
mod fixed_hash;
mod generic;
mod hash_writer;
mod hashing;
mod integers;
mod micro_tari;
mod script;
mod vec;
use std::io;

pub use hash_writer::ConsensusHashWriter;
pub use hashing::{ConsensusHasher, DomainSeparatedConsensusHasher};
pub use vec::MaxSizeVec;

pub use self::bytes::MaxSizeBytes;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,87 +20,93 @@
// WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE
// USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

use std::io::Write;
use std::{io, io::Write, marker::PhantomData};

use digest::{consts::U32, Digest, FixedOutput, Update};
use digest::{consts::U32, Digest};
use tari_common_types::types::HashDigest;
use tari_crypto::hashing::{DomainSeparatedHasher, DomainSeparation};

use crate::consensus::ConsensusEncoding;

/// Domain separated consensus encoding hasher.
pub struct DomainSeparatedConsensusHasher<M: DomainSeparation>(PhantomData<M>);

impl<M: DomainSeparation> DomainSeparatedConsensusHasher<M> {
#[allow(clippy::new_ret_no_self)]
pub fn new(label: &'static str) -> ConsensusHasher<DomainSeparatedHasher<HashDigest, M>> {
ConsensusHasher::new(DomainSeparatedHasher::new_with_label(label))
}
}

#[derive(Clone)]
pub struct ConsensusHashWriter<H> {
digest: H,
pub struct ConsensusHasher<D> {
writer: WriteHashWrapper<D>,
}

impl<H: Digest> ConsensusHashWriter<H> {
pub fn new(digest: H) -> Self {
Self { digest }
impl<D: Digest> ConsensusHasher<D> {
pub fn new(digest: D) -> Self {
Self {
writer: WriteHashWrapper(digest),
}
}
}

impl<H> ConsensusHashWriter<H>
where H: FixedOutput<OutputSize = U32> + Update
impl<D> ConsensusHasher<D>
where D: Digest<OutputSize = U32>
{
pub fn finalize(self) -> [u8; 32] {
self.digest.finalize_fixed().into()
self.writer.0.finalize().into()
}

pub fn update_consensus_encode<T: ConsensusEncoding>(&mut self, data: &T) {
pub fn update_consensus_encode<T: ConsensusEncoding + ?Sized>(&mut self, data: &T) {
// UNWRAP: ConsensusEncode MUST only error if the writer errors, HashWriter::write is infallible
data.consensus_encode(self)
data.consensus_encode(&mut self.writer)
.expect("Incorrect implementation of ConsensusEncoding encountered. Implementations MUST be infallible.");
}

pub fn chain<T: ConsensusEncoding>(mut self, data: &T) -> Self {
self.update_consensus_encode(data);
self
}
}

pub fn into_digest(self) -> H {
self.digest
impl Default for ConsensusHasher<HashDigest> {
fn default() -> Self {
ConsensusHasher::new(HashDigest::new())
}
}

impl<H: Update> Write for ConsensusHashWriter<H> {
fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
self.digest.update(buf);
/// This private struct wraps a Digest and implements the Write trait to satisfy the consensus encoding trait..
#[derive(Clone)]
struct WriteHashWrapper<D>(D);

impl<D: Digest> Write for WriteHashWrapper<D> {
fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
self.0.update(buf);
Ok(buf.len())
}

fn flush(&mut self) -> std::io::Result<()> {
fn flush(&mut self) -> io::Result<()> {
Ok(())
}
}

impl Default for ConsensusHashWriter<HashDigest> {
fn default() -> Self {
ConsensusHashWriter::new(HashDigest::new())
}
}

#[cfg(test)]
mod test {
use rand::{rngs::OsRng, RngCore};
use tari_common_types::types::HashDigest;
mod tests {
use tari_crypto::{hash::blake2::Blake256, hash_domain};

use super::*;

#[test]
fn it_updates_the_digest_state() {
let mut writer = ConsensusHashWriter::default();
let mut data = [0u8; 1024];
OsRng.fill_bytes(&mut data);

// Even if data is streamed in chunks, the preimage and therefore the resulting hash are the same
writer.write_all(&data[0..256]).unwrap();
writer.write_all(&data[256..500]).unwrap();
writer.write_all(&data[500..1024]).unwrap();
let hash = writer.finalize();
let empty: [u8; 32] = Update::chain(HashDigest::new(), [0u8; 1024]).finalize_fixed().into();
assert_ne!(hash, empty);

let mut writer = ConsensusHashWriter::default();
writer.write_all(&data).unwrap();
assert_eq!(writer.finalize(), hash);
fn it_hashes_using_the_domain_hasher() {
hash_domain!(TestHashDomain, "tari.test", 0);
let expected_hash = DomainSeparatedHasher::<Blake256, TestHashDomain>::new_with_label("foo")
.chain(b"\xff\x01")
.finalize();
let hash = DomainSeparatedConsensusHasher::<TestHashDomain>::new("foo")
.chain(&255u64)
.finalize();

assert_eq!(hash, expected_hash.as_ref());
}
}
3 changes: 2 additions & 1 deletion base_layer/core/src/consensus/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,8 @@ pub use consensus_encoding::{
ConsensusDecoding,
ConsensusEncoding,
ConsensusEncodingSized,
ConsensusHashWriter,
ConsensusHasher,
DomainSeparatedConsensusHasher,
MaxSizeBytes,
MaxSizeVec,
ToConsensusBytes,
Expand Down
4 changes: 4 additions & 0 deletions base_layer/core/src/transactions/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,9 @@
pub mod aggregated_body;

mod crypto_factories;

pub use crypto_factories::CryptoFactories;
use tari_crypto::hash_domain;

mod coinbase_builder;
pub use coinbase_builder::{CoinbaseBuildError, CoinbaseBuilder};
Expand All @@ -24,3 +26,5 @@ pub mod weight;

#[macro_use]
pub mod test_helpers;

hash_domain!(TransactionHashDomain, "com.tari.base_layer.core.transactions", 0);
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,7 @@ pub const MAX_TRANSACTION_RECIPIENTS: usize = 15;
//---------------------------------------- Crate functions ----------------------------------------------------//

use super::tari_amount::MicroTari;
use crate::{consensus::ConsensusHashWriter, covenants::Covenant};
use crate::{consensus::ConsensusHasher, covenants::Covenant};

/// Implement the canonical hashing function for TransactionOutput and UnblindedOutput for use in
/// ordering as well as for the output hash calculation for TransactionInput.
Expand All @@ -103,7 +103,7 @@ pub(super) fn hash_output(
encrypted_value: &EncryptedValue,
minimum_value_promise: MicroTari,
) -> [u8; 32] {
let common_hash = ConsensusHashWriter::default()
let common_hash = ConsensusHasher::default()
.chain(&version)
.chain(features)
.chain(commitment)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@

use tari_common_types::types::{Commitment, FixedHash};

use crate::consensus::ConsensusHashWriter;
use crate::consensus::ConsensusHasher;

#[derive(Debug, Clone, Copy)]
pub struct CheckpointChallenge(FixedHash);
Expand All @@ -35,7 +35,7 @@ impl CheckpointChallenge {
checkpoint_number: u64,
) -> Self {
// TODO: Use new tari_crypto domain-separated hashing
let hash = ConsensusHashWriter::default()
let hash = ConsensusHasher::default()
.chain(contract_id)
.chain(checkpoint_commitment)
.chain(merkle_root)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ use serde::{Deserialize, Serialize};
use tari_common_types::types::{FixedHash, PublicKey};

use crate::{
consensus::{ConsensusDecoding, ConsensusEncoding, ConsensusEncodingSized, ConsensusHashWriter, MaxSizeVec},
consensus::{ConsensusDecoding, ConsensusEncoding, ConsensusEncodingSized, ConsensusHasher, MaxSizeVec},
transactions::transaction_components::FixedString,
};

Expand All @@ -50,7 +50,7 @@ impl ContractDefinition {
}

pub fn calculate_contract_id(&self) -> FixedHash {
ConsensusHashWriter::default().chain(self).finalize().into()
ConsensusHasher::default().chain(self).finalize().into()
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ use tari_script::{ExecutionStack, ScriptContext, StackItem, TariScript};

use super::{TransactionInputVersion, TransactionOutputVersion};
use crate::{
consensus::{ConsensusDecoding, ConsensusEncoding, ConsensusHashWriter, MaxSizeBytes},
consensus::{ConsensusDecoding, ConsensusEncoding, ConsensusHasher, DomainSeparatedConsensusHasher, MaxSizeBytes},
covenants::Covenant,
transactions::{
tari_amount::MicroTari,
Expand All @@ -52,6 +52,7 @@ use crate::{
TransactionError,
UnblindedOutput,
},
TransactionHashDomain,
},
};

Expand Down Expand Up @@ -170,13 +171,15 @@ impl TransactionInput {
commitment: &Commitment,
) -> [u8; 32] {
match version {
TransactionInputVersion::V0 | TransactionInputVersion::V1 => ConsensusHashWriter::default()
.chain(nonce_commitment)
.chain(script)
.chain(input_data)
.chain(script_public_key)
.chain(commitment)
.finalize(),
TransactionInputVersion::V0 | TransactionInputVersion::V1 => {
DomainSeparatedConsensusHasher::<TransactionHashDomain>::new("script_challenge")
.chain(nonce_commitment)
.chain(script)
.chain(input_data)
.chain(script_public_key)
.chain(commitment)
.finalize()
},
}
}

Expand Down Expand Up @@ -378,7 +381,7 @@ impl TransactionInput {
ref minimum_value_promise,
} => {
// TODO: Change this hash to what is in RFC-0121/Consensus Encoding #testnet-reset
let writer = ConsensusHashWriter::default()
let writer = ConsensusHasher::default()
.chain(version)
.chain(features)
.chain(commitment)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ use tari_utilities::{hex::Hex, message_format::MessageFormat, Hashable};

use super::TransactionKernelVersion;
use crate::{
consensus::{ConsensusDecoding, ConsensusEncoding, ConsensusHashWriter},
consensus::{ConsensusDecoding, ConsensusEncoding, ConsensusHasher},
transactions::{
tari_amount::MicroTari,
transaction_components::{KernelFeatures, TransactionError},
Expand Down Expand Up @@ -147,7 +147,7 @@ impl TransactionKernel {
impl Hashable for TransactionKernel {
/// Produce a canonical hash for a transaction kernel.
fn hash(&self) -> Vec<u8> {
ConsensusHashWriter::default().chain(self).finalize().to_vec()
ConsensusHasher::default().chain(self).finalize().to_vec()
}
}

Expand Down
Loading

0 comments on commit ad11ec5

Please sign in to comment.