diff --git a/pkg/protocol/engine/booker/inmemorybooker/booker.go b/pkg/protocol/engine/booker/inmemorybooker/booker.go index 6199b5653..6d420ea69 100644 --- a/pkg/protocol/engine/booker/inmemorybooker/booker.go +++ b/pkg/protocol/engine/booker/inmemorybooker/booker.go @@ -8,6 +8,7 @@ import ( "github.com/iotaledger/hive.go/lo" "github.com/iotaledger/hive.go/runtime/module" "github.com/iotaledger/hive.go/runtime/options" + "github.com/iotaledger/iota-core/pkg/model" "github.com/iotaledger/iota-core/pkg/protocol/engine" "github.com/iotaledger/iota-core/pkg/protocol/engine/blocks" "github.com/iotaledger/iota-core/pkg/protocol/engine/booker" @@ -27,7 +28,8 @@ type Booker struct { ledger ledger.Ledger - retainBlockFailure func(id iotago.BlockID, reason api.BlockFailureReason) + loadBlockFromStorage func(id iotago.BlockID) (*model.Block, bool) + retainBlockFailure func(id iotago.BlockID, reason api.BlockFailureReason) errorHandler func(error) apiProvider iotago.APIProvider @@ -42,7 +44,7 @@ func NewProvider(opts ...options.Option[Booker]) module.Provider[*engine.Engine, b.ledger = e.Ledger b.ledger.HookConstructed(func() { b.spendDAG = b.ledger.SpendDAG() - + b.loadBlockFromStorage = e.Block b.ledger.MemPool().OnTransactionAttached(func(transaction mempool.TransactionMetadata) { transaction.OnAccepted(func() { b.events.TransactionAccepted.Trigger(transaction) @@ -193,6 +195,7 @@ func (b *Booker) inheritSpenders(block *blocks.Block) (spenderIDs ds.Set[iotago. parentBlock, exists := b.blockCache.Block(parent.ID) if !exists { b.retainBlockFailure(block.ID(), api.BlockFailureParentNotFound) + return nil, ierrors.Errorf("parent %s does not exist", parent.ID) } @@ -202,6 +205,21 @@ func (b *Booker) inheritSpenders(block *blocks.Block) (spenderIDs ds.Set[iotago. case iotago.WeakParentType: spenderIDsToInherit.AddAll(parentBlock.PayloadSpenderIDs()) case iotago.ShallowLikeParentType: + // If parent block is a RootBlock, then make sure that the block contains a transaction; + // otherwise, the reference is invalid. + if parentBlock.IsRootBlock() { + parentModelBlock, exists := b.loadBlockFromStorage(parent.ID) + if !exists { + return nil, ierrors.Wrapf(err, "shallow like parent %s does not exist in storage", parent.ID.String()) + } + + if _, hasTx := parentModelBlock.SignedTransaction(); !hasTx { + return nil, ierrors.Wrapf(err, "shallow like parent %s does not contain a conflicting transaction", parent.ID.String()) + } + + break + } + // Check whether the parent contains a conflicting TX, // otherwise reference is invalid and the block should be marked as invalid as well. if signedTransaction, hasTx := parentBlock.SignedTransaction(); !hasTx || !parentBlock.PayloadSpenderIDs().Has(lo.PanicOnErr(signedTransaction.Transaction.ID())) { diff --git a/pkg/tests/booker_test.go b/pkg/tests/booker_test.go index abd3db23b..8f37d0f57 100644 --- a/pkg/tests/booker_test.go +++ b/pkg/tests/booker_test.go @@ -2,6 +2,7 @@ package tests import ( "testing" + "time" "github.com/iotaledger/hive.go/lo" "github.com/iotaledger/hive.go/runtime/options" @@ -775,3 +776,57 @@ func Test_SpendPendingCommittedRace(t *testing.T) { ts.AssertTransactionsExist(wallet.Transactions("tx1", "tx2"), false, node1, node2) } } + +func Test_RootBlockShallowLike(t *testing.T) { + ts := testsuite.NewTestSuite(t, + testsuite.WithProtocolParametersOptions( + iotago.WithTimeProviderOptions( + 0, + testsuite.GenesisTimeWithOffsetBySlots(1000, testsuite.DefaultSlotDurationInSeconds), + testsuite.DefaultSlotDurationInSeconds, + 3, + ), + iotago.WithLivenessOptions( + 10, + 10, + 2, + 4, + 5, + ), + ), + + testsuite.WithWaitFor(5*time.Second), + ) + defer ts.Shutdown() + + node1 := ts.AddValidatorNode("node1") + wallet := ts.AddDefaultWallet(node1) + ts.Run(true, map[string][]options.Option[protocol.Protocol]{}) + + tx1 := wallet.CreateBasicOutputsEquallyFromInput("tx1", 1, "Genesis:0") + + ts.IssueBasicBlockWithOptions("block1", wallet, tx1, mock.WithIssuingTime(ts.API.TimeProvider().SlotStartTime(1))) + ts.IssueBasicBlockWithOptions("block2", wallet, &iotago.TaggedData{}, mock.WithIssuingTime(ts.API.TimeProvider().SlotStartTime(1))) + + ts.AssertTransactionsExist(wallet.Transactions("tx1"), true, node1) + + ts.AssertBlocksInCacheConflicts(map[*blocks.Block][]string{ + ts.Block("block1"): {"tx1"}, + }, node1) + + ts.AssertTransactionInCacheConflicts(map[*iotago.Transaction][]string{ + wallet.Transaction("tx1"): {"tx1"}, + }, node1) + + ts.IssueBlocksAtSlots("", []iotago.SlotIndex{2, 3, 4}, 2, "block", ts.Nodes(), true, false) + + ts.AssertActiveRootBlocks(ts.Blocks("Genesis", "block1", "block2", "2.1-node1"), ts.Nodes()...) + + ts.IssueBasicBlockWithOptions("block-shallow-like-valid", wallet, &iotago.TaggedData{}, mock.WithStrongParents(ts.BlockID("4.1-node1")), mock.WithShallowLikeParents(ts.BlockID("block1")), mock.WithIssuingTime(ts.API.TimeProvider().SlotStartTime(5))) + ts.AssertBlocksInCacheBooked(ts.Blocks("block-shallow-like-valid"), true, node1) + ts.AssertBlocksInCacheInvalid(ts.Blocks("block-shallow-like-valid"), false, node1) + + ts.IssueBasicBlockWithOptions("block-shallow-like-invalid", wallet, &iotago.TaggedData{}, mock.WithStrongParents(ts.BlockID("4.1-node1")), mock.WithShallowLikeParents(ts.BlockID("block2")), mock.WithIssuingTime(ts.API.TimeProvider().SlotStartTime(5))) + ts.AssertBlocksInCacheBooked(ts.Blocks("block-shallow-like-invalid"), false, node1) + ts.AssertBlocksInCacheInvalid(ts.Blocks("block-shallow-like-invalid"), true, node1) +}