diff --git a/api_protocol_parameters.go b/api_protocol_parameters.go index b2ebe104c..12ffc5cc1 100644 --- a/api_protocol_parameters.go +++ b/api_protocol_parameters.go @@ -48,8 +48,10 @@ type basicProtocolParameters struct { // and commitments in its past-cone to ATT and lastCommittedSlot respectively. LivenessThreshold SlotIndex `serix:"16,mapKey=livenessThreshold"` // MinCommittableAge is the minimum age relative to the accepted tangle time slot index that a slot can be committed. + // For example, if the last accepted slot is in slot 100, and minCommittableAge=10, then the latest committed slot can be at most 100-10=90. MinCommittableAge SlotIndex `serix:"17,mapKey=minCommittableAge"` // MaxCommittableAge is the maximum age for a slot commitment to be included in a block relative to the slot index of the block issuing time. + // For example, if the last accepted slot is in slot 100, and maxCommittableAge=20, then the oldest referencable commitment is 100-20=80. MaxCommittableAge SlotIndex `serix:"18,mapKey=maxCommittableAge"` // EpochNearingThreshold is used by the epoch orchestrator to detect the slot that should trigger a new committee // selection for the next and upcoming epoch. diff --git a/api_v3.go b/api_v3.go index c5c87a0a9..29cfae054 100644 --- a/api_v3.go +++ b/api_v3.go @@ -6,7 +6,6 @@ import ( "time" "github.com/iotaledger/hive.go/ierrors" - "github.com/iotaledger/hive.go/lo" "github.com/iotaledger/hive.go/serializer/v2" "github.com/iotaledger/hive.go/serializer/v2/serix" ) @@ -494,11 +493,6 @@ func V3API(protoParams ProtocolParameters) API { serix.TypeSettings{}.WithLengthPrefixType(serix.LengthPrefixTypeAsUint16).WithArrayRules(txV3UnlocksArrRules), )) must(api.RegisterValidators(Transaction{}, nil, func(ctx context.Context, tx Transaction) error { - // limit unlock block count = input count - if len(tx.Unlocks) != len(tx.Essence.Inputs) { - return ierrors.Errorf("unlock block count must match inputs in essence, %d vs. %d", len(tx.Unlocks), len(tx.Essence.Inputs)) - } - return tx.syntacticallyValidate(v3) })) must(api.RegisterInterfaceObjects((*TxEssencePayload)(nil), (*TaggedData)(nil))) @@ -537,31 +531,7 @@ func V3API(protoParams ProtocolParameters) API { return nil }, func(ctx context.Context, protocolBlock ProtocolBlock) error { - if protoParams.Version() != protocolBlock.ProtocolVersion { - return ierrors.Errorf("mismatched protocol version: wanted %d, got %d in block", protoParams.Version(), protocolBlock.ProtocolVersion) - } - - block := protocolBlock.Block - if len(block.WeakParentIDs()) > 0 { - // weak parents must be disjunct to the rest of the parents - nonWeakParents := lo.KeyOnlyBy(append(block.StrongParentIDs(), block.ShallowLikeParentIDs()...), func(v BlockID) BlockID { - return v - }) - - for _, parent := range block.WeakParentIDs() { - if _, contains := nonWeakParents[parent]; contains { - return ierrors.Errorf("weak parents must be disjunct to the rest of the parents") - } - } - } - - if validationBlock, ok := block.(*ValidationBlock); ok { - if validationBlock.HighestSupportedVersion < protocolBlock.ProtocolVersion { - return ierrors.Errorf("highest supported version %d must be greater equal protocol version %d", validationBlock.HighestSupportedVersion, protocolBlock.ProtocolVersion) - } - } - - return nil + return protocolBlock.syntacticallyValidate(v3) })) } diff --git a/block.go b/block.go index 5cfd339fe..7468a81fe 100644 --- a/block.go +++ b/block.go @@ -11,6 +11,7 @@ import ( hiveEd25519 "github.com/iotaledger/hive.go/crypto/ed25519" "github.com/iotaledger/hive.go/ierrors" + "github.com/iotaledger/hive.go/lo" "github.com/iotaledger/hive.go/serializer/v2" "github.com/iotaledger/hive.go/serializer/v2/byteutils" "github.com/iotaledger/iota.go/v4/hexutil" @@ -27,6 +28,16 @@ const ( BlockTypeValidationMaxParents = BlockMaxParents + 42 ) +var ( + ErrWeakParentsInvalid = ierrors.New("weak parents must be disjunct to the rest of the parents") + ErrCommitmentTooOld = ierrors.New("a block cannot commit to a slot that is older than the block's slot minus maxCommittableAge") + ErrCommitmentTooRecent = ierrors.New("a block cannot commit to a slot that is more recent than the block's slot minus minCommittableAge") + ErrCommitmentInputTooOld = ierrors.New("a block cannot contain a commitment input with index older than the block's slot minus maxCommittableAge") + ErrCommitmentInputTooRecent = ierrors.New("a block cannot contain a commitment input with index more recent than the block's slot minus minCommittableAge") + ErrInvalidBlockVersion = ierrors.New("block has invalid protocol version") + ErrCommitmentInputNewerThanCommitment = ierrors.New("a block cannot contain a commitment input with index newer than the commitment index") +) + // BlockType denotes a type of Block. type BlockType byte @@ -286,6 +297,49 @@ func (b *ProtocolBlock) WorkScore(workScoreStructure *WorkScoreStructure) (WorkS return workScoreHeader.Add(workScoreBlock, workScoreSignature) } +// syntacticallyValidate syntactically validates the ProtocolBlock. +func (b *ProtocolBlock) syntacticallyValidate(api API) error { + if api.ProtocolParameters().Version() != b.ProtocolVersion { + return ierrors.Wrapf(ErrInvalidBlockVersion, "mismatched protocol version: wanted %d, got %d in block", api.ProtocolParameters().Version(), b.ProtocolVersion) + } + + block := b.Block + if len(block.WeakParentIDs()) > 0 { + // weak parents must be disjunct to the rest of the parents + nonWeakParents := lo.KeyOnlyBy(append(block.StrongParentIDs(), block.ShallowLikeParentIDs()...), func(v BlockID) BlockID { + return v + }) + + for _, parent := range block.WeakParentIDs() { + if _, contains := nonWeakParents[parent]; contains { + return ierrors.Wrapf(ErrWeakParentsInvalid, "weak parents (%s) cannot have common elements with strong parents (%s) or shallow likes (%s)", block.WeakParentIDs(), block.StrongParentIDs(), block.ShallowLikeParentIDs()) + } + } + } + + minCommittableAge := api.ProtocolParameters().MinCommittableAge() + maxCommittableAge := api.ProtocolParameters().MaxCommittableAge() + commitmentIndex := b.SlotCommitmentID.Index() + blockID, err := b.ID(api) + if err != nil { + return ierrors.Wrapf(err, "failed to syntactically validate block") + } + blockIndex := blockID.Index() + + // check that commitment is not too recent. + if commitmentIndex > 0 && // Don't filter commitments to genesis based on being too recent. + blockIndex < commitmentIndex+minCommittableAge { + return ierrors.Wrapf(ErrCommitmentTooRecent, "block at slot %d committing to slot %d", blockIndex, b.SlotCommitmentID.Index()) + } + + // Check that commitment is not too old. + if blockIndex > commitmentIndex+maxCommittableAge { + return ierrors.Wrapf(ErrCommitmentTooOld, "block at slot %d committing to slot %d, max committable age %d", blockIndex, b.SlotCommitmentID.Index(), maxCommittableAge) + } + + return b.Block.syntacticallyValidate(api, b) +} + type Block interface { Type() BlockType @@ -295,6 +349,8 @@ type Block interface { Hash(api API) (Identifier, error) + syntacticallyValidate(api API, protocolBlock *ProtocolBlock) error + ProcessableObject } @@ -361,6 +417,42 @@ func (b *BasicBlock) WorkScore(workScoreStructure *WorkScoreStructure) (WorkScor return workScoreBytes.Add(workScoreMissingParents, workScorePayload) } +// syntacticallyValidate syntactically validates the BasicBlock. +func (b *BasicBlock) syntacticallyValidate(api API, protocolBlock *ProtocolBlock) error { + if b.Payload != nil && b.Payload.PayloadType() == PayloadTransaction { + blockID, err := protocolBlock.ID(api) + if err != nil { + // TODO: wrap error + return err + } + blockIndex := blockID.Index() + + minCommittableAge := api.ProtocolParameters().MinCommittableAge() + maxCommittableAge := api.ProtocolParameters().MaxCommittableAge() + + tx, _ := b.Payload.(*Transaction) + if cInput := tx.CommitmentInput(); cInput != nil { + cInputIndex := cInput.CommitmentID.Index() + // check that commitment input is not too recent. + if cInputIndex > 0 && // Don't filter commitments to genesis based on being too recent. + blockIndex < cInputIndex+minCommittableAge { // filter commitments to future slots. + return ierrors.Wrapf(ErrCommitmentInputTooRecent, "block at slot %d with commitment input to slot %d", blockIndex, cInput.CommitmentID.Index()) + } + // Check that commitment input is not too old. + if blockIndex > cInputIndex+maxCommittableAge { + return ierrors.Wrapf(ErrCommitmentInputTooOld, "block at slot %d committing to slot %d, max committable age %d", blockIndex, cInput.CommitmentID.Index(), maxCommittableAge) + } + + if cInputIndex > protocolBlock.SlotCommitmentID.Index() { + return ierrors.Wrapf(ErrCommitmentInputNewerThanCommitment, "transaction in a block contains CommitmentInput to slot %d while max allowed is %d", cInput.CommitmentID.Index(), protocolBlock.SlotCommitmentID.Index()) + } + + } + } + + return nil +} + // ValidationBlock represents a validation vertex in the Tangle/BlockDAG. type ValidationBlock struct { // The parents the block references. @@ -403,6 +495,15 @@ func (b *ValidationBlock) WorkScore(_ *WorkScoreStructure) (WorkScore, error) { return 0, nil } +// syntacticallyValidate syntactically validates the ValidationBlock. +func (b *ValidationBlock) syntacticallyValidate(_ API, protocolBlock *ProtocolBlock) error { + if b.HighestSupportedVersion < protocolBlock.ProtocolVersion { + return ierrors.Errorf("highest supported version %d must be greater equal protocol version %d", b.HighestSupportedVersion, protocolBlock.ProtocolVersion) + } + + return nil +} + // ParentsType is a type that defines the type of the parent. type ParentsType uint8 diff --git a/block_test.go b/block_test.go index da4786b08..682647cd6 100644 --- a/block_test.go +++ b/block_test.go @@ -1,15 +1,20 @@ package iotago_test import ( + "crypto/ed25519" "fmt" "testing" "time" "github.com/stretchr/testify/require" + hiveEd25519 "github.com/iotaledger/hive.go/crypto/ed25519" + "github.com/iotaledger/hive.go/lo" "github.com/iotaledger/hive.go/serializer/v2" "github.com/iotaledger/hive.go/serializer/v2/serix" iotago "github.com/iotaledger/iota.go/v4" + "github.com/iotaledger/iota.go/v4/api" + "github.com/iotaledger/iota.go/v4/builder" "github.com/iotaledger/iota.go/v4/hexutil" "github.com/iotaledger/iota.go/v4/tpkg" ) @@ -49,21 +54,304 @@ func TestBlock_DeSerialize(t *testing.T) { } } +func createBlockWithParents(t *testing.T, strongParents, weakParents, shallowLikeParent iotago.BlockIDs, apiProvider *api.EpochBasedProvider) error { + t.Helper() + + apiForSlot := apiProvider.LatestAPI() + + block, err := builder.NewBasicBlockBuilder(apiForSlot). + StrongParents(strongParents). + WeakParents(weakParents). + ShallowLikeParents(shallowLikeParent). + IssuingTime(time.Now()). + SlotCommitmentID(iotago.NewCommitment(apiForSlot.Version(), apiForSlot.TimeProvider().SlotFromTime(time.Now())-apiForSlot.ProtocolParameters().MinCommittableAge(), iotago.CommitmentID{}, iotago.Identifier{}, 0).MustID()). + Build() + require.NoError(t, err) + + return lo.Return2(apiForSlot.Encode(block, serix.WithValidation())) +} + +func createBlockAtSlot(t *testing.T, blockIndex, commitmentIndex iotago.SlotIndex, apiProvider *api.EpochBasedProvider) error { + t.Helper() + + apiForSlot := apiProvider.APIForSlot(blockIndex) + + block, err := builder.NewBasicBlockBuilder(apiForSlot). + StrongParents(iotago.BlockIDs{tpkg.RandBlockID()}). + IssuingTime(apiForSlot.TimeProvider().SlotStartTime(blockIndex)). + SlotCommitmentID(iotago.NewCommitment(apiForSlot.Version(), commitmentIndex, iotago.CommitmentID{}, iotago.Identifier{}, 0).MustID()). + Build() + require.NoError(t, err) + + return lo.Return2(apiForSlot.Encode(block, serix.WithValidation())) +} + +func createBlockAtSlotWithVersion(t *testing.T, blockIndex iotago.SlotIndex, version iotago.Version, apiProvider *api.EpochBasedProvider) error { + t.Helper() + + apiForSlot := apiProvider.APIForSlot(blockIndex) + block, err := builder.NewBasicBlockBuilder(apiForSlot). + ProtocolVersion(version). + StrongParents(iotago.BlockIDs{iotago.BlockID{}}). + IssuingTime(apiForSlot.TimeProvider().SlotStartTime(blockIndex)). + SlotCommitmentID(iotago.NewCommitment(apiForSlot.Version(), blockIndex-apiForSlot.ProtocolParameters().MinCommittableAge(), iotago.CommitmentID{}, iotago.Identifier{}, 0).MustID()). + Build() + require.NoError(t, err) + + return lo.Return2(apiForSlot.Encode(block, serix.WithValidation())) +} + +//nolint:unparam // in the test we always issue at blockIndex=100, but let's keep this flexibility. +func createBlockAtSlotWithPayload(t *testing.T, blockIndex, commitmentIndex iotago.SlotIndex, payload iotago.Payload, apiProvider *api.EpochBasedProvider) error { + t.Helper() + + apiForSlot := apiProvider.APIForSlot(blockIndex) + + block, err := builder.NewBasicBlockBuilder(apiForSlot). + StrongParents(iotago.BlockIDs{tpkg.RandBlockID()}). + IssuingTime(apiForSlot.TimeProvider().SlotStartTime(blockIndex)). + SlotCommitmentID(iotago.NewCommitment(apiForSlot.Version(), commitmentIndex, iotago.CommitmentID{}, iotago.Identifier{}, 0).MustID()). + Payload(payload). + Build() + require.NoError(t, err) + + return lo.Return2(apiForSlot.Encode(block, serix.WithValidation())) +} + func TestProtocolBlock_ProtocolVersionSyntactical(t *testing.T) { - block := &iotago.ProtocolBlock{ - BlockHeader: iotago.BlockHeader{ - ProtocolVersion: tpkg.TestAPI.Version() + 1, - SlotCommitmentID: iotago.NewEmptyCommitment(tpkg.TestAPI.Version()).MustID(), - }, - Signature: tpkg.RandEd25519Signature(), - Block: &iotago.BasicBlock{ - StrongParents: tpkg.SortedRandBlockIDs(1), - Payload: nil, + apiProvider := api.NewEpochBasedProvider( + api.WithAPIForMissingVersionCallback( + func(version iotago.Version) (iotago.API, error) { + return iotago.V3API(iotago.NewV3ProtocolParameters(iotago.WithVersion(version))), nil + }, + ), + ) + apiProvider.AddProtocolParametersAtEpoch(iotago.NewV3ProtocolParameters(), 0) + apiProvider.AddProtocolParametersAtEpoch(iotago.NewV3ProtocolParameters(iotago.WithVersion(4)), 3) + + timeProvider := apiProvider.CurrentAPI().TimeProvider() + + require.ErrorIs(t, createBlockAtSlotWithVersion(t, timeProvider.EpochStart(1), 2, apiProvider), iotago.ErrInvalidBlockVersion) + + require.NoError(t, createBlockAtSlotWithVersion(t, timeProvider.EpochEnd(1), 3, apiProvider)) + + require.NoError(t, createBlockAtSlotWithVersion(t, timeProvider.EpochEnd(2), 3, apiProvider)) + + require.ErrorIs(t, createBlockAtSlotWithVersion(t, timeProvider.EpochStart(3), 3, apiProvider), iotago.ErrInvalidBlockVersion) + + require.NoError(t, createBlockAtSlotWithVersion(t, timeProvider.EpochStart(3), 4, apiProvider)) + + require.NoError(t, createBlockAtSlotWithVersion(t, timeProvider.EpochEnd(3), 4, apiProvider)) + + require.NoError(t, createBlockAtSlotWithVersion(t, timeProvider.EpochStart(5), 4, apiProvider)) + + apiProvider.AddProtocolParametersAtEpoch(iotago.NewV3ProtocolParameters(iotago.WithVersion(5)), 10) + + require.NoError(t, createBlockAtSlotWithVersion(t, timeProvider.EpochEnd(9), 4, apiProvider)) + + require.ErrorIs(t, createBlockAtSlotWithVersion(t, timeProvider.EpochStart(10), 4, apiProvider), iotago.ErrInvalidBlockVersion) + + require.NoError(t, createBlockAtSlotWithVersion(t, timeProvider.EpochStart(10), 5, apiProvider)) +} + +func TestProtocolBlock_Commitments(t *testing.T) { + // with the following parameters, a block issued in slot 100 can commit between slot 80 and 90 + apiProvider := api.NewEpochBasedProvider() + apiProvider.AddProtocolParametersAtEpoch( + iotago.NewV3ProtocolParameters( + iotago.WithTimeProviderOptions(time.Now().Add(-20*time.Minute).Unix(), 10, 13), + iotago.WithLivenessOptions(3, 11, 21, 4), + ), 0) + + require.ErrorIs(t, createBlockAtSlot(t, 100, 78, apiProvider), iotago.ErrCommitmentTooOld) + + require.ErrorIs(t, createBlockAtSlot(t, 100, 90, apiProvider), iotago.ErrCommitmentTooRecent) + + require.NoError(t, createBlockAtSlot(t, 100, 89, apiProvider)) + + require.NoError(t, createBlockAtSlot(t, 100, 80, apiProvider)) + + require.NoError(t, createBlockAtSlot(t, 100, 85, apiProvider)) +} + +func TestProtocolBlock_Commitments1(t *testing.T) { + // with the following parameters, a block issued in slot 100 can commit between slot 80 and 90 + apiProvider := api.NewEpochBasedProvider() + apiProvider.AddProtocolParametersAtEpoch( + iotago.NewV3ProtocolParameters( + iotago.WithTimeProviderOptions(time.Now().Add(-20*time.Minute).Unix(), 10, 13), + iotago.WithLivenessOptions(3, 7, 21, 4), + ), 0) + + require.ErrorIs(t, createBlockAtSlot(t, 10, 4, apiProvider), iotago.ErrCommitmentTooRecent) + +} + +func TestProtocolBlock_WeakParents(t *testing.T) { + // with the following parameters, a block issued in slot 100 can commit between slot 80 and 90 + apiProvider := api.NewEpochBasedProvider() + apiProvider.AddProtocolParametersAtEpoch( + iotago.NewV3ProtocolParameters( + iotago.WithTimeProviderOptions(time.Now().Add(-20*time.Minute).Unix(), 10, 13), + iotago.WithLivenessOptions(3, 10, 20, 4), + ), 0) + strongParent1 := tpkg.RandBlockID() + strongParent2 := tpkg.RandBlockID() + weakParent1 := tpkg.RandBlockID() + weakParent2 := tpkg.RandBlockID() + shallowLikeParent1 := tpkg.RandBlockID() + shallowLikeParent2 := tpkg.RandBlockID() + require.ErrorIs(t, createBlockWithParents( + t, + iotago.BlockIDs{strongParent1, strongParent2}, + iotago.BlockIDs{weakParent1, weakParent2, shallowLikeParent2}, + iotago.BlockIDs{shallowLikeParent1, shallowLikeParent2}, + apiProvider, + ), iotago.ErrWeakParentsInvalid) + + require.ErrorIs(t, createBlockWithParents( + t, + iotago.BlockIDs{strongParent1, strongParent2}, + iotago.BlockIDs{weakParent1, weakParent2, strongParent2}, + iotago.BlockIDs{shallowLikeParent1, shallowLikeParent2}, + apiProvider, + ), iotago.ErrWeakParentsInvalid) + + require.NoError(t, createBlockWithParents( + t, + iotago.BlockIDs{strongParent1, strongParent2}, + iotago.BlockIDs{weakParent1, weakParent2}, + iotago.BlockIDs{shallowLikeParent1, shallowLikeParent2}, + apiProvider, + )) + + require.NoError(t, createBlockWithParents( + t, + iotago.BlockIDs{strongParent1, strongParent2}, + iotago.BlockIDs{weakParent1, weakParent2}, + iotago.BlockIDs{shallowLikeParent1, shallowLikeParent2, strongParent2}, + apiProvider, + )) +} + +func TestProtocolBlock_TransactionCommitmentInput(t *testing.T) { + keyPair := hiveEd25519.GenerateKeyPair() + // We derive a dummy account from addr. + addr := iotago.Ed25519AddressFromPubKey(keyPair.PublicKey[:]) + output := &iotago.BasicOutput{ + Amount: 100000, + Conditions: iotago.BasicOutputUnlockConditions{ + &iotago.AddressUnlockCondition{ + Address: addr, + }, }, } + // with the following parameters, block issued in slot 110 can contain a transaction with commitment input referencing + // commitments between 90 and slot that the block commits to (100 at most) + apiProvider := api.NewEpochBasedProvider() + apiProvider.AddProtocolParametersAtEpoch( + iotago.NewV3ProtocolParameters( + iotago.WithTimeProviderOptions(time.Now().Add(-20*time.Minute).Unix(), 10, 13), + iotago.WithLivenessOptions(3, 11, 21, 4), + ), 0) + + commitmentInputTooOld, err := builder.NewTransactionBuilder(apiProvider.LatestAPI()). + AddInput(&builder.TxInput{ + UnlockTarget: addr, + InputID: tpkg.RandOutputID(0), + Input: output, + }). + AddOutput(output). + AddContextInput(&iotago.CommitmentInput{CommitmentID: iotago.NewSlotIdentifier(78, tpkg.Rand32ByteArray())}). + Build(iotago.NewInMemoryAddressSigner(iotago.AddressKeys{Address: addr, Keys: ed25519.PrivateKey(keyPair.PrivateKey[:])})) + + require.NoError(t, err) + + require.ErrorIs(t, createBlockAtSlotWithPayload(t, 100, 79, commitmentInputTooOld, apiProvider), iotago.ErrCommitmentInputTooOld) + + commitmentInputTooRecent, err := builder.NewTransactionBuilder(apiProvider.LatestAPI()). + AddInput(&builder.TxInput{ + UnlockTarget: addr, + InputID: tpkg.RandOutputID(0), + Input: output, + }). + AddOutput(output). + AddContextInput(&iotago.CommitmentInput{CommitmentID: iotago.NewSlotIdentifier(90, tpkg.Rand32ByteArray())}). + Build(iotago.NewInMemoryAddressSigner(iotago.AddressKeys{Address: addr, Keys: ed25519.PrivateKey(keyPair.PrivateKey[:])})) + + require.NoError(t, err) + + require.ErrorIs(t, createBlockAtSlotWithPayload(t, 100, 89, commitmentInputTooRecent, apiProvider), iotago.ErrCommitmentInputTooRecent) + + commitmentInputNewerThanBlockCommitment, err := builder.NewTransactionBuilder(apiProvider.LatestAPI()). + AddInput(&builder.TxInput{ + UnlockTarget: addr, + InputID: tpkg.RandOutputID(0), + Input: output, + }). + AddOutput(output). + AddContextInput(&iotago.CommitmentInput{CommitmentID: iotago.NewSlotIdentifier(85, tpkg.Rand32ByteArray())}). + Build(iotago.NewInMemoryAddressSigner(iotago.AddressKeys{Address: addr, Keys: ed25519.PrivateKey(keyPair.PrivateKey[:])})) + + require.NoError(t, err) + + require.ErrorIs(t, createBlockAtSlotWithPayload(t, 100, 79, commitmentInputNewerThanBlockCommitment, apiProvider), iotago.ErrCommitmentInputNewerThanCommitment) + + commitmentCorrect, err := builder.NewTransactionBuilder(apiProvider.LatestAPI()). + AddInput(&builder.TxInput{ + UnlockTarget: addr, + InputID: tpkg.RandOutputID(0), + Input: output, + }). + AddOutput(output). + AddContextInput(&iotago.CommitmentInput{CommitmentID: iotago.NewSlotIdentifier(79, tpkg.Rand32ByteArray())}). + Build(iotago.NewInMemoryAddressSigner(iotago.AddressKeys{Address: addr, Keys: ed25519.PrivateKey(keyPair.PrivateKey[:])})) + + require.NoError(t, err) + + require.NoError(t, createBlockAtSlotWithPayload(t, 100, 89, commitmentCorrect, apiProvider)) + + commitmentCorrectOldest, err := builder.NewTransactionBuilder(apiProvider.LatestAPI()). + AddInput(&builder.TxInput{ + UnlockTarget: addr, + InputID: tpkg.RandOutputID(0), + Input: output, + }). + AddOutput(output). + AddContextInput(&iotago.CommitmentInput{CommitmentID: iotago.NewSlotIdentifier(79, tpkg.Rand32ByteArray())}). + Build(iotago.NewInMemoryAddressSigner(iotago.AddressKeys{Address: addr, Keys: ed25519.PrivateKey(keyPair.PrivateKey[:])})) + + require.NoError(t, err) + + require.NoError(t, createBlockAtSlotWithPayload(t, 100, 79, commitmentCorrectOldest, apiProvider)) + + commitmentCorrectNewest, err := builder.NewTransactionBuilder(apiProvider.LatestAPI()). + AddInput(&builder.TxInput{ + UnlockTarget: addr, + InputID: tpkg.RandOutputID(0), + Input: output, + }). + AddOutput(output). + AddContextInput(&iotago.CommitmentInput{CommitmentID: iotago.NewSlotIdentifier(89, tpkg.Rand32ByteArray())}). + Build(iotago.NewInMemoryAddressSigner(iotago.AddressKeys{Address: addr, Keys: ed25519.PrivateKey(keyPair.PrivateKey[:])})) + + require.NoError(t, err) + + require.NoError(t, createBlockAtSlotWithPayload(t, 100, 89, commitmentCorrectNewest, apiProvider)) + + commitmentCorrectMiddle, err := builder.NewTransactionBuilder(apiProvider.LatestAPI()). + AddInput(&builder.TxInput{ + UnlockTarget: addr, + InputID: tpkg.RandOutputID(0), + Input: output, + }). + AddOutput(output). + AddContextInput(&iotago.CommitmentInput{CommitmentID: iotago.NewSlotIdentifier(85, tpkg.Rand32ByteArray())}). + Build(iotago.NewInMemoryAddressSigner(iotago.AddressKeys{Address: addr, Keys: ed25519.PrivateKey(keyPair.PrivateKey[:])})) + + require.NoError(t, err) - _, err := tpkg.TestAPI.Encode(block, serix.WithValidation()) - require.ErrorContains(t, err, "mismatched protocol version") + require.NoError(t, createBlockAtSlotWithPayload(t, 100, 89, commitmentCorrectMiddle, apiProvider)) } func TestProtocolBlock_DeserializationNotEnoughData(t *testing.T) { diff --git a/transaction.go b/transaction.go index ee2c8a984..8a631830e 100644 --- a/transaction.go +++ b/transaction.go @@ -155,6 +155,11 @@ func (t *Transaction) String() string { // syntacticallyValidate syntactically validates the Transaction. func (t *Transaction) syntacticallyValidate(api API) error { + // limit unlock block count = input count + if len(t.Unlocks) != len(t.Essence.Inputs) { + return ierrors.Errorf("unlock block count must match inputs in essence, %d vs. %d", len(t.Unlocks), len(t.Essence.Inputs)) + } + if err := t.Essence.syntacticallyValidate(api.ProtocolParameters()); err != nil { return ierrors.Errorf("transaction essence is invalid: %w", err) } diff --git a/vm/stardust/stvf_test.go b/vm/stardust/stvf_test.go index 6dd531b47..64f7bc98e 100644 --- a/vm/stardust/stvf_test.go +++ b/vm/stardust/stvf_test.go @@ -2512,7 +2512,7 @@ func TestDelegationOutput_ValidateStateTransition(t *testing.T) { minCommittableAge := tpkg.TestAPI.ProtocolParameters().MinCommittableAge() maxCommittableAge := tpkg.TestAPI.ProtocolParameters().MaxCommittableAge() - // Commitment indices that will always end up being in current epoch, no matter if + // Commitment indices that will always end up being in the current epoch no matter if // future or past bounded. epochStartCommitmentIndex := epochStartSlot - minCommittableAge epochEndCommitmentIndex := epochEndSlot - maxCommittableAge