From d56da1aa86e0854c4d51635c45d82135393dfbea Mon Sep 17 00:00:00 2001 From: Stan Bondi Date: Tue, 25 Jan 2022 11:09:29 +0200 Subject: [PATCH] fix(core)!: add missing consensus encoding length byte rangeproof & covenants (#3730) Description --- - Adds consensus encode impl for RangeProof and ComSig - Adds length varint for covenant - Move version as first encoded byte - Implement zero alloc hashing for outputs and witness hash - Limit covenant and tariscript byte size - Migration to set length byte in existing wallet transactions - Ensure that an RPC message can never be zero length. - covenant serde implementation does not use consensus encoding Motivation and Context --- Rangeproof is not fixed size and so requires a length varint. It is important to ensure that a remote peer cannot cause unbounded allocation, so various length checks are implemented. The wallet previously only ever stored a 0-byte covenant which is now invalid encoding. Potential fix for WriteZero, ensuring that all rpc messages are non-zero in length (rare, but possible). How Has This Been Tested? --- Existing tests updated, manually - mined blocks on dibbler --- .../src/conversions/transaction_input.rs | 5 +- .../src/conversions/transaction_output.rs | 5 +- .../src/conversions/unblinded_output.rs | 5 +- .../src/blocks/faucets/dibbler_faucet.json | 2 +- base_layer/core/src/common/hash_writer.rs | 54 +++++++++++++ base_layer/core/src/common/limited_reader.rs | 52 +++++++++++++ base_layer/core/src/common/mod.rs | 2 + .../core/src/consensus/consensus_encoding.rs | 1 + .../src/consensus/consensus_encoding/bytes.rs | 19 ++++- .../consensus/consensus_encoding/crypto.rs | 59 +++++++++++++- .../consensus_encoding/micro_tari.rs | 54 +++++++++++++ .../consensus/consensus_encoding/script.rs | 32 +++++++- base_layer/core/src/covenants/arguments.rs | 29 +++---- base_layer/core/src/covenants/covenant.rs | 52 ++++++++++--- base_layer/core/src/covenants/decoder.rs | 11 +-- base_layer/core/src/covenants/encoder.rs | 13 +--- base_layer/core/src/covenants/macros.rs | 19 ++--- base_layer/core/src/covenants/serde.rs | 11 +-- base_layer/core/src/proto/transaction.rs | 9 +-- .../transaction/asset_output_features.rs | 14 ++-- .../src/transactions/transaction/error.rs | 10 +++ .../transaction/kernel_features.rs | 17 ++++ .../transaction/mint_non_fungible_features.rs | 9 ++- .../core/src/transactions/transaction/mod.rs | 27 ++++--- .../transaction/output_features_version.rs | 9 ++- .../side_chain_checkpoint_features.rs | 2 +- .../transaction/template_parameter.rs | 11 ++- .../core/src/transactions/transaction/test.rs | 19 +++-- .../transaction/transaction_input.rs | 77 ++++++++----------- .../transaction/transaction_input_version.rs | 36 ++++++++- .../transaction/transaction_kernel.rs | 32 ++++---- .../transaction/transaction_kernel_version.rs | 36 ++++++++- .../transaction/transaction_output.rs | 21 ++--- .../transaction/transaction_output_version.rs | 36 ++++++++- .../transaction/unblinded_output.rs | 2 +- .../src/output_manager_service/service.rs | 4 +- .../storage/sqlite_db/new_output_sql.rs | 3 + .../storage/sqlite_db/output_sql.rs | 3 +- .../wallet/src/utxo_scanner_service/error.rs | 4 +- .../utxo_scanner_service/utxo_scanner_task.rs | 10 +-- comms/src/protocol/rpc/client/mod.rs | 8 +- .../helpers/transactionBuilder.js | 48 +++++++----- 42 files changed, 644 insertions(+), 228 deletions(-) create mode 100644 base_layer/core/src/common/hash_writer.rs create mode 100644 base_layer/core/src/common/limited_reader.rs create mode 100644 base_layer/core/src/consensus/consensus_encoding/micro_tari.rs diff --git a/applications/tari_app_grpc/src/conversions/transaction_input.rs b/applications/tari_app_grpc/src/conversions/transaction_input.rs index 31d1853e7d..b35fcb1391 100644 --- a/applications/tari_app_grpc/src/conversions/transaction_input.rs +++ b/applications/tari_app_grpc/src/conversions/transaction_input.rs @@ -24,7 +24,6 @@ use std::convert::{TryFrom, TryInto}; use tari_common_types::types::{Commitment, PublicKey}; use tari_core::{ - consensus::{ConsensusDecoding, ToConsensusBytes}, covenants::Covenant, transactions::transaction::{TransactionInput, TransactionInputVersion}, }; @@ -55,7 +54,7 @@ impl TryFrom for TransactionInput { let sender_offset_public_key = PublicKey::from_bytes(input.sender_offset_public_key.as_bytes()).map_err(|err| format!("{:?}", err))?; - let covenant = Covenant::consensus_decode(&mut input.covenant.as_slice()).map_err(|err| err.to_string())?; + let covenant = Covenant::from_bytes(&input.covenant).map_err(|err| err.to_string())?; Ok(TransactionInput::new_with_output_data( TransactionInputVersion::try_from( @@ -129,7 +128,7 @@ impl TryFrom for grpc::TransactionInput { covenant: input .covenant() .map_err(|_| "Non-compact Transaction input should contain covenant".to_string())? - .to_consensus_bytes(), + .to_bytes(), version: input.version as u32, }) } diff --git a/applications/tari_app_grpc/src/conversions/transaction_output.rs b/applications/tari_app_grpc/src/conversions/transaction_output.rs index 6ba01ec951..1e3b91257d 100644 --- a/applications/tari_app_grpc/src/conversions/transaction_output.rs +++ b/applications/tari_app_grpc/src/conversions/transaction_output.rs @@ -24,7 +24,6 @@ use std::convert::{TryFrom, TryInto}; use tari_common_types::types::{BulletRangeProof, Commitment, PublicKey}; use tari_core::{ - consensus::{ConsensusDecoding, ToConsensusBytes}, covenants::Covenant, transactions::transaction::{TransactionOutput, TransactionOutputVersion}, }; @@ -55,7 +54,7 @@ impl TryFrom for TransactionOutput { .ok_or_else(|| "Metadata signature not provided".to_string())? .try_into() .map_err(|_| "Metadata signature could not be converted".to_string())?; - let covenant = Covenant::consensus_decode(&mut output.covenant.as_slice()).map_err(|err| err.to_string())?; + let covenant = Covenant::from_bytes(&output.covenant).map_err(|err| err.to_string())?; Ok(Self::new( TransactionOutputVersion::try_from( u8::try_from(output.version).map_err(|_| "Invalid version: overflowed u8")?, @@ -86,7 +85,7 @@ impl From for grpc::TransactionOutput { signature_u: Vec::from(output.metadata_signature.u().as_bytes()), signature_v: Vec::from(output.metadata_signature.v().as_bytes()), }), - covenant: output.covenant.to_consensus_bytes(), + covenant: output.covenant.to_bytes(), version: output.version as u32, } } diff --git a/applications/tari_app_grpc/src/conversions/unblinded_output.rs b/applications/tari_app_grpc/src/conversions/unblinded_output.rs index f5916609bf..3979087040 100644 --- a/applications/tari_app_grpc/src/conversions/unblinded_output.rs +++ b/applications/tari_app_grpc/src/conversions/unblinded_output.rs @@ -24,7 +24,6 @@ use std::convert::{TryFrom, TryInto}; use tari_common_types::types::{PrivateKey, PublicKey}; use tari_core::{ - consensus::{ConsensusDecoding, ToConsensusBytes}, covenants::Covenant, transactions::{ tari_amount::MicroTari, @@ -52,7 +51,7 @@ impl From for grpc::UnblindedOutput { signature_v: Vec::from(output.metadata_signature.v().as_bytes()), }), script_lock_height: output.script_lock_height, - covenant: output.covenant.to_consensus_bytes(), + covenant: output.covenant.to_bytes(), } } } @@ -86,7 +85,7 @@ impl TryFrom for UnblindedOutput { .try_into() .map_err(|_| "Metadata signature could not be converted".to_string())?; - let covenant = Covenant::consensus_decode(&mut output.covenant.as_slice()).map_err(|err| err.to_string())?; + let covenant = Covenant::from_bytes(&output.covenant).map_err(|err| err.to_string())?; Ok(Self::new( TransactionOutputVersion::try_from(0u8)?, diff --git a/base_layer/core/src/blocks/faucets/dibbler_faucet.json b/base_layer/core/src/blocks/faucets/dibbler_faucet.json index d8c2c29487..e171ffcee5 100644 --- a/base_layer/core/src/blocks/faucets/dibbler_faucet.json +++ b/base_layer/core/src/blocks/faucets/dibbler_faucet.json @@ -3998,4 +3998,4 @@ {"version":"V0","features":{"version":"V0","flags":{"bits":0},"maturity":0,"metadata":[],"unique_id":null,"parent_public_key":null,"asset":null,"mint_non_fungible":null,"sidechain_checkpoint":null},"commitment":"580d59e6b65746d005650bd11b45ae223bba9d09d4e6d41c2e7b0fa2d2f0a467","proof":"689d4a503d042e37e11921b8998e8ab61bee10a6c4d252b1a81306bb2e03f00072c1c0df431f98ff99fc9ad6599dc83d4694f44003e6d2d1107b94b88924d2651cf5641ef5257db58b64abaf422c6f2d63ec8bae2e92197cb9c3a8b4fa24bc2dda8ec6c0bd900e508803031c9bf1deee6c398879331fd7d60ffe8eb07fd30d5e8516a25279a3a35fbbb48f84113bd1457ae173e39f503fc38c7d16f545bcd10474f470abfe56f872ac21301cc676ee3e2a365e4cc8bf7f82a3167bd75aa4f60035920b5afb272f31a44e1d2f17f5b3cc5beb420b53321d5c4bf88e2848403a0b1206844455b6914846b9ad9163cdc556a4241d7b48d6a060d4846f2a0f372c00a4dc6839b2ee8327d8cbefa2e66d342dbbee75c0b7c24f6a7014f32490a0b566841b76e48917b22ca0b5321ba99ce994c14b6ce2537b096b24bf6958c9dd6828ec3ca5e9deb36dd126f58339befe3d11ebec3af1db2649c0095917fb8d11e467405908e182370abb13150347578c092df58007d80253878647f5b4c8a48286354090ca49325bbad4f9565bac89a16ebb4ecc8b4aea6585f0f7d827f868d5480ee67bd66a76170875953a391de4aebb73ead8f83f602af30929529096abbdd74408aa0f5a2314c007734ef4769d569abfe8f5ccced6858c3f7206490810d5855492393976352b0a2ff211529c52ff6139238c03bda2ee84cf6c61564d39826e5c84fe56915894d1c62e316d00c50092304538e8de3667cde81ca07f63bac30676c272e72e11b608b30d421adc8f45e2e216b969c5aad0f6d28de0de65c54be5610e9f7ba5ed845398ed3ce0f4b1cb3ff53a139382c4503a2eb3071432c1a1906cdb38f2cb393d686e155f87a16d20e0c63c400cd551e455f192f1d8e465f6f00cdfac32f26b2860adfd9f6886a5e5c5daa49febb52c05bb5acb8ce5e8a42e6d0b","script":"73","sender_offset_public_key":"347adc2cfd94eb141f3051d5dca6e351f6605c00487b98310e34088025ff8e2e","metadata_signature":{"public_nonce":"3c9b1272c23d3a86c4d5f1329f6ee5306ad8ba873841452d4a242b5865d49a68","u":"04e8da54e730a5ae4e8a540606961a5004237724aadd522daf843ed0885ee603","v":"6fddd03c511c6a8c9b5e59e3ae18965ae379074af97ce1c2998dd393e8b97a01"},"covenant":""} {"version":"V0","features":{"version":"V0","flags":{"bits":0},"maturity":0,"metadata":[],"unique_id":null,"parent_public_key":null,"asset":null,"mint_non_fungible":null,"sidechain_checkpoint":null},"commitment":"eca7bb64c58719924cc6f644dd6b7c9e62896825d4f94d18c84261dff9bdd57a","proof":"3889b6683e2611f02cc6b2f498eff13fc323a8141a5a774438ceead1287f606c02007ca5fbed8500957430c9567b94b5a20055161854c575d3dff3e2c1beec53ee131e61886eef21474e1a9f9d070f044965e1265ee40c9e83820b9fd6fa653b4a2821620623f73846a2cfc00f06f2f651d5d8f310912d24e6ee3c11519fca2440ccba4327360f306281209e78510702c7fec160bfbd3a4aa3b689ec828aa1040d49977b9bd7840280b74702d6a8306ae4c3326420d305c1f5e92a75f2107b080e88ccc9b3f9ec661d472111522b9ac2bc8e962482c334fa845a1783fc63d60c12028c6c85a9f1ff7e5288967e02688fcad74aa3e77ec65fb66e164cb763d879a240579fa2feb67b6ec6302aaa60936812229eb1e064430bc286630b41f6a3349cf1b09cf202f9668b42a8f04f6a070e3ab8d40c9024e666f64adbeaaea55a7c98d6b799e06001ddbe2947b7d46ce10d8fac84f642b7ed77e3ccc6153057d360587ac3e9bf0df49523807fdc2e6f0b2e2c4955ecd268617538f16c08f6374f3b62394cf1f3401fc325464eb7c8c51230e4a97e2a705af5c638f0f33616b69d5242e991aafaff244c6f658519072991705dd5b77b15c6c22e83fe63b039122651de7e3c55c2e4e040e80e0b45fde6975738a439f61c50ddd0453d01cdbaefc342c2b228f08538739887b8802a063e155c09199699a3917b5f5e0e2d0915fed21dd4c65c2fc808ee9dc98a14a9097d14e00b21bbb442eb4140d9f6c00e8f4b002494c61343748f27254fd3c30e8a7bf7d84301ce9d699082fc08f11d0428e722775e79b9bb87e73bcef77835159e2cd55f183b2cf4c50a0451a2a1b3a38cd7a945a0e63b6d89a141d5de82233d28795f459e5d02feaf46460a06e791165cccef029dc87f5d087f65571ba304596963b4b366aca6ee9b7981a069452918f0ae3200","script":"73","sender_offset_public_key":"068ab2fa03005837681d3afdfea3b48b914ebec7a390c72fce13a84a617dec46","metadata_signature":{"public_nonce":"687c668ff89129fb570ae18228a895d8c039a38c31cf084b5832dc7336576f73","u":"ecda1e75067b591960d911e6e8d786b04be207689edb64aae2175f87c7d87909","v":"af920b783d48a2bd9b406d9302e29f59f047bcae0df09a8915c60762f7ef8701"},"covenant":""} {"version":"V0","features":{"version":"V0","flags":{"bits":0},"maturity":0,"metadata":[],"unique_id":null,"parent_public_key":null,"asset":null,"mint_non_fungible":null,"sidechain_checkpoint":null},"commitment":"ee4bc0b809687dc8dbbd0472be03a65d3a78d7c789cba86b12b916d8ac51a827","proof":"00ed8c51475945ec39f71a87b6daee9c5ac987d20e0511f6d92d8fa1d5ad5060befaad3058c4d8404064fb83f569bd978956eddbee0afbae5a67074c678f444f786813ecdf50bc1bd388561de479999b77a90ea0fdcfcbcb932daf12781b0d51061e01623ffc16fb1484a93a291d4e7bcb4ca3b4894dc6d58ee56887ce39b204539082017efd537831b91935878009cd4bfb5edd5fed48f50cbe3fab09b2ad0c38b2226e861bcb7a78aa28c21f737900e4c2d12c148abf9d5b7c90b6a779b00c4eee9c45e62044dc798f4d3ca8f816fb6b40420ffcd0540b99b92eb719bad90faa2b190a041908f3e9211e62736e92b3d42cfe94ffeab6fe885955b7162801595ac43b6098ac65df726a640fce5f6ae95005b655645cb65848f159a484ed70215248f0483225db2e01ac9d9f10d8fc51e3e317c38b16df6aa1ce3f26960ee603fc3ca08c8d547672694209cbeaf72137d23ef601b4b5987a3b8cf3fb4dfc4e0cf05bfdc128f9f49b1a2f8294c3cc63cf7760634e3b8a725b59bb93b236333145d0cdf86ddb22b885d4c50de94e24fe2b7313fae7aa7b7970f87ff3a19e605b79989b2cb8710d8fceaae20c84348084cdcc4257cd6fe7dd541c43afcaf048694f7ad64e0661e0be85f185ab391567a84215e2bff582e724e9ad54b8004815f524d239b18e831cfe57a3710fb06ca3cef76d4e08a7f8ab7b6526359b1622302538acbdb95c0f82cbeeaad700c157bdbd2a74d85582f6b5ac7d65010cb37377f37440022db6ac63eced6d2fdcdb78a9c9d944de6a989e4e4641c79380961838885d688c1e3e6850c081fcfe8ea6ecfa68b60435fe3affc3fbf2d28ce80d6ac6f052db56d8281cfd7f1128faff5219dc347c88f8e4385e91cb7151426aca1fdc7d0e51e55c030c25cf31aded5c86e12ff5d7f499aa770864ff1b6cb80b1133b0f601","script":"73","sender_offset_public_key":"22375dd307b7a1e1b282b86aa7edaad630e866cc851a14c957fefba09a19451d","metadata_signature":{"public_nonce":"cc2c4ad9a8e27cc5c1dc8cec469d6f2e30d520f7ca99b5cdef4c618357217e3a","u":"1a5236ef22e481a927f372d0cf467577477526e7d5b19674137a146046f7b90f","v":"71bf2d796b8878617ed06b7d30f44f5bc6a9dc380de5811ca3ae9b1799dcd508"},"covenant":""} - {"version":"V0","features":{"bits":0},"fee":0,"lock_height":0,"excess":"b8159195e8e7ce37ff5e03eff242093a72d13f1fc46ed1c169efd6e0cee34e4a","excess_sig":{"public_nonce":"0c6be155f074e897c1eb9773edf4114a6d2561a819c2b0589aea774cd37d1169","signature":"6ee44e359cedf04113484842200079675ba590648a537a46817319b714834905"}} \ No newline at end of file + {"version":"V0","features":{"bits":0},"fee":0,"lock_height":0,"excess":"b8159195e8e7ce37ff5e03eff242093a72d13f1fc46ed1c169efd6e0cee34e4a","excess_sig":{"public_nonce":"0c6be155f074e897c1eb9773edf4114a6d2561a819c2b0589aea774cd37d1169","signature":"6ee44e359cedf04113484842200079675ba590648a537a46817319b714834905"}} diff --git a/base_layer/core/src/common/hash_writer.rs b/base_layer/core/src/common/hash_writer.rs new file mode 100644 index 0000000000..ef31cec1fb --- /dev/null +++ b/base_layer/core/src/common/hash_writer.rs @@ -0,0 +1,54 @@ +// Copyright 2022, The Tari Project +// +// Redistribution and use in source and binary forms, with or without modification, are permitted provided that the +// following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following +// disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the +// following disclaimer in the documentation and/or other materials provided with the distribution. +// +// 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote +// products derived from this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, +// INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, +// 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 digest::{consts::U32, Digest, FixedOutput, Update}; + +pub struct HashWriter { + digest: H, +} + +impl HashWriter { + pub fn new(digest: H) -> Self { + Self { digest } + } +} + +impl HashWriter +where H: FixedOutput +{ + pub fn finalize(self) -> [u8; 32] { + self.digest.finalize_fixed().into() + } +} + +impl Write for HashWriter { + fn write(&mut self, buf: &[u8]) -> std::io::Result { + self.digest.update(buf); + Ok(buf.len()) + } + + fn flush(&mut self) -> std::io::Result<()> { + Ok(()) + } +} diff --git a/base_layer/core/src/common/limited_reader.rs b/base_layer/core/src/common/limited_reader.rs new file mode 100644 index 0000000000..fb29bd3337 --- /dev/null +++ b/base_layer/core/src/common/limited_reader.rs @@ -0,0 +1,52 @@ +// Copyright 2022, The Tari Project +// +// Redistribution and use in source and binary forms, with or without modification, are permitted provided that the +// following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following +// disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the +// following disclaimer in the documentation and/or other materials provided with the distribution. +// +// 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote +// products derived from this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, +// INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, +// 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, io::Read}; + +pub struct LimitedBytesReader { + byte_limit: usize, + num_read: usize, + inner: R, +} + +impl LimitedBytesReader { + pub fn new(byte_limit: usize, reader: R) -> Self { + Self { + byte_limit, + num_read: 0, + inner: reader, + } + } +} +impl Read for LimitedBytesReader { + fn read(&mut self, buf: &mut [u8]) -> std::io::Result { + let read = self.inner.read(buf)?; + self.num_read += read; + if self.num_read > self.byte_limit { + return Err(io::Error::new( + io::ErrorKind::InvalidInput, + format!("Read more bytes than the maximum ({})", self.byte_limit), + )); + } + Ok(read) + } +} diff --git a/base_layer/core/src/common/mod.rs b/base_layer/core/src/common/mod.rs index 4da315a60e..c6d79d23dd 100644 --- a/base_layer/core/src/common/mod.rs +++ b/base_layer/core/src/common/mod.rs @@ -21,5 +21,7 @@ // USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. pub mod byte_counter; +pub mod hash_writer; +pub mod limited_reader; #[cfg(feature = "base_node")] pub mod rolling_vec; diff --git a/base_layer/core/src/consensus/consensus_encoding.rs b/base_layer/core/src/consensus/consensus_encoding.rs index 1b80051c90..e972903932 100644 --- a/base_layer/core/src/consensus/consensus_encoding.rs +++ b/base_layer/core/src/consensus/consensus_encoding.rs @@ -24,6 +24,7 @@ mod bytes; mod crypto; mod generic; mod integers; +mod micro_tari; mod script; mod vec; diff --git a/base_layer/core/src/consensus/consensus_encoding/bytes.rs b/base_layer/core/src/consensus/consensus_encoding/bytes.rs index 9d608b45eb..90eaf76e1a 100644 --- a/base_layer/core/src/consensus/consensus_encoding/bytes.rs +++ b/base_layer/core/src/consensus/consensus_encoding/bytes.rs @@ -23,6 +23,7 @@ use std::{ io, io::{Error, Read, Write}, + ops::Deref, }; use integer_encoding::{VarInt, VarIntReader, VarIntWriter}; @@ -56,7 +57,7 @@ impl From> for Vec { } impl ConsensusDecoding for MaxSizeBytes { - fn consensus_decode(reader: &mut R) -> Result { + fn consensus_decode(reader: &mut R) -> Result { let len = reader.read_varint()?; if len > MAX { return Err(io::Error::new( @@ -70,6 +71,20 @@ impl ConsensusDecoding for MaxSizeBytes { } } +impl AsRef<[u8]> for MaxSizeBytes { + fn as_ref(&self) -> &[u8] { + &self.inner + } +} + +impl Deref for MaxSizeBytes { + type Target = [u8]; + + fn deref(&self) -> &Self::Target { + &self.inner + } +} + impl ConsensusEncoding for &[u8] { fn consensus_encode(&self, writer: &mut W) -> Result { let len = self.len(); @@ -100,7 +115,7 @@ impl ConsensusEncodingSized for [u8; N] { } impl ConsensusDecoding for [u8; N] { - fn consensus_decode(reader: &mut R) -> Result { + fn consensus_decode(reader: &mut R) -> Result { let mut buf = [0u8; N]; reader.read_exact(&mut buf)?; Ok(buf) diff --git a/base_layer/core/src/consensus/consensus_encoding/crypto.rs b/base_layer/core/src/consensus/consensus_encoding/crypto.rs index 481ecef77d..b8887fdc66 100644 --- a/base_layer/core/src/consensus/consensus_encoding/crypto.rs +++ b/base_layer/core/src/consensus/consensus_encoding/crypto.rs @@ -20,13 +20,16 @@ // 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, io::Read}; +use std::{ + io, + io::{Error, Read, Write}, +}; -use tari_common_types::types::{Commitment, PrivateKey, PublicKey, Signature}; +use tari_common_types::types::{ComSignature, Commitment, PrivateKey, PublicKey, RangeProof, Signature}; use tari_crypto::keys::{PublicKey as PublicKeyTrait, SecretKey}; use tari_utilities::ByteArray; -use crate::consensus::{ConsensusDecoding, ConsensusEncoding, ConsensusEncodingSized}; +use crate::consensus::{ConsensusDecoding, ConsensusEncoding, ConsensusEncodingSized, MaxSizeBytes}; //---------------------------------- PublicKey --------------------------------------------// @@ -87,7 +90,7 @@ impl ConsensusEncoding for Commitment { impl ConsensusEncodingSized for Commitment { fn consensus_encode_exact_size(&self) -> usize { - 32 + PublicKey::key_length() } } @@ -133,3 +136,51 @@ impl ConsensusDecoding for Signature { Ok(Signature::new(pub_nonce, sig)) } } + +//---------------------------------- RangeProof --------------------------------------------// + +impl ConsensusEncoding for RangeProof { + fn consensus_encode(&self, writer: &mut W) -> Result { + self.0.consensus_encode(writer) + } +} + +impl ConsensusEncodingSized for RangeProof { + fn consensus_encode_exact_size(&self) -> usize { + self.0.consensus_encode_exact_size() + } +} + +impl ConsensusDecoding for RangeProof { + fn consensus_decode(reader: &mut R) -> Result { + const MAX_RANGEPROOF_SIZE: usize = 1024; + let bytes = MaxSizeBytes::::consensus_decode(reader)?; + Ok(Self(bytes.into())) + } +} + +//---------------------------------- Commitment Signature --------------------------------------------// + +impl ConsensusEncoding for ComSignature { + fn consensus_encode(&self, writer: &mut W) -> Result { + let mut written = self.u().consensus_encode(writer)?; + written += self.v().consensus_encode(writer)?; + written += self.public_nonce().consensus_encode(writer)?; + Ok(written) + } +} + +impl ConsensusEncodingSized for ComSignature { + fn consensus_encode_exact_size(&self) -> usize { + PrivateKey::key_length() * 2 + PublicKey::key_length() + } +} + +impl ConsensusDecoding for ComSignature { + fn consensus_decode(reader: &mut R) -> Result { + let u = PrivateKey::consensus_decode(reader)?; + let v = PrivateKey::consensus_decode(reader)?; + let nonce = Commitment::consensus_decode(reader)?; + Ok(ComSignature::new(nonce, u, v)) + } +} diff --git a/base_layer/core/src/consensus/consensus_encoding/micro_tari.rs b/base_layer/core/src/consensus/consensus_encoding/micro_tari.rs new file mode 100644 index 0000000000..7dcec2f591 --- /dev/null +++ b/base_layer/core/src/consensus/consensus_encoding/micro_tari.rs @@ -0,0 +1,54 @@ +// Copyright 2022, The Tari Project +// +// Redistribution and use in source and binary forms, with or without modification, are permitted provided that the +// following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following +// disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the +// following disclaimer in the documentation and/or other materials provided with the distribution. +// +// 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote +// products derived from this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, +// INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, +// 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::{Error, Read, Write}, + mem, +}; + +use crate::{ + consensus::{ConsensusDecoding, ConsensusEncoding, ConsensusEncodingSized}, + transactions::tari_amount::MicroTari, +}; + +const U64_SIZE: usize = mem::size_of::(); + +impl ConsensusEncoding for MicroTari { + fn consensus_encode(&self, writer: &mut W) -> Result { + writer.write_all(&self.0.to_le_bytes()[..])?; + Ok(U64_SIZE) + } +} + +impl ConsensusEncodingSized for MicroTari { + fn consensus_encode_exact_size(&self) -> usize { + U64_SIZE + } +} + +impl ConsensusDecoding for MicroTari { + fn consensus_decode(reader: &mut R) -> Result { + let mut buf = [0u8; U64_SIZE]; + reader.read_exact(&mut buf)?; + Ok(u64::from_le_bytes(buf).into()) + } +} diff --git a/base_layer/core/src/consensus/consensus_encoding/script.rs b/base_layer/core/src/consensus/consensus_encoding/script.rs index 82a744f63c..0486ff7881 100644 --- a/base_layer/core/src/consensus/consensus_encoding/script.rs +++ b/base_layer/core/src/consensus/consensus_encoding/script.rs @@ -20,11 +20,14 @@ // 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, io::Write}; +use std::{ + io, + io::{Read, Write}, +}; use tari_crypto::script::{ExecutionStack, TariScript}; -use crate::consensus::{ConsensusEncoding, ConsensusEncodingSized}; +use crate::consensus::{ConsensusDecoding, ConsensusEncoding, ConsensusEncodingSized, MaxSizeBytes}; impl ConsensusEncoding for TariScript { fn consensus_encode(&self, writer: &mut W) -> Result { @@ -32,8 +35,23 @@ impl ConsensusEncoding for TariScript { } } +/// TODO: implement zero-alloc ConsensusEncodingSized for TariScript impl ConsensusEncodingSized for TariScript {} +impl ConsensusDecoding for TariScript { + fn consensus_decode(reader: &mut R) -> Result { + const MAX_SCRIPT_SIZE: usize = 4096; + let script_bytes = MaxSizeBytes::::consensus_decode(reader)?; + let script = TariScript::from_bytes(&script_bytes).map_err(|err| { + io::Error::new( + io::ErrorKind::InvalidInput, + format!("Failed to deserialize bytes: {}", err), + ) + })?; + Ok(script) + } +} + impl ConsensusEncoding for ExecutionStack { fn consensus_encode(&self, writer: &mut W) -> Result { self.as_bytes().consensus_encode(writer) @@ -41,3 +59,13 @@ impl ConsensusEncoding for ExecutionStack { } impl ConsensusEncodingSized for ExecutionStack {} + +impl ConsensusDecoding for ExecutionStack { + fn consensus_decode(reader: &mut R) -> Result { + const MAX_STACK_SIZE: usize = 4096; + let bytes = MaxSizeBytes::::consensus_decode(reader)?; + let stack = + ExecutionStack::from_bytes(&bytes).map_err(|err| io::Error::new(io::ErrorKind::InvalidInput, err))?; + Ok(stack) + } +} diff --git a/base_layer/core/src/covenants/arguments.rs b/base_layer/core/src/covenants/arguments.rs index fd664dd5d3..ccb6f2b9c6 100644 --- a/base_layer/core/src/covenants/arguments.rs +++ b/base_layer/core/src/covenants/arguments.rs @@ -25,13 +25,13 @@ use std::{ io, }; -use integer_encoding::{VarIntReader, VarIntWriter}; +use integer_encoding::VarIntWriter; use tari_common_types::types::{Commitment, PublicKey}; use tari_crypto::script::TariScript; use tari_utilities::hex::{to_hex, Hex}; use crate::{ - consensus::{ConsensusDecoding, ConsensusEncoding, ConsensusEncodingSized}, + consensus::{ConsensusDecoding, ConsensusEncoding, MaxSizeBytes}, covenants::{ byte_codes, covenant::Covenant, @@ -42,7 +42,6 @@ use crate::{ }, }; -const MAX_TARISCRIPT_ARG_SIZE: usize = 4096; const MAX_COVENANT_ARG_SIZE: usize = 4096; const MAX_BYTES_ARG_SIZE: usize = 4096; @@ -80,17 +79,19 @@ impl CovenantArg { }, ARG_COMMITMENT => Ok(CovenantArg::Commitment(Commitment::consensus_decode(reader)?)), ARG_TARI_SCRIPT => { - let buf = reader.read_variable_length_bytes(MAX_TARISCRIPT_ARG_SIZE)?; - let script = TariScript::from_bytes(&buf[..])?; + let script = TariScript::consensus_decode(reader)?; Ok(CovenantArg::TariScript(script)) }, ARG_COVENANT => { let buf = reader.read_variable_length_bytes(MAX_COVENANT_ARG_SIZE)?; - let covenant = Covenant::consensus_decode(&mut buf.as_slice())?; + // Do not use consensus_decoding here because the compiler infinitely recurses to resolve the R generic, + // R becomes the reader of this call and so on. This impl has an arg limit anyway and so is safe + // TODO: Impose a limit on depth of covenants within covenants + let covenant = Covenant::from_bytes(&buf)?; Ok(CovenantArg::Covenant(covenant)) }, ARG_UINT => { - let v = reader.read_varint::()?; + let v = u64::consensus_decode(reader)?; Ok(CovenantArg::Uint(v)) }, ARG_OUTPUT_FIELD => { @@ -108,8 +109,8 @@ impl CovenantArg { Ok(CovenantArg::OutputFields(fields)) }, ARG_BYTES => { - let buf = reader.read_variable_length_bytes(MAX_BYTES_ARG_SIZE)?; - Ok(CovenantArg::Bytes(buf)) + let buf = MaxSizeBytes::::consensus_decode(reader)?; + Ok(CovenantArg::Bytes(buf.into())) }, _ => Err(CovenantDecodeError::UnknownArgByteCode { code }), @@ -137,21 +138,21 @@ impl CovenantArg { }, TariScript(script) => { written += writer.write_u8_fixed(ARG_TARI_SCRIPT)?; - written += writer.write_variable_length_bytes(&script.as_bytes())?; + written += script.consensus_encode(writer)?; }, Covenant(covenant) => { written += writer.write_u8_fixed(ARG_COVENANT)?; - let len = covenant.consensus_encode_exact_size(); + let len = covenant.get_byte_length(); written += writer.write_varint(len)?; written += covenant.write_to(writer)?; }, Uint(int) => { written += writer.write_u8_fixed(ARG_UINT)?; - written += writer.write_varint(*int)?; + written += int.consensus_encode(writer)?; }, OutputField(field) => { written += writer.write_u8_fixed(ARG_OUTPUT_FIELD)?; - written += writer.write_varint(field.as_byte())?; + written += writer.write_u8_fixed(field.as_byte())?; }, OutputFields(fields) => { written += writer.write_u8_fixed(ARG_OUTPUT_FIELDS)?; @@ -159,7 +160,7 @@ impl CovenantArg { }, Bytes(bytes) => { written += writer.write_u8_fixed(ARG_BYTES)?; - written += writer.write_variable_length_bytes(bytes)?; + written += bytes.consensus_encode(writer)?; }, } diff --git a/base_layer/core/src/covenants/covenant.rs b/base_layer/core/src/covenants/covenant.rs index 895ac54dbc..5f993b166f 100644 --- a/base_layer/core/src/covenants/covenant.rs +++ b/base_layer/core/src/covenants/covenant.rs @@ -22,8 +22,10 @@ use std::{io, iter::FromIterator}; +use integer_encoding::{VarInt, VarIntReader, VarIntWriter}; + use crate::{ - common::byte_counter::ByteCounter, + common::{byte_counter::ByteCounter, limited_reader::LimitedBytesReader}, consensus::{ConsensusDecoding, ConsensusEncoding, ConsensusEncodingSized}, covenants::{ context::CovenantContext, @@ -37,6 +39,8 @@ use crate::{ transactions::transaction::{TransactionInput, TransactionOutput}, }; +const MAX_COVENANT_BYTES: usize = 4096; + #[derive(Debug, Clone, PartialEq, Eq, Default)] pub struct Covenant { tokens: Vec, @@ -51,13 +55,28 @@ impl Covenant { if bytes.is_empty() { return Ok(Self::new()); } + if bytes.len() > MAX_COVENANT_BYTES { + return Err(CovenantDecodeError::ExceededMaxBytes); + } CovenantTokenDecoder::new(&mut bytes).collect() } + pub fn to_bytes(&self) -> Vec { + let mut buf = Vec::with_capacity(self.get_byte_length()); + self.write_to(&mut buf).unwrap(); + buf + } + pub(super) fn write_to(&self, writer: &mut W) -> Result { CovenantTokenEncoder::new(self.tokens.as_slice()).write_to(writer) } + pub(super) fn get_byte_length(&self) -> usize { + let mut counter = ByteCounter::new(); + self.write_to(&mut counter).unwrap(); + counter.get() + } + pub fn execute<'a>( &self, block_height: u64, @@ -96,21 +115,33 @@ impl Covenant { impl ConsensusEncoding for Covenant { fn consensus_encode(&self, writer: &mut W) -> Result { - self.write_to(writer) + let len = self.get_byte_length(); + let mut written = writer.write_varint(len)?; + written += self.write_to(writer)?; + Ok(written) } } impl ConsensusEncodingSized for Covenant { fn consensus_encode_exact_size(&self) -> usize { - let mut byte_counter = ByteCounter::new(); - self.write_to(&mut byte_counter).expect("unreachable panic"); - byte_counter.get() + let len = self.get_byte_length(); + len.required_space() + len } } impl ConsensusDecoding for Covenant { fn consensus_decode(reader: &mut R) -> Result { - CovenantTokenDecoder::new(reader) + let len = reader.read_varint::()?; + // Check the length varint - this may be maliciously misreported + if len > MAX_COVENANT_BYTES { + return Err(io::Error::new( + io::ErrorKind::InvalidInput, + "covenants: Length varint exceeded maximum", + )); + } + // Ensure that no more than the maximum bytes can be read + let mut limited = LimitedBytesReader::new(MAX_COVENANT_BYTES, reader); + CovenantTokenDecoder::new(&mut limited) .collect::>() .map_err(|err| io::Error::new(io::ErrorKind::InvalidInput, err)) } @@ -159,9 +190,10 @@ mod test { use super::*; #[test] - fn it_encodes_to_empty_bytes() { + fn it_encodes_to_bytes() { let bytes = Covenant::new().to_consensus_bytes(); - assert_eq!(bytes.len(), 0); + assert_eq!(bytes[0], 0); + assert_eq!(bytes.len(), 1); } } @@ -170,8 +202,8 @@ mod test { #[test] fn it_is_identity_if_empty_bytes() { - let empty_buf = &[] as &[u8; 0]; - let covenant = Covenant::consensus_decode(&mut &empty_buf[..]).unwrap(); + let empty_cov = &[0u8]; + let covenant = Covenant::consensus_decode(&mut &empty_cov[..]).unwrap(); let outputs = create_outputs(10, Default::default()); let input = create_input(); diff --git a/base_layer/core/src/covenants/decoder.rs b/base_layer/core/src/covenants/decoder.rs index e2ea1aa099..428004c9c6 100644 --- a/base_layer/core/src/covenants/decoder.rs +++ b/base_layer/core/src/covenants/decoder.rs @@ -73,10 +73,10 @@ pub enum CovenantDecodeError { UnknownByteCode { code: u8 }, #[error("Unexpected EoF, expected {expected}")] UnexpectedEof { expected: &'static str }, - // #[error("Byte array error: {0}")] - // ByteArrayError(#[from] ByteArrayError), #[error("Tari script error: {0}")] ScriptError(#[from] ScriptError), + #[error("Covenant exceeded maximum bytes")] + ExceededMaxBytes, #[error(transparent)] Io(#[from] io::Error), } @@ -126,7 +126,6 @@ mod test { use super::*; use crate::{ - consensus::ToConsensusBytes, covenant, covenants::{arguments::CovenantArg, fields::OutputField, filters::CovenantFilter}, }; @@ -142,11 +141,13 @@ mod test { let hash = from_hex("53563b674ba8e5166adb57afa8355bcf2ee759941eef8f8959b802367c2558bd").unwrap(); let mut hash_buf = [0u8; 32]; hash_buf.copy_from_slice(hash.as_slice()); - let bytes = covenant!(fields_hashed_eq( + let mut bytes = Vec::new(); + covenant!(fields_hashed_eq( @fields(@field::commitment, @field::features_metadata), @hash(hash_buf), )) - .to_consensus_bytes(); + .write_to(&mut bytes) + .unwrap(); let mut buf = bytes.as_slice(); let mut decoder = CovenantTokenDecoder::new(&mut buf); let token = decoder.next().unwrap().unwrap(); diff --git a/base_layer/core/src/covenants/encoder.rs b/base_layer/core/src/covenants/encoder.rs index b4269fe3b9..f18c2e372c 100644 --- a/base_layer/core/src/covenants/encoder.rs +++ b/base_layer/core/src/covenants/encoder.rs @@ -20,9 +20,7 @@ // 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::{convert::TryFrom, io}; - -use integer_encoding::VarIntWriter; +use std::io; use crate::covenants::token::CovenantToken; @@ -46,7 +44,6 @@ impl<'a> CovenantTokenEncoder<'a> { pub(super) trait CovenentWriteExt: io::Write { fn write_u8_fixed(&mut self, v: u8) -> Result; - fn write_variable_length_bytes(&mut self, buf: &[u8]) -> Result; } impl CovenentWriteExt for W { @@ -54,12 +51,4 @@ impl CovenentWriteExt for W { self.write_all(&[v])?; Ok(1) } - - fn write_variable_length_bytes(&mut self, buf: &[u8]) -> Result { - let len = u16::try_from(buf.len()).map_err(|err| io::Error::new(io::ErrorKind::InvalidInput, err))?; - let mut written = self.write_varint(len)?; - written += buf.len(); - self.write_all(buf)?; - Ok(written) - } } diff --git a/base_layer/core/src/covenants/macros.rs b/base_layer/core/src/covenants/macros.rs index 309ca37d40..6e4893e4ca 100644 --- a/base_layer/core/src/covenants/macros.rs +++ b/base_layer/core/src/covenants/macros.rs @@ -130,10 +130,7 @@ mod test { use tari_test_utils::unpack_enum; use tari_utilities::hex::{from_hex, Hex}; - use crate::{ - consensus::{ConsensusDecoding, ToConsensusBytes}, - covenants::{arguments::CovenantArg, filters::CovenantFilter, token::CovenantToken, Covenant}, - }; + use crate::covenants::{arguments::CovenantArg, filters::CovenantFilter, token::CovenantToken, Covenant}; #[test] fn simple() { @@ -149,7 +146,7 @@ mod test { fn fields() { let covenant = covenant!(and(identity(), fields_preserved(@fields(@field::commitment, @field::sender_offset_public_key)))); - assert_eq!(covenant.to_consensus_bytes().to_hex(), "21203108020002"); + assert_eq!(covenant.to_bytes().to_hex(), "21203108020002"); } #[test] @@ -159,7 +156,7 @@ mod test { let mut hash = [0u8; 32]; hash.copy_from_slice(hash_vec.as_slice()); let covenant = covenant!(output_hash_eq(@hash(hash))); - assert_eq!(covenant.to_consensus_bytes().to_hex(), format!("3001{}", hash_str)); + assert_eq!(covenant.to_bytes().to_hex(), format!("3001{}", hash_str)); let covenant = covenant!(and( identity(), @@ -169,7 +166,7 @@ mod test { ) )); assert_eq!( - covenant.to_consensus_bytes().to_hex(), + covenant.to_bytes().to_hex(), "21202220310153563b674ba8e5166adb57afa8355bcf2ee759941eef8f8959b802367c2558bd" ); } @@ -180,7 +177,7 @@ mod test { identity(), and(identity(), and(not(identity(),), and(identity(), identity()))) )); - assert_eq!(covenant.to_consensus_bytes().to_hex(), "23202120212420212020"); + assert_eq!(covenant.to_bytes().to_hex(), "23202120212420212020"); let h = from_hex("53563b674ba8e5166adb57afa8355bcf2ee759941eef8f8959b802367c2558bd").unwrap(); let mut hash = [0u8; 32]; hash.copy_from_slice(h.as_slice()); @@ -195,7 +192,7 @@ mod test { field_eq(@field::features_maturity, @uint(42)) )); assert_eq!( - covenant.to_consensus_bytes().to_hex(), + covenant.to_bytes().to_hex(), "21222032080200090153563b674ba8e5166adb57afa8355bcf2ee759941eef8f8959b802367c2558bd330706062a" ); } @@ -204,7 +201,7 @@ mod test { fn covenant() { let bytes = vec![0xba, 0xda, 0x55]; let covenant = covenant!(field_eq(@field::covenant, @covenant(and(field_eq(@field::features_unique_id, @bytes(bytes), identity()))))); - assert_eq!(covenant.to_consensus_bytes().to_hex(), "330703050a213307070903bada5520"); + assert_eq!(covenant.to_bytes().to_hex(), "330703050a213307070903bada5520"); } #[test] @@ -220,7 +217,7 @@ mod test { let script = script!(HashSha256 PushHash(Box::new(hash)) Equal IfThen PushPubKey(Box::new(dest_pk)) Else CheckHeightVerify(100) PushPubKey(Box::new(sender_pk)) EndIf); let covenant = covenant!(field_eq(@field::script, @script(script.clone()))); - let decoded = Covenant::consensus_decode(&mut covenant.to_consensus_bytes().as_slice()).unwrap(); + let decoded = Covenant::from_bytes(&covenant.to_bytes()).unwrap(); assert_eq!(covenant, decoded); unpack_enum!(CovenantArg::TariScript(decoded_script) = decoded.tokens()[2].as_arg().unwrap()); assert_eq!(script, *decoded_script); diff --git a/base_layer/core/src/covenants/serde.rs b/base_layer/core/src/covenants/serde.rs index bda6dce813..7b737d0bbc 100644 --- a/base_layer/core/src/covenants/serde.rs +++ b/base_layer/core/src/covenants/serde.rs @@ -31,15 +31,12 @@ use serde::{ }; use tari_utilities::hex::{from_hex, Hex}; -use crate::{ - consensus::{ConsensusDecoding, ToConsensusBytes}, - covenants::Covenant, -}; +use crate::covenants::Covenant; impl Serialize for Covenant { fn serialize(&self, ser: S) -> Result where S: Serializer { - let bytes = self.to_consensus_bytes(); + let bytes = self.to_bytes(); if ser.is_human_readable() { ser.serialize_str(&bytes.to_hex()) } else { @@ -68,9 +65,9 @@ impl<'de> Visitor<'de> for CovenantVisitor { self.visit_str(&v) } - fn visit_bytes(self, mut v: &[u8]) -> Result + fn visit_bytes(self, v: &[u8]) -> Result where E: Error { - Covenant::consensus_decode(&mut v).map_err(|e| E::custom(e.to_string())) + Covenant::from_bytes(v).map_err(|e| E::custom(e.to_string())) } fn visit_borrowed_bytes(self, v: &'de [u8]) -> Result diff --git a/base_layer/core/src/proto/transaction.rs b/base_layer/core/src/proto/transaction.rs index c766b20e44..a6ac8c1800 100644 --- a/base_layer/core/src/proto/transaction.rs +++ b/base_layer/core/src/proto/transaction.rs @@ -35,7 +35,6 @@ use tari_crypto::{ use tari_utilities::convert::try_convert_all; use crate::{ - consensus::{ConsensusDecoding, ToConsensusBytes}, covenants::Covenant, proto, transactions::{ @@ -141,7 +140,7 @@ impl TryFrom for TransactionInput { ExecutionStack::from_bytes(input.input_data.as_slice()).map_err(|err| format!("{:?}", err))?, script_signature, sender_offset_public_key, - Covenant::consensus_decode(&mut input.covenant.as_slice()).map_err(|err| err.to_string())?, + Covenant::from_bytes(&input.covenant).map_err(|err| err.to_string())?, )) } else { if input.output_hash.is_empty() { @@ -199,7 +198,7 @@ impl TryFrom for proto::types::TransactionInput { covenant: input .covenant() .map_err(|_| "Non-compact Transaction input should contain covenant".to_string())? - .to_consensus_bytes(), + .to_bytes(), version: input.version as u32, }) } @@ -234,7 +233,7 @@ impl TryFrom for TransactionOutput { .try_into() .map_err(|_| "Metadata signature could not be converted".to_string())?; - let covenant = Covenant::consensus_decode(&mut output.covenant.as_slice()).map_err(|err| err.to_string())?; + let covenant = Covenant::from_bytes(&output.covenant).map_err(|err| err.to_string())?; Ok(Self::new( TransactionOutputVersion::try_from( @@ -260,7 +259,7 @@ impl From for proto::types::TransactionOutput { script: output.script.as_bytes(), sender_offset_public_key: output.sender_offset_public_key.as_bytes().to_vec(), metadata_signature: Some(output.metadata_signature.into()), - covenant: output.covenant.to_consensus_bytes(), + covenant: output.covenant.to_bytes(), version: output.version as u32, } } diff --git a/base_layer/core/src/transactions/transaction/asset_output_features.rs b/base_layer/core/src/transactions/transaction/asset_output_features.rs index 70cc4f85aa..7901727232 100644 --- a/base_layer/core/src/transactions/transaction/asset_output_features.rs +++ b/base_layer/core/src/transactions/transaction/asset_output_features.rs @@ -20,7 +20,10 @@ // 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::{Error, Read, Write}; +use std::{ + io, + io::{Read, Write}, +}; use serde::{Deserialize, Serialize}; use tari_common_types::types::PublicKey; @@ -39,7 +42,7 @@ pub struct AssetOutputFeatures { } impl ConsensusEncoding for AssetOutputFeatures { - fn consensus_encode(&self, writer: &mut W) -> Result { + fn consensus_encode(&self, writer: &mut W) -> Result { let mut written = self.public_key.consensus_encode(writer)?; written += self.template_ids_implemented.consensus_encode(writer)?; written += self.template_parameters.consensus_encode(writer)?; @@ -50,14 +53,13 @@ impl ConsensusEncoding for AssetOutputFeatures { impl ConsensusEncodingSized for AssetOutputFeatures {} impl ConsensusDecoding for AssetOutputFeatures { - fn consensus_decode(reader: &mut R) -> Result { + fn consensus_decode(reader: &mut R) -> Result { let public_key = PublicKey::consensus_decode(reader)?; const MAX_TEMPLATES: usize = 50; - let template_ids_implemented = as ConsensusDecoding>::consensus_decode(reader)?; + let template_ids_implemented = MaxSizeVec::::consensus_decode(reader)?; const MAX_TEMPLATE_PARAMS: usize = 50; - let template_parameters = - as ConsensusDecoding>::consensus_decode(reader)?; + let template_parameters = MaxSizeVec::::consensus_decode(reader)?; Ok(Self { public_key, diff --git a/base_layer/core/src/transactions/transaction/error.rs b/base_layer/core/src/transactions/transaction/error.rs index 631c745f9c..996517846a 100644 --- a/base_layer/core/src/transactions/transaction/error.rs +++ b/base_layer/core/src/transactions/transaction/error.rs @@ -23,6 +23,8 @@ // Portions of this file were originally copyrighted (c) 2018 The Grin Developers, issued under the Apache License, // Version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0. +use std::io; + use serde::{Deserialize, Serialize}; use tari_crypto::{range_proof::RangeProofError, script::ScriptError, signatures::CommitmentSignatureError}; use thiserror::Error; @@ -66,6 +68,8 @@ pub enum TransactionError { MissingTransactionInputData, #[error("Error executing covenant: {0}")] CovenantError(String), + #[error("Consensus encoding error: {0}")] + ConsensusEncodingError(String), } impl From for TransactionError { @@ -73,3 +77,9 @@ impl From for TransactionError { TransactionError::CovenantError(err.to_string()) } } + +impl From for TransactionError { + fn from(err: io::Error) -> Self { + TransactionError::ConsensusEncodingError(err.to_string()) + } +} diff --git a/base_layer/core/src/transactions/transaction/kernel_features.rs b/base_layer/core/src/transactions/transaction/kernel_features.rs index b6df7b168f..2491485454 100644 --- a/base_layer/core/src/transactions/transaction/kernel_features.rs +++ b/base_layer/core/src/transactions/transaction/kernel_features.rs @@ -20,8 +20,12 @@ // 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::{Error, Write}; + use serde::{Deserialize, Serialize}; +use crate::consensus::{ConsensusEncoding, ConsensusEncodingSized}; + bitflags! { /// Options for a kernel's structure or use. /// TODO: expand to accommodate Tari DAN transaction types, such as namespace and validator node registrations @@ -37,3 +41,16 @@ impl KernelFeatures { KernelFeatures::COINBASE_KERNEL } } + +impl ConsensusEncoding for KernelFeatures { + fn consensus_encode(&self, writer: &mut W) -> Result { + writer.write_all(&[self.bits][..])?; + Ok(1) + } +} + +impl ConsensusEncodingSized for KernelFeatures { + fn consensus_encode_exact_size(&self) -> usize { + 1 + } +} diff --git a/base_layer/core/src/transactions/transaction/mint_non_fungible_features.rs b/base_layer/core/src/transactions/transaction/mint_non_fungible_features.rs index fc714169aa..3110254b44 100644 --- a/base_layer/core/src/transactions/transaction/mint_non_fungible_features.rs +++ b/base_layer/core/src/transactions/transaction/mint_non_fungible_features.rs @@ -20,7 +20,10 @@ // 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::{Error, Read, Write}; +use std::{ + io, + io::{Read, Write}, +}; use serde::{Deserialize, Serialize}; use tari_common_types::types::{Commitment, PublicKey}; @@ -36,7 +39,7 @@ pub struct MintNonFungibleFeatures { } impl ConsensusEncoding for MintNonFungibleFeatures { - fn consensus_encode(&self, writer: &mut W) -> Result { + fn consensus_encode(&self, writer: &mut W) -> Result { let mut written = self.asset_public_key.consensus_encode(writer)?; written += self.asset_owner_commitment.consensus_encode(writer)?; Ok(written) @@ -50,7 +53,7 @@ impl ConsensusEncodingSized for MintNonFungibleFeatures { } impl ConsensusDecoding for MintNonFungibleFeatures { - fn consensus_decode(reader: &mut R) -> Result { + fn consensus_decode(reader: &mut R) -> Result { let asset_public_key = PublicKey::consensus_decode(reader)?; let asset_owner_commitment = Commitment::consensus_decode(reader)?; Ok(Self { diff --git a/base_layer/core/src/transactions/transaction/mod.rs b/base_layer/core/src/transactions/transaction/mod.rs index 5619c3c632..eb397b8e07 100644 --- a/base_layer/core/src/transactions/transaction/mod.rs +++ b/base_layer/core/src/transactions/transaction/mod.rs @@ -37,7 +37,7 @@ pub use output_flags::OutputFlags; pub use rewind_result::RewindResult; pub use side_chain_checkpoint_features::SideChainCheckpointFeatures; use tari_common_types::types::{Commitment, HashDigest}; -use tari_crypto::{script::TariScript, tari_utilities::ByteArray}; +use tari_crypto::script::TariScript; pub use template_parameter::TemplateParameter; pub use transaction::Transaction; pub use transaction_builder::TransactionBuilder; @@ -50,8 +50,6 @@ pub use transaction_output_version::TransactionOutputVersion; pub use unblinded_output::UnblindedOutput; pub use unblinded_output_builder::UnblindedOutputBuilder; -use crate::{consensus::ToConsensusBytes, covenants::Covenant}; - mod asset_output_features; mod error; mod full_rewind_result; @@ -88,6 +86,8 @@ pub const MAX_TRANSACTION_RECIPIENTS: usize = 15; //---------------------------------------- Crate functions ----------------------------------------------------// +use crate::{common::hash_writer::HashWriter, consensus::ConsensusEncoding, 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. /// @@ -96,19 +96,18 @@ pub const MAX_TRANSACTION_RECIPIENTS: usize = 15; /// b) Range proofs are committed to elsewhere and so we'd be hashing them twice (and as mentioned, this is slow) /// c) TransactionInputs will now have the same hash as UTXOs, which makes locating STXOs easier when doing reorgs pub(super) fn hash_output( + version: TransactionOutputVersion, features: &OutputFeatures, commitment: &Commitment, script: &TariScript, covenant: &Covenant, - version: TransactionOutputVersion, -) -> Vec { - HashDigest::new() - .chain(features.to_consensus_bytes()) - .chain(commitment.as_bytes()) - // .chain(range proof) // See docs as to why we exclude this - .chain(script.as_bytes()) - .chain(covenant.to_consensus_bytes()) - .chain((version as u8).to_le_bytes()) - .finalize() - .to_vec() +) -> [u8; 32] { + let mut hasher = HashWriter::new(HashDigest::new()); + // unwrap: hashwriter is infallible + version.consensus_encode(&mut hasher).unwrap(); + features.consensus_encode(&mut hasher).unwrap(); + commitment.consensus_encode(&mut hasher).unwrap(); + script.consensus_encode(&mut hasher).unwrap(); + covenant.consensus_encode(&mut hasher).unwrap(); + hasher.finalize() } diff --git a/base_layer/core/src/transactions/transaction/output_features_version.rs b/base_layer/core/src/transactions/transaction/output_features_version.rs index ac2e8a4b12..7f0c602388 100644 --- a/base_layer/core/src/transactions/transaction/output_features_version.rs +++ b/base_layer/core/src/transactions/transaction/output_features_version.rs @@ -1,6 +1,7 @@ use std::{ convert::{TryFrom, TryInto}, - io::{Error, ErrorKind, Read, Write}, + io, + io::{ErrorKind, Read, Write}, }; use serde::{Deserialize, Serialize}; @@ -35,7 +36,7 @@ impl TryFrom for OutputFeaturesVersion { } impl ConsensusEncoding for OutputFeaturesVersion { - fn consensus_encode(&self, writer: &mut W) -> Result { + fn consensus_encode(&self, writer: &mut W) -> Result { writer.write_all(&[self.as_u8()])?; Ok(1) } @@ -48,12 +49,12 @@ impl ConsensusEncodingSized for OutputFeaturesVersion { } impl ConsensusDecoding for OutputFeaturesVersion { - fn consensus_decode(reader: &mut R) -> Result { + fn consensus_decode(reader: &mut R) -> Result { let mut buf = [0u8; 1]; reader.read_exact(&mut buf)?; let version = buf[0] .try_into() - .map_err(|_| Error::new(ErrorKind::InvalidInput, format!("Unknown version {}", buf[0])))?; + .map_err(|_| io::Error::new(ErrorKind::InvalidInput, format!("Unknown version {}", buf[0])))?; Ok(version) } } diff --git a/base_layer/core/src/transactions/transaction/side_chain_checkpoint_features.rs b/base_layer/core/src/transactions/transaction/side_chain_checkpoint_features.rs index 447a9d4896..57c411b170 100644 --- a/base_layer/core/src/transactions/transaction/side_chain_checkpoint_features.rs +++ b/base_layer/core/src/transactions/transaction/side_chain_checkpoint_features.rs @@ -59,7 +59,7 @@ impl ConsensusDecoding for SideChainCheckpointFeatures { let merkle_root = <[u8; 32] as ConsensusDecoding>::consensus_decode(reader)?.to_vec(); const MAX_COMMITTEE_KEYS: usize = 50; - let committee = as ConsensusDecoding>::consensus_decode(reader)?; + let committee = MaxSizeVec::::consensus_decode(reader)?; Ok(Self { merkle_root, diff --git a/base_layer/core/src/transactions/transaction/template_parameter.rs b/base_layer/core/src/transactions/transaction/template_parameter.rs index 3ab991af67..501b20dfad 100644 --- a/base_layer/core/src/transactions/transaction/template_parameter.rs +++ b/base_layer/core/src/transactions/transaction/template_parameter.rs @@ -20,7 +20,10 @@ // 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::{Error, Read, Write}; +use std::{ + io, + io::{Read, Write}, +}; use integer_encoding::{VarIntReader, VarIntWriter}; use serde::{Deserialize, Serialize}; @@ -35,7 +38,7 @@ pub struct TemplateParameter { } impl ConsensusEncoding for TemplateParameter { - fn consensus_encode(&self, writer: &mut W) -> Result { + fn consensus_encode(&self, writer: &mut W) -> Result { let mut written = writer.write_varint(self.template_id)?; written += writer.write_varint(self.template_data_version)?; written += self.template_data.consensus_encode(writer)?; @@ -46,11 +49,11 @@ impl ConsensusEncoding for TemplateParameter { impl ConsensusEncodingSized for TemplateParameter {} impl ConsensusDecoding for TemplateParameter { - fn consensus_decode(reader: &mut R) -> Result { + fn consensus_decode(reader: &mut R) -> Result { let template_id = reader.read_varint()?; let template_data_version = reader.read_varint()?; const MAX_TEMPLATE_DATA_LEN: usize = 1024; - let template_data = as ConsensusDecoding>::consensus_decode(reader)?; + let template_data = MaxSizeBytes::::consensus_decode(reader)?; Ok(Self { template_id, diff --git a/base_layer/core/src/transactions/transaction/test.rs b/base_layer/core/src/transactions/transaction/test.rs index 22553975ce..087d21a590 100644 --- a/base_layer/core/src/transactions/transaction/test.rs +++ b/base_layer/core/src/transactions/transaction/test.rs @@ -176,7 +176,7 @@ fn kernel_hash() { .unwrap(); assert_eq!( &k.hash().to_hex(), - "1b772d53cb42b54553b8049c6b010449405278262155c4354a680522d673df54" + "ce54718b33405e8fc96ed68044af21febc84c7a74c2aa9d792947f2571c7a61b" ); } @@ -195,7 +195,7 @@ fn kernel_metadata() { .unwrap(); assert_eq!( &k.hash().to_hex(), - "7a86132527fe2c40c09cc39270c69dc44ba6fe94bc9850c0f9940397be72a394" + "db1522441628687beb21d4d8279e107e733aec9c8b7d513ef3c35b05c1e0150c" ) } @@ -282,9 +282,8 @@ fn check_cut_through() { assert_eq!(tx.body.kernels().len(), 1); let factories = CryptoFactories::default(); - assert!(tx - .validate_internal_consistency(false, &factories, None, None, u64::MAX) - .is_ok()); + tx.validate_internal_consistency(false, &factories, None, None, u64::MAX) + .unwrap(); let schema = txn_schema!(from: vec![outputs[1].clone()], to: vec![1 * T, 2 * T]); let (tx2, _outputs) = test_helpers::spend_utxos(schema); @@ -315,14 +314,13 @@ fn check_cut_through() { } // Validate basis transaction where cut-through has not been applied. - assert!(tx3 - .validate_internal_consistency(false, &factories, None, None, u64::MAX) - .is_ok()); + tx3.validate_internal_consistency(false, &factories, None, None, u64::MAX) + .unwrap(); // tx3_cut_through has manual cut-through, it should not be possible so this should fail - assert!(tx3_cut_through + tx3_cut_through .validate_internal_consistency(false, &factories, None, None, u64::MAX) - .is_err()); + .unwrap_err(); } #[test] @@ -488,6 +486,7 @@ mod output_features { mod validate_internal_consistency { use super::*; + use crate::consensus::ToConsensusBytes; fn test_case( input_params: &UtxoTestParams, diff --git a/base_layer/core/src/transactions/transaction/transaction_input.rs b/base_layer/core/src/transactions/transaction/transaction_input.rs index 6693517e32..cdb2d37570 100644 --- a/base_layer/core/src/transactions/transaction/transaction_input.rs +++ b/base_layer/core/src/transactions/transaction/transaction_input.rs @@ -30,15 +30,7 @@ use std::{ use blake2::Digest; use serde::{Deserialize, Serialize}; -use tari_common_types::types::{ - Challenge, - ComSignature, - Commitment, - CommitmentFactory, - HashDigest, - HashOutput, - PublicKey, -}; +use tari_common_types::types::{ComSignature, Commitment, CommitmentFactory, HashDigest, HashOutput, PublicKey}; use tari_crypto::{ commitment::HomomorphicCommitmentFactory, script::{ExecutionStack, ScriptContext, StackItem, TariScript}, @@ -47,13 +39,12 @@ use tari_crypto::{ use super::{TransactionInputVersion, TransactionOutputVersion}; use crate::{ - consensus::ToConsensusBytes, + common::hash_writer::HashWriter, + consensus::ConsensusEncoding, covenants::Covenant, - transactions::transaction::{ - transaction_output::TransactionOutput, - OutputFeatures, - TransactionError, - UnblindedOutput, + transactions::{ + transaction, + transaction::{transaction_output::TransactionOutput, OutputFeatures, TransactionError, UnblindedOutput}, }, }; @@ -163,14 +154,13 @@ impl TransactionInput { script_public_key: &PublicKey, commitment: &Commitment, ) -> Vec { - Challenge::new() - .chain(nonce_commitment.as_bytes()) - .chain(script.as_bytes().as_slice()) - .chain(input_data.as_bytes().as_slice()) - .chain(script_public_key.as_bytes()) - .chain(commitment.as_bytes()) - .finalize() - .to_vec() + let mut writer = HashWriter::new(HashDigest::new()); + nonce_commitment.consensus_encode(&mut writer).unwrap(); + script.consensus_encode(&mut writer).unwrap(); + input_data.consensus_encode(&mut writer).unwrap(); + script_public_key.consensus_encode(&mut writer).unwrap(); + commitment.consensus_encode(&mut writer).unwrap(); + writer.finalize().to_vec() } pub fn commitment(&self) -> Result<&Commitment, TransactionError> { @@ -311,20 +301,13 @@ impl TransactionInput { match self.spent_output { SpentOutput::OutputHash(ref h) => h.clone(), SpentOutput::OutputData { + version, ref commitment, ref script, ref features, ref covenant, - version, .. - } => HashDigest::new() - .chain(features.to_consensus_bytes()) - .chain(commitment.as_bytes()) - .chain(script.as_bytes()) - .chain(covenant.to_consensus_bytes()) - .chain((version as u8).to_le_bytes()) - .finalize() - .to_vec(), + } => transaction::hash_output(version, features, commitment, script, covenant).to_vec(), } } @@ -337,25 +320,25 @@ impl TransactionInput { match self.spent_output { SpentOutput::OutputHash(_) => Err(TransactionError::MissingTransactionInputData), SpentOutput::OutputData { + version, ref features, ref commitment, ref script, ref sender_offset_public_key, ref covenant, - version, - } => Ok(HashDigest::new() - .chain(features.to_consensus_bytes()) - .chain(commitment.to_consensus_bytes()) - .chain(script.as_bytes()) - .chain(sender_offset_public_key.to_consensus_bytes()) - .chain(self.script_signature.u().to_consensus_bytes()) - .chain(self.script_signature.v().to_consensus_bytes()) - .chain(self.script_signature.public_nonce().to_consensus_bytes()) - .chain(self.input_data.as_bytes()) - .chain(covenant.to_consensus_bytes()) - .chain((version as u8).to_le_bytes()) - .finalize() - .to_vec()), + } => { + let mut writer = HashWriter::new(HashDigest::new()); + version.consensus_encode(&mut writer)?; + features.consensus_encode(&mut writer)?; + commitment.consensus_encode(&mut writer)?; + script.consensus_encode(&mut writer)?; + sender_offset_public_key.consensus_encode(&mut writer)?; + self.script_signature.consensus_encode(&mut writer)?; + self.input_data.consensus_encode(&mut writer)?; + covenant.consensus_encode(&mut writer)?; + + Ok(writer.finalize().to_vec()) + }, } } @@ -431,12 +414,12 @@ impl Ord for TransactionInput { pub enum SpentOutput { OutputHash(HashOutput), OutputData { + version: TransactionOutputVersion, features: OutputFeatures, commitment: Commitment, script: TariScript, sender_offset_public_key: PublicKey, /// The transaction covenant covenant: Covenant, - version: TransactionOutputVersion, }, } diff --git a/base_layer/core/src/transactions/transaction/transaction_input_version.rs b/base_layer/core/src/transactions/transaction/transaction_input_version.rs index 5c3d10e20a..ce31e72768 100644 --- a/base_layer/core/src/transactions/transaction/transaction_input_version.rs +++ b/base_layer/core/src/transactions/transaction/transaction_input_version.rs @@ -1,7 +1,13 @@ -use std::convert::TryFrom; +use std::{ + convert::{TryFrom, TryInto}, + io, + io::{ErrorKind, Read, Write}, +}; use serde::{Deserialize, Serialize}; +use crate::consensus::{ConsensusDecoding, ConsensusEncoding, ConsensusEncodingSized}; + #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] #[repr(u8)] pub enum TransactionInputVersion { @@ -12,6 +18,10 @@ impl TransactionInputVersion { pub fn get_current_version() -> Self { Self::V0 } + + pub fn as_u8(self) -> u8 { + self as u8 + } } impl TryFrom for TransactionInputVersion { @@ -24,3 +34,27 @@ impl TryFrom for TransactionInputVersion { } } } + +impl ConsensusEncoding for TransactionInputVersion { + fn consensus_encode(&self, writer: &mut W) -> Result { + writer.write_all(&[self.as_u8()])?; + Ok(1) + } +} + +impl ConsensusEncodingSized for TransactionInputVersion { + fn consensus_encode_exact_size(&self) -> usize { + 1 + } +} + +impl ConsensusDecoding for TransactionInputVersion { + fn consensus_decode(reader: &mut R) -> Result { + let mut buf = [0u8; 1]; + reader.read_exact(&mut buf)?; + let version = buf[0] + .try_into() + .map_err(|_| io::Error::new(ErrorKind::InvalidInput, format!("Unknown version {}", buf[0])))?; + Ok(version) + } +} diff --git a/base_layer/core/src/transactions/transaction/transaction_kernel.rs b/base_layer/core/src/transactions/transaction/transaction_kernel.rs index bddb42df36..5ffe7bb9dd 100644 --- a/base_layer/core/src/transactions/transaction/transaction_kernel.rs +++ b/base_layer/core/src/transactions/transaction/transaction_kernel.rs @@ -34,10 +34,14 @@ use tari_common_types::types::{Commitment, HashDigest, Signature}; use tari_crypto::tari_utilities::{hex::Hex, message_format::MessageFormat, ByteArray, Hashable}; use super::TransactionKernelVersion; -use crate::transactions::{ - tari_amount::MicroTari, - transaction::{KernelFeatures, TransactionError}, - transaction_protocol::{build_challenge, TransactionMetadata}, +use crate::{ + common::hash_writer::HashWriter, + consensus::ConsensusEncoding, + transactions::{ + tari_amount::MicroTari, + transaction::{KernelFeatures, TransactionError}, + transaction_protocol::{build_challenge, TransactionMetadata}, + }, }; /// The transaction kernel tracks the excess for a given transaction. For an explanation of what the excess is, and @@ -125,16 +129,16 @@ impl Hashable for TransactionKernel { /// Produce a canonical hash for a transaction kernel. The hash is given by /// $$ H(feature_bits | fee | lock_height | P_excess | R_sum | s_sum) fn hash(&self) -> Vec { - HashDigest::new() - .chain(&[self.features.bits()]) - .chain(u64::from(self.fee).to_le_bytes()) - .chain(self.lock_height.to_le_bytes()) - .chain(self.excess.as_bytes()) - .chain(self.excess_sig.get_public_nonce().as_bytes()) - .chain(self.excess_sig.get_signature().as_bytes()) - .chain((self.version as u8).to_le_bytes()) - .finalize() - .to_vec() + let mut writer = HashWriter::new(HashDigest::new()); + // unwraps: HashWriter is infallible + self.version.consensus_encode(&mut writer).unwrap(); + self.features.consensus_encode(&mut writer).unwrap(); + self.fee.consensus_encode(&mut writer).unwrap(); + self.lock_height.consensus_encode(&mut writer).unwrap(); + self.excess.consensus_encode(&mut writer).unwrap(); + self.excess_sig.consensus_encode(&mut writer).unwrap(); + + writer.finalize().to_vec() } } diff --git a/base_layer/core/src/transactions/transaction/transaction_kernel_version.rs b/base_layer/core/src/transactions/transaction/transaction_kernel_version.rs index 155c1584c2..15efe2c8ac 100644 --- a/base_layer/core/src/transactions/transaction/transaction_kernel_version.rs +++ b/base_layer/core/src/transactions/transaction/transaction_kernel_version.rs @@ -1,7 +1,13 @@ -use std::convert::TryFrom; +use std::{ + convert::{TryFrom, TryInto}, + io, + io::{ErrorKind, Read, Write}, +}; use serde::{Deserialize, Serialize}; +use crate::consensus::{ConsensusDecoding, ConsensusEncoding, ConsensusEncodingSized}; + #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] #[repr(u8)] pub enum TransactionKernelVersion { @@ -12,6 +18,10 @@ impl TransactionKernelVersion { pub fn get_current_version() -> Self { Self::V0 } + + pub fn as_u8(self) -> u8 { + self as u8 + } } impl TryFrom for TransactionKernelVersion { type Error = String; @@ -23,3 +33,27 @@ impl TryFrom for TransactionKernelVersion { } } } + +impl ConsensusEncoding for TransactionKernelVersion { + fn consensus_encode(&self, writer: &mut W) -> Result { + writer.write_all(&[self.as_u8()])?; + Ok(1) + } +} + +impl ConsensusEncodingSized for TransactionKernelVersion { + fn consensus_encode_exact_size(&self) -> usize { + 1 + } +} + +impl ConsensusDecoding for TransactionKernelVersion { + fn consensus_decode(reader: &mut R) -> Result { + let mut buf = [0u8; 1]; + reader.read_exact(&mut buf)?; + let version = buf[0] + .try_into() + .map_err(|_| io::Error::new(ErrorKind::InvalidInput, format!("Unknown version {}", buf[0])))?; + Ok(version) + } +} diff --git a/base_layer/core/src/transactions/transaction/transaction_output.rs b/base_layer/core/src/transactions/transaction/transaction_output.rs index b56edc3610..728a0d4e08 100644 --- a/base_layer/core/src/transactions/transaction/transaction_output.rs +++ b/base_layer/core/src/transactions/transaction/transaction_output.rs @@ -54,7 +54,8 @@ use tari_crypto::{ use super::TransactionOutputVersion; use crate::{ - consensus::{ConsensusEncodingSized, ToConsensusBytes}, + common::hash_writer::HashWriter, + consensus::{ConsensusEncoding, ConsensusEncodingSized, ToConsensusBytes}, covenants::Covenant, transactions::{ tari_amount::MicroTari, @@ -251,7 +252,7 @@ impl TransactionOutput { ) -> Challenge { Challenge::new() .chain(public_commitment_nonce.to_consensus_bytes()) - .chain(script.as_bytes()) + .chain(script.to_consensus_bytes()) .chain(features.to_consensus_bytes()) .chain(sender_offset_public_key.to_consensus_bytes()) .chain(commitment.to_consensus_bytes()) @@ -346,13 +347,12 @@ impl TransactionOutput { } pub fn witness_hash(&self) -> Vec { - HashDigest::new() - .chain(self.proof.as_bytes()) - .chain(self.metadata_signature.u().as_bytes()) - .chain(self.metadata_signature.v().as_bytes()) - .chain(self.metadata_signature.public_nonce().as_bytes()) - .finalize() - .to_vec() + let mut hasher = HashWriter::new(HashDigest::new()); + // unwrap: HashWriter is infallible + self.proof.consensus_encode(&mut hasher).unwrap(); + self.metadata_signature.consensus_encode(&mut hasher).unwrap(); + + hasher.finalize().to_vec() } pub fn get_metadata_size(&self) -> usize { @@ -366,12 +366,13 @@ impl TransactionOutput { impl Hashable for TransactionOutput { fn hash(&self) -> Vec { transaction::hash_output( + self.version, &self.features, &self.commitment, &self.script, &self.covenant, - self.version, ) + .to_vec() } } diff --git a/base_layer/core/src/transactions/transaction/transaction_output_version.rs b/base_layer/core/src/transactions/transaction/transaction_output_version.rs index 8192e23fae..862fad8385 100644 --- a/base_layer/core/src/transactions/transaction/transaction_output_version.rs +++ b/base_layer/core/src/transactions/transaction/transaction_output_version.rs @@ -23,10 +23,16 @@ // Portions of this file were originally copyrighted (c) 2018 The Grin Developers, issued under the Apache License, // Version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0. -use std::convert::TryFrom; +use std::{ + convert::{TryFrom, TryInto}, + io, + io::{ErrorKind, Read, Write}, +}; use serde::{Deserialize, Serialize}; +use crate::consensus::{ConsensusDecoding, ConsensusEncoding, ConsensusEncodingSized}; + #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] #[repr(u8)] pub enum TransactionOutputVersion { @@ -37,6 +43,10 @@ impl TransactionOutputVersion { pub fn get_current_version() -> Self { Self::V0 } + + pub fn as_u8(self) -> u8 { + self as u8 + } } impl TryFrom for TransactionOutputVersion { @@ -49,3 +59,27 @@ impl TryFrom for TransactionOutputVersion { } } } + +impl ConsensusEncoding for TransactionOutputVersion { + fn consensus_encode(&self, writer: &mut W) -> Result { + writer.write_all(&[self.as_u8()])?; + Ok(1) + } +} + +impl ConsensusEncodingSized for TransactionOutputVersion { + fn consensus_encode_exact_size(&self) -> usize { + 1 + } +} + +impl ConsensusDecoding for TransactionOutputVersion { + fn consensus_decode(reader: &mut R) -> Result { + let mut buf = [0u8; 1]; + reader.read_exact(&mut buf)?; + let version = buf[0] + .try_into() + .map_err(|_| io::Error::new(ErrorKind::InvalidInput, format!("Unknown version {}", buf[0])))?; + Ok(version) + } +} diff --git a/base_layer/core/src/transactions/transaction/unblinded_output.rs b/base_layer/core/src/transactions/transaction/unblinded_output.rs index e9e3a8e862..16f4769cce 100644 --- a/base_layer/core/src/transactions/transaction/unblinded_output.rs +++ b/base_layer/core/src/transactions/transaction/unblinded_output.rs @@ -261,7 +261,7 @@ impl UnblindedOutput { // Note: added to the struct to ensure consistency between `commitment`, `spending_key` and `value`. pub fn hash(&self, factories: &CryptoFactories) -> Vec { let commitment = factories.commitment.commit_value(&self.spending_key, self.value.into()); - transaction::hash_output(&self.features, &commitment, &self.script, &self.covenant, self.version) + transaction::hash_output(self.version, &self.features, &commitment, &self.script, &self.covenant).to_vec() } } diff --git a/base_layer/wallet/src/output_manager_service/service.rs b/base_layer/wallet/src/output_manager_service/service.rs index d742ef5e6c..2a8aedd8ac 100644 --- a/base_layer/wallet/src/output_manager_service/service.rs +++ b/base_layer/wallet/src/output_manager_service/service.rs @@ -680,7 +680,9 @@ where .consensus_constants .transaction_weight() .round_up_metadata_size( - OutputFeatures::default().consensus_encode_exact_size() + script![Nop].consensus_encode_exact_size(), + OutputFeatures::default().consensus_encode_exact_size() + + script![Nop].consensus_encode_exact_size() + + Covenant::new().consensus_encode_exact_size(), ); let utxo_selection = self diff --git a/base_layer/wallet/src/output_manager_service/storage/sqlite_db/new_output_sql.rs b/base_layer/wallet/src/output_manager_service/storage/sqlite_db/new_output_sql.rs index b6a76c6f57..cbd7d3ef37 100644 --- a/base_layer/wallet/src/output_manager_service/storage/sqlite_db/new_output_sql.rs +++ b/base_layer/wallet/src/output_manager_service/storage/sqlite_db/new_output_sql.rs @@ -58,6 +58,7 @@ pub struct NewOutputSql { pub received_in_tx_id: Option, pub coinbase_block_height: Option, pub features_json: String, + pub covenant: Vec, } impl NewOutputSql { @@ -97,6 +98,7 @@ impl NewOutputSql { reason: format!("Could not parse features from JSON:{}", s), } })?, + covenant: output.unblinded_output.covenant.to_bytes(), }) } @@ -144,6 +146,7 @@ impl From for NewOutputSql { received_in_tx_id: o.received_in_tx_id, coinbase_block_height: o.coinbase_block_height, features_json: o.features_json, + covenant: o.covenant, } } } diff --git a/base_layer/wallet/src/output_manager_service/storage/sqlite_db/output_sql.rs b/base_layer/wallet/src/output_manager_service/storage/sqlite_db/output_sql.rs index 2fa01528bf..c33f29faa3 100644 --- a/base_layer/wallet/src/output_manager_service/storage/sqlite_db/output_sql.rs +++ b/base_layer/wallet/src/output_manager_service/storage/sqlite_db/output_sql.rs @@ -30,7 +30,6 @@ use tari_common_types::{ types::{ComSignature, Commitment, PrivateKey, PublicKey}, }; use tari_core::{ - consensus::ConsensusDecoding, covenants::Covenant, transactions::{ tari_amount::MicroTari, @@ -563,7 +562,7 @@ impl TryFrom for DbUnblindedOutput { })?, ), o.script_lock_height as u64, - Covenant::consensus_decode(&mut o.covenant.as_slice()).map_err(|e| { + Covenant::from_bytes(&o.covenant).map_err(|e| { error!( target: LOG_TARGET, "Could not create Covenant from stored bytes ({}), They might be encrypted", e diff --git a/base_layer/wallet/src/utxo_scanner_service/error.rs b/base_layer/wallet/src/utxo_scanner_service/error.rs index d479e82fae..64e5e34e54 100644 --- a/base_layer/wallet/src/utxo_scanner_service/error.rs +++ b/base_layer/wallet/src/utxo_scanner_service/error.rs @@ -46,8 +46,8 @@ pub enum UtxoScannerError { UtxoScanningError(String), #[error("Hex conversion error: {0}")] HexError(#[from] HexError), - #[error("Error converting a type")] - ConversionError, + #[error("Error converting a type: {0}")] + ConversionError(String), #[error("Output manager error: `{0}`")] OutputManagerError(#[from] OutputManagerError), #[error("UTXO Import error: `{0}`")] diff --git a/base_layer/wallet/src/utxo_scanner_service/utxo_scanner_task.rs b/base_layer/wallet/src/utxo_scanner_service/utxo_scanner_task.rs index 0786cea322..a56bb36e4d 100644 --- a/base_layer/wallet/src/utxo_scanner_service/utxo_scanner_task.rs +++ b/base_layer/wallet/src/utxo_scanner_service/utxo_scanner_task.rs @@ -264,7 +264,7 @@ where TBackend: WalletBackend + 'static let tip_info = client.get_tip_info().await?; let chain_height = tip_info.metadata.map(|m| m.height_of_longest_chain()).unwrap_or(0); let end_header = client.get_header_by_height(chain_height).await?; - let end_header = BlockHeader::try_from(end_header).map_err(|_| UtxoScannerError::ConversionError)?; + let end_header = BlockHeader::try_from(end_header).map_err(UtxoScannerError::ConversionError)?; Ok(end_header) } @@ -300,7 +300,7 @@ where TBackend: WalletBackend + 'static if sb.height <= current_tip_height { if found_scanned_block.is_none() { let header = BlockHeader::try_from(client.get_header_by_height(sb.height).await?) - .map_err(|_| UtxoScannerError::ConversionError)?; + .map_err(UtxoScannerError::ConversionError)?; let header_hash = header.hash(); if header_hash != sb.header_hash { missing_scanned_blocks.push(sb.clone()); @@ -326,7 +326,7 @@ where TBackend: WalletBackend + 'static } else { // If we are not at the tip scanning should resume from the next header in the chain let next_header = BlockHeader::try_from(client.get_header_by_height(sb.height + 1).await?) - .map_err(|_| UtxoScannerError::ConversionError)?; + .map_err(UtxoScannerError::ConversionError)?; let next_header_hash = next_header.hash(); (sb.height + 1, next_header_hash) }; @@ -421,7 +421,7 @@ where TBackend: WalletBackend + 'static let outputs = response .outputs .into_iter() - .map(|utxo| TransactionOutput::try_from(utxo).map_err(|_| UtxoScannerError::ConversionError)) + .map(|utxo| TransactionOutput::try_from(utxo).map_err(UtxoScannerError::ConversionError)) .collect::, _>>()?; total_scanned += outputs.len(); @@ -622,7 +622,7 @@ where TBackend: WalletBackend + 'static }, }; let header = client.get_header_by_height(block_height).await?; - let header = BlockHeader::try_from(header).map_err(|_| UtxoScannerError::ConversionError)?; + let header = BlockHeader::try_from(header).map_err(UtxoScannerError::ConversionError)?; let header_hash = header.hash(); info!( target: LOG_TARGET, diff --git a/comms/src/protocol/rpc/client/mod.rs b/comms/src/protocol/rpc/client/mod.rs index cfe8ffc9d3..b4ae9ac68a 100644 --- a/comms/src/protocol/rpc/client/mod.rs +++ b/comms/src/protocol/rpc/client/mod.rs @@ -815,9 +815,15 @@ where TSubstream: AsyncRead + AsyncWrite + Unpin + Send + StreamId } fn next_request_id(&mut self) -> u16 { - let next_id = self.next_request_id; + let mut next_id = self.next_request_id; // request_id is allowed to wrap around back to 0 self.next_request_id = self.next_request_id.wrapping_add(1); + // We dont want request id of zero because that is the default for varint on protobuf, so it is possible for the + // entire message to be zero bytes (WriteZero IO error) + if next_id == 0 { + next_id += 1; + self.next_request_id += 1; + } next_id } diff --git a/integration_tests/helpers/transactionBuilder.js b/integration_tests/helpers/transactionBuilder.js index 146bd92acc..5f55fb4b12 100644 --- a/integration_tests/helpers/transactionBuilder.js +++ b/integration_tests/helpers/transactionBuilder.js @@ -37,22 +37,20 @@ class TransactionBuilder { const bufFromOpt = (opt, encoding = "hex") => opt ? Buffer.concat([ - Buffer.from([1]), + Buffer.from([0x01]), encoding ? Buffer.from(opt, encoding) : opt, ]) - : Buffer.from([0]); + : Buffer.from([0x00]); - // Add length byte to unique id - note this only works until 128 bytes (TODO: varint encoding) + // Add length byte to unique id - note this only works until 127 bytes (TODO: varint encoding) let unique_id = features.unique_id - ? Buffer.concat([ - Buffer.from([features.unique_id.length]), - features.unique_id, - ]) + ? this.toLengthEncoded(features.unique_id) : null; return Buffer.concat([ - Buffer.from([0]), - Buffer.from([parseInt(features.maturity)]), + // version + Buffer.from([0x00]), + Buffer.from([parseInt(features.maturity || 0)]), Buffer.from([features.flags]), bufFromOpt(features.parent_public_key, "hex"), bufFromOpt(unique_id, false), @@ -63,16 +61,22 @@ class TransactionBuilder { // TODO: SideChainCheckpointFeatures bufFromOpt(null), // TODO: metadata (len is 0) - Buffer.from([0]), + Buffer.from([0x00]), ]); } + toLengthEncoded(buf) { + // TODO: varint encoding, this is only valid up to len=127 + return Buffer.concat([Buffer.from([buf.length]), buf]); + } + buildMetaChallenge( script, features, scriptOffsetPublicKey, publicNonce, - commitment + commitment, + covenant ) { const KEY = null; // optional key const OUTPUT_LENGTH = 32; // bytes @@ -83,12 +87,11 @@ class TransactionBuilder { // base_layer/core/src/transactions/transaction/transaction_output.rs blake2bUpdate(context, buf_nonce); - blake2bUpdate(context, script); + blake2bUpdate(context, this.toLengthEncoded(script)); blake2bUpdate(context, features_buffer); blake2bUpdate(context, script_offset_public_key); blake2bUpdate(context, commitment); - // Empty covenant is 0 bytes - // blake2bUpdate(context, covenant_bytes); + blake2bUpdate(context, this.toLengthEncoded(covenant)); const final = blake2bFinal(context); return Buffer.from(final).toString("hex"); } @@ -106,8 +109,8 @@ class TransactionBuilder { let buff_publicNonce = Buffer.from(publicNonce, "hex"); let buff_public_key = Buffer.from(public_key, "hex"); blake2bUpdate(context, buff_publicNonce); - blake2bUpdate(context, script); - blake2bUpdate(context, input_data); + blake2bUpdate(context, this.toLengthEncoded(script)); + blake2bUpdate(context, this.toLengthEncoded(input_data)); blake2bUpdate(context, buff_public_key); blake2bUpdate(context, commitment); let final = blake2bFinal(context); @@ -119,6 +122,8 @@ class TransactionBuilder { let OUTPUT_LENGTH = 32; // bytes let context = blake2bInit(OUTPUT_LENGTH, KEY); const features_buffer = this.featuresToConsensusBytes(features); + // Version + blake2bUpdate(context, [0x00]); blake2bUpdate(context, features_buffer); blake2bUpdate(context, commitment); blake2bUpdate(context, script); @@ -184,6 +189,7 @@ class TransactionBuilder { signature_v: Buffer.from(script_sig.v, "hex"), }, sender_offset_public_key: input.output.sender_offset_public_key, + covenant: Buffer.from([]), }, amount: input.amount, privateKey: input.privateKey, @@ -222,6 +228,7 @@ class TransactionBuilder { ); let nopScriptBytes = Buffer.from([0x73]); + let covenantBytes = Buffer.from([]); let rangeproofFactory = tari_crypto.RangeProofFactory.new(); let rangeproof = rangeproofFactory.create_proof( @@ -246,7 +253,8 @@ class TransactionBuilder { outputFeatures, scriptOffsetPublicKey, public_nonce, - commitment + commitment, + covenantBytes ); let total_key = combineTwoTariKeys( scriptOffsetPrivateKey.toString(), @@ -275,6 +283,7 @@ class TransactionBuilder { signature_u: Buffer.from(meta_sig.u, "hex"), signature_v: Buffer.from(meta_sig.v, "hex"), }, + covenant: covenantBytes, }, }; this.outputs.push(output); @@ -375,6 +384,7 @@ class TransactionBuilder { generateCoinbase(value, privateKey, fee, lockHeight) { let coinbase = tari_crypto.commit(privateKey, BigInt(value + fee)); let nopScriptBytes = Buffer.from([0x73]); + let covenantBytes = Buffer.from([]); let outputFeatures = { flags: 1, maturity: lockHeight, @@ -432,7 +442,8 @@ class TransactionBuilder { outputFeatures, scriptOffsetPublicKey, public_nonce_c, - commitment + commitment, + covenantBytes ); let total_key = combineTwoTariKeys( scriptOffsetPrivateKey.toString(), @@ -459,6 +470,7 @@ class TransactionBuilder { signature_u: Buffer.from(meta_sig.u, "hex"), signature_v: Buffer.from(meta_sig.v, "hex"), }, + covenant: covenantBytes, }, ], kernels: [