From fb5f2c06d099e709280a6536b91aa135a0185fcf Mon Sep 17 00:00:00 2001 From: Piotr Macek <4007944+piotrm50@users.noreply.github.com> Date: Tue, 7 Nov 2023 13:02:46 +0100 Subject: [PATCH] Implement test that locks and unlocks an account due to negative BIC. --- pkg/tests/accounts_test.go | 224 +++++++++++++++++++++- pkg/testsuite/blocks.go | 16 ++ pkg/testsuite/mock/node.go | 13 +- pkg/testsuite/mock/wallet_transactions.go | 18 +- 4 files changed, 257 insertions(+), 14 deletions(-) diff --git a/pkg/tests/accounts_test.go b/pkg/tests/accounts_test.go index 99886583d..588f4cd66 100644 --- a/pkg/tests/accounts_test.go +++ b/pkg/tests/accounts_test.go @@ -3,8 +3,13 @@ package tests import ( "testing" + "github.com/stretchr/testify/require" + + "github.com/iotaledger/hive.go/lo" "github.com/iotaledger/iota-core/pkg/model" "github.com/iotaledger/iota-core/pkg/protocol/engine/accounts" + "github.com/iotaledger/iota-core/pkg/protocol/engine/blocks" + "github.com/iotaledger/iota-core/pkg/protocol/engine/utxoledger" "github.com/iotaledger/iota-core/pkg/testsuite" "github.com/iotaledger/iota-core/pkg/testsuite/mock" "github.com/iotaledger/iota-core/pkg/testsuite/snapshotcreator" @@ -45,11 +50,11 @@ func Test_TransitionAndDestroyAccount(t *testing.T) { ) defer ts.Shutdown() - // add a validator node to the network. This will add a validator account to the snapshot. + // Add a validator node to the network. This will add a validator account to the snapshot. node1 := ts.AddValidatorNode("node1") - // add a non-validator node to the network. This will not add any accounts to the snapshot. + // Add a non-validator node to the network. This will not add any accounts to the snapshot. _ = ts.AddNode("node2") - // add a default block issuer to the network. This will add another block issuer account to the snapshot. + // Add a default block issuer to the network. This will add another block issuer account to the snapshot. wallet := ts.AddGenesisWallet("default", node1, iotago.MaxBlockIssuanceCredits/2) ts.Run(true) @@ -452,6 +457,219 @@ func Test_ImplicitAccounts(t *testing.T) { ts.Wait(ts.Nodes()...) } +func Test_NegativeBIC(t *testing.T) { + ts := testsuite.NewTestSuite(t, + testsuite.WithProtocolParametersOptions( + iotago.WithTimeProviderOptions( + testsuite.GenesisTimeWithOffsetBySlots(200, testsuite.DefaultSlotDurationInSeconds), + testsuite.DefaultSlotDurationInSeconds, + 8, + ), + iotago.WithLivenessOptions( + testsuite.DefaultLivenessThresholdLowerBoundInSeconds, + testsuite.DefaultLivenessThresholdUpperBoundInSeconds, + testsuite.DefaultMinCommittableAge, + 100, + testsuite.DefaultEpochNearingThreshold, + ), + ), + ) + defer ts.Shutdown() + + // Add a validator node to the network. This will add a validator account to the snapshot. + node1 := ts.AddValidatorNode("node1") + node2 := ts.AddNode("node2") + + validatorBIC := iotago.MaxBlockIssuanceCredits / 2 + // Add a default block issuer to the network. This will add another block issuer account to the snapshot. + walletBIC := iotago.BlockIssuanceCredits(499) + wallet := ts.AddGenesisWallet("default", node2, walletBIC) + + ts.Run(false) + + // check that the accounts added in the genesis snapshot were added to the account manager correctly. + // validator node account. + validatorAccountOutput := ts.AccountOutput("Genesis:1") + ts.AssertAccountData(&accounts.AccountData{ + ID: node1.Validator.AccountID, + Credits: accounts.NewBlockIssuanceCredits(validatorBIC, 0), + OutputID: validatorAccountOutput.OutputID(), + ExpirySlot: iotago.MaxSlotIndex, + BlockIssuerKeys: node1.Validator.BlockIssuerKeys(), + StakeEndEpoch: iotago.MaxEpochIndex, + ValidatorStake: mock.MinValidatorAccountAmount, + }, ts.Nodes()...) + // default wallet block issuer account. + blockIssuerAccountOutput := ts.AccountOutput("Genesis:2") + ts.AssertAccountData(&accounts.AccountData{ + ID: wallet.BlockIssuer.AccountID, + Credits: accounts.NewBlockIssuanceCredits(walletBIC, 0), + OutputID: blockIssuerAccountOutput.OutputID(), + ExpirySlot: iotago.MaxSlotIndex, + BlockIssuerKeys: wallet.BlockIssuer.BlockIssuerKeys(), + }, ts.Nodes()...) + + // MODIFY EXISTING GENESIS ACCOUNT + var block1Slot iotago.SlotIndex = 1 + var latestParent *blocks.Block + // Issue one block from each of the two block-issuers - one will go negative and the other has enough BICs. + { + block1Commitment := iotago.NewEmptyCommitment(ts.API.ProtocolParameters().Version()) + block1Commitment.ReferenceManaCost = ts.API.ProtocolParameters().CongestionControlParameters().MinReferenceManaCost + block11 := ts.IssueBasicBlockAtSlotWithOptions("block1.1", block1Slot, ts.Wallet("node1"), &iotago.TaggedData{}, mock.WithSlotCommitment(block1Commitment)) + block12 := ts.IssueBasicBlockAtSlotWithOptions("block1.2", block1Slot, wallet, &iotago.TaggedData{}, mock.WithStrongParents(block11.ID()), mock.WithSlotCommitment(block1Commitment)) + + // Commit BIC burns and check account states. + latestParent = ts.CommitUntilSlot(ts.BlockID("block1.2").Slot(), block12) + + burned := iotago.BlockIssuanceCredits(block11.WorkScore()) * iotago.BlockIssuanceCredits(ts.API.ProtocolParameters().CongestionControlParameters().MinReferenceManaCost) + + validatorBIC -= burned + walletBIC -= burned + + ts.AssertAccountData(&accounts.AccountData{ + ID: node1.Validator.AccountID, + Credits: accounts.NewBlockIssuanceCredits(validatorBIC, block1Slot), + OutputID: validatorAccountOutput.OutputID(), + ExpirySlot: iotago.MaxSlotIndex, + BlockIssuerKeys: node1.Validator.BlockIssuerKeys(), + StakeEndEpoch: iotago.MaxEpochIndex, + ValidatorStake: mock.MinValidatorAccountAmount, + }, ts.Nodes()...) + ts.AssertAccountData(&accounts.AccountData{ + ID: wallet.BlockIssuer.AccountID, + Credits: accounts.NewBlockIssuanceCredits(walletBIC, block1Slot), + ExpirySlot: iotago.MaxSlotIndex, + OutputID: blockIssuerAccountOutput.OutputID(), + BlockIssuerKeys: wallet.BlockIssuer.BlockIssuerKeys(), + }, ts.Nodes()...) + } + + block2Slot := latestParent.ID().Slot() + + // Try to issue more blocks from each of the issuers - one succeeds in issuing a block, + // the other has the block rejected in the CommitmentFilter as his account has negative BIC value. + { + + block21 := ts.IssueBasicBlockAtSlotWithOptions("block2.1", block2Slot, ts.Wallet("node1"), &iotago.TaggedData{}) + + block22 := ts.IssueBasicBlockAtSlotWithOptions("block2.2", block2Slot, wallet, &iotago.TaggedData{}, mock.WithStrongParents(ts.BlockID("block2.1"))) + + ts.AssertBlockFiltered([]*blocks.Block{block22}, iotago.ErrNegativeBIC, wallet.Node) + + latestParent = ts.CommitUntilSlot(ts.BlockID("block2.1").Slot(), block21) + + burned := iotago.BlockIssuanceCredits(block21.WorkScore()) * iotago.BlockIssuanceCredits(ts.API.ProtocolParameters().CongestionControlParameters().MinReferenceManaCost) + + validatorBIC -= burned + + ts.AssertAccountData(&accounts.AccountData{ + ID: node1.Validator.AccountID, + Credits: accounts.NewBlockIssuanceCredits(validatorBIC, block2Slot), + OutputID: validatorAccountOutput.OutputID(), + ExpirySlot: iotago.MaxSlotIndex, + BlockIssuerKeys: node1.Validator.BlockIssuerKeys(), + StakeEndEpoch: iotago.MaxEpochIndex, + ValidatorStake: mock.MinValidatorAccountAmount, + LatestSupportedProtocolVersionAndHash: model.VersionAndHash{ + Version: ts.API.ProtocolParameters().Version(), + Hash: lo.PanicOnErr(ts.API.ProtocolParameters().Hash()), + }, + }, ts.Nodes()...) + ts.AssertAccountData(&accounts.AccountData{ + ID: wallet.BlockIssuer.AccountID, + Credits: accounts.NewBlockIssuanceCredits(walletBIC, block1Slot), + ExpirySlot: iotago.MaxSlotIndex, + OutputID: blockIssuerAccountOutput.OutputID(), + BlockIssuerKeys: wallet.BlockIssuer.BlockIssuerKeys(), + }, ts.Nodes()...) + } + + block3Slot := latestParent.ID().Slot() + + // Allot some mana to the locked account to unlock it. + { + allottedBIC := iotago.BlockIssuanceCredits(500) + tx1, err := wallet.CreateSignedTransactionWithOptions( + "TX1", + mock.WithInputs(utxoledger.Outputs{wallet.Output("Genesis:0")}), + mock.WithOutputs(iotago.Outputs[iotago.Output]{wallet.Output("Genesis:0").Output()}), + mock.WithSlotCreated(block3Slot), + mock.WithAllotments(iotago.Allotments{&iotago.Allotment{ + AccountID: wallet.BlockIssuer.AccountID, + Mana: iotago.Mana(allottedBIC), + }}), + ) + require.NoError(t, err) + + block3Commitment := node1.Protocol.MainEngineInstance().Storage.Settings().LatestCommitment().Commitment() + block31 := ts.IssueBasicBlockAtSlotWithOptions("block3.1", block3Slot, ts.Wallet("node1"), tx1, mock.WithStrongParents(latestParent.ID()), mock.WithSlotCommitment(block3Commitment)) + + latestParent = ts.CommitUntilSlot(ts.BlockID("block3.1").Slot(), block31) + + burned := iotago.BlockIssuanceCredits(block31.WorkScore()) * iotago.BlockIssuanceCredits(ts.API.ProtocolParameters().CongestionControlParameters().MinReferenceManaCost) + + validatorBIC -= burned + walletBIC += allottedBIC + + ts.AssertAccountData(&accounts.AccountData{ + ID: node1.Validator.AccountID, + Credits: accounts.NewBlockIssuanceCredits(validatorBIC, block3Slot), + OutputID: validatorAccountOutput.OutputID(), + ExpirySlot: iotago.MaxSlotIndex, + BlockIssuerKeys: node1.Validator.BlockIssuerKeys(), + StakeEndEpoch: iotago.MaxEpochIndex, + ValidatorStake: mock.MinValidatorAccountAmount, + LatestSupportedProtocolVersionAndHash: model.VersionAndHash{ + Version: ts.API.ProtocolParameters().Version(), + Hash: lo.PanicOnErr(ts.API.ProtocolParameters().Hash()), + }, + }, ts.Nodes()...) + ts.AssertAccountData(&accounts.AccountData{ + ID: wallet.BlockIssuer.AccountID, + Credits: accounts.NewBlockIssuanceCredits(walletBIC, block3Slot), + ExpirySlot: iotago.MaxSlotIndex, + OutputID: blockIssuerAccountOutput.OutputID(), + BlockIssuerKeys: wallet.BlockIssuer.BlockIssuerKeys(), + }, ts.Nodes()...) + } + + block4Slot := latestParent.ID().Slot() + + // Issue block from the unlocked account to make sure that it's actually unlocked. + { + + block4 := ts.IssueBasicBlockAtSlotWithOptions("block4", block4Slot, wallet, &iotago.TaggedData{}, mock.WithStrongParents(latestParent.ID())) + + latestParent = ts.CommitUntilSlot(ts.BlockID("block4").Slot(), block4) + + burned := iotago.BlockIssuanceCredits(block4.WorkScore()) * iotago.BlockIssuanceCredits(ts.API.ProtocolParameters().CongestionControlParameters().MinReferenceManaCost) + + walletBIC -= burned + + ts.AssertAccountData(&accounts.AccountData{ + ID: node1.Validator.AccountID, + Credits: accounts.NewBlockIssuanceCredits(validatorBIC, block3Slot), + OutputID: validatorAccountOutput.OutputID(), + ExpirySlot: iotago.MaxSlotIndex, + BlockIssuerKeys: node1.Validator.BlockIssuerKeys(), + StakeEndEpoch: iotago.MaxEpochIndex, + ValidatorStake: mock.MinValidatorAccountAmount, + LatestSupportedProtocolVersionAndHash: model.VersionAndHash{ + Version: ts.API.ProtocolParameters().Version(), + Hash: lo.PanicOnErr(ts.API.ProtocolParameters().Hash()), + }, + }, ts.Nodes()...) + ts.AssertAccountData(&accounts.AccountData{ + ID: wallet.BlockIssuer.AccountID, + Credits: accounts.NewBlockIssuanceCredits(walletBIC, block4Slot), + ExpirySlot: iotago.MaxSlotIndex, + OutputID: blockIssuerAccountOutput.OutputID(), + BlockIssuerKeys: wallet.BlockIssuer.BlockIssuerKeys(), + }, ts.Nodes()...) + } +} + /* For Mana allotment and stored: 1. Collect potential and stored on the input side. diff --git a/pkg/testsuite/blocks.go b/pkg/testsuite/blocks.go index 90e5c70ac..07691a1cb 100644 --- a/pkg/testsuite/blocks.go +++ b/pkg/testsuite/blocks.go @@ -58,6 +58,22 @@ func (t *TestSuite) AssertBlocksExist(blocks []*blocks.Block, expectedExist bool } } +func (t *TestSuite) AssertBlockFiltered(blocks []*blocks.Block, reason error, node *mock.Node) { + for _, block := range blocks { + t.Eventually(func() error { + for _, filteredBlockEvent := range node.FilteredBlocks() { + if filteredBlockEvent.Block.ID() == block.ID() { + if ierrors.Is(filteredBlockEvent.Reason, reason) { + return nil + } + } + } + + return ierrors.Errorf("AssertBlockFiltered: %s: block %s was not filtered by the CommitmentFilter", node.Name, block.ID()) + }) + } +} + func (t *TestSuite) assertBlocksInCacheWithFunc(expectedBlocks []*blocks.Block, propertyName string, expectedPropertyState bool, propertyFunc func(*blocks.Block) bool, nodes ...*mock.Node) { mustNodes(nodes) diff --git a/pkg/testsuite/mock/node.go b/pkg/testsuite/mock/node.go index b9bde387f..19afa5299 100644 --- a/pkg/testsuite/mock/node.go +++ b/pkg/testsuite/mock/node.go @@ -69,8 +69,9 @@ type Node struct { candidateEngineActivatedCount atomic.Uint32 mainEngineSwitchedCount atomic.Uint32 - mutex syncutils.RWMutex - attachedBlocks []*blocks.Block + mutex syncutils.RWMutex + attachedBlocks []*blocks.Block + filteredBlockEvents []*commitmentfilter.BlockFilteredEvent } func NewNode(t *testing.T, net *Network, partition string, name string, validator bool) *Node { @@ -149,6 +150,10 @@ func (n *Node) hookEvents() { events.CandidateEngineActivated.Hook(func(e *engine.Engine) { n.candidateEngineActivatedCount.Add(1) }) events.MainEngineSwitched.Hook(func(e *engine.Engine) { n.mainEngineSwitchedCount.Add(1) }) + + events.Engine.CommitmentFilter.BlockFiltered.Hook(func(event *commitmentfilter.BlockFilteredEvent) { + n.filteredBlockEvents = append(n.filteredBlockEvents, event) + }) } func (n *Node) hookLogging(failOnBlockFiltered bool) { @@ -506,6 +511,10 @@ func (n *Node) CandidateEngineActivatedCount() int { return int(n.candidateEngineActivatedCount.Load()) } +func (n *Node) FilteredBlocks() []*commitmentfilter.BlockFilteredEvent { + return n.filteredBlockEvents +} + func (n *Node) MainEngineSwitchedCount() int { return int(n.mainEngineSwitchedCount.Load()) } diff --git a/pkg/testsuite/mock/wallet_transactions.go b/pkg/testsuite/mock/wallet_transactions.go index 3d275b7ce..50c2cc1f1 100644 --- a/pkg/testsuite/mock/wallet_transactions.go +++ b/pkg/testsuite/mock/wallet_transactions.go @@ -37,7 +37,7 @@ func (w *Wallet) CreateAccountFromInput(transactionName string, inputName string outputStates = append(outputStates, remainderOutput) } - signedTransaction := lo.PanicOnErr(w.createSignedTransactionWithOptions( + signedTransaction := lo.PanicOnErr(w.CreateSignedTransactionWithOptions( transactionName, WithContextInputs(iotago.TxEssenceContextInputs{ &iotago.CommitmentInput{ @@ -86,7 +86,7 @@ func (w *Wallet) CreateDelegationFromInput(transactionName string, inputName str } // create the signed transaction - signedTransaction := lo.PanicOnErr(w.createSignedTransactionWithOptions( + signedTransaction := lo.PanicOnErr(w.CreateSignedTransactionWithOptions( transactionName, WithContextInputs(iotago.TxEssenceContextInputs{ &iotago.CommitmentInput{ @@ -120,7 +120,7 @@ func (w *Wallet) DelayedClaimingTransition(transactionName string, inputName str delegationOutput := delegationBuilder.MustBuild() - signedTransaction := lo.PanicOnErr(w.createSignedTransactionWithOptions( + signedTransaction := lo.PanicOnErr(w.CreateSignedTransactionWithOptions( transactionName, WithContextInputs(iotago.TxEssenceContextInputs{ &iotago.CommitmentInput{ @@ -149,7 +149,7 @@ func (w *Wallet) TransitionAccount(transactionName string, inputName string, opt accountBuilder := builder.NewAccountOutputBuilderFromPrevious(accountOutput) accountOutput = options.Apply(accountBuilder, opts).MustBuild() - signedTransaction := lo.PanicOnErr(w.createSignedTransactionWithOptions( + signedTransaction := lo.PanicOnErr(w.CreateSignedTransactionWithOptions( transactionName, WithAccountInput(input), WithContextInputs(iotago.TxEssenceContextInputs{ @@ -182,7 +182,7 @@ func (w *Wallet) DestroyAccount(transactionName string, inputName string, creati Features: iotago.BasicOutputFeatures{}, }} - signedTransaction := lo.PanicOnErr(w.createSignedTransactionWithOptions( + signedTransaction := lo.PanicOnErr(w.CreateSignedTransactionWithOptions( transactionName, WithContextInputs(iotago.TxEssenceContextInputs{ &iotago.BlockIssuanceCreditInput{ @@ -222,7 +222,7 @@ func (w *Wallet) CreateImplicitAccountFromInput(transactionName string, inputNam Features: iotago.BasicOutputFeatures{}, } - signedTransaction := lo.PanicOnErr(w.createSignedTransactionWithOptions( + signedTransaction := lo.PanicOnErr(w.CreateSignedTransactionWithOptions( transactionName, WithInputs(utxoledger.Outputs{input}), WithOutputs(iotago.Outputs[iotago.Output]{implicitAccountOutput, remainderBasicOutput}), @@ -254,7 +254,7 @@ func (w *Wallet) TransitionImplicitAccountToAccountOutput(transactionName string AccountID(iotago.AccountIDFromOutputID(input.OutputID())), opts).MustBuild() - signedTransaction := lo.PanicOnErr(w.createSignedTransactionWithOptions( + signedTransaction := lo.PanicOnErr(w.CreateSignedTransactionWithOptions( transactionName, WithContextInputs(iotago.TxEssenceContextInputs{ &iotago.BlockIssuanceCreditInput{ @@ -312,7 +312,7 @@ func (w *Wallet) CreateBasicOutputsEquallyFromInputs(transactionName string, out }) } - signedTransaction := lo.PanicOnErr(w.createSignedTransactionWithOptions( + signedTransaction := lo.PanicOnErr(w.CreateSignedTransactionWithOptions( transactionName, WithInputs(inputStates), WithOutputs(outputStates), @@ -321,7 +321,7 @@ func (w *Wallet) CreateBasicOutputsEquallyFromInputs(transactionName string, out return signedTransaction } -func (w *Wallet) createSignedTransactionWithOptions(transactionName string, opts ...options.Option[builder.TransactionBuilder]) (*iotago.SignedTransaction, error) { +func (w *Wallet) CreateSignedTransactionWithOptions(transactionName string, opts ...options.Option[builder.TransactionBuilder]) (*iotago.SignedTransaction, error) { currentAPI := w.Node.Protocol.CommittedAPI() txBuilder := builder.NewTransactionBuilder(currentAPI)