diff --git a/api/core.go b/api/core.go index b5f3ada5d..b8d5946e1 100644 --- a/api/core.go +++ b/api/core.go @@ -218,59 +218,58 @@ const ( TxFailureIssuerFeatureNotUnlocked TransactionFailureReason = 27 TxFailureStakingRewardInputMissing TransactionFailureReason = 28 - TxFailureStakingBlockIssuerFeatureMissing TransactionFailureReason = 29 - TxFailureStakingCommitmentInputMissing TransactionFailureReason = 30 - TxFailureStakingRewardClaimingInvalid TransactionFailureReason = 31 - TxFailureStakingFeatureRemovedBeforeUnbonding TransactionFailureReason = 32 - TxFailureStakingFeatureModifiedBeforeUnbonding TransactionFailureReason = 33 - TxFailureStakingStartEpochInvalid TransactionFailureReason = 34 - TxFailureStakingEndEpochTooEarly TransactionFailureReason = 35 - - TxFailureBlockIssuerCommitmentInputMissing TransactionFailureReason = 36 - TxFailureBlockIssuanceCreditInputMissing TransactionFailureReason = 37 - TxFailureBlockIssuerNotExpired TransactionFailureReason = 38 - TxFailureBlockIssuerExpiryTooEarly TransactionFailureReason = 39 - TxFailureManaMovedOffBlockIssuerAccount TransactionFailureReason = 40 - TxFailureAccountLocked TransactionFailureReason = 41 - - TxFailureTimelockCommitmentInputMissing TransactionFailureReason = 42 - TxFailureTimelockNotExpired TransactionFailureReason = 43 - - TxFailureExpirationCommitmentInputMissing TransactionFailureReason = 44 - TxFailureExpirationNotUnlockable TransactionFailureReason = 45 - - TxFailureReturnAmountNotFulFilled TransactionFailureReason = 46 - - TxFailureNewChainOutputHasNonZeroedID TransactionFailureReason = 47 - TxFailureChainOutputImmutableFeaturesChanged TransactionFailureReason = 48 - - TxFailureImplicitAccountDestructionDisallowed TransactionFailureReason = 49 - TxFailureMultipleImplicitAccountCreationAddresses TransactionFailureReason = 50 - - TxFailureAccountInvalidFoundryCounter TransactionFailureReason = 51 - - TxFailureAnchorInvalidStateTransition TransactionFailureReason = 52 - TxFailureAnchorInvalidGovernanceTransition TransactionFailureReason = 53 - - TxFailureFoundryTransitionWithoutAccount TransactionFailureReason = 54 - TxFailureFoundrySerialInvalid TransactionFailureReason = 55 - - TxFailureDelegationCommitmentInputMissing TransactionFailureReason = 56 - TxFailureDelegationRewardInputMissing TransactionFailureReason = 57 - TxFailureDelegationRewardsClaimingInvalid TransactionFailureReason = 58 - TxFailureDelegationOutputTransitionedTwice TransactionFailureReason = 59 - TxFailureDelegationModified TransactionFailureReason = 60 - TxFailureDelegationStartEpochInvalid TransactionFailureReason = 61 - TxFailureDelegationAmountMismatch TransactionFailureReason = 62 - TxFailureDelegationEndEpochNotZero TransactionFailureReason = 63 - TxFailureDelegationEndEpochInvalid TransactionFailureReason = 64 - - TxFailureCapabilitiesNativeTokenBurningNotAllowed TransactionFailureReason = 65 - TxFailureCapabilitiesManaBurningNotAllowed TransactionFailureReason = 66 - TxFailureCapabilitiesAccountDestructionNotAllowed TransactionFailureReason = 67 - TxFailureCapabilitiesAnchorDestructionNotAllowed TransactionFailureReason = 68 - TxFailureCapabilitiesFoundryDestructionNotAllowed TransactionFailureReason = 69 - TxFailureCapabilitiesNFTDestructionNotAllowed TransactionFailureReason = 70 + TxFailureStakingCommitmentInputMissing TransactionFailureReason = 29 + TxFailureStakingRewardClaimingInvalid TransactionFailureReason = 30 + TxFailureStakingFeatureRemovedBeforeUnbonding TransactionFailureReason = 31 + TxFailureStakingFeatureModifiedBeforeUnbonding TransactionFailureReason = 32 + TxFailureStakingStartEpochInvalid TransactionFailureReason = 33 + TxFailureStakingEndEpochTooEarly TransactionFailureReason = 34 + + TxFailureBlockIssuerCommitmentInputMissing TransactionFailureReason = 35 + TxFailureBlockIssuanceCreditInputMissing TransactionFailureReason = 36 + TxFailureBlockIssuerNotExpired TransactionFailureReason = 37 + TxFailureBlockIssuerExpiryTooEarly TransactionFailureReason = 38 + TxFailureManaMovedOffBlockIssuerAccount TransactionFailureReason = 39 + TxFailureAccountLocked TransactionFailureReason = 40 + + TxFailureTimelockCommitmentInputMissing TransactionFailureReason = 41 + TxFailureTimelockNotExpired TransactionFailureReason = 42 + + TxFailureExpirationCommitmentInputMissing TransactionFailureReason = 43 + TxFailureExpirationNotUnlockable TransactionFailureReason = 44 + + TxFailureReturnAmountNotFulFilled TransactionFailureReason = 45 + + TxFailureNewChainOutputHasNonZeroedID TransactionFailureReason = 46 + TxFailureChainOutputImmutableFeaturesChanged TransactionFailureReason = 47 + + TxFailureImplicitAccountDestructionDisallowed TransactionFailureReason = 48 + TxFailureMultipleImplicitAccountCreationAddresses TransactionFailureReason = 49 + + TxFailureAccountInvalidFoundryCounter TransactionFailureReason = 50 + + TxFailureAnchorInvalidStateTransition TransactionFailureReason = 51 + TxFailureAnchorInvalidGovernanceTransition TransactionFailureReason = 52 + + TxFailureFoundryTransitionWithoutAccount TransactionFailureReason = 53 + TxFailureFoundrySerialInvalid TransactionFailureReason = 54 + + TxFailureDelegationCommitmentInputMissing TransactionFailureReason = 55 + TxFailureDelegationRewardInputMissing TransactionFailureReason = 56 + TxFailureDelegationRewardsClaimingInvalid TransactionFailureReason = 57 + TxFailureDelegationOutputTransitionedTwice TransactionFailureReason = 58 + TxFailureDelegationModified TransactionFailureReason = 59 + TxFailureDelegationStartEpochInvalid TransactionFailureReason = 60 + TxFailureDelegationAmountMismatch TransactionFailureReason = 61 + TxFailureDelegationEndEpochNotZero TransactionFailureReason = 62 + TxFailureDelegationEndEpochInvalid TransactionFailureReason = 63 + + TxFailureCapabilitiesNativeTokenBurningNotAllowed TransactionFailureReason = 64 + TxFailureCapabilitiesManaBurningNotAllowed TransactionFailureReason = 65 + TxFailureCapabilitiesAccountDestructionNotAllowed TransactionFailureReason = 66 + TxFailureCapabilitiesAnchorDestructionNotAllowed TransactionFailureReason = 67 + TxFailureCapabilitiesFoundryDestructionNotAllowed TransactionFailureReason = 68 + TxFailureCapabilitiesNFTDestructionNotAllowed TransactionFailureReason = 69 TxFailureSemanticValidationFailed TransactionFailureReason = 255 ) @@ -337,7 +336,6 @@ var txErrorsFailureReasonMap = map[error]TransactionFailureReason{ // staking feature iotago.ErrStakingRewardInputMissing: TxFailureStakingRewardInputMissing, - iotago.ErrStakingBlockIssuerFeatureMissing: TxFailureStakingBlockIssuerFeatureMissing, iotago.ErrStakingCommitmentInputMissing: TxFailureStakingCommitmentInputMissing, iotago.ErrStakingRewardClaimingInvalid: TxFailureStakingRewardClaimingInvalid, iotago.ErrStakingFeatureRemovedBeforeUnbonding: TxFailureStakingFeatureRemovedBeforeUnbonding, diff --git a/api/core_test.go b/api/core_test.go index 8323e6a52..f62a3d1e1 100644 --- a/api/core_test.go +++ b/api/core_test.go @@ -550,7 +550,7 @@ func Test_CoreAPIJSONSerialization(t *testing.T) { "transactionId": "0x010000000000000000000000000000000000000000000000000000000000000000000000", "transactionState": "failed", "earliestAttachmentSlot": 5, - "transactionFailureReason": 58, + "transactionFailureReason": 57, "transactionFailureDetails": "details" }`, }, diff --git a/output.go b/output.go index bcfa016e6..533fc531e 100644 --- a/output.go +++ b/output.go @@ -476,10 +476,15 @@ func OutputsSyntacticalAccount() ElementValidationFunc[Output] { return ierrors.WithMessagef(ErrAccountOutputCyclicAddress, "output %d", index) } - if stakingFeat := accountOutput.FeatureSet().Staking(); stakingFeat != nil { + accountFeatures := accountOutput.FeatureSet() + if stakingFeat := accountFeatures.Staking(); stakingFeat != nil { if accountOutput.Amount < stakingFeat.StakedAmount { return ierrors.WithMessagef(ErrAccountOutputAmountLessThanStakedAmount, "output %d", index) } + + if accountFeatures.BlockIssuer() == nil { + return ierrors.WithMessagef(ErrStakingBlockIssuerFeatureMissing, "output %d", index) + } } return nil diff --git a/output_test.go b/output_test.go index 5d357c5fc..43138c538 100644 --- a/output_test.go +++ b/output_test.go @@ -491,6 +491,11 @@ func TestOutputsSyntacticalNativeTokensCount(t *testing.T) { } func TestOutputsSyntacticalAccount(t *testing.T) { + exampleBlockIssuerFeature := &iotago.BlockIssuerFeature{ + ExpirySlot: 3, + BlockIssuerKeys: tpkg.RandBlockIssuerKeys(2), + } + tests := []struct { name string outputs iotago.Outputs[iotago.Output] @@ -567,6 +572,7 @@ func TestOutputsSyntacticalAccount(t *testing.T) { &iotago.AddressUnlockCondition{Address: tpkg.RandAccountAddress()}, }, Features: iotago.AccountOutputFeatures{ + exampleBlockIssuerFeature, &iotago.StakingFeature{StakedAmount: OneIOTA}, }, }, @@ -584,6 +590,7 @@ func TestOutputsSyntacticalAccount(t *testing.T) { &iotago.AddressUnlockCondition{Address: tpkg.RandAccountAddress()}, }, Features: iotago.AccountOutputFeatures{ + exampleBlockIssuerFeature, &iotago.StakingFeature{StakedAmount: OneIOTA}, }, }, @@ -601,25 +608,59 @@ func TestOutputsSyntacticalAccount(t *testing.T) { &iotago.AddressUnlockCondition{Address: tpkg.RandAccountAddress()}, }, Features: iotago.AccountOutputFeatures{ + exampleBlockIssuerFeature, &iotago.StakingFeature{StakedAmount: OneIOTA + 1}, }, }, }, wantErr: iotago.ErrAccountOutputAmountLessThanStakedAmount, }, + { + name: "ok - staking feature present with block issuer feature", + outputs: iotago.Outputs[iotago.Output]{ + &iotago.AccountOutput{ + Amount: OneIOTA, + AccountID: tpkg.Rand32ByteArray(), + FoundryCounter: 1337, + UnlockConditions: iotago.AccountOutputUnlockConditions{ + &iotago.AddressUnlockCondition{Address: tpkg.RandAccountAddress()}, + }, + Features: iotago.AccountOutputFeatures{ + exampleBlockIssuerFeature, + &iotago.StakingFeature{StakedAmount: OneIOTA}, + }, + }, + }, + wantErr: nil, + }, + { + name: "fail - staking feature present without block issuer feature", + outputs: iotago.Outputs[iotago.Output]{ + &iotago.AccountOutput{ + Amount: OneIOTA, + AccountID: tpkg.Rand32ByteArray(), + FoundryCounter: 1337, + UnlockConditions: iotago.AccountOutputUnlockConditions{ + &iotago.AddressUnlockCondition{Address: tpkg.RandAccountAddress()}, + }, + Features: iotago.AccountOutputFeatures{ + &iotago.StakingFeature{StakedAmount: OneIOTA}, + }, + }, + }, + wantErr: iotago.ErrStakingBlockIssuerFeatureMissing, + }, } valFunc := iotago.OutputsSyntacticalAccount() for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - t.Run(tt.name, func(t *testing.T) { - var runErr error - for index, output := range tt.outputs { - if err := valFunc(index, output); err != nil { - runErr = err - } + var runErr error + for index, output := range tt.outputs { + if err := valFunc(index, output); err != nil { + runErr = err } - require.ErrorIs(t, runErr, tt.wantErr) - }) + } + require.ErrorIs(t, runErr, tt.wantErr) }) } } diff --git a/vm/nova/stvf_test.go b/vm/nova/stvf_test.go index f16b4ac34..3ea647230 100644 --- a/vm/nova/stvf_test.go +++ b/vm/nova/stvf_test.go @@ -350,46 +350,6 @@ func TestAccountOutput_ValidateStateTransition(t *testing.T) { }, wantErr: iotago.ErrStakingEndEpochTooEarly, }, - { - name: "fail - staking feature without block issuer feature", - next: &iotago.AccountOutput{ - Amount: 100, - AccountID: iotago.AccountID{}, - UnlockConditions: iotago.AccountOutputUnlockConditions{ - &iotago.AddressUnlockCondition{Address: tpkg.RandEd25519Address()}, - }, - Features: iotago.AccountOutputFeatures{ - &iotago.StakingFeature{ - StakedAmount: 50, - FixedCost: 5, - StartEpoch: currentEpoch, - EndEpoch: iotago.MaxEpochIndex, - }, - }, - }, - input: nil, - transType: iotago.ChainTransitionTypeGenesis, - svCtx: &vm.Params{ - API: tpkg.ZeroCostTestAPI, - WorkingSet: &vm.WorkingSet{ - Commitment: &iotago.Commitment{ - Slot: currentSlot, - }, - UnlockedAddrs: vm.UnlockedAddresses{ - exampleIssuer.Key(): {UnlockedAtInputIndex: 0}, - }, - Tx: &iotago.Transaction{ - API: tpkg.ZeroCostTestAPI, - TransactionEssence: &iotago.TransactionEssence{ - CreationSlot: currentSlot, - Capabilities: iotago.TransactionCapabilitiesBitMaskWithCapabilities(iotago.WithTransactionCanDoAnything()), - }, - }, - BIC: exampleBIC, - }, - }, - wantErr: iotago.ErrStakingBlockIssuerFeatureMissing, - }, { name: "ok - valid staking transition", input: &vm.ChainOutputWithIDs{ @@ -776,68 +736,6 @@ func TestAccountOutput_ValidateStateTransition(t *testing.T) { }, wantErr: iotago.ErrStakingEndEpochTooEarly, }, - { - name: "fail - account removes block issuer feature while having a staking feature", - input: &vm.ChainOutputWithIDs{ - OutputID: tpkg.RandOutputIDWithCreationSlot(1000, 0), - ChainID: exampleAccountID, - Output: &iotago.AccountOutput{ - Amount: 100, - AccountID: exampleAccountID, - UnlockConditions: iotago.AccountOutputUnlockConditions{ - &iotago.AddressUnlockCondition{Address: exampleAddress}, - }, - Features: iotago.AccountOutputFeatures{ - &iotago.StakingFeature{ - StakedAmount: 50, - FixedCost: 5, - StartEpoch: currentEpoch, - EndEpoch: iotago.MaxEpochIndex, - }, - &iotago.BlockIssuerFeature{ - BlockIssuerKeys: tpkg.RandBlockIssuerKeys(1), - ExpirySlot: 990, - }, - }, - }, - }, - next: &iotago.AccountOutput{ - Amount: 100, - AccountID: exampleAccountID, - UnlockConditions: iotago.AccountOutputUnlockConditions{ - &iotago.AddressUnlockCondition{Address: exampleAddress}, - }, - Features: iotago.AccountOutputFeatures{ - &iotago.StakingFeature{ - StakedAmount: 50, - FixedCost: 5, - StartEpoch: currentEpoch, - EndEpoch: iotago.MaxEpochIndex, - }, - }, - }, - transType: iotago.ChainTransitionTypeStateChange, - svCtx: &vm.Params{ - API: tpkg.ZeroCostTestAPI, - WorkingSet: &vm.WorkingSet{ - Commitment: &iotago.Commitment{ - Slot: currentSlot, - }, - UnlockedAddrs: vm.UnlockedAddresses{ - exampleIssuer.Key(): {UnlockedAtInputIndex: 0}, - }, - Tx: &iotago.Transaction{ - API: tpkg.ZeroCostTestAPI, - TransactionEssence: &iotago.TransactionEssence{ - CreationSlot: currentSlot, - Capabilities: iotago.TransactionCapabilitiesBitMaskWithCapabilities(iotago.WithTransactionCanDoAnything()), - }, - }, - BIC: exampleBIC, - }, - }, - wantErr: iotago.ErrStakingBlockIssuerFeatureMissing, - }, { name: "fail - expired staking feature removed without specifying reward input", input: &vm.ChainOutputWithIDs{ diff --git a/vm/nova/vm.go b/vm/nova/vm.go index 4352a0d75..1b0e75c32 100644 --- a/vm/nova/vm.go +++ b/vm/nova/vm.go @@ -305,7 +305,7 @@ func accountGenesisValid(vmParams *vm.Params, next *iotago.AccountOutput, accoun } if stakingFeat := next.FeatureSet().Staking(); stakingFeat != nil { - if err := accountStakingGenesisValidation(vmParams, next, stakingFeat); err != nil { + if err := accountStakingGenesisValidation(vmParams, stakingFeat); err != nil { return ierrors.Join(iotago.ErrInvalidStakingTransition, err) } } @@ -466,16 +466,15 @@ func accountStakingSTVF(vmParams *vm.Params, current *iotago.AccountOutput, next if futureBoundedEpoch <= currentStakingFeat.EndEpoch { earliestUnbondingEpoch := pastBoundedEpoch + vmParams.API.ProtocolParameters().StakingUnbondingPeriod() - nextHasBlockIssuerFeat := next.FeatureSet().BlockIssuer() != nil return accountStakingNonExpiredValidation( - currentStakingFeat, nextStakingFeat, earliestUnbondingEpoch, nextHasBlockIssuerFeat, + currentStakingFeat, nextStakingFeat, earliestUnbondingEpoch, ) } - return accountStakingExpiredValidation(vmParams, next, currentStakingFeat, nextStakingFeat, isRemovingStakingFeature) + return accountStakingExpiredValidation(vmParams, currentStakingFeat, nextStakingFeat, isRemovingStakingFeature) } else if nextStakingFeat != nil { - return accountStakingGenesisValidation(vmParams, next, nextStakingFeat) + return accountStakingGenesisValidation(vmParams, nextStakingFeat) } return nil @@ -484,7 +483,7 @@ func accountStakingSTVF(vmParams *vm.Params, current *iotago.AccountOutput, next // Validates the rules for a newly added Staking Feature in an account, // or one which was effectively removed and added within the same transaction. // This is allowed as long as the epoch range of the old and new feature are disjoint. -func accountStakingGenesisValidation(vmParams *vm.Params, next *iotago.AccountOutput, stakingFeat *iotago.StakingFeature) error { +func accountStakingGenesisValidation(vmParams *vm.Params, stakingFeat *iotago.StakingFeature) error { commitment := vmParams.WorkingSet.Commitment if commitment == nil { panic("commitment input should be present for staking features on the output side which should be validated syntactically") @@ -503,10 +502,6 @@ func accountStakingGenesisValidation(vmParams *vm.Params, next *iotago.AccountOu return ierrors.Wrapf(iotago.ErrStakingEndEpochTooEarly, "(i.e. end epoch %d should be >= %d)", stakingFeat.EndEpoch, unbondingEpoch) } - if next.FeatureSet().BlockIssuer() == nil { - return iotago.ErrStakingBlockIssuerFeatureMissing - } - return nil } @@ -516,16 +511,11 @@ func accountStakingNonExpiredValidation( currentStakingFeat *iotago.StakingFeature, nextStakingFeat *iotago.StakingFeature, earliestUnbondingEpoch iotago.EpochIndex, - nextHasBlockIssuerFeat bool, ) error { if nextStakingFeat == nil { return ierrors.Wrapf(iotago.ErrInvalidStakingTransition, "%w", iotago.ErrStakingFeatureRemovedBeforeUnbonding) } - if !nextHasBlockIssuerFeat { - return ierrors.Wrapf(iotago.ErrInvalidStakingTransition, "%w", iotago.ErrStakingBlockIssuerFeatureMissing) - } - if currentStakingFeat.StakedAmount != nextStakingFeat.StakedAmount || currentStakingFeat.FixedCost != nextStakingFeat.FixedCost || currentStakingFeat.StartEpoch != nextStakingFeat.StartEpoch { @@ -544,7 +534,6 @@ func accountStakingNonExpiredValidation( // i.e. the current epoch is equal or after the end epoch. func accountStakingExpiredValidation( vmParams *vm.Params, - next *iotago.AccountOutput, currentStakingFeat *iotago.StakingFeature, nextStakingFeat *iotago.StakingFeature, isRemovingStakingFeature *bool, @@ -553,7 +542,7 @@ func accountStakingExpiredValidation( *isRemovingStakingFeature = true } else if !currentStakingFeat.Equal(nextStakingFeat) { // If an expired feature is changed it must be transitioned as if newly added. - if err := accountStakingGenesisValidation(vmParams, next, nextStakingFeat); err != nil { + if err := accountStakingGenesisValidation(vmParams, nextStakingFeat); err != nil { return ierrors.Wrapf(iotago.ErrInvalidStakingTransition, "%w: rewards claiming without removing the feature requires updating the feature", err) } // If staking feature genesis validation succeeds, the start epoch has been reset which means the new epoch range