From 2f36d3f362b30589939adf85dc5cf2d0eee1e6e3 Mon Sep 17 00:00:00 2001 From: Yevhenii Babichenko Date: Mon, 17 May 2021 12:02:47 +0300 Subject: [PATCH 1/3] add explicit spending counter to account transaction inputs --- .../src/accounting/account/account_state.rs | 2 +- chain-impl-mockchain/src/ledger/ledger.rs | 68 +++++++++++++----- .../src/ledger/tests/transaction_tests.rs | 2 +- .../src/testing/data/address.rs | 6 +- chain-impl-mockchain/src/transaction/input.rs | 69 +++++++++++++++---- .../src/transaction/transaction.rs | 1 + 6 files changed, 113 insertions(+), 35 deletions(-) diff --git a/chain-impl-mockchain/src/accounting/account/account_state.rs b/chain-impl-mockchain/src/accounting/account/account_state.rs index 925d185f4..2c11fcf02 100644 --- a/chain-impl-mockchain/src/accounting/account/account_state.rs +++ b/chain-impl-mockchain/src/accounting/account/account_state.rs @@ -189,7 +189,7 @@ impl AccountState { /// the counter is incremented. A matching counter /// needs to be used in the spending phase to make /// sure we have non-replayability of a transaction. -#[derive(Debug, Clone, Copy, PartialEq, Eq)] +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] pub struct SpendingCounter(pub(crate) u32); impl SpendingCounter { diff --git a/chain-impl-mockchain/src/ledger/ledger.rs b/chain-impl-mockchain/src/ledger/ledger.rs index 4eaf1aba6..74291f8e7 100644 --- a/chain-impl-mockchain/src/ledger/ledger.rs +++ b/chain-impl-mockchain/src/ledger/ledger.rs @@ -6,7 +6,6 @@ use super::governance::{Governance, ParametersGovernanceAction, TreasuryGovernan use super::leaderlog::LeadersParticipationRecord; use super::pots::Pots; use super::reward_info::{EpochRewardsInfo, RewardsInfoParameters}; -use crate::chaineval::HeaderContentEvalContext; use crate::chaintypes::{ChainLength, ConsensusType, HeaderId}; use crate::config::{self, ConfigParam}; use crate::date::{BlockDate, Epoch}; @@ -22,6 +21,7 @@ use crate::treasury::Treasury; use crate::value::*; use crate::vote::{CommitteeId, VotePlanLedger, VotePlanLedgerError, VotePlanStatus}; use crate::{account, certificate, legacy, multisig, setting, stake, update, utxo}; +use crate::{account::SpendingCounter, chaineval::HeaderContentEvalContext}; use crate::{ certificate::{PoolId, VoteAction, VotePlan}, chaineval::ConsensusEvalContext, @@ -307,6 +307,11 @@ pub enum Error { VotePlan(#[from] VotePlanLedgerError), #[error("Scripts addresses are not yet supported by the system")] ScriptsAddressNotAllowedYet, + #[error("Invalid spending counter: expected {} but got {} instead", expected.0, got.0)] + InvalidSpendingCounter { + expected: SpendingCounter, + got: SpendingCounter, + }, } impl LedgerParameters { @@ -1025,7 +1030,7 @@ impl Ledger { InputEnum::UtxoInput(_) => { return Err(Error::VoteCastInvalidTransaction); } - InputEnum::AccountInput(account_id, _value) => { + InputEnum::AccountInput(account_id, _spending_counter, _value) => { committee.insert(account_id.as_ref().try_into().unwrap()); } } @@ -1048,7 +1053,7 @@ impl Ledger { ) -> Result<(Self, Value), Error> { let sign_data_hash = tx.transaction_sign_data_hash(); - let (account_id, value, witness) = { + let (account_id, spending_counter, value, witness) = { check::valid_vote_cast(tx)?; let input = tx.inputs().iter().next().unwrap(); @@ -1056,9 +1061,9 @@ impl Ledger { InputEnum::UtxoInput(_) => { return Err(Error::VoteCastInvalidTransaction); } - InputEnum::AccountInput(account_id, value) => { + InputEnum::AccountInput(account_id, spending_counter, value) => { let witness = tx.witnesses().iter().next().unwrap(); - (account_id, value, witness) + (account_id, spending_counter, value, witness) } } }; @@ -1079,6 +1084,7 @@ impl Ledger { &sign_data_hash, &account_id, witness, + spending_counter, value, )?; } @@ -1089,6 +1095,7 @@ impl Ledger { &sign_data_hash, &account_id, witness, + spending_counter, value, )?; } @@ -1273,7 +1280,7 @@ impl Ledger { ) -> Result<(Self, Value), Error> { let sign_data_hash = tx.transaction_sign_data_hash(); - let (account_id, value, witness) = { + let (account_id, spending_counter, value, witness) = { check::valid_stake_owner_delegation_transaction(tx)?; let input = tx.inputs().iter().next().unwrap(); @@ -1281,9 +1288,9 @@ impl Ledger { InputEnum::UtxoInput(_) => { return Err(Error::OwnerStakeDelegationInvalidTransaction); } - InputEnum::AccountInput(account_id, value) => { + InputEnum::AccountInput(account_id, spending_counter, value) => { let witness = tx.witnesses().iter().next().unwrap(); - (account_id, value, witness) + (account_id, spending_counter, value, witness) } } }; @@ -1304,6 +1311,7 @@ impl Ledger { &sign_data_hash, &account_id, witness, + spending_counter, value, )?; self.accounts = single.set_delegation( @@ -1318,6 +1326,7 @@ impl Ledger { &sign_data_hash, &account_id, witness, + spending_counter, value, )?; self.multisig = multi.set_delegation( @@ -1434,7 +1443,7 @@ impl Ledger { InputEnum::UtxoInput(utxo) => { self = self.apply_input_to_utxo(&sign_data_hash, &utxo, &witness)? } - InputEnum::AccountInput(account_id, value) => { + InputEnum::AccountInput(account_id, spending_counter, value) => { match match_identifier_witness(&account_id, &witness)? { MatchingIdentifierWitness::Single(account_id, witness) => { self.accounts = input_single_account_verify( @@ -1443,6 +1452,7 @@ impl Ledger { &sign_data_hash, &account_id, witness, + spending_counter, value, )? } @@ -1453,6 +1463,7 @@ impl Ledger { &sign_data_hash, &account_id, witness, + spending_counter, value, )? } @@ -1702,16 +1713,23 @@ fn match_identifier_witness<'a>( } fn input_single_account_verify<'a>( - mut ledger: account::Ledger, + ledger: account::Ledger, block0_hash: &HeaderId, sign_data_hash: &TransactionSignDataHash, account: &account::Identifier, witness: &'a account::Witness, + spending_counter: SpendingCounter, value: Value, ) -> Result { // .remove_value() check if there's enough value and if not, returns a Err. - let (new_ledger, spending_counter) = ledger.remove_value(&account, value)?; - ledger = new_ledger; + let (new_ledger, spending_counter_ledger) = ledger.remove_value(&account, value)?; + + if spending_counter != spending_counter_ledger { + return Err(Error::InvalidSpendingCounter { + expected: spending_counter_ledger, + got: spending_counter, + }); + } let tidsc = WitnessAccountData::new(block0_hash, sign_data_hash, spending_counter); let verified = witness.verify(account.as_ref(), &tidsc); @@ -1721,19 +1739,29 @@ fn input_single_account_verify<'a>( witness: Witness::Account(witness.clone()), }); }; - Ok(ledger) + + Ok(new_ledger) } fn input_multi_account_verify<'a>( - mut ledger: multisig::Ledger, + ledger: multisig::Ledger, block0_hash: &HeaderId, sign_data_hash: &TransactionSignDataHash, account: &multisig::Identifier, witness: &'a multisig::Witness, + spending_counter: SpendingCounter, value: Value, ) -> Result { // .remove_value() check if there's enough value and if not, returns a Err. - let (new_ledger, declaration, spending_counter) = ledger.remove_value(&account, value)?; + let (new_ledger, declaration, spending_counter_ledger) = + ledger.remove_value(&account, value)?; + + if spending_counter != spending_counter_ledger { + return Err(Error::InvalidSpendingCounter { + expected: spending_counter_ledger, + got: spending_counter, + }); + } let data_to_verify = WitnessMultisigData::new(&block0_hash, sign_data_hash, spending_counter); if !witness.verify(declaration, &data_to_verify) { @@ -1742,8 +1770,8 @@ fn input_multi_account_verify<'a>( witness: Witness::Multisig(witness.clone()), }); } - ledger = new_ledger; - Ok(ledger) + + Ok(new_ledger) } #[cfg(test)] @@ -1923,6 +1951,7 @@ mod tests { block0_hash: HeaderId, sign_data_hash: TransactionSignDataHash, witness: account::Witness, + spending_counter: SpendingCounter, ) -> TestResult { let mut account_ledger = account::Ledger::new(); account_ledger = account_ledger @@ -1934,6 +1963,7 @@ mod tests { &sign_data_hash, &id, &witness, + spending_counter, value_to_sub, ); @@ -1962,6 +1992,7 @@ mod tests { &sign_data_hash, &id, &to_account_witness(&signed_tx.witnesses().iter().next().unwrap()), + account.spending_counter.unwrap(), value_to_sub, ); assert!(result.is_ok()) @@ -1998,6 +2029,7 @@ mod tests { &sign_data_hash, &id, &to_account_witness(&signed_tx.witnesses().iter().next().unwrap()), + account.spending_counter.unwrap(), value_to_sub, ); assert!(result.is_err()) @@ -2033,6 +2065,7 @@ mod tests { &sign_data_hash, &id, &to_account_witness(&signed_tx.witnesses().iter().next().unwrap()), + account.spending_counter.unwrap(), value_to_sub, ); assert!(result.is_err()) @@ -2062,6 +2095,7 @@ mod tests { &sign_data_hash, &non_existing_account.public_key().into(), &to_account_witness(&signed_tx.witnesses().iter().next().unwrap()), + account.spending_counter.unwrap(), value_to_sub, ); assert!(result.is_err()) diff --git a/chain-impl-mockchain/src/ledger/tests/transaction_tests.rs b/chain-impl-mockchain/src/ledger/tests/transaction_tests.rs index d1cd0c2e2..bfd2f7e2d 100644 --- a/chain-impl-mockchain/src/ledger/tests/transaction_tests.rs +++ b/chain-impl-mockchain/src/ledger/tests/transaction_tests.rs @@ -63,7 +63,7 @@ pub fn duplicated_account_transaction() { Err(err) => panic!("first transaction should be succesful but {}", err), Ok(_) => { assert_err_match!( - ledger::Error::AccountInvalidSignature { .. }, + ledger::Error::InvalidSpendingCounter { .. }, test_ledger.apply_transaction(fragment2) ); } diff --git a/chain-impl-mockchain/src/testing/data/address.rs b/chain-impl-mockchain/src/testing/data/address.rs index b1b1d95a3..6a902964d 100644 --- a/chain-impl-mockchain/src/testing/data/address.rs +++ b/chain-impl-mockchain/src/testing/data/address.rs @@ -118,7 +118,11 @@ impl AddressData { pub fn make_input(&self, value: Value, utxo: Option>) -> Input { match self.address.kind() { - Kind::Account { .. } => Input::from_account_public_key(self.public_key(), value), + Kind::Account { .. } => Input::from_account_public_key( + self.public_key(), + self.spending_counter.unwrap(), + value, + ), Kind::Single { .. } | Kind::Group { .. } => { Input::from_utxo_entry(utxo.unwrap_or_else(|| { panic!( diff --git a/chain-impl-mockchain/src/transaction/input.rs b/chain-impl-mockchain/src/transaction/input.rs index 88c0fcfdf..9c34d6013 100644 --- a/chain-impl-mockchain/src/transaction/input.rs +++ b/chain-impl-mockchain/src/transaction/input.rs @@ -1,5 +1,5 @@ use super::utxo::UtxoPointer; -use crate::account::Identifier; +use crate::account::{Identifier, SpendingCounter}; use crate::fragment::FragmentId; use crate::key::SpendingPublicKey; use crate::utxo::Entry; @@ -10,7 +10,7 @@ use chain_core::mempack::{ReadBuf, ReadError, Readable}; use chain_core::property; use chain_crypto::PublicKey; -pub const INPUT_SIZE: usize = 41; +pub const INPUT_SIZE: usize = 45; pub const INPUT_PTR_SIZE: usize = 32; @@ -22,6 +22,7 @@ impl UnspecifiedAccountIdentifier { pub fn to_single_account(&self) -> Option { PublicKey::from_binary(&self.0).map(|x| x.into()).ok() } + pub fn to_multi_account(&self) -> multisig::Identifier { multisig::Identifier::from(self.0) } @@ -66,6 +67,7 @@ pub enum AccountIdentifier { pub struct Input { index_or_account: u8, value: Value, + spending_counter: SpendingCounter, input_ptr: [u8; INPUT_PTR_SIZE], } @@ -81,7 +83,7 @@ impl From for [u8; INPUT_PTR_SIZE] { } pub enum InputEnum { - AccountInput(UnspecifiedAccountIdentifier, Value), + AccountInput(UnspecifiedAccountIdentifier, SpendingCounter, Value), UtxoInput(UtxoPointer), } @@ -90,9 +92,12 @@ impl From<[u8; INPUT_SIZE]> for Input { use std::convert::TryFrom; let index_or_account = data[0]; let value = Value::try_from(&data[1..9]).unwrap(); + let mut spending_counter_buffer = [0u8; 4]; + spending_counter_buffer.copy_from_slice(&data[9..13]); + let spending_counter = SpendingCounter(u32::from_be_bytes(spending_counter_buffer)); let mut input_ptr = [0u8; INPUT_PTR_SIZE]; - input_ptr.copy_from_slice(&data[9..]); - Input::new(index_or_account, value, input_ptr) + input_ptr.copy_from_slice(&data[13..]); + Input::new(index_or_account, value, Some(spending_counter), input_ptr) } } @@ -101,14 +106,22 @@ impl Input { let mut out = [0u8; INPUT_SIZE]; out[0] = self.index_or_account; out[1..9].copy_from_slice(&self.value.0.to_be_bytes()); - out[9..].copy_from_slice(&self.input_ptr); + out[9..13].copy_from_slice(&self.spending_counter.0.to_be_bytes()); + out[13..].copy_from_slice(&self.input_ptr); out } - pub fn new(index_or_account: u8, value: Value, input_ptr: [u8; INPUT_PTR_SIZE]) -> Self { + pub fn new( + index_or_account: u8, + value: Value, + spending_counter: Option, + input_ptr: [u8; INPUT_PTR_SIZE], + ) -> Self { + let spending_counter = spending_counter.unwrap_or(SpendingCounter(0)); Input { index_or_account, value, + spending_counter, input_ptr, } } @@ -131,6 +144,7 @@ impl Input { Input { index_or_account: utxo_pointer.output_index, value: utxo_pointer.value, + spending_counter: SpendingCounter(0), input_ptr, } } @@ -141,35 +155,54 @@ impl Input { Input { index_or_account: utxo_entry.output_index, value: utxo_entry.output.value, + spending_counter: SpendingCounter(0), input_ptr, } } - pub fn from_account_public_key(public_key: SpendingPublicKey, value: Value) -> Self { + pub fn from_account_public_key( + public_key: SpendingPublicKey, + spending_counter: SpendingCounter, + value: Value, + ) -> Self { Input::from_account( UnspecifiedAccountIdentifier::from_single_account(Identifier::from(public_key)), + spending_counter, value, ) } - pub fn from_account(id: UnspecifiedAccountIdentifier, value: Value) -> Self { + pub fn from_account( + id: UnspecifiedAccountIdentifier, + spending_counter: SpendingCounter, + value: Value, + ) -> Self { let mut input_ptr = [0u8; INPUT_PTR_SIZE]; input_ptr.copy_from_slice(&id.0); Input { index_or_account: 0xff, value, + spending_counter, input_ptr, } } - pub fn from_account_single(id: account::Identifier, value: Value) -> Self { + pub fn from_account_single( + id: account::Identifier, + spending_counter: SpendingCounter, + value: Value, + ) -> Self { let id = UnspecifiedAccountIdentifier::from_single_account(id); - Input::from_account(id, value) + Input::from_account(id, spending_counter, value) } - pub fn from_multisig_account(id: multisig::Identifier, value: Value) -> Self { + pub fn from_multisig_account( + id: multisig::Identifier, + spending_counter: SpendingCounter, + value: Value, + ) -> Self { let id = UnspecifiedAccountIdentifier::from_multi_account(id); - Input::from_account(id, value) + Input::from_account(id, spending_counter, value) } pub fn to_enum(&self) -> InputEnum { @@ -177,7 +210,7 @@ impl Input { InputType::Account => { let account_identifier = self.input_ptr; let id = UnspecifiedAccountIdentifier(account_identifier); - InputEnum::AccountInput(id, self.value) + InputEnum::AccountInput(id, self.spending_counter, self.value) } InputType::Utxo => InputEnum::UtxoInput(UtxoPointer::new( FragmentId::from(self.input_ptr), @@ -189,7 +222,9 @@ impl Input { pub fn from_enum(ie: InputEnum) -> Input { match ie { - InputEnum::AccountInput(id, value) => Self::from_account(id, value), + InputEnum::AccountInput(id, spending_counter, value) => { + Self::from_account(id, spending_counter, value) + } InputEnum::UtxoInput(utxo_pointer) => Self::from_utxo(utxo_pointer), } } @@ -218,11 +253,13 @@ impl property::Deserialize for Input { let mut codec = Codec::new(reader); let index_or_account = codec.get_u8()?; let value = Value::deserialize(&mut codec)?; + let spending_counter = SpendingCounter(codec.get_u32()?); let mut input_ptr = [0; INPUT_PTR_SIZE]; codec.into_inner().read_exact(&mut input_ptr)?; Ok(Input { index_or_account, value, + spending_counter, input_ptr, }) } @@ -232,10 +269,12 @@ impl Readable for Input { fn read(buf: &mut ReadBuf) -> Result { let index_or_account = buf.get_u8()?; let value = Value::read(buf)?; + let spending_counter = SpendingCounter(buf.get_u32()?); let input_ptr = <[u8; INPUT_PTR_SIZE]>::read(buf)?; Ok(Input { index_or_account, value, + spending_counter, input_ptr, }) } diff --git a/chain-impl-mockchain/src/transaction/transaction.rs b/chain-impl-mockchain/src/transaction/transaction.rs index c553baeaa..4c0a9e69d 100644 --- a/chain-impl-mockchain/src/transaction/transaction.rs +++ b/chain-impl-mockchain/src/transaction/transaction.rs @@ -364,6 +364,7 @@ impl

Transaction

{ pub fn nb_witnesses(&self) -> u8 { self.tstruct.nb_inputs } + pub fn nb_outputs(&self) -> u8 { self.tstruct.nb_outputs } From fdafec78ccff3ab77e4092b20f98534f78308c75 Mon Sep 17 00:00:00 2001 From: Yevhenii Babichenko Date: Mon, 17 May 2021 12:08:52 +0300 Subject: [PATCH 2/3] remove spending counter from account witness data --- chain-impl-mockchain/src/ledger/ledger.rs | 3 +-- .../src/testing/builders/witness_builder.rs | 9 +++------ chain-impl-mockchain/src/transaction/witness.rs | 10 ++-------- 3 files changed, 6 insertions(+), 16 deletions(-) diff --git a/chain-impl-mockchain/src/ledger/ledger.rs b/chain-impl-mockchain/src/ledger/ledger.rs index 74291f8e7..81abaa2af 100644 --- a/chain-impl-mockchain/src/ledger/ledger.rs +++ b/chain-impl-mockchain/src/ledger/ledger.rs @@ -1731,7 +1731,7 @@ fn input_single_account_verify<'a>( }); } - let tidsc = WitnessAccountData::new(block0_hash, sign_data_hash, spending_counter); + let tidsc = WitnessAccountData::new(block0_hash, sign_data_hash); let verified = witness.verify(account.as_ref(), &tidsc); if verified == chain_crypto::Verification::Failed { return Err(Error::AccountInvalidSignature { @@ -2763,7 +2763,6 @@ mod tests { let witness = Witness::new_account( &test_ledger.block0_hash, &tx_builder.get_auth_data_for_witness().hash(), - SpendingCounter::zero(), |d| faucet.private_key().sign(d), ); diff --git a/chain-impl-mockchain/src/testing/builders/witness_builder.rs b/chain-impl-mockchain/src/testing/builders/witness_builder.rs index 489ecef66..c09797038 100644 --- a/chain-impl-mockchain/src/testing/builders/witness_builder.rs +++ b/chain-impl-mockchain/src/testing/builders/witness_builder.rs @@ -22,12 +22,9 @@ pub fn make_witness( transaction_hash: &TransactionSignDataHash, ) -> Witness { match addres_data.address.kind() { - Kind::Account(_) => Witness::new_account( - block0, - transaction_hash, - addres_data.spending_counter.unwrap(), - |d| addres_data.private_key().sign(d), - ), + Kind::Account(_) => Witness::new_account(block0, transaction_hash, |d| { + addres_data.private_key().sign(d) + }), _ => Witness::new_utxo(block0, transaction_hash, |d| { addres_data.private_key().sign(d) }), diff --git a/chain-impl-mockchain/src/transaction/witness.rs b/chain-impl-mockchain/src/transaction/witness.rs index 0517cc2de..47a076220 100644 --- a/chain-impl-mockchain/src/transaction/witness.rs +++ b/chain-impl-mockchain/src/transaction/witness.rs @@ -98,14 +98,9 @@ impl AsRef<[u8]> for WitnessUtxoData { pub struct WitnessAccountData(Vec); impl WitnessAccountData { - pub fn new( - block0: &HeaderId, - transaction_id: &TransactionSignDataHash, - spending_counter: account::SpendingCounter, - ) -> Self { + pub fn new(block0: &HeaderId, transaction_id: &TransactionSignDataHash) -> Self { let mut v = Vec::with_capacity(69); witness_data_common(&mut v, WITNESS_TAG_ACCOUNT, block0, transaction_id); - v.extend_from_slice(&spending_counter.to_bytes()); WitnessAccountData(v) } } @@ -164,13 +159,12 @@ impl Witness { pub fn new_account( block0: &HeaderId, sign_data_hash: &TransactionSignDataHash, - spending_counter: account::SpendingCounter, sign: F, ) -> Self where F: FnOnce(&WitnessAccountData) -> account::Witness, { - let wud = WitnessAccountData::new(block0, sign_data_hash, spending_counter); + let wud = WitnessAccountData::new(block0, sign_data_hash); let sig = sign(&wud); Witness::Account(sig) } From 8637bd0538f3f5a08e96c0fa0d9a382da288d6aa Mon Sep 17 00:00:00 2001 From: Yevhenii Babichenko Date: Mon, 17 May 2021 16:45:57 +0300 Subject: [PATCH 3/3] add account identifier to invalid spending counter error Also added Display implementation for account identifier. --- chain-impl-mockchain/src/ledger/ledger.rs | 5 ++++- chain-impl-mockchain/src/transaction/input.rs | 8 ++++++++ 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/chain-impl-mockchain/src/ledger/ledger.rs b/chain-impl-mockchain/src/ledger/ledger.rs index 81abaa2af..720a61850 100644 --- a/chain-impl-mockchain/src/ledger/ledger.rs +++ b/chain-impl-mockchain/src/ledger/ledger.rs @@ -307,10 +307,11 @@ pub enum Error { VotePlan(#[from] VotePlanLedgerError), #[error("Scripts addresses are not yet supported by the system")] ScriptsAddressNotAllowedYet, - #[error("Invalid spending counter: expected {} but got {} instead", expected.0, got.0)] + #[error("Invalid spending counter for account {account}: expected {} but got {} instead", expected.0, got.0)] InvalidSpendingCounter { expected: SpendingCounter, got: SpendingCounter, + account: UnspecifiedAccountIdentifier, }, } @@ -1728,6 +1729,7 @@ fn input_single_account_verify<'a>( return Err(Error::InvalidSpendingCounter { expected: spending_counter_ledger, got: spending_counter, + account: UnspecifiedAccountIdentifier::from_single_account(account.clone()), }); } @@ -1760,6 +1762,7 @@ fn input_multi_account_verify<'a>( return Err(Error::InvalidSpendingCounter { expected: spending_counter_ledger, got: spending_counter, + account: UnspecifiedAccountIdentifier::from_multi_account(account.clone()), }); } diff --git a/chain-impl-mockchain/src/transaction/input.rs b/chain-impl-mockchain/src/transaction/input.rs index 9c34d6013..eb9507ea2 100644 --- a/chain-impl-mockchain/src/transaction/input.rs +++ b/chain-impl-mockchain/src/transaction/input.rs @@ -1,3 +1,5 @@ +use core::fmt; + use super::utxo::UtxoPointer; use crate::account::{Identifier, SpendingCounter}; use crate::fragment::FragmentId; @@ -53,6 +55,12 @@ impl From<[u8; INPUT_PTR_SIZE]> for UnspecifiedAccountIdentifier { } } +impl fmt::Display for UnspecifiedAccountIdentifier { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{}", hex::encode(&self.0)) + } +} + #[derive(Debug, Clone, PartialEq, Eq, Hash)] pub enum AccountIdentifier { Single(account::Identifier),