Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Check storage deposit for Outputs #1176

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 4 additions & 2 deletions bee-message/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,6 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

### Added

- Example that counts the number of allocations while scoring PoWs

### Changed

### Deprecated
Expand All @@ -23,6 +21,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## 0.2.0 - 2022-XX-XX

### Added

- `ByteCost` and storage deposit verification;

### Changed

- Serialize and deserialize all the types using `packable` instead of `bee-common::packable`;
Expand Down
40 changes: 40 additions & 0 deletions bee-message/src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,23 @@ pub enum Error {
#[cfg_attr(doc_cfg, doc(cfg(feature = "cpt2")))]
InvalidDustAllowanceAmount(<DustAllowanceAmount as TryFrom<u64>>::Error),
InvalidStorageDepositAmount(<StorageDepositAmount as TryFrom<u64>>::Error),
// The above is used by `Packable` to denote out-of-range values. The following denotes the actual amount.
InsufficientStorageDepositAmount {
amount: u64,
required: u64,
},
StorageDepositReturnExceedsOutputAmount {
deposit: u64,
amount: u64,
},
InsufficientStorageDepositReturnAmount {
deposit: u64,
required: u64,
},
UnnecessaryStorageDepositReturnCondition {
logical_amount: u64,
required: u64,
},
InvalidEssenceKind(u8),
InvalidFeatureBlockCount(<FeatureBlockCount as TryFrom<usize>>::Error),
InvalidFeatureBlockKind(u8),
Expand Down Expand Up @@ -188,6 +205,29 @@ impl fmt::Display for Error {
Error::InvalidStorageDepositAmount(amount) => {
write!(f, "invalid storage deposit amount: {}", amount)
}
Error::InsufficientStorageDepositAmount { amount, required } => {
write!(
f,
"insufficient output amount for storage deposit: {amount} (should be at least {required})"
)
}
Error::InsufficientStorageDepositReturnAmount { deposit, required } => {
write!(
f,
"the return deposit ({deposit}) must be greater than the minimum storage deposit ({required})"
)
}
Error::StorageDepositReturnExceedsOutputAmount { deposit, amount } => write!(
f,
"storage deposit return of {deposit} exceeds the original output amount of {amount}"
),
Error::UnnecessaryStorageDepositReturnCondition {
logical_amount,
required,
} => write!(
f,
"no storage deposit return is needed, the logical output amount {logical_amount} already covers the required deposit {required}"
),
Error::InvalidEssenceKind(k) => write!(f, "invalid essence kind: {}", k),
Error::InvalidFeatureBlockCount(count) => write!(f, "invalid feature block count: {}", count),
Error::InvalidFeatureBlockKind(k) => write!(f, "invalid feature block kind: {}", k),
Expand Down
53 changes: 50 additions & 3 deletions bee-message/src/output/byte_cost.rs
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,11 @@ impl ByteCostConfig {
pub trait ByteCost {
/// Different fields in a type lead to different storage requirements for the ledger state.
fn weighted_bytes(&self, config: &ByteCostConfig) -> u64;

/// Computes the byte cost given a [`ByteCostConfig`].
fn byte_cost(&self, config: &ByteCostConfig) -> u64 {
config.v_byte_cost * (self.weighted_bytes(config) + config.v_byte_offset)
}
}

impl<T: ByteCost, const N: usize> ByteCost for [T; N] {
Expand All @@ -99,7 +104,49 @@ impl<T: ByteCost, const N: usize> ByteCost for [T; N] {
}
}

/// Computes the storage cost for `[crate::output::Output]`s.
pub fn minimum_storage_deposit(config: &ByteCostConfig, output: &impl ByteCost) -> u64 {
config.v_byte_cost * output.weighted_bytes(config) + config.v_byte_offset
#[cfg(test)]
mod test {
use crate::output::{ByteCost, ByteCostConfig, ByteCostConfigBuilder, Output};

use bee_test::rand::output::{rand_alias_output, rand_basic_output, rand_foundry_output, rand_nft_output};
grtlr marked this conversation as resolved.
Show resolved Hide resolved

use packable::{Packable, PackableExt};

const BYTE_COST: u64 = 1;
const FACTOR_KEY: u64 = 10;
const FACTOR_DATA: u64 = 1;

fn config() -> ByteCostConfig {
ByteCostConfigBuilder::new()
.byte_cost(BYTE_COST)
.key_factor(FACTOR_KEY)
.data_factor(FACTOR_DATA)
.finish()
}

// We have to jump through hoops here because the randomly generated outputs from `bee_test` have a different
// type then the Outputs of this crate (although they are technically the same).
fn convert<T: PackableExt>(rand_output: impl Packable) -> T {
let bytes = rand_output.pack_to_vec();
// Safety: We know it's the right type.
T::unpack_unverified(bytes).unwrap()
}

fn output_in_range(output: Output, range: std::ops::RangeInclusive<u64>) {
let cost = output.byte_cost(&config());
assert!(
range.contains(&cost),
"{:#?} has a required byte cost of {}",
output,
cost
);
}

#[test]
fn valid_byte_cost_range() {
output_in_range(Output::Alias(convert(rand_alias_output())), 445..=29_620);
output_in_range(Output::Basic(convert(rand_basic_output())), 414..=13_485);
output_in_range(Output::Foundry(convert(rand_foundry_output())), 496..=21_365);
output_in_range(Output::Nft(convert(rand_nft_output())), 435..=21_734);
}
}
71 changes: 68 additions & 3 deletions bee-message/src/output/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,8 @@ pub(crate) use alias::StateMetadataLength;
pub use alias::{AliasOutput, AliasOutputBuilder};
pub use alias_id::AliasId;
pub use basic::{BasicOutput, BasicOutputBuilder};
pub use byte_cost::{minimum_storage_deposit, ByteCost, ByteCostConfig, ByteCostConfigBuilder};
pub(crate) use byte_cost::ByteCost;
pub use byte_cost::{ByteCostConfig, ByteCostConfigBuilder};
pub use chain_id::ChainId;
#[cfg(feature = "cpt2")]
pub(crate) use cpt2::signature_locked_dust_allowance::DustAllowanceAmount;
Expand All @@ -51,9 +52,9 @@ pub use token_scheme::TokenScheme;
pub use treasury::TreasuryOutput;
pub(crate) use treasury::TreasuryOutputAmount;
pub(crate) use unlock_condition::StorageDepositAmount;
pub use unlock_condition::{UnlockCondition, UnlockConditions};
pub use unlock_condition::{AddressUnlockCondition, UnlockCondition, UnlockConditions};

use crate::{constant::IOTA_SUPPLY, Error};
use crate::{address::Address, constant::IOTA_SUPPLY, Error};

use derive_more::From;
use packable::{bounded::BoundedU64, PackableExt};
Expand Down Expand Up @@ -201,10 +202,74 @@ impl Output {
Self::Nft(output) => Some(output.immutable_feature_blocks()),
}
}

/// Verify if a valid storage deposit was made. Each [`Output`] has to have an amount that covers its associated
/// byte cost, given by [`ByteCostConfig`]. If there is a
/// [`StorageDepositReturnUnlockCondition`](unlock_condition::StorageDepositReturnUnlockCondition), its amount
/// is also checked.
pub fn verify_storage_deposit(&self, config: &ByteCostConfig) -> Result<(), Error> {
let required_output_amount = self.byte_cost(config);

if self.amount() < required_output_amount {
return Err(Error::InsufficientStorageDepositAmount {
amount: self.amount(),
required: required_output_amount,
});
}

if let Some(return_condition) = self
.unlock_conditions()
.and_then(UnlockConditions::storage_deposit_return)
{
// We can't return more tokens than were originally contained in the output.
// `0` ≤ `Amount` - `Return Amount`
if return_condition.amount() > self.amount() {
return Err(Error::StorageDepositReturnExceedsOutputAmount {
deposit: return_condition.amount(),
amount: self.amount(),
});
}

let minimum_deposit = minimum_storage_deposit(config, return_condition.return_address());

// `Return Amount` must be ≥ than `Minimum Storage Deposit`
if return_condition.amount() < minimum_deposit {
return Err(Error::InsufficientStorageDepositReturnAmount {
deposit: return_condition.amount(),
required: minimum_deposit,
});
}

// Check if the storage deposit return was required in the first place.
// `Amount` - `Return Amount` ≤ `Required Storage Deposit of the Output`
if self.amount() - return_condition.amount() > required_output_amount {
return Err(Error::UnnecessaryStorageDepositReturnCondition {
logical_amount: self.amount() - return_condition.amount(),
required: required_output_amount,
});
}
}

Ok(())
}
}

impl ByteCost for Output {
fn weighted_bytes(&self, config: &ByteCostConfig) -> u64 {
self.packed_len() as u64 * config.v_byte_factor_data
}
}

/// Computes the minimum amount that a storage deposit has to match to allow creating a return [`Output`] back to the
/// sender [`Address`].
fn minimum_storage_deposit(config: &ByteCostConfig, address: &Address) -> u64 {
let address_condition = UnlockCondition::Address(AddressUnlockCondition::new(*address));
// Safety: This can never fail because the amount will always be within the valid range. Also, the actual value is
// not important, we are only interested in the storage requirements of the type.
let basic_output = BasicOutputBuilder::new(OutputAmount::MIN)
.unwrap()
.add_unlock_condition(address_condition)
.finish()
.unwrap();
Output::Basic(basic_output).byte_cost(config)
}
35 changes: 0 additions & 35 deletions bee-message/tests/byte_cost.rs

This file was deleted.

2 changes: 1 addition & 1 deletion bee-protocol/src/workers/message/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,6 @@ pub(crate) use payload::{
MilestonePayloadWorker, PayloadWorker, PayloadWorkerEvent, TaggedDataPayloadWorker, TaggedDataPayloadWorkerEvent,
TransactionPayloadWorker,
};
pub(crate) use processor::{ProcessorWorker, ProcessorWorkerEvent};
pub(crate) use processor::{ProcessorWorker, ProcessorWorkerConfig, ProcessorWorkerEvent};
pub use submitter::{MessageSubmitterError, MessageSubmitterWorker, MessageSubmitterWorkerEvent};
pub(crate) use unreferenced_inserter::{UnreferencedMessageInserterWorker, UnreferencedMessageInserterWorkerEvent};
36 changes: 29 additions & 7 deletions bee-protocol/src/workers/message/processor.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ use crate::{

use bee_gossip::PeerId;
use bee_message::{
output::ByteCostConfig,
payload::{transaction::TransactionEssence, Payload},
Message, MessageId,
};
Expand All @@ -44,12 +45,18 @@ pub(crate) struct ProcessorWorker {
pub(crate) tx: mpsc::UnboundedSender<ProcessorWorkerEvent>,
}

#[derive(Clone)]
pub(crate) struct ProcessorWorkerConfig {
pub(crate) network_id: u64,
pub(crate) byte_cost: ByteCostConfig,
}

#[async_trait]
impl<N: Node> Worker<N> for ProcessorWorker
where
N::Backend: StorageBackend,
{
type Config = u64;
type Config = ProcessorWorkerConfig;
type Error = Infallible;

fn dependencies() -> &'static [TypeId] {
Expand Down Expand Up @@ -102,10 +109,10 @@ where
let metrics = metrics.clone();
let peer_manager = peer_manager.clone();
let bus = bus.clone();
let network_id = config;
let config = config.clone();

tokio::spawn(async move {
while let Ok(ProcessorWorkerEvent {
'next_event: while let Ok(ProcessorWorkerEvent {
from,
message_packet,
notifier,
Expand Down Expand Up @@ -137,13 +144,28 @@ where
if let Some(Payload::Transaction(transaction)) = message.payload() {
let TransactionEssence::Regular(essence) = transaction.essence();

if essence.network_id() != network_id {
if essence.network_id() != config.network_id {
notify_invalid_message(
format!("Incompatible network ID {} != {}.", essence.network_id(), network_id),
format!(
"Incompatible network ID {} != {}.",
essence.network_id(),
config.network_id
),
&metrics,
notifier,
);
continue;
continue 'next_event;
}

for (i, output) in essence.outputs().iter().enumerate() {
if let Err(error) = output.verify_storage_deposit(&config.byte_cost) {
notify_invalid_message(
format!("Invalid output i={i}: {}", error),
&metrics,
notifier,
);
continue 'next_event;
}
}
}

Expand All @@ -161,7 +183,7 @@ where
})
.unwrap_or_default();
}
continue;
continue 'next_event;
};

// Send the propagation event ASAP to allow the propagator to do its thing
Expand Down
9 changes: 6 additions & 3 deletions bee-protocol/src/workers/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -25,8 +25,8 @@ pub(crate) use heartbeater::HeartbeaterWorker;
pub(crate) use index_updater::{IndexUpdaterWorker, IndexUpdaterWorkerEvent};
pub(crate) use message::{
HasherWorker, HasherWorkerEvent, MilestonePayloadWorker, PayloadWorker, PayloadWorkerEvent, ProcessorWorker,
TaggedDataPayloadWorker, TaggedDataPayloadWorkerEvent, TransactionPayloadWorker, UnreferencedMessageInserterWorker,
UnreferencedMessageInserterWorkerEvent,
ProcessorWorkerConfig, TaggedDataPayloadWorker, TaggedDataPayloadWorkerEvent, TransactionPayloadWorker,
UnreferencedMessageInserterWorker, UnreferencedMessageInserterWorkerEvent,
};
pub use message::{MessageSubmitterError, MessageSubmitterWorker, MessageSubmitterWorkerEvent};
pub use metrics::MetricsWorker;
Expand Down Expand Up @@ -66,7 +66,10 @@ where
network_name: network_id.0,
})
.with_worker_cfg::<HasherWorker>(config.clone())
.with_worker_cfg::<ProcessorWorker>(network_id.1)
.with_worker_cfg::<ProcessorWorker>(ProcessorWorkerConfig {
network_id: network_id.1,
byte_cost: config.byte_cost.clone(),
})
.with_worker::<MessageResponderWorker>()
.with_worker::<MilestoneResponderWorker>()
.with_worker::<MessageRequesterWorker>()
Expand Down