diff --git a/feat_test.go b/feat_test.go index 704abfc5d..4a192c30e 100644 --- a/feat_test.go +++ b/feat_test.go @@ -428,6 +428,7 @@ func TestBlockIssuerFeatureSyntacticValidation(t *testing.T) { bik3, }), } + t.TransactionEssence.ContextInputs = append(t.TransactionEssence.ContextInputs, tpkg.RandCommitmentInput()) }, ), Target: &iotago.SignedTransaction{}, @@ -442,6 +443,7 @@ func TestBlockIssuerFeatureSyntacticValidation(t *testing.T) { bik3, }), } + t.TransactionEssence.ContextInputs = append(t.TransactionEssence.ContextInputs, tpkg.RandCommitmentInput()) }), Target: &iotago.SignedTransaction{}, SeriErr: iotago.ErrArrayValidationOrderViolatesLexicalOrder, @@ -458,6 +460,7 @@ func TestBlockIssuerFeatureSyntacticValidation(t *testing.T) { bik2, }), } + t.TransactionEssence.ContextInputs = append(t.TransactionEssence.ContextInputs, tpkg.RandCommitmentInput()) }), Target: &iotago.SignedTransaction{}, SeriErr: iotago.ErrArrayValidationViolatesUniqueness, @@ -469,6 +472,7 @@ func TestBlockIssuerFeatureSyntacticValidation(t *testing.T) { t.Outputs = iotago.TxEssenceOutputs{ accountWithKeys(iotago.BlockIssuerKeys{}), } + t.TransactionEssence.ContextInputs = append(t.TransactionEssence.ContextInputs, tpkg.RandCommitmentInput()) }), Target: &iotago.SignedTransaction{}, SeriErr: serializer.ErrArrayValidationMinElementsNotReached, @@ -480,6 +484,7 @@ func TestBlockIssuerFeatureSyntacticValidation(t *testing.T) { t.Outputs = iotago.TxEssenceOutputs{ accountWithKeys(tpkg.RandBlockIssuerKeys(iotago.MaxBlockIssuerKeysCount + 1)), } + t.TransactionEssence.ContextInputs = append(t.TransactionEssence.ContextInputs, tpkg.RandCommitmentInput()) }), Target: &iotago.SignedTransaction{}, SeriErr: serializer.ErrArrayValidationMaxElementsExceeded, diff --git a/output.go b/output.go index 4bc34b9ea..bcfa016e6 100644 --- a/output.go +++ b/output.go @@ -886,3 +886,27 @@ func OutputsSyntacticalMetadataFeatureMaxSize() ElementValidationFunc[Output] { return nil } } + +// Checks that a Commitment Input is present for +// - Accounts with a Staking Feature. +// - Accounts with a Block Issuer Feature. +// - Delegation Outputs. +func OutputsSyntacticalCommitmentInput(hasCommitmentInput bool) ElementValidationFunc[Output] { + return func(index int, output Output) error { + hasStakingFeature := output.FeatureSet().Staking() != nil + if hasStakingFeature && !hasCommitmentInput { + return ierrors.Wrapf(ErrStakingCommitmentInputMissing, "output %d", index) + } + + hasBlockIssuerFeature := output.FeatureSet().BlockIssuer() != nil + if hasBlockIssuerFeature && !hasCommitmentInput { + return ierrors.Wrapf(ErrBlockIssuerCommitmentInputMissing, "output %d", index) + } + + if output.Type() == OutputDelegation && !hasCommitmentInput { + return ierrors.Wrapf(ErrDelegationCommitmentInputMissing, "output %d", index) + } + + return nil + } +} diff --git a/transaction.go b/transaction.go index ee6f6e6ac..6d00253a5 100644 --- a/transaction.go +++ b/transaction.go @@ -259,6 +259,7 @@ func (t *Transaction) SyntacticallyValidate(api API) error { } var maxManaValue Mana = (1 << protoParams.ManaParameters().BitsCount) - 1 + hasCommitmentInput := t.CommitmentInput() != nil return SyntacticallyValidateOutputs(t.Outputs, OutputsSyntacticalUnlockConditionLexicalOrderAndUniqueness(), @@ -276,6 +277,7 @@ func (t *Transaction) SyntacticallyValidate(api API) error { OutputsSyntacticalDelegation(), OutputsSyntacticalAddressRestrictions(), OutputsSyntacticalImplicitAccountCreationAddress(), + OutputsSyntacticalCommitmentInput(hasCommitmentInput), ) } diff --git a/transaction_test.go b/transaction_test.go index 70e98724c..020500803 100644 --- a/transaction_test.go +++ b/transaction_test.go @@ -1260,3 +1260,99 @@ func TestTransactionIDsLexicalOrderAndUniqueness(t *testing.T) { t.Run(tt.Name, tt.Run) } } + +func TestCommitmentInputSyntacticalValidation(t *testing.T) { + accountWithFeatures := func(feats iotago.AccountOutputFeatures) *iotago.AccountOutput { + return &iotago.AccountOutput{ + Amount: 100_000_000, + UnlockConditions: iotago.AccountOutputUnlockConditions{ + &iotago.AddressUnlockCondition{ + Address: tpkg.RandAccountAddress(), + }, + }, + ImmutableFeatures: iotago.AccountOutputImmFeatures{}, + Features: feats, + } + } + + tests := []*frameworks.DeSerializeTest{ + // fail - BlockIssuerFeature on output side without Commitment Input + { + Name: "fail - BlockIssuerFeature on output side without Commitment Input", + Source: tpkg.RandSignedTransaction(tpkg.ZeroCostTestAPI, func(t *iotago.Transaction) { + t.Outputs = iotago.TxEssenceOutputs{ + accountWithFeatures( + iotago.AccountOutputFeatures{ + &iotago.BlockIssuerFeature{ + ExpirySlot: 100, + BlockIssuerKeys: tpkg.RandBlockIssuerKeys(3), + }, + }, + ), + } + // Make sure there are no Context Inputs added by the rand function for this test. + t.TransactionEssence.ContextInputs = nil + }), + Target: &iotago.SignedTransaction{}, + SeriErr: iotago.ErrBlockIssuerCommitmentInputMissing, + DeSeriErr: iotago.ErrBlockIssuerCommitmentInputMissing, + }, + // fail - StakingFeature on output side without Commitment Input + { + Name: "fail - StakingFeature on output side without Commitment Input", + Source: tpkg.RandSignedTransaction(tpkg.ZeroCostTestAPI, func(t *iotago.Transaction) { + t.Outputs = iotago.TxEssenceOutputs{ + accountWithFeatures( + iotago.AccountOutputFeatures{ + &iotago.BlockIssuerFeature{ + ExpirySlot: 100, + BlockIssuerKeys: tpkg.RandBlockIssuerKeys(3), + }, + &iotago.StakingFeature{ + StakedAmount: 1, + FixedCost: 1, + StartEpoch: 10, + EndEpoch: 12, + }, + }, + ), + } + // Make sure there are no Context Inputs added by the rand function for this test. + t.TransactionEssence.ContextInputs = nil + }), + Target: &iotago.SignedTransaction{}, + SeriErr: iotago.ErrStakingCommitmentInputMissing, + DeSeriErr: iotago.ErrStakingCommitmentInputMissing, + }, + // fail - Delegation Output on output side without Commitment Input + { + Name: "fail - Delegation Output on output side without Commitment Input", + Source: tpkg.RandSignedTransaction(tpkg.ZeroCostTestAPI, func(t *iotago.Transaction) { + t.Outputs = iotago.TxEssenceOutputs{ + &iotago.DelegationOutput{ + Amount: 10, + DelegatedAmount: 10, + DelegationID: tpkg.RandDelegationID(), + ValidatorAddress: tpkg.RandAccountAddress(), + StartEpoch: 10, + EndEpoch: 12, + UnlockConditions: iotago.DelegationOutputUnlockConditions{ + &iotago.AddressUnlockCondition{ + Address: tpkg.RandEd25519Address(), + }, + }, + }, + } + // Make sure there are no Context Inputs added by the rand function for this test. + t.TransactionEssence.ContextInputs = nil + }), + Target: &iotago.SignedTransaction{}, + SeriErr: iotago.ErrDelegationCommitmentInputMissing, + DeSeriErr: iotago.ErrDelegationCommitmentInputMissing, + }, + } + + for _, tt := range tests { + t.Run(tt.Name, tt.Run) + } +} diff --git a/vm/nova/vm.go b/vm/nova/vm.go index 7177e344f..4352a0d75 100644 --- a/vm/nova/vm.go +++ b/vm/nova/vm.go @@ -293,7 +293,7 @@ func accountGenesisValid(vmParams *vm.Params, next *iotago.AccountOutput, accoun if nextBlockIssuerFeat := next.FeatureSet().BlockIssuer(); nextBlockIssuerFeat != nil { if vmParams.WorkingSet.Commitment == nil { - return ierrors.Join(iotago.ErrInvalidBlockIssuerTransition, iotago.ErrBlockIssuerCommitmentInputMissing) + panic("commitment input should be present for block issuer features on the output side which should be validated syntactically") } pastBoundedSlot := vmParams.PastBoundedSlotIndex(vmParams.WorkingSet.Commitment.Slot) @@ -485,10 +485,9 @@ func accountStakingSTVF(vmParams *vm.Params, current *iotago.AccountOutput, next // 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 { - // It should already never be nil here, but for 100% safety, we'll check again. commitment := vmParams.WorkingSet.Commitment if commitment == nil { - return iotago.ErrStakingCommitmentInputMissing + panic("commitment input should be present for staking features on the output side which should be validated syntactically") } pastBoundedSlot := vmParams.PastBoundedSlotIndex(commitment.Slot) diff --git a/vm/nova/vm_test.go b/vm/nova/vm_test.go index 18746c16d..bde570aeb 100644 --- a/vm/nova/vm_test.go +++ b/vm/nova/vm_test.go @@ -7526,7 +7526,7 @@ func TestTxSemanticImplicitAccountCreationAndTransition(t *testing.T) { name string inputs []TestInput keys []iotago.AddressKeys - resolvedCommitmentInput iotago.Commitment + resolvedCommitmentInput *iotago.Commitment resolvedBICInputSet vm.BlockIssuanceCreditInputSet outputs []iotago.Output wantErr error @@ -7686,7 +7686,7 @@ func TestTxSemanticImplicitAccountCreationAndTransition(t *testing.T) { resolvedBICInputSet: vm.BlockIssuanceCreditInputSet{ accountID1: iotago.BlockIssuanceCredits(0), }, - resolvedCommitmentInput: iotago.Commitment{ + resolvedCommitmentInput: &iotago.Commitment{ Slot: commitmentSlot, }, outputs: []iotago.Output{ @@ -7735,7 +7735,7 @@ func TestTxSemanticImplicitAccountCreationAndTransition(t *testing.T) { resolvedBICInputSet: vm.BlockIssuanceCreditInputSet{ accountID1: iotago.BlockIssuanceCredits(0), }, - resolvedCommitmentInput: iotago.Commitment{ + resolvedCommitmentInput: &iotago.Commitment{ Slot: commitmentSlot, }, outputs: []iotago.Output{ @@ -7792,7 +7792,7 @@ func TestTxSemanticImplicitAccountCreationAndTransition(t *testing.T) { resolvedBICInputSet: vm.BlockIssuanceCreditInputSet{ accountID1: iotago.BlockIssuanceCredits(0), }, - resolvedCommitmentInput: iotago.Commitment{ + resolvedCommitmentInput: &iotago.Commitment{ Slot: commitmentSlot, }, outputs: []iotago.Output{ @@ -7829,7 +7829,7 @@ func TestTxSemanticImplicitAccountCreationAndTransition(t *testing.T) { resolvedBICInputSet: vm.BlockIssuanceCreditInputSet{ accountID1: iotago.BlockIssuanceCredits(0), }, - resolvedCommitmentInput: iotago.Commitment{ + resolvedCommitmentInput: &iotago.Commitment{ Slot: commitmentSlot, }, outputs: []iotago.Output{ @@ -7862,7 +7862,7 @@ func TestTxSemanticImplicitAccountCreationAndTransition(t *testing.T) { resolvedBICInputSet: vm.BlockIssuanceCreditInputSet{ accountID1: iotago.BlockIssuanceCredits(0), }, - resolvedCommitmentInput: iotago.Commitment{ + resolvedCommitmentInput: &iotago.Commitment{ Slot: commitmentSlot, }, outputs: []iotago.Output{ @@ -7909,7 +7909,7 @@ func TestTxSemanticImplicitAccountCreationAndTransition(t *testing.T) { resolvedBICInputSet: vm.BlockIssuanceCreditInputSet{ accountID1: iotago.BlockIssuanceCredits(0), }, - resolvedCommitmentInput: iotago.Commitment{ + resolvedCommitmentInput: &iotago.Commitment{ Slot: commitmentSlot, }, outputs: []iotago.Output{ @@ -7965,7 +7965,7 @@ func TestTxSemanticImplicitAccountCreationAndTransition(t *testing.T) { resolvedBICInputSet: vm.BlockIssuanceCreditInputSet{ accountID1: iotago.BlockIssuanceCredits(0), }, - resolvedCommitmentInput: iotago.Commitment{ + resolvedCommitmentInput: &iotago.Commitment{ Slot: commitmentSlot, }, outputs: []iotago.Output{ @@ -8022,7 +8022,7 @@ func TestTxSemanticImplicitAccountCreationAndTransition(t *testing.T) { accountID1: iotago.BlockIssuanceCredits(0), accountID2: iotago.BlockIssuanceCredits(0), }, - resolvedCommitmentInput: iotago.Commitment{ + resolvedCommitmentInput: &iotago.Commitment{ Slot: commitmentSlot, }, outputs: []iotago.Output{ @@ -8084,7 +8084,7 @@ func TestTxSemanticImplicitAccountCreationAndTransition(t *testing.T) { resolvedBICInputSet: vm.BlockIssuanceCreditInputSet{ accountID1: iotago.BlockIssuanceCredits(0), }, - resolvedCommitmentInput: iotago.Commitment{ + resolvedCommitmentInput: &iotago.Commitment{ Slot: commitmentSlot, }, outputs: []iotago.Output{ @@ -8131,6 +8131,19 @@ func TestTxSemanticImplicitAccountCreationAndTransition(t *testing.T) { iotago.TransactionCapabilitiesBitMaskWithCapabilities(iotago.WithTransactionCanBurnNativeTokens(true)), ) + // Add the BIC and Commitment Inputs to the TX builder since they are required syntactically. + // Note that this has no effect on the actual test. + for accountID := range tests[idx].resolvedBICInputSet { + txBuilder.AddBlockIssuanceCreditInput(&iotago.BlockIssuanceCreditInput{ + AccountID: accountID, + }) + } + if tests[idx].resolvedCommitmentInput != nil { + txBuilder.AddCommitmentInput(&iotago.CommitmentInput{ + CommitmentID: tests[idx].resolvedCommitmentInput.MustID(), + }) + } + for _, input := range tests[idx].inputs { txBuilder.AddInput(&builder.TxInput{ UnlockTarget: input.unlockTarget, @@ -8148,7 +8161,7 @@ func TestTxSemanticImplicitAccountCreationAndTransition(t *testing.T) { tx := lo.PanicOnErr(txBuilder.Build()) resolvedInputs.BlockIssuanceCreditInputSet = tests[idx].resolvedBICInputSet - resolvedInputs.CommitmentInput = &tests[idx].resolvedCommitmentInput + resolvedInputs.CommitmentInput = tests[idx].resolvedCommitmentInput t.Run(tt.name, func(t *testing.T) { var err error