diff --git a/CHANGELOG.md b/CHANGELOG.md index 01ddd52..5b39203 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -35,6 +35,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Change transactions to have an optional crossover - Update STCT to use Schnorr signatures [#34] - Update dependencies +- Update `phoenix-core` to `0.17` +- Update `dusk-pki` to `0.11` +- Update `dusk-schnorr` to `0.11` +- Update `dusk-jubjub` to `0.12` +- Update `dusk-poseidon` to `0.26` +- Update `dusk-plonk` to `0.12` +- Update `dusk-bls12_381-sign` to `0.4` ### Fixed diff --git a/Cargo.toml b/Cargo.toml index 1f7678a..01de56a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "dusk-wallet-core" -version = "0.15.0-rc.0" +version = "0.19.0-piecrust.0.6" edition = "2021" description = "The core functionality of the Dusk wallet" license = "MPL-2.0" @@ -9,24 +9,20 @@ license = "MPL-2.0" rand_core = "^0.6" rand_chacha = { version = "^0.3", default-features = false } sha2 = { version = "^0.10", default-features = false } -phoenix-core = { version = "0.16.0-rc", features = ["canon"] } -dusk-pki = { version = "0.10.0-rc", features = ["canon"] } +phoenix-core = { version = "0.18", default-features = false, features = ["alloc", "rkyv-impl"] } +dusk-pki = { version = "0.11", default-features = false } dusk-bytes = "^0.1" -dusk-schnorr = { version = "0.10.0-rc", default-features = false } -dusk-jubjub = { version = "0.11", default-features = false } -dusk-poseidon = { version = "0.25.0-rc", features = [ - "canon", -], default-features = false } -dusk-plonk = { version = "0.10", default-features = false } -rusk-abi = "0.7" -canonical = "0.7" -dusk-bls12_381-sign = { version = "0.3.0-rc", default-features = false, features = [ - "canon", -] } +dusk-schnorr = { version = "0.12", default-features = false } +dusk-jubjub = { version = "0.12", default-features = false } +dusk-poseidon = { version = "0.29.1-rc.0", default-features = false } +dusk-merkle = { version = "0.4.1-rc.0", features = ["poseidon", "rkyv-impl"] } +dusk-plonk = { version = "0.13", default-features = false } +rusk-abi = { version = "0.9.0-piecrust.0.6", default-features = false } +dusk-bls12_381-sign = { version = "0.4", default-features = false } +rkyv = { version = "0.7", default-features = false } [dev-dependencies] rand = "^0.8" -canonical_derive = "0.7" [lib] crate-type = ["cdylib", "rlib"] diff --git a/rust-toolchain b/rust-toolchain index 61d71c8..67946b1 100644 --- a/rust-toolchain +++ b/rust-toolchain @@ -1 +1 @@ -nightly-2022-08-28 +nightly-2023-01-05 diff --git a/src/ffi.rs b/src/ffi.rs index 68a7589..ad0e449 100644 --- a/src/ffi.rs +++ b/src/ffi.rs @@ -6,20 +6,20 @@ //! The foreign function interface for the wallet. +use alloc::string::String; use alloc::vec::Vec; use core::mem; use core::num::NonZeroU32; use core::ptr; -use canonical::{Canon, Source}; use dusk_bls12_381_sign::PublicKey; use dusk_bytes::Write; use dusk_bytes::{DeserializableSlice, Serializable}; use dusk_jubjub::{BlsScalar, JubJubAffine, JubJubScalar}; +use dusk_merkle::poseidon::Opening as PoseidonOpening; use dusk_pki::{PublicSpendKey, ViewKey}; use dusk_plonk::prelude::Proof; -use dusk_poseidon::tree::PoseidonBranch; use dusk_schnorr::Signature; use phoenix_core::{Crossover, Fee, Note}; use rand_core::{ @@ -155,6 +155,8 @@ pub unsafe extern "C" fn public_spend_key( #[no_mangle] pub unsafe extern "C" fn execute( contract_id: *const [u8; 32], + call_name_ptr: *mut u8, + call_name_len: *const u32, call_data_ptr: *mut u8, call_data_len: *const u32, sender_index: *const u64, @@ -162,11 +164,18 @@ pub unsafe extern "C" fn execute( gas_limit: *const u64, gas_price: *const u64, ) -> u8 { - let contract_id = ContractId::from(*contract_id); + let contract_id = ContractId::from_bytes(*contract_id); - // SAFETY: the buffer is expected to have been allocated with the + // SAFETY: these buffers are expected to have been allocated with the // correct size. If this is not the case problems with the allocator // *may* happen. + let call_name = Vec::from_raw_parts( + call_name_ptr, + call_name_len as usize, + call_name_len as usize, + ); + let call_name = unwrap_or_bail!(String::from_utf8(call_name)); + let call_data = Vec::from_raw_parts( call_data_ptr, call_data_len as usize, @@ -178,6 +187,7 @@ pub unsafe extern "C" fn execute( unwrap_or_bail!(WALLET.execute( &mut FfiRng, contract_id, + call_name, call_data, *sender_index, &refund, @@ -332,9 +342,6 @@ impl Store for FfiStore { } } -// 512 KB for a buffer. -const OPENING_BUF_SIZE: usize = 0x10000; - const STCT_INPUT_SIZE: usize = Fee::SIZE + Crossover::SIZE + u64::SIZE @@ -459,23 +466,28 @@ impl StateClient for FfiStateClient { fn fetch_opening( &self, note: &Note, - ) -> Result, Self::Error> { - let mut opening_buf = [0u8; OPENING_BUF_SIZE]; + ) -> Result, Self::Error> { + const OPENING_BUF_SIZE: usize = 3000; + let mut opening_buf = Vec::with_capacity(OPENING_BUF_SIZE); let mut opening_len = 0; let note = note.to_bytes(); unsafe { - let r = fetch_opening(¬e, &mut opening_buf[0], &mut opening_len); + let r = fetch_opening( + ¬e, + opening_buf.as_mut_ptr(), + &mut opening_len, + ); if r != 0 { return Err(r); } } - let mut source = Source::new(&opening_buf[..opening_len as usize]); - let branch = PoseidonBranch::decode(&mut source).map_err( - Error::::from, - )?; + let branch = rkyv::from_bytes(&opening_buf[..opening_len as usize]) + .map_err( + Error::::from, + )?; Ok(branch) } @@ -666,11 +678,12 @@ impl From> Error::Prover(_) => 251, Error::NotEnoughBalance => 250, Error::NoteCombinationProblem => 249, - Error::Canon(_) => 248, + Error::Rkyv => 248, Error::Phoenix(_) => 247, Error::AlreadyStaked { .. } => 246, Error::NotStaked { .. } => 245, Error::NoReward { .. } => 244, + Error::Utf8(_) => 243, } } } diff --git a/src/imp.rs b/src/imp.rs index 7634c63..916ae4e 100644 --- a/src/imp.rs +++ b/src/imp.rs @@ -6,13 +6,14 @@ use crate::tx::UnprovenTransaction; use crate::{ - BalanceInfo, ProverClient, StakeInfo, StateClient, Store, Transaction, + BalanceInfo, ProverClient, StakeInfo, StateClient, Store, MAX_CALL_SIZE, }; +use core::convert::Infallible; + +use alloc::string::{FromUtf8Error, String}; use alloc::vec::Vec; -use canonical::EncodeToVec; -use canonical::{Canon, CanonError}; use dusk_bls12_381_sign::{PublicKey, SecretKey, Signature}; use dusk_bytes::{Error as BytesError, Serializable}; use dusk_jubjub::{BlsScalar, JubJubScalar}; @@ -20,17 +21,34 @@ use dusk_pki::{ Ownable, PublicSpendKey, SecretKey as SchnorrKey, SecretSpendKey, StealthAddress, }; -use dusk_poseidon::cipher::PoseidonCipher; -use dusk_poseidon::sponge; use dusk_schnorr::Signature as SchnorrSignature; -use phoenix_core::{Crossover, Error as PhoenixError, Fee, Note, NoteType}; +use phoenix_core::transaction::*; +use phoenix_core::{Error as PhoenixError, Fee, Note, NoteType}; use rand_core::{CryptoRng, Error as RngError, RngCore}; +use rkyv::ser::serializers::{ + AllocScratchError, AllocSerializer, CompositeSerializerError, + SharedSerializeMapError, +}; +use rkyv::validation::validators::CheckDeserializeError; +use rkyv::Serialize; use rusk_abi::ContractId; const MAX_INPUT_NOTES: usize = 4; +const TX_STAKE: &str = "stake"; +const TX_UNSTAKE: &str = "unstake"; +const TX_WITHDRAW: &str = "withdraw"; +const TX_ADD_ALLOWLIST: &str = "allow"; + +type SerializerError = CompositeSerializerError< + Infallible, + AllocScratchError, + SharedSerializeMapError, +>; + /// The error type returned by this crate. #[derive(Debug)] +#[allow(clippy::large_enum_variant)] pub enum Error { /// Underlying store error. Store(S::Error), @@ -38,12 +56,14 @@ pub enum Error { State(SC::Error), /// Error originating from the prover client. Prover(PC::Error), - /// Canonical stores. - Canon(CanonError), + /// Rkyv serialization. + Rkyv, /// Random number generator error. Rng(RngError), /// Serialization and deserialization of Dusk types. Bytes(BytesError), + /// Bytes were meant to be utf8 but aren't. + Utf8(FromUtf8Error), /// Originating from the transaction model. Phoenix(PhoenixError), /// Not enough balance to perform transaction. @@ -92,6 +112,22 @@ impl Error { } } +impl From + for Error +{ + fn from(_: SerializerError) -> Self { + Self::Rkyv + } +} + +impl + From> for Error +{ + fn from(_: CheckDeserializeError) -> Self { + Self::Rkyv + } +} + impl From for Error { @@ -108,19 +144,19 @@ impl From } } -impl From +impl From for Error { - fn from(pe: PhoenixError) -> Self { - Self::Phoenix(pe) + fn from(err: FromUtf8Error) -> Self { + Self::Utf8(err) } } -impl From +impl From for Error { - fn from(ce: CanonError) -> Self { - Self::Canon(ce) + fn from(pe: PhoenixError) -> Self { + Self::Phoenix(pe) } } @@ -160,11 +196,6 @@ impl Wallet { } } -const TX_STAKE: u8 = 0x00; -const TX_UNSTAKE: u8 = 0x01; -const TX_WITHDRAW: u8 = 0x02; -const TX_ADD_ALLOWLIST: u8 = 0x03; - impl Wallet where S: Store, @@ -286,6 +317,7 @@ where &self, rng: &mut Rng, contract_id: ContractId, + call_name: String, call_data: C, sender_index: u64, refund: &PublicSpendKey, @@ -294,7 +326,7 @@ where ) -> Result> where Rng: RngCore + CryptoRng, - C: Canon, + C: Serialize>, { let sender = self .store @@ -309,7 +341,9 @@ where )?; let fee = Fee::new(rng, gas_limit, gas_price, refund); - let call = (contract_id, call_data.encode_to_vec()); + + let call_data = rkyv::to_bytes(&call_data)?.to_vec(); + let call = (contract_id, call_name, call_data); let utx = UnprovenTransaction::new( rng, @@ -423,11 +457,19 @@ where fee.gas_limit = gas_limit; fee.gas_price = gas_price; - let contract_id = rusk_abi::stake_contract(); + let contract_id = rusk_abi::STAKE_CONTRACT; let address = rusk_abi::contract_to_scalar(&contract_id); - let stct_signature = - sign_stct(rng, &sender, &fee, &crossover, value, &address); + let contract_id = rusk_abi::contract_to_scalar(&contract_id); + + let stct_message = + stct_signature_message(&crossover, value, contract_id); + let stct_message = dusk_poseidon::sponge::hash(&stct_message); + + let sk_r = *sender.sk_r(fee.stealth_address()).as_ref(); + let secret = SchnorrKey::from(sk_r); + + let stct_signature = SchnorrSignature::new(&secret, rng, stct_message); let spend_proof = self .prover @@ -445,10 +487,16 @@ where let signature = stake_sign(&sk, &pk, stake.counter, value); - let call_data = - (TX_STAKE, pk, signature, value, spend_proof).encode_to_vec(); + let stake = Stake { + public_key: pk, + signature, + value, + proof: spend_proof, + }; - let call = (contract_id, call_data); + let call_data = rkyv::to_bytes::<_, MAX_CALL_SIZE>(&stake)?.to_vec(); + let call = + (rusk_abi::STAKE_CONTRACT, String::from(TX_STAKE), call_data); let utx = UnprovenTransaction::new( rng, @@ -532,12 +580,19 @@ where let signature = unstake_sign(&sk, &pk, stake.counter, unstake_note); - let call_data = - (TX_UNSTAKE, pk, signature, unstake_note, unstake_proof) - .encode_to_vec(); + let unstake = Unstake { + public_key: pk, + signature, + note: unstake_note, + proof: unstake_proof, + }; - let contract_id = rusk_abi::stake_contract(); - let call = (contract_id, call_data); + let call_data = rkyv::to_bytes::<_, MAX_CALL_SIZE>(&unstake)?.to_vec(); + let call = ( + rusk_abi::STAKE_CONTRACT, + String::from(TX_UNSTAKE), + call_data, + ); let utx = UnprovenTransaction::new( rng, @@ -610,11 +665,16 @@ where fee.gas_limit = gas_limit; fee.gas_price = gas_price; - let call_data = - (TX_WITHDRAW, pk, signature, address, nonce).encode_to_vec(); + let withdraw = Withdraw { + public_key: pk, + signature, + address, + nonce, + }; + let call_data = rkyv::to_bytes::<_, MAX_CALL_SIZE>(&withdraw)?.to_vec(); - let contract_id = rusk_abi::stake_contract(); - let call = (contract_id, call_data); + let contract_id = rusk_abi::STAKE_CONTRACT; + let call = (contract_id, String::from(TX_WITHDRAW), call_data); let utx = UnprovenTransaction::new( rng, @@ -650,11 +710,11 @@ where .retrieve_ssk(sender_index) .map_err(Error::from_store_err)?; - let sk = self + let owner_sk = self .store .retrieve_sk(owner_index) .map_err(Error::from_store_err)?; - let pk = PublicKey::from(&sk); + let owner_pk = PublicKey::from(&owner_sk); let (inputs, outputs) = self.inputs_and_change_output( rng, @@ -663,10 +723,12 @@ where gas_limit * gas_price, )?; - let stake = - self.state.fetch_stake(&pk).map_err(Error::from_state_err)?; + let stake = self + .state + .fetch_stake(&owner_pk) + .map_err(Error::from_state_err)?; - let signature = allow_sign(&sk, &pk, stake.counter, staker); + let signature = allow_sign(&owner_sk, &owner_pk, stake.counter, staker); // Since we're not transferring value *to* the contract the crossover // shouldn't contain a value. As such the note used to created it should @@ -680,11 +742,15 @@ where fee.gas_limit = gas_limit; fee.gas_price = gas_price; - let call_data = - (TX_ADD_ALLOWLIST, *staker, pk, signature).encode_to_vec(); + let allow = Allow { + public_key: *staker, + owner: owner_pk, + signature, + }; + let call_data = rkyv::to_bytes::<_, MAX_CALL_SIZE>(&allow)?.to_vec(); - let contract_id = rusk_abi::stake_contract(); - let call = (contract_id, call_data); + let contract_id = rusk_abi::STAKE_CONTRACT; + let call = (contract_id, String::from(TX_ADD_ALLOWLIST), call_data); let utx = UnprovenTransaction::new( rng, @@ -824,56 +890,6 @@ fn pick_lexicographic bool>( None } -const STCT_MESSAGE_SIZE: usize = 5 + PoseidonCipher::cipher_size(); - -// TODO: this is copied from the circuits. We should find a way to reuse this -// instead of duplicating it. -fn sign_stct( - rng: &mut Rng, - ssk: &SecretSpendKey, - fee: &Fee, - crossover: &Crossover, - value: u64, - address: &BlsScalar, -) -> SchnorrSignature { - let sk_r = *ssk.sk_r(fee.stealth_address()).as_ref(); - let secret = SchnorrKey::from(sk_r); - - let message = { - let mut message = [BlsScalar::zero(); STCT_MESSAGE_SIZE]; - let mut m = message.iter_mut(); - - crossover - .value_commitment() - .to_hash_inputs() - .iter() - .zip(m.by_ref()) - .for_each(|(c, m)| *m = *c); - - if let Some(m) = m.next() { - *m = *crossover.nonce(); - } - - crossover - .encrypted_data() - .cipher() - .iter() - .zip(m.by_ref()) - .for_each(|(c, m)| *m = *c); - - if let Some(m) = m.next() { - *m = value.into(); - } - if let Some(m) = m.next() { - *m = *address; - } - - sponge::hash(&message) - }; - - SchnorrSignature::new(&secret, rng, message) -} - /// Creates a signature compatible with what the stake contract expects for a /// stake transaction. /// diff --git a/src/lib.rs b/src/lib.rs index 6ab91ba..c38cd46 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -8,7 +8,8 @@ #![deny(missing_docs)] #![deny(clippy::all)] -#![no_std] +#![allow(clippy::result_large_err)] +// #![no_std] #[macro_use] extern crate alloc; @@ -23,9 +24,9 @@ use alloc::vec::Vec; use dusk_bls12_381_sign::{PublicKey, SecretKey}; use dusk_bytes::{DeserializableSlice, Serializable, Write}; use dusk_jubjub::{BlsScalar, JubJubAffine, JubJubScalar}; +use dusk_merkle::poseidon::Opening as PoseidonOpening; use dusk_pki::{SecretSpendKey, ViewKey}; use dusk_plonk::proof_system::Proof; -use dusk_poseidon::tree::PoseidonBranch; use dusk_schnorr::Signature; use phoenix_core::{Crossover, Fee, Note}; use rand_chacha::ChaCha12Rng; @@ -33,10 +34,15 @@ use rand_core::SeedableRng; use sha2::{Digest, Sha256}; pub use imp::*; -pub use tx::{Transaction, UnprovenTransaction, UnprovenTransactionInput}; +pub use phoenix_core::Transaction; +pub use tx::{UnprovenTransaction, UnprovenTransactionInput}; +use phoenix_core::transaction::*; pub use rusk_abi::POSEIDON_TREE_DEPTH; +/// The maximum size of call data. +pub const MAX_CALL_SIZE: usize = rusk_abi::ARGBUF_LEN; + /// Stores the cryptographic material necessary to derive cryptographic keys. pub trait Store { /// The error type returned from the store. @@ -77,8 +83,8 @@ pub trait Store { pub fn derive_ssk(seed: &[u8; 64], index: u64) -> SecretSpendKey { let mut hash = Sha256::new(); - hash.update(&seed); - hash.update(&index.to_le_bytes()); + hash.update(seed); + hash.update(index.to_le_bytes()); hash.update(b"SSK"); let hash = hash.finalize().into(); @@ -96,8 +102,8 @@ pub fn derive_ssk(seed: &[u8; 64], index: u64) -> SecretSpendKey { pub fn derive_sk(seed: &[u8; 64], index: u64) -> SecretKey { let mut hash = Sha256::new(); - hash.update(&seed); - hash.update(&index.to_le_bytes()); + hash.update(seed); + hash.update(index.to_le_bytes()); hash.update(b"SK"); let hash = hash.finalize().into(); @@ -168,7 +174,7 @@ pub trait StateClient { fn fetch_opening( &self, note: &Note, - ) -> Result, Self::Error>; + ) -> Result, Self::Error>; /// Queries the node for the stake of a key. If the key has no stake, a /// `Default` stake info should be returned. @@ -224,6 +230,16 @@ pub struct StakeInfo { pub counter: u64, } +impl From for StakeInfo { + fn from(data: StakeData) -> Self { + StakeInfo { + amount: data.amount, + reward: data.reward, + counter: data.counter, + } + } +} + impl Serializable<32> for StakeInfo { type Error = dusk_bytes::Error; diff --git a/src/tx.rs b/src/tx.rs index 57fd702..57b941b 100644 --- a/src/tx.rs +++ b/src/tx.rs @@ -6,313 +6,28 @@ use crate::StateClient; +use alloc::string::String; use alloc::vec::Vec; -use core::mem; -use canonical::{Canon, CanonError, Sink, Source}; use dusk_bytes::{ DeserializableSlice, Error as BytesError, Serializable, Write, }; use dusk_jubjub::{BlsScalar, JubJubAffine, JubJubExtended}; +use dusk_merkle::poseidon::Opening as PoseidonOpening; use dusk_pki::{Ownable, SecretSpendKey}; use dusk_plonk::prelude::{JubJubScalar, Proof}; -use dusk_poseidon::tree::PoseidonBranch; use dusk_schnorr::Proof as SchnorrSig; +use phoenix_core::transaction::Transaction; use phoenix_core::{Crossover, Fee, Note}; use rand_core::{CryptoRng, RngCore}; use rusk_abi::hash::Hasher; -use rusk_abi::{ContractId, POSEIDON_TREE_DEPTH}; - -const CONTRACT_ID_SIZE: usize = mem::size_of::(); - -/// The structure sent over the network representing a transaction. -#[derive(Debug, Clone)] -pub struct Transaction { - nullifiers: Vec, - outputs: Vec, - anchor: BlsScalar, - proof: Vec, - fee: Fee, - crossover: Option, - call: Option<(ContractId, Vec)>, -} - -// This implements `Canon` to allow a transaction to pass directly though the -// transfer contract. -impl Canon for Transaction { - fn encode(&self, sink: &mut Sink) { - 0u8.encode(sink); // tag included to call `execute` in the contract. - self.anchor.encode(sink); - self.nullifiers.encode(sink); - self.fee.encode(sink); - self.crossover.encode(sink); - self.outputs.encode(sink); - self.proof.encode(sink); - self.call.encode(sink); - } - - fn decode(source: &mut Source) -> Result { - u8::decode(source)?; - - let anchor = Canon::decode(source)?; - let nullifiers = Canon::decode(source)?; - let fee = Canon::decode(source)?; - let crossover = Canon::decode(source)?; - let outputs = Canon::decode(source)?; - let proof = Canon::decode(source)?; - let call = Canon::decode(source)?; - - Ok(Transaction { - anchor, - nullifiers, - fee, - crossover, - outputs, - proof, - call, - }) - } - - fn encoded_len(&self) -> usize { - 0u8.encoded_len() - + self.anchor.encoded_len() - + self.nullifiers.encoded_len() - + self.fee.encoded_len() - + self.crossover.encoded_len() - + self.outputs.encoded_len() - + self.proof.encoded_len() - + self.call.encoded_len() - } -} - -impl Transaction { - /// Creates a transaction from the skeleton and the proof. - fn new(tx_skel: TransactionSkeleton, proof: Proof) -> Self { - Self { - proof: proof.to_bytes().to_vec(), - nullifiers: tx_skel.nullifiers, - outputs: tx_skel.outputs, - anchor: tx_skel.anchor, - fee: tx_skel.fee, - crossover: tx_skel.crossover, - call: tx_skel.call, - } - } - - /// Hashes the transaction excluding. - pub fn hash(&self) -> BlsScalar { - let skel = TransactionSkeleton::from(self.clone()); - skel.hash() - } - - /// Serializes the transaction into a variable length byte buffer. - #[allow(unused_must_use)] - pub fn to_var_bytes(&self) -> Vec { - // compute the serialized size to preallocate space - let size = u64::SIZE - + self.nullifiers.len() * BlsScalar::SIZE - + u64::SIZE - + self.outputs.len() * Note::SIZE - + BlsScalar::SIZE - + Fee::SIZE - + Proof::SIZE - + u64::SIZE - + self.crossover.map_or(0, |_| Crossover::SIZE) - + u64::SIZE - + self - .call - .as_ref() - .map(|(_, cdata)| CONTRACT_ID_SIZE + cdata.len()) - .unwrap_or(0); - - let mut bytes = vec![0u8; size]; - let mut writer = &mut bytes[..]; - - writer.write(&(self.nullifiers.len() as u64).to_bytes()); - for input in &self.nullifiers { - writer.write(&input.to_bytes()); - } - - writer.write(&(self.outputs.len() as u64).to_bytes()); - for output in &self.outputs { - writer.write(&output.to_bytes()); - } - - writer.write(&self.anchor.to_bytes()); - writer.write(&self.fee.to_bytes()); - writer.write(&self.proof); - - write_crossover(&mut writer, self.crossover); - - write_optional_call(&mut writer, self.call.as_ref()); - - bytes - } - - /// Deserializes the transaction from a bytes buffer. - pub fn from_slice(buf: &[u8]) -> Result { - let mut buffer = buf; - - let num_inputs = u64::from_reader(&mut buffer)? as usize; - let mut nullifiers = Vec::with_capacity(num_inputs); - - for _ in 0..num_inputs { - nullifiers.push(BlsScalar::from_reader(&mut buffer)?); - } - - let num_outputs = u64::from_reader(&mut buffer)? as usize; - let mut outputs = Vec::with_capacity(num_outputs); - - for _ in 0..num_outputs { - outputs.push(Note::from_reader(&mut buffer)?); - } - - let anchor = BlsScalar::from_reader(&mut buffer)?; - let fee = Fee::from_reader(&mut buffer)?; - let proof = Proof::from_reader(&mut buffer)?.to_bytes().to_vec(); - - let crossover = read_crossover(&mut buffer)?; - - let call = read_optional_call(&mut buffer)?; - - Ok(Self { - nullifiers, - outputs, - anchor, - fee, - crossover, - call, - proof, - }) - } - - /// The nullifiers in the transaction. - pub fn inputs(&self) -> &[BlsScalar] { - &self.nullifiers - } - - /// The output notes of the transaction. - pub fn outputs(&self) -> &[Note] { - &self.outputs - } - - /// The anchor of the transaction. - pub fn anchor(&self) -> BlsScalar { - self.anchor - } - - /// The proof of the transaction. - pub fn proof(&self) -> &[u8] { - &self.proof - } - - /// The fee of the transaction. - pub fn fee(&self) -> &Fee { - &self.fee - } - - /// The crossover of the transaction. - pub fn crossover(&self) -> Option<&Crossover> { - self.crossover.as_ref() - } - - /// The call data of the transaction. - pub fn call(&self) -> Option<&(ContractId, Vec)> { - self.call.as_ref() - } -} - -/// Transaction skeleton. -struct TransactionSkeleton { - nullifiers: Vec, - outputs: Vec, - anchor: BlsScalar, - fee: Fee, - crossover: Option, - call: Option<(ContractId, Vec)>, -} - -impl TransactionSkeleton { - fn new( - nullifiers: Vec, - outputs: Vec, - anchor: BlsScalar, - fee: Fee, - crossover: Option, - call: Option<(ContractId, Vec)>, - ) -> Self { - Self { - nullifiers, - outputs, - anchor, - fee, - crossover, - call, - } - } - - fn hash(&self) -> BlsScalar { - let mut hasher = Hasher::new(); - - for nullifier in &self.nullifiers { - hasher.update(nullifier.to_bytes()); - } - for note in &self.outputs { - hasher.update(note.to_bytes()); - } - - hasher = hasher - .chain_update(self.anchor.to_bytes()) - .chain_update(self.fee.to_bytes()); - - if let Some(crossover) = &self.crossover { - hasher.update(&crossover.to_bytes()); - } - - if let Some((cid, cdata)) = &self.call { - hasher.update(cid.as_bytes()); - hasher.update(cdata); - } - - hasher.finalize() - } -} - -impl From for TransactionSkeleton { - fn from(tx: Transaction) -> Self { - Self { - nullifiers: tx.nullifiers, - outputs: tx.outputs, - anchor: tx.anchor, - fee: tx.fee, - crossover: tx.crossover, - call: tx.call, - } - } -} - -impl From for TransactionSkeleton { - fn from(utx: UnprovenTransaction) -> Self { - Self { - nullifiers: utx - .inputs - .iter() - .map(UnprovenTransactionInput::nullifier) - .collect(), - outputs: utx.outputs.iter().map(|o| o.0).collect(), - anchor: utx.anchor, - fee: utx.fee, - crossover: utx.crossover.map(|c| c.0), - call: utx.call, - } - } -} +use rusk_abi::{ContractId, CONTRACT_ID_BYTES, POSEIDON_TREE_DEPTH}; /// An input to a transaction that is yet to be proven. #[derive(Debug, Clone)] pub struct UnprovenTransactionInput { nullifier: BlsScalar, - opening: PoseidonBranch, + opening: PoseidonOpening<(), POSEIDON_TREE_DEPTH, 4>, note: Note, value: u64, blinder: JubJubScalar, @@ -327,7 +42,7 @@ impl UnprovenTransactionInput { note: Note, value: u64, blinder: JubJubScalar, - opening: PoseidonBranch, + opening: PoseidonOpening<(), POSEIDON_TREE_DEPTH, 4>, tx_hash: BlsScalar, ) -> Self { let nullifier = note.gen_nullifier(ssk); @@ -351,12 +66,9 @@ impl UnprovenTransactionInput { pub fn to_var_bytes(&self) -> Vec { let affine_pkr = JubJubAffine::from(&self.pk_r_prime); - // TODO Magic number for the buffer size here. - // Should be corrected once dusk-poseidon implements `Serializable` for - // `PoseidonBranch`. - let mut opening_bytes = [0; opening_buf_size(POSEIDON_TREE_DEPTH)]; - let mut sink = Sink::new(&mut opening_bytes[..]); - self.opening.encode(&mut sink); + let opening_bytes = rkyv::to_bytes::<_, 256>(&self.opening) + .expect("Rkyv serialization should always succeed for an opening") + .to_vec(); let mut bytes = Vec::with_capacity( BlsScalar::SIZE @@ -374,7 +86,7 @@ impl UnprovenTransactionInput { bytes.extend_from_slice(&self.blinder.to_bytes()); bytes.extend_from_slice(&affine_pkr.to_bytes()); bytes.extend_from_slice(&self.sig.to_bytes()); - bytes.extend_from_slice(&opening_bytes); + bytes.extend(opening_bytes); bytes } @@ -391,8 +103,10 @@ impl UnprovenTransactionInput { JubJubExtended::from(JubJubAffine::from_reader(&mut bytes)?); let sig = SchnorrSig::from_reader(&mut bytes)?; - let mut source = Source::new(bytes); - let opening = PoseidonBranch::decode(&mut source) + // `to_vec` is required here otherwise `rkyv` will throw an alignment + // error + #[allow(clippy::unnecessary_to_owned)] + let opening = rkyv::from_bytes(&bytes.to_vec()) .map_err(|_| BytesError::InvalidData)?; Ok(Self { @@ -412,7 +126,7 @@ impl UnprovenTransactionInput { } /// Returns the opening of the input. - pub fn opening(&self) -> &PoseidonBranch { + pub fn opening(&self) -> &PoseidonOpening<(), POSEIDON_TREE_DEPTH, 4> { &self.opening } @@ -442,10 +156,6 @@ impl UnprovenTransactionInput { } } -const fn opening_buf_size(depth: usize) -> usize { - (depth + 2) * (BlsScalar::SIZE * 5 + 8) -} - /// A transaction that is yet to be proven. The purpose of this is solely to /// send to the node to perform a circuit proof. #[derive(Debug, Clone)] @@ -455,7 +165,7 @@ pub struct UnprovenTransaction { anchor: BlsScalar, fee: Fee, crossover: Option<(Crossover, u64, JubJubScalar)>, - call: Option<(ContractId, Vec)>, + call: Option<(ContractId, String, Vec)>, } impl UnprovenTransaction { @@ -469,7 +179,7 @@ impl UnprovenTransaction { outputs: Vec<(Note, u64, JubJubScalar)>, fee: Fee, crossover: Option<(Crossover, u64, JubJubScalar)>, - call: Option<(ContractId, Vec)>, + call: Option<(ContractId, String, Vec)>, ) -> Result { let nullifiers: Vec = inputs .iter() @@ -484,15 +194,19 @@ impl UnprovenTransaction { let anchor = state.fetch_anchor()?; - let skel = TransactionSkeleton::new( - nullifiers, - outputs.iter().map(|o| o.0).collect(), - anchor, - fee, - crossover.map(|c| c.0), - call, + let hash_outputs: Vec = outputs.iter().map(|o| o.0).collect(); + let hash_crossover = crossover.map(|c| c.0); + + let hash_call = call.clone().map(|c| (c.0.to_bytes(), c.1, c.2)); + let hash_bytes = Transaction::hash_input_bytes_from_components( + &nullifiers, + &hash_outputs, + &anchor, + &fee, + &hash_crossover, + &hash_call, ); - let hash = skel.hash(); + let hash = Hasher::digest(hash_bytes); let inputs: Vec = inputs .into_iter() @@ -510,14 +224,29 @@ impl UnprovenTransaction { anchor, fee, crossover, - call: skel.call, + call, }) } /// Consumes self and a proof to generate a transaction. pub fn prove(self, proof: Proof) -> Transaction { - let skel = TransactionSkeleton::from(self); - Transaction::new(skel, proof) + Transaction { + anchor: self.anchor, + nullifiers: self + .inputs + .into_iter() + .map(|input| input.nullifier) + .collect(), + outputs: self + .outputs + .into_iter() + .map(|(note, _, _)| note) + .collect(), + fee: self.fee, + crossover: self.crossover.map(|c| c.0), + proof: proof.to_bytes().to_vec(), + call: self.call.map(|c| (c.0.to_bytes(), c.1, c.2)), + } } /// Serialize the transaction to a variable length byte buffer. @@ -571,7 +300,9 @@ impl UnprovenTransaction { + self .call .as_ref() - .map(|(_, cdata)| CONTRACT_ID_SIZE + cdata.len()) + .map(|(_, cname, cdata)| { + CONTRACT_ID_BYTES + u64::SIZE + cname.len() + cdata.len() + }) .unwrap_or(0); let mut buf = vec![0; size]; @@ -592,7 +323,7 @@ impl UnprovenTransaction { writer.write(&self.fee.to_bytes()); write_crossover_value_blinder(&mut writer, self.crossover); - write_optional_call(&mut writer, self.call.as_ref()); + write_optional_call(&mut writer, &self.call); buf } @@ -638,7 +369,22 @@ impl UnprovenTransaction { /// Returns the hash of the transaction. pub fn hash(&self) -> BlsScalar { - TransactionSkeleton::from(self.clone()).hash() + let nullifiers: Vec = + self.inputs.iter().map(|input| input.nullifier).collect(); + + let hash_outputs: Vec = + self.outputs.iter().map(|(note, _, _)| *note).collect(); + let hash_crossover = self.crossover.map(|c| c.0); + let hash_bytes = self.call.clone().map(|c| (c.0.to_bytes(), c.1, c.2)); + + Hasher::digest(Transaction::hash_input_bytes_from_components( + &nullifiers, + &hash_outputs, + &self.anchor, + &self.fee, + &hash_crossover, + &hash_bytes, + )) } /// Returns the inputs to the transaction. @@ -667,7 +413,7 @@ impl UnprovenTransaction { } /// Returns the call of the transaction. - pub fn call(&self) -> Option<&(ContractId, Vec)> { + pub fn call(&self) -> Option<&(ContractId, String, Vec)> { self.call.as_ref() } } @@ -678,12 +424,18 @@ impl UnprovenTransaction { /// data. fn write_optional_call( writer: &mut W, - call: Option<&(ContractId, Vec)>, + call: &Option<(ContractId, String, Vec)>, ) -> Result<(), BytesError> { match call { - Some((cid, cdata)) => { + Some((cid, cname, cdata)) => { writer.write(&1_u64.to_bytes())?; + writer.write(cid.as_bytes())?; + + let cname_len = cname.len() as u64; + writer.write(&cname_len.to_bytes())?; + writer.write(cname.as_bytes())?; + writer.write(cdata)?; } None => { @@ -694,21 +446,48 @@ fn write_optional_call( Ok(()) } -fn write_crossover( - writer: &mut W, - crossover: Option, -) -> Result<(), BytesError> { - match crossover { - Some(c) => { - writer.write(&1_u64.to_bytes())?; - writer.write(&c.to_bytes())?; - } - None => { - writer.write(&0_u64.to_bytes())?; +/// Reads an optional call from the given buffer. This should be called at the +/// end of parsing other fields since it consumes the entirety of the buffer. +fn read_optional_call( + buffer: &mut &[u8], +) -> Result)>, BytesError> { + let mut call = None; + + if u64::from_reader(buffer)? != 0 { + let buf_len = buffer.len(); + + // needs to be at least the size of a contract ID and have some call + // data. + if buf_len < CONTRACT_ID_BYTES { + return Err(BytesError::BadLength { + found: buf_len, + expected: CONTRACT_ID_BYTES, + }); } + let (mid_buffer, mut buffer_left) = { + let (buf, left) = buffer.split_at(CONTRACT_ID_BYTES); + + let mut mid_buf = [0u8; CONTRACT_ID_BYTES]; + mid_buf.copy_from_slice(buf); + + (mid_buf, left) + }; + + let contract_id = ContractId::from(mid_buffer); + + let buffer = &mut buffer_left; + + let cname_len = u64::from_reader(buffer)?; + let (cname_bytes, buffer_left) = buffer.split_at(cname_len as usize); + + let cname = String::from_utf8(cname_bytes.to_vec()) + .map_err(|_| BytesError::InvalidData)?; + + let call_data = Vec::from(buffer_left); + call = Some((contract_id, cname, call_data)); } - Ok(()) + Ok(call) } fn write_crossover_value_blinder( @@ -730,16 +509,6 @@ fn write_crossover_value_blinder( Ok(()) } -/// Reads an optional crossover from the given buffer. -fn read_crossover(buffer: &mut &[u8]) -> Result, BytesError> { - let ser = match u64::from_reader(buffer)? { - 0 => None, - _ => Some(Crossover::from_reader(buffer)?), - }; - - Ok(ser) -} - /// Reads an optional crossover from the given buffer. fn read_crossover_value_blinder( buffer: &mut &[u8], @@ -756,32 +525,3 @@ fn read_crossover_value_blinder( Ok(ser) } - -/// Reads an optional call from the given buffer. This should be called at the -/// end of parsing other fields since it consumes the entirety of the buffer. -fn read_optional_call( - buffer: &mut &[u8], -) -> Result)>, BytesError> { - let mut call = None; - - if u64::from_reader(buffer)? != 0 { - let buf_len = buffer.len(); - - // needs to be at least the size of a contract ID and have some call - // data. - if buf_len < CONTRACT_ID_SIZE { - return Err(BytesError::BadLength { - found: buf_len, - expected: CONTRACT_ID_SIZE, - }); - } - let (cid_buffer, cdata_buffer) = buffer.split_at(CONTRACT_ID_SIZE); - - let contract_id = ContractId::from(cid_buffer); - let call_data = Vec::from(cdata_buffer); - - call = Some((contract_id, call_data)); - } - - Ok(call) -} diff --git a/tests/mock.rs b/tests/mock.rs index 4c68b79..2ed8d7b 100644 --- a/tests/mock.rs +++ b/tests/mock.rs @@ -6,13 +6,11 @@ //! Mocks of the traits supplied by the user of the crate.. -use canonical::{Canon, Sink, Source}; -use canonical_derive::Canon; use dusk_bls12_381_sign::PublicKey; use dusk_jubjub::{BlsScalar, JubJubAffine, JubJubScalar}; +use dusk_merkle::poseidon::{Item, Opening as PoseidonOpening, Tree}; use dusk_pki::{PublicSpendKey, ViewKey}; use dusk_plonk::prelude::Proof; -use dusk_poseidon::tree::PoseidonBranch; use dusk_schnorr::Signature; use dusk_wallet_core::{ EnrichedNote, ProverClient, StakeInfo, StateClient, Store, Transaction, @@ -20,7 +18,20 @@ use dusk_wallet_core::{ }; use phoenix_core::{Crossover, Fee, Note, NoteType}; use rand_core::{CryptoRng, RngCore}; -use rusk_abi::ContractId; + +fn default_opening() -> PoseidonOpening<(), POSEIDON_TREE_DEPTH, 4> { + // Build a "default" opening + const POS: u64 = 42; + let mut tree = Tree::new(); + tree.insert( + POS, + Item { + hash: BlsScalar::zero(), + data: (), + }, + ); + tree.opening(POS).unwrap() +} /// Create a new wallet meant for tests. It includes a client that will always /// return a random anchor (same every time), and the default opening. @@ -35,7 +46,7 @@ pub fn mock_wallet( let notes = new_notes(rng, &psk, note_values); let anchor = BlsScalar::random(rng); - let opening = Default::default(); + let opening = default_opening(); let state = TestStateClient::new(notes, anchor, opening); let prover = TestProverClient; @@ -44,20 +55,20 @@ pub fn mock_wallet( } /// Create a new wallet equivalent in all ways to `mock_wallet`, but serializing -/// and deserializing a `Transaction` using `Canon`. +/// and deserializing a `Transaction` using `rkyv`. pub fn mock_canon_wallet( rng: &mut Rng, note_values: &[u64], -) -> Wallet { +) -> Wallet { let store = TestStore::new(rng); let psk = store.retrieve_ssk(0).unwrap().public_spend_key(); let notes = new_notes(rng, &psk, note_values); let anchor = BlsScalar::random(rng); - let opening = Default::default(); + let opening = default_opening(); let state = TestStateClient::new(notes, anchor, opening); - let prover = CanonProverClient { + let prover = RkyvProverClient { prover: TestProverClient, }; @@ -75,7 +86,7 @@ pub fn mock_serde_wallet( let notes = new_notes(rng, &psk, note_values); let anchor = BlsScalar::random(rng); - let opening = Default::default(); + let opening = default_opening(); let state = TestStateClient::new(notes, anchor, opening); let prover = SerdeProverClient { @@ -128,7 +139,7 @@ impl Store for TestStore { pub struct TestStateClient { notes: Vec, anchor: BlsScalar, - opening: PoseidonBranch, + opening: PoseidonOpening<(), POSEIDON_TREE_DEPTH, 4>, } impl TestStateClient { @@ -136,7 +147,7 @@ impl TestStateClient { fn new( notes: Vec, anchor: BlsScalar, - opening: PoseidonBranch, + opening: PoseidonOpening<(), POSEIDON_TREE_DEPTH, 4>, ) -> Self { Self { notes, @@ -170,8 +181,8 @@ impl StateClient for TestStateClient { fn fetch_opening( &self, _: &Note, - ) -> Result, Self::Error> { - Ok(self.opening.clone()) + ) -> Result, Self::Error> { + Ok(self.opening) } fn fetch_stake(&self, _pk: &PublicKey) -> Result { @@ -218,28 +229,11 @@ impl ProverClient for TestProverClient { } #[derive(Debug)] -pub struct CanonProverClient { +pub struct RkyvProverClient { prover: TestProverClient, } -#[allow(clippy::large_enum_variant)] -#[derive(Debug, Clone, Canon)] -pub enum Call { - Execute { - anchor: BlsScalar, - nullifiers: Vec, - fee: Fee, - crossover: Option, - notes: Vec, - spend_proof: Vec, - call: Option<(ContractId, Transaction)>, - }, - SomeOtherVariant { - anchor: BlsScalar, - }, -} - -impl ProverClient for CanonProverClient { +impl ProverClient for RkyvProverClient { type Error = (); fn compute_proof_and_propagate( @@ -250,13 +244,17 @@ impl ProverClient for CanonProverClient { let tx = utx_clone.prove(Proof::default()); - let mut buf = [0u8; 4096]; - let mut sink = Sink::new(&mut buf); - tx.encode(&mut sink); - drop(sink); + let bytes = rkyv::to_bytes::<_, 65536>(&tx) + .expect("Encoding a tx should succeed") + .to_vec(); + + let decoded_tx: Transaction = rkyv::from_bytes(&bytes) + .expect("Deserializing a transaction should succeed"); - let mut source = Source::new(&buf); - let _ = Call::decode(&mut source).expect("decoding to Call to go fine"); + assert_eq!( + tx, decoded_tx, + "Encoded and decoded transaction should be equal" + ); self.prover.compute_proof_and_propagate(utx) }