From abcdd6173f0a3bd656a5fc2e0b6381bc7905f9e1 Mon Sep 17 00:00:00 2001 From: Thoralf-M <46689931+Thoralf-M@users.noreply.github.com> Date: Mon, 29 Jan 2024 10:53:19 +0100 Subject: [PATCH] Update delegation creation and transition rules (#1421) * Add end_epoch delegation transition rules * u32 slot index * no std * Remove todo * Review comments, fix SlotCommitmentId length, update tests * Resolve conflicts * Remove epoch_start in favor of first_slot_index * fmt * Review suggestions * Resolve conflicts * Review suggestions * Cleanup * Update creation check * Address review comments * Use max_committable_age * Move things, clarify * Move bounded_slot methods to SlotCommitmentId * typo * Simplify * Add genesis slot offset to slot calculations * Accept impl Into * Apply suggestions from code review Co-authored-by: Thibault Martinez * impl Into for consistency * Align param order * Remove into * Align registration slot calculation with TIP * Add genesis_slot offset to range * Fmt, no_std * Clippy --------- Co-authored-by: Thibault Martinez --- bindings/core/src/error.rs | 4 +- sdk/src/types/block/output/delegation.rs | 4 +- sdk/src/types/block/protocol/mod.rs | 10 ++- .../types/block/semantic/state_transition.rs | 77 ++++++++++++++++++- sdk/src/types/block/slot/commitment_id.rs | 21 +++++ sdk/src/types/block/slot/epoch.rs | 51 +++++++++--- sdk/src/types/block/slot/index.rs | 13 +++- 7 files changed, 154 insertions(+), 26 deletions(-) diff --git a/bindings/core/src/error.rs b/bindings/core/src/error.rs index 81fd1f9b98..ad52cd9fb8 100644 --- a/bindings/core/src/error.rs +++ b/bindings/core/src/error.rs @@ -63,8 +63,8 @@ impl Serialize for Error { kind: self.as_ref().to_owned(), error: match &self { // Only Client and wallet have a proper serde impl - Self::Client(e) => serde_json::to_value(e).map_err(|e| serde::ser::Error::custom(e))?, - Self::Wallet(e) => serde_json::to_value(e).map_err(|e| serde::ser::Error::custom(e))?, + Self::Client(e) => serde_json::to_value(e).map_err(serde::ser::Error::custom)?, + Self::Wallet(e) => serde_json::to_value(e).map_err(serde::ser::Error::custom)?, _ => serde_json::Value::String(self.to_string()), }, } diff --git a/sdk/src/types/block/output/delegation.rs b/sdk/src/types/block/output/delegation.rs index ea16d4f4b8..2d7a1799cb 100644 --- a/sdk/src/types/block/output/delegation.rs +++ b/sdk/src/types/block/output/delegation.rs @@ -324,7 +324,7 @@ impl DelegationOutput { // Transition, just without full SemanticValidationContext. pub(crate) fn transition_inner(current_state: &Self, next_state: &Self) -> Result<(), StateTransitionError> { #[allow(clippy::nonminimal_bool)] - if !(current_state.delegation_id.is_null() && !next_state.delegation_id().is_null()) { + if !(current_state.delegation_id.is_null() && !next_state.delegation_id.is_null()) { return Err(StateTransitionError::NonDelayedClaimingTransition); } @@ -334,7 +334,7 @@ impl DelegationOutput { { return Err(StateTransitionError::MutatedImmutableField); } - // TODO add end_epoch validation rules + Ok(()) } } diff --git a/sdk/src/types/block/protocol/mod.rs b/sdk/src/types/block/protocol/mod.rs index d2ee4ca4b5..be12bde2f3 100644 --- a/sdk/src/types/block/protocol/mod.rs +++ b/sdk/src/types/block/protocol/mod.rs @@ -194,12 +194,16 @@ impl ProtocolParameters { /// Gets the first [`SlotIndex`] of a given [`EpochIndex`]. pub fn first_slot_of(&self, epoch_index: impl Into) -> SlotIndex { - epoch_index.into().first_slot_index(self.slots_per_epoch_exponent()) + epoch_index + .into() + .first_slot_index(self.genesis_slot, self.slots_per_epoch_exponent()) } /// Gets the last [`SlotIndex`] of a given [`EpochIndex`]. pub fn last_slot_of(&self, epoch_index: impl Into) -> SlotIndex { - epoch_index.into().last_slot_index(self.slots_per_epoch_exponent()) + epoch_index + .into() + .last_slot_index(self.genesis_slot, self.slots_per_epoch_exponent()) } /// Calculates the number of slots before the next epoch. @@ -226,7 +230,7 @@ impl ProtocolParameters { /// Gets the [`EpochIndex`] of a given [`SlotIndex`]. pub fn epoch_index_of(&self, slot_index: impl Into) -> EpochIndex { - EpochIndex::from_slot_index(slot_index.into(), self.slots_per_epoch_exponent()) + EpochIndex::from_slot_index(slot_index, self.genesis_slot, self.slots_per_epoch_exponent()) } /// Calculates the duration of an epoch in seconds. diff --git a/sdk/src/types/block/semantic/state_transition.rs b/sdk/src/types/block/semantic/state_transition.rs index 8ab3364e03..2d61a5d3e4 100644 --- a/sdk/src/types/block/semantic/state_transition.rs +++ b/sdk/src/types/block/semantic/state_transition.rs @@ -2,6 +2,7 @@ // SPDX-License-Identifier: Apache-2.0 use crate::types::block::{ + context_input::CommitmentContextInput, output::{ AccountOutput, AnchorOutput, BasicOutput, ChainId, DelegationOutput, FoundryOutput, NftOutput, Output, TokenScheme, @@ -25,6 +26,7 @@ pub enum StateTransitionError { InvalidBlockIssuerTransition, IssuerNotUnlocked, MissingAccountForFoundry, + MissingCommitmentContextInput, MutatedFieldWithoutRights, MutatedImmutableField, NonDelayedClaimingTransition, @@ -327,7 +329,9 @@ impl StateTransitionVerifier for NftOutput { } impl StateTransitionVerifier for DelegationOutput { - fn creation(next_state: &Self, _context: &SemanticValidationContext<'_>) -> Result<(), StateTransitionError> { + fn creation(next_state: &Self, context: &SemanticValidationContext<'_>) -> Result<(), StateTransitionError> { + let protocol_parameters = &context.protocol_parameters; + if !next_state.delegation_id().is_null() { return Err(StateTransitionError::NonZeroCreatedId); } @@ -340,15 +344,82 @@ impl StateTransitionVerifier for DelegationOutput { return Err(StateTransitionError::NonZeroDelegationEndEpoch); } + let slot_commitment_id = context + .transaction + .context_inputs() + .iter() + .find(|i| i.kind() == CommitmentContextInput::KIND) + .map(|s| s.as_commitment().slot_commitment_id()) + .ok_or(StateTransitionError::MissingCommitmentContextInput)?; + + let past_bounded_slot_index = slot_commitment_id.past_bounded_slot(protocol_parameters.max_committable_age); + let past_bounded_epoch_index = past_bounded_slot_index.to_epoch_index( + protocol_parameters.genesis_slot, + protocol_parameters.slots_per_epoch_exponent, + ); + + let registration_slot = (past_bounded_epoch_index + 1).registration_slot( + protocol_parameters.genesis_slot, + protocol_parameters.slots_per_epoch_exponent, + protocol_parameters.epoch_nearing_threshold, + ); + + let expected_start_epoch = if past_bounded_slot_index <= registration_slot { + past_bounded_epoch_index + 1 + } else { + past_bounded_epoch_index + 2 + }; + + if next_state.start_epoch() != expected_start_epoch { + // TODO: specific tx failure reason https://github.com/iotaledger/iota-core/issues/679 + return Err(StateTransitionError::TransactionFailure( + TransactionFailureReason::SemanticValidationFailed, + )); + } + Ok(()) } fn transition( current_state: &Self, next_state: &Self, - _context: &SemanticValidationContext<'_>, + context: &SemanticValidationContext<'_>, ) -> Result<(), StateTransitionError> { - Self::transition_inner(current_state, next_state) + Self::transition_inner(current_state, next_state)?; + + let protocol_parameters = &context.protocol_parameters; + + let slot_commitment_id = context + .transaction + .context_inputs() + .iter() + .find(|i| i.kind() == CommitmentContextInput::KIND) + .map(|s| s.as_commitment().slot_commitment_id()) + .ok_or(StateTransitionError::MissingCommitmentContextInput)?; + + let future_bounded_slot_index = slot_commitment_id.future_bounded_slot(protocol_parameters.min_committable_age); + let future_bounded_epoch_index = future_bounded_slot_index.to_epoch_index( + protocol_parameters.genesis_slot, + protocol_parameters.slots_per_epoch_exponent, + ); + + let registration_slot = (future_bounded_epoch_index + 1).registration_slot( + protocol_parameters.genesis_slot, + protocol_parameters.slots_per_epoch_exponent, + protocol_parameters.epoch_nearing_threshold, + ); + + let expected_end_epoch = if future_bounded_slot_index <= registration_slot { + future_bounded_epoch_index + } else { + future_bounded_epoch_index + 1 + }; + + if next_state.end_epoch() != expected_end_epoch { + return Err(StateTransitionError::NonDelayedClaimingTransition); + } + + Ok(()) } fn destruction( diff --git a/sdk/src/types/block/slot/commitment_id.rs b/sdk/src/types/block/slot/commitment_id.rs index 0f3fc071a5..84c24c1c47 100644 --- a/sdk/src/types/block/slot/commitment_id.rs +++ b/sdk/src/types/block/slot/commitment_id.rs @@ -1,6 +1,8 @@ // Copyright 2023 IOTA Stiftung // SPDX-License-Identifier: Apache-2.0 +use super::SlotIndex; + crate::impl_id!( /// The hash of a [`SlotCommitment`](crate::types::block::slot::SlotCommitment). pub SlotCommitmentHash { @@ -9,3 +11,22 @@ crate::impl_id!( /// A [`SlotCommitment`](crate::types::block::slot::SlotCommitment) identifier. pub SlotCommitmentId; ); + +impl SlotCommitmentId { + /// Calculates the past bounded slot for the given slot of the SlotCommitment. + /// Given any slot index of a commitment input, the result of this function is a slot index + /// that is at least equal to the slot of the block in which it was issued, or higher. + /// That means no commitment input can be chosen such that the index lies behind the slot index of the block, + /// hence the past is bounded. + pub fn past_bounded_slot(self, max_committable_age: u32) -> SlotIndex { + self.slot_index() + max_committable_age + } + /// Calculates the future bounded slot for the given slot of the SlotCommitment. + /// Given any slot index of a commitment input, the result of this function is a slot index + /// that is at most equal to the slot of the block in which it was issued, or lower. + /// That means no commitment input can be chosen such that the index lies ahead of the slot index of the block, + /// hence the future is bounded. + pub fn future_bounded_slot(self, min_committable_age: u32) -> SlotIndex { + self.slot_index() + min_committable_age + } +} diff --git a/sdk/src/types/block/slot/epoch.rs b/sdk/src/types/block/slot/epoch.rs index a26ccd49ce..14a06a2f4b 100644 --- a/sdk/src/types/block/slot/epoch.rs +++ b/sdk/src/types/block/slot/epoch.rs @@ -56,23 +56,48 @@ pub struct EpochIndex(pub u32); impl EpochIndex { /// Gets the range of slots this epoch contains. - pub fn slot_index_range(&self, slots_per_epoch_exponent: u8) -> core::ops::RangeInclusive { - self.first_slot_index(slots_per_epoch_exponent)..=self.last_slot_index(slots_per_epoch_exponent) + pub fn slot_index_range( + &self, + genesis_slot: impl Into + Copy, + slots_per_epoch_exponent: u8, + ) -> core::ops::RangeInclusive { + self.first_slot_index(genesis_slot, slots_per_epoch_exponent) + ..=self.last_slot_index(genesis_slot, slots_per_epoch_exponent) } /// Gets the epoch index given a [`SlotIndex`]. - pub fn from_slot_index(slot_index: SlotIndex, slots_per_epoch_exponent: u8) -> Self { - Self(*slot_index >> slots_per_epoch_exponent) + pub fn from_slot_index( + slot_index: impl Into, + genesis_slot: impl Into, + slots_per_epoch_exponent: u8, + ) -> Self { + let genesis_slot = genesis_slot.into(); + let slot_index = slot_index.into(); + if slot_index <= genesis_slot { + return Self(0); + } + Self(*(slot_index - genesis_slot) >> slots_per_epoch_exponent) } /// Gets the first [`SlotIndex`] of this epoch. - pub fn first_slot_index(self, slots_per_epoch_exponent: u8) -> SlotIndex { - SlotIndex::from_epoch_index(self, slots_per_epoch_exponent) + pub fn first_slot_index(self, genesis_slot: impl Into, slots_per_epoch_exponent: u8) -> SlotIndex { + SlotIndex::from_epoch_index(self, genesis_slot, slots_per_epoch_exponent) } /// Gets the last [`SlotIndex`] of this epoch. - pub fn last_slot_index(self, slots_per_epoch_exponent: u8) -> SlotIndex { - SlotIndex::from_epoch_index(self + 1, slots_per_epoch_exponent) - 1 + pub fn last_slot_index(self, genesis_slot: impl Into, slots_per_epoch_exponent: u8) -> SlotIndex { + SlotIndex::from_epoch_index(self + 1, genesis_slot, slots_per_epoch_exponent) - 1 + } + + /// Returns the slot at the end of which the validator and delegator registration ends and the voting power + /// for the epoch is calculated. + pub fn registration_slot( + &self, + genesis_slot: impl Into, + slots_per_epoch_exponent: u8, + epoch_nearing_threshold: u32, + ) -> SlotIndex { + self.first_slot_index(genesis_slot, slots_per_epoch_exponent) - epoch_nearing_threshold - 1 } } @@ -131,18 +156,20 @@ mod test { ..Default::default() }; let slot_index = SlotIndex(3000); - let epoch_index = EpochIndex::from_slot_index(slot_index, params.slots_per_epoch_exponent()); + let epoch_index = + EpochIndex::from_slot_index(slot_index, params.genesis_slot, params.slots_per_epoch_exponent()); assert_eq!(epoch_index, EpochIndex(2)); assert_eq!( - epoch_index.slot_index_range(params.slots_per_epoch_exponent()), + epoch_index.slot_index_range(params.genesis_slot, params.slots_per_epoch_exponent()), SlotIndex(2048)..=SlotIndex(3071) ); let slot_index = SlotIndex(10 * params.slots_per_epoch() + 2000); - let epoch_index = EpochIndex::from_slot_index(slot_index, params.slots_per_epoch_exponent()); + let epoch_index = + EpochIndex::from_slot_index(slot_index, params.genesis_slot, params.slots_per_epoch_exponent()); assert_eq!(epoch_index, EpochIndex(11)); assert_eq!( - epoch_index.slot_index_range(params.slots_per_epoch_exponent()), + epoch_index.slot_index_range(params.genesis_slot, params.slots_per_epoch_exponent()), SlotIndex(11 * params.slots_per_epoch())..=SlotIndex(12 * params.slots_per_epoch() - 1) ); } diff --git a/sdk/src/types/block/slot/index.rs b/sdk/src/types/block/slot/index.rs index 23ea5b1f02..5a7b4d0f14 100644 --- a/sdk/src/types/block/slot/index.rs +++ b/sdk/src/types/block/slot/index.rs @@ -46,12 +46,17 @@ pub struct SlotIndex(pub u32); impl SlotIndex { /// Gets the [`EpochIndex`] of this slot. - pub fn to_epoch_index(self, slots_per_epoch_exponent: u8) -> EpochIndex { - EpochIndex::from_slot_index(self, slots_per_epoch_exponent) + pub fn to_epoch_index(self, genesis_slot: impl Into, slots_per_epoch_exponent: u8) -> EpochIndex { + EpochIndex::from_slot_index(self, genesis_slot, slots_per_epoch_exponent) } - pub fn from_epoch_index(epoch_index: EpochIndex, slots_per_epoch_exponent: u8) -> Self { - Self(*epoch_index << slots_per_epoch_exponent) + /// Gets the first [`SlotIndex`] of the provided epoch. + pub fn from_epoch_index( + epoch_index: EpochIndex, + genesis_slot: impl Into, + slots_per_epoch_exponent: u8, + ) -> Self { + genesis_slot.into() + Self(*epoch_index << slots_per_epoch_exponent) } /// Gets the slot index of a unix timestamp in seconds.