From 6961e39800a95dcdfc21851131c9b204676b5932 Mon Sep 17 00:00:00 2001 From: Thibault Martinez Date: Tue, 26 Sep 2023 15:09:22 +0200 Subject: [PATCH] Finish Delegation TIP compliance (#1306) * Docs clean * Remove immutable feature flags * Add NullDelegationValidatorId error * impl StateTransitionVerifier for DelegationOutput * Add some checks * Add NonDelayedClaimingTransition * Add todo * Fmt * Update sdk/src/types/block/output/delegation.rs Co-authored-by: Thoralf-M <46689931+Thoralf-M@users.noreply.github.com> * IOTA tokens -> coins * validator_id -> validator_address --------- Co-authored-by: Thoralf-M <46689931+Thoralf-M@users.noreply.github.com> --- sdk/src/types/api/core/response.rs | 4 +- sdk/src/types/block/error.rs | 2 + sdk/src/types/block/output/account.rs | 11 +- sdk/src/types/block/output/basic.rs | 2 +- sdk/src/types/block/output/delegation.rs | 148 ++++++++++++------ sdk/src/types/block/output/foundry.rs | 2 +- sdk/src/types/block/output/mod.rs | 5 + sdk/src/types/block/output/nft.rs | 2 +- .../types/block/output/state_transition.rs | 3 + sdk/src/types/block/slot/epoch.rs | 6 + 10 files changed, 124 insertions(+), 61 deletions(-) diff --git a/sdk/src/types/api/core/response.rs b/sdk/src/types/api/core/response.rs index 43ef4ea3b3..7998f3f271 100644 --- a/sdk/src/types/api/core/response.rs +++ b/sdk/src/types/api/core/response.rs @@ -206,10 +206,10 @@ pub struct ManaRewardsResponse { pub struct CommitteeResponse { /// The epoch index of the committee. pub epoch_index: EpochIndex, - /// The total amount of delegated and staked IOTA tokens in the selected committee. + /// The total amount of delegated and staked IOTA coins in the selected committee. #[serde(with = "string")] pub total_stake: u64, - /// The total amount of staked IOTA tokens in the selected committee. + /// The total amount of staked IOTA coins in the selected committee. #[serde(with = "string")] pub total_validator_stake: u64, /// The validators of the committee. diff --git a/sdk/src/types/block/error.rs b/sdk/src/types/block/error.rs index fa060a803f..0ac9c990b0 100644 --- a/sdk/src/types/block/error.rs +++ b/sdk/src/types/block/error.rs @@ -171,6 +171,7 @@ pub enum Error { UnsupportedOutputKind(u8), DuplicateOutputChain(ChainId), InvalidField(&'static str), + NullDelegationValidatorId, } #[cfg(feature = "std")] @@ -371,6 +372,7 @@ impl fmt::Display for Error { Self::UnsupportedOutputKind(k) => write!(f, "unsupported output kind: {k}"), Self::DuplicateOutputChain(chain_id) => write!(f, "duplicate output chain {chain_id}"), Self::InvalidField(field) => write!(f, "invalid field: {field}"), + Self::NullDelegationValidatorId => write!(f, "null delegation validator ID"), } } } diff --git a/sdk/src/types/block/output/account.rs b/sdk/src/types/block/output/account.rs index 20b30b17ae..5e1d832928 100644 --- a/sdk/src/types/block/output/account.rs +++ b/sdk/src/types/block/output/account.rs @@ -370,7 +370,7 @@ pub(crate) type StateMetadataLength = BoundedU16<0, { AccountOutput::STATE_METAD /// Describes an account in the ledger that can be controlled by the state and governance controllers. #[derive(Clone, Debug, Eq, PartialEq, Ord, PartialOrd, Hash)] pub struct AccountOutput { - // Amount of IOTA tokens held by the output. + // Amount of IOTA coins held by the output. amount: u64, mana: u64, // Native tokens held by the output. @@ -780,29 +780,20 @@ pub(crate) mod dto { pub struct AccountOutputDto { #[serde(rename = "type")] pub kind: u8, - // Amount of IOTA tokens held by the output. #[serde(with = "string")] pub amount: u64, #[serde(with = "string")] pub mana: u64, - // Native tokens held by the output. #[serde(skip_serializing_if = "Vec::is_empty", default)] pub native_tokens: Vec, - // Unique identifier of the account. pub account_id: AccountId, - // A counter that must increase by 1 every time the account is state transitioned. pub state_index: u32, - // Metadata that can only be changed by the state controller. #[serde(skip_serializing_if = "<[_]>::is_empty", default, with = "prefix_hex_bytes")] pub state_metadata: Box<[u8]>, - // A counter that denotes the number of foundries created by this account. pub foundry_counter: u32, - // pub unlock_conditions: Vec, - // #[serde(skip_serializing_if = "Vec::is_empty", default)] pub features: Vec, - // #[serde(skip_serializing_if = "Vec::is_empty", default)] pub immutable_features: Vec, } diff --git a/sdk/src/types/block/output/basic.rs b/sdk/src/types/block/output/basic.rs index 51a26add26..e28f0a4cc2 100644 --- a/sdk/src/types/block/output/basic.rs +++ b/sdk/src/types/block/output/basic.rs @@ -215,7 +215,7 @@ impl From<&BasicOutput> for BasicOutputBuilder { #[packable(unpack_error = Error)] #[packable(unpack_visitor = ProtocolParameters)] pub struct BasicOutput { - /// Amount of IOTA tokens to deposit with this output. + /// Amount of IOTA coins to deposit with this output. #[packable(verify_with = verify_output_amount_packable)] amount: u64, /// Amount of stored Mana held by this output. diff --git a/sdk/src/types/block/output/delegation.rs b/sdk/src/types/block/output/delegation.rs index d3aae55fd9..5c926cd55f 100644 --- a/sdk/src/types/block/output/delegation.rs +++ b/sdk/src/types/block/output/delegation.rs @@ -12,16 +12,14 @@ use packable::{ use crate::types::{ block::{ - address::Address, + address::{AccountAddress, Address}, output::{ - account::AccountId, chain_id::ChainId, - feature::FeatureFlags, unlock_condition::{ verify_allowed_unlock_conditions, UnlockCondition, UnlockConditionFlags, UnlockConditions, }, verify_output_amount_min, verify_output_amount_packable, verify_output_amount_supply, Output, - OutputBuilderAmount, OutputId, Rent, RentStructure, + OutputBuilderAmount, OutputId, Rent, RentStructure, StateTransitionError, StateTransitionVerifier, }, protocol::ProtocolParameters, semantic::{TransactionFailureReason, ValidationContext}, @@ -56,7 +54,7 @@ pub struct DelegationOutputBuilder { amount: OutputBuilderAmount, delegated_amount: u64, delegation_id: DelegationId, - validator_id: AccountId, + validator_address: AccountAddress, start_epoch: EpochIndex, end_epoch: EpochIndex, unlock_conditions: BTreeSet, @@ -68,13 +66,13 @@ impl DelegationOutputBuilder { amount: u64, delegated_amount: u64, delegation_id: DelegationId, - validator_id: AccountId, + validator_address: AccountAddress, ) -> Self { Self::new( OutputBuilderAmount::Amount(amount), delegated_amount, delegation_id, - validator_id, + validator_address, ) } @@ -84,13 +82,13 @@ impl DelegationOutputBuilder { rent_structure: RentStructure, delegated_amount: u64, delegation_id: DelegationId, - validator_id: AccountId, + validator_address: AccountAddress, ) -> Self { Self::new( OutputBuilderAmount::MinimumStorageDeposit(rent_structure), delegated_amount, delegation_id, - validator_id, + validator_address, ) } @@ -98,13 +96,13 @@ impl DelegationOutputBuilder { amount: OutputBuilderAmount, delegated_amount: u64, delegation_id: DelegationId, - validator_id: AccountId, + validator_address: AccountAddress, ) -> Self { Self { amount, delegated_amount, delegation_id, - validator_id, + validator_address, start_epoch: 0.into(), end_epoch: 0.into(), unlock_conditions: BTreeSet::new(), @@ -129,9 +127,9 @@ impl DelegationOutputBuilder { self } - /// Sets the validator ID to the provided value. - pub fn with_validator_id(mut self, validator_id: AccountId) -> Self { - self.validator_id = validator_id; + /// Sets the validator address to the provided value. + pub fn with_validator_address(mut self, validator_address: AccountAddress) -> Self { + self.validator_address = validator_address; self } @@ -174,8 +172,12 @@ impl DelegationOutputBuilder { self } - /// Finishes the builder into a [`DelegationOutput`] without amount verification. + /// Finishes the builder into a [`DelegationOutput`] without parameters verification. pub fn finish(self) -> Result { + if self.validator_address.is_null() { + return Err(Error::NullDelegationValidatorId); + } + let unlock_conditions = UnlockConditions::from_set(self.unlock_conditions)?; verify_unlock_conditions::(&unlock_conditions)?; @@ -184,7 +186,7 @@ impl DelegationOutputBuilder { amount: 1u64, delegated_amount: self.delegated_amount, delegation_id: self.delegation_id, - validator_id: self.validator_id, + validator_address: self.validator_address, start_epoch: self.start_epoch, end_epoch: self.end_epoch, unlock_conditions, @@ -202,7 +204,7 @@ impl DelegationOutputBuilder { Ok(output) } - /// Finishes the builder into a [`DelegationOutput`] with amount verification. + /// Finishes the builder into a [`DelegationOutput`] with parameters verification. pub fn finish_with_params<'a>( self, params: impl Into> + Send, @@ -228,7 +230,7 @@ impl From<&DelegationOutput> for DelegationOutputBuilder { amount: OutputBuilderAmount::Amount(output.amount), delegated_amount: output.delegated_amount, delegation_id: output.delegation_id, - validator_id: output.validator_id, + validator_address: output.validator_address, start_epoch: output.start_epoch, end_epoch: output.end_epoch, unlock_conditions: output.unlock_conditions.iter().cloned().collect(), @@ -236,21 +238,22 @@ impl From<&DelegationOutput> for DelegationOutputBuilder { } } -/// Describes a Delegation output, which delegates its contained IOTA tokens as voting power to a validator. +/// An output which delegates its contained IOTA coins as voting power to a validator. #[derive(Clone, Debug, Eq, PartialEq, Ord, PartialOrd, Hash)] pub struct DelegationOutput { - // Amount of IOTA tokens held by the output. + /// Amount of IOTA coins to deposit with this output. amount: u64, - /// The amount of delegated coins. + /// Amount of delegated IOTA coins. delegated_amount: u64, - /// Unique identifier of the Delegation Output, which is the BLAKE2b-256 hash of the Output ID that created it. + /// Unique identifier of the delegation output. delegation_id: DelegationId, - /// The Account ID of the validator to which this output is delegating. - validator_id: AccountId, - /// The index of the first epoch for which this output delegates. + /// Account address of the validator to which this output is delegating. + validator_address: AccountAddress, + /// Index of the first epoch for which this output delegates. start_epoch: EpochIndex, - /// The index of the last epoch for which this output delegates. + /// Index of the last epoch for which this output delegates. end_epoch: EpochIndex, + /// Define how the output can be unlocked in a transaction. unlock_conditions: UnlockConditions, } @@ -259,17 +262,15 @@ impl DelegationOutput { pub const KIND: u8 = 7; /// The set of allowed [`UnlockCondition`]s for a [`DelegationOutput`]. pub const ALLOWED_UNLOCK_CONDITIONS: UnlockConditionFlags = UnlockConditionFlags::ADDRESS; - /// The set of allowed immutable [`Feature`]s for a [`DelegationOutput`]. - pub const ALLOWED_IMMUTABLE_FEATURES: FeatureFlags = FeatureFlags::ISSUER; /// Creates a new [`DelegationOutputBuilder`] with a provided amount. pub fn build_with_amount( amount: u64, delegated_amount: u64, delegation_id: DelegationId, - validator_id: AccountId, + validator_address: AccountAddress, ) -> DelegationOutputBuilder { - DelegationOutputBuilder::new_with_amount(amount, delegated_amount, delegation_id, validator_id) + DelegationOutputBuilder::new_with_amount(amount, delegated_amount, delegation_id, validator_address) } /// Creates a new [`DelegationOutputBuilder`] with a provided rent structure. @@ -278,13 +279,13 @@ impl DelegationOutput { rent_structure: RentStructure, delegated_amount: u64, delegation_id: DelegationId, - validator_id: AccountId, + validator_address: AccountAddress, ) -> DelegationOutputBuilder { DelegationOutputBuilder::new_with_minimum_storage_deposit( rent_structure, delegated_amount, delegation_id, - validator_id, + validator_address, ) } @@ -308,9 +309,9 @@ impl DelegationOutput { self.delegation_id.or_from_output_id(output_id) } - /// Returns the validator ID of the [`DelegationOutput`]. - pub fn validator_id(&self) -> &AccountId { - &self.validator_id + /// Returns the validator address of the [`DelegationOutput`]. + pub fn validator_address(&self) -> &AccountAddress { + &self.validator_address } /// Returns the start epoch of the [`DelegationOutput`]. @@ -355,6 +356,53 @@ impl DelegationOutput { .locked_address(self.address(), context.essence.creation_slot()) .unlock(unlock, inputs, context) } + + // Transition, just without full ValidationContext. + pub(crate) fn transition_inner(current_state: &Self, next_state: &Self) -> Result<(), StateTransitionError> { + if !(current_state.delegation_id.is_null() && !next_state.delegation_id().is_null()) { + return Err(StateTransitionError::NonDelayedClaimingTransition); + } + + if current_state.delegated_amount != next_state.delegated_amount + || current_state.start_epoch != next_state.start_epoch + || current_state.validator_address != next_state.validator_address + { + return Err(StateTransitionError::MutatedImmutableField); + } + // TODO add end_epoch validation rules + Ok(()) + } +} + +impl StateTransitionVerifier for DelegationOutput { + fn creation(next_state: &Self, _context: &ValidationContext<'_>) -> Result<(), StateTransitionError> { + if !next_state.delegation_id.is_null() { + return Err(StateTransitionError::NonZeroCreatedId); + } + + if next_state.amount != next_state.delegated_amount { + return Err(StateTransitionError::InvalidDelegatedAmount); + } + + if next_state.end_epoch != 0 { + return Err(StateTransitionError::NonZeroDelegationEndEpoch); + } + + Ok(()) + } + + fn transition( + current_state: &Self, + next_state: &Self, + _context: &ValidationContext<'_>, + ) -> Result<(), StateTransitionError> { + Self::transition_inner(current_state, next_state) + } + + fn destruction(_current_state: &Self, _context: &ValidationContext<'_>) -> Result<(), StateTransitionError> { + // TODO handle mana rewards + Ok(()) + } } impl Packable for DelegationOutput { @@ -365,7 +413,7 @@ impl Packable for DelegationOutput { self.amount.pack(packer)?; self.delegated_amount.pack(packer)?; self.delegation_id.pack(packer)?; - self.validator_id.pack(packer)?; + self.validator_address.pack(packer)?; self.start_epoch.pack(packer)?; self.end_epoch.pack(packer)?; self.unlock_conditions.pack(packer)?; @@ -383,7 +431,12 @@ impl Packable for DelegationOutput { let delegated_amount = u64::unpack::<_, VERIFY>(unpacker, &()).coerce()?; let delegation_id = DelegationId::unpack::<_, VERIFY>(unpacker, &()).coerce()?; - let validator_id = AccountId::unpack::<_, VERIFY>(unpacker, &()).coerce()?; + let validator_address = AccountAddress::unpack::<_, VERIFY>(unpacker, &()).coerce()?; + + if validator_address.is_null() { + return Err(Error::NullDelegationValidatorId).map_err(UnpackError::Packable); + } + let start_epoch = EpochIndex::unpack::<_, VERIFY>(unpacker, &()).coerce()?; let end_epoch = EpochIndex::unpack::<_, VERIFY>(unpacker, &()).coerce()?; let unlock_conditions = UnlockConditions::unpack::<_, VERIFY>(unpacker, visitor)?; @@ -394,7 +447,7 @@ impl Packable for DelegationOutput { amount, delegated_amount, delegation_id, - validator_id, + validator_address, start_epoch, end_epoch, unlock_conditions, @@ -442,7 +495,7 @@ pub(crate) mod dto { #[serde(with = "string")] pub delegated_amount: u64, pub delegation_id: DelegationId, - pub validator_id: AccountId, + pub validator_address: AccountAddress, start_epoch: EpochIndex, end_epoch: EpochIndex, pub unlock_conditions: Vec, @@ -455,7 +508,7 @@ pub(crate) mod dto { amount: value.amount(), delegated_amount: value.delegated_amount(), delegation_id: *value.delegation_id(), - validator_id: *value.validator_id(), + validator_address: *value.validator_address(), start_epoch: value.start_epoch(), end_epoch: value.end_epoch(), unlock_conditions: value.unlock_conditions().iter().map(Into::into).collect::<_>(), @@ -475,7 +528,7 @@ pub(crate) mod dto { dto.amount, dto.delegated_amount, dto.delegation_id, - dto.validator_id, + dto.validator_address, ) .with_start_epoch(dto.start_epoch) .with_end_epoch(dto.end_epoch); @@ -494,7 +547,7 @@ pub(crate) mod dto { amount: OutputBuilderAmount, delegated_amount: u64, delegation_id: &DelegationId, - validator_id: &AccountId, + validator_address: &AccountAddress, start_epoch: impl Into, end_epoch: impl Into, unlock_conditions: Vec, @@ -502,15 +555,18 @@ pub(crate) mod dto { ) -> Result { let params = params.into(); let mut builder = match amount { - OutputBuilderAmount::Amount(amount) => { - DelegationOutputBuilder::new_with_amount(amount, delegated_amount, *delegation_id, *validator_id) - } + OutputBuilderAmount::Amount(amount) => DelegationOutputBuilder::new_with_amount( + amount, + delegated_amount, + *delegation_id, + *validator_address, + ), OutputBuilderAmount::MinimumStorageDeposit(rent_structure) => { DelegationOutputBuilder::new_with_minimum_storage_deposit( rent_structure, delegated_amount, *delegation_id, - *validator_id, + *validator_address, ) } } diff --git a/sdk/src/types/block/output/foundry.rs b/sdk/src/types/block/output/foundry.rs index ae07e1ac26..cc75a4b8cf 100644 --- a/sdk/src/types/block/output/foundry.rs +++ b/sdk/src/types/block/output/foundry.rs @@ -329,7 +329,7 @@ impl From<&FoundryOutput> for FoundryOutputBuilder { /// Describes a foundry output that is controlled by an account. #[derive(Clone, Debug, Eq, PartialEq, Ord, PartialOrd, Hash)] pub struct FoundryOutput { - /// Amount of IOTA tokens to deposit with this output. + /// Amount of IOTA coins to deposit with this output. amount: u64, /// Native tokens held by this output. native_tokens: NativeTokens, diff --git a/sdk/src/types/block/output/mod.rs b/sdk/src/types/block/output/mod.rs index 65b93ad1e2..c9829eb5a0 100644 --- a/sdk/src/types/block/output/mod.rs +++ b/sdk/src/types/block/output/mod.rs @@ -352,6 +352,7 @@ impl Output { (None, Some(Self::Account(next_state))) => AccountOutput::creation(next_state, context), (None, Some(Self::Foundry(next_state))) => FoundryOutput::creation(next_state, context), (None, Some(Self::Nft(next_state))) => NftOutput::creation(next_state, context), + (None, Some(Self::Delegation(next_state))) => DelegationOutput::creation(next_state, context), // Transitions. (Some(Self::Account(current_state)), Some(Self::Account(next_state))) => { @@ -363,11 +364,15 @@ impl Output { (Some(Self::Nft(current_state)), Some(Self::Nft(next_state))) => { NftOutput::transition(current_state, next_state, context) } + (Some(Self::Delegation(current_state)), Some(Self::Delegation(next_state))) => { + DelegationOutput::transition(current_state, next_state, context) + } // Destructions. (Some(Self::Account(current_state)), None) => AccountOutput::destruction(current_state, context), (Some(Self::Foundry(current_state)), None) => FoundryOutput::destruction(current_state, context), (Some(Self::Nft(current_state)), None) => NftOutput::destruction(current_state, context), + (Some(Self::Delegation(current_state)), None) => DelegationOutput::destruction(current_state, context), // Unsupported. _ => Err(StateTransitionError::UnsupportedStateTransition), diff --git a/sdk/src/types/block/output/nft.rs b/sdk/src/types/block/output/nft.rs index d905af23de..957e384e51 100644 --- a/sdk/src/types/block/output/nft.rs +++ b/sdk/src/types/block/output/nft.rs @@ -287,7 +287,7 @@ impl From<&NftOutput> for NftOutputBuilder { /// Describes an NFT output, a globally unique token with metadata attached. #[derive(Clone, Debug, Eq, PartialEq, Ord, PartialOrd, Hash)] pub struct NftOutput { - /// Amount of IOTA tokens to deposit with this output. + /// Amount of IOTA coins to deposit with this output. amount: u64, /// Amount of stored Mana held by this output. mana: u64, diff --git a/sdk/src/types/block/output/state_transition.rs b/sdk/src/types/block/output/state_transition.rs index b5b5b99163..55dae28e86 100644 --- a/sdk/src/types/block/output/state_transition.rs +++ b/sdk/src/types/block/output/state_transition.rs @@ -14,14 +14,17 @@ pub enum StateTransitionError { InconsistentNativeTokensMint, InconsistentNativeTokensTransition, InconsistentNativeTokensMeltBurn, + InvalidDelegatedAmount, IssuerNotUnlocked, MissingAccountForFoundry, MutatedFieldWithoutRights, MutatedImmutableField, + NonDelayedClaimingTransition, NonMonotonicallyIncreasingNativeTokens, NonZeroCreatedId, NonZeroCreatedFoundryCounter, NonZeroCreatedStateIndex, + NonZeroDelegationEndEpoch, UnsortedCreatedFoundries, UnsupportedStateIndexOperation { current_state: u32, next_state: u32 }, UnsupportedStateTransition, diff --git a/sdk/src/types/block/slot/epoch.rs b/sdk/src/types/block/slot/epoch.rs index 197fa3d99d..e6afa3bd54 100644 --- a/sdk/src/types/block/slot/epoch.rs +++ b/sdk/src/types/block/slot/epoch.rs @@ -87,6 +87,12 @@ impl From for u64 { } } +impl PartialEq for EpochIndex { + fn eq(&self, other: &u64) -> bool { + self.0 == *other + } +} + #[cfg(feature = "serde")] string_serde_impl!(EpochIndex);