Skip to content

Commit

Permalink
Merge pull request #551 from iotaledger/reward-tests
Browse files Browse the repository at this point in the history
Add Reward Input tests
  • Loading branch information
PhilippGackstatter authored Nov 30, 2023
2 parents eb51b7b + 80e4cb3 commit aefe705
Show file tree
Hide file tree
Showing 10 changed files with 455 additions and 35 deletions.
11 changes: 8 additions & 3 deletions pkg/protocol/engine/ledger/ledger/ledger.go
Original file line number Diff line number Diff line change
Expand Up @@ -545,15 +545,20 @@ func (l *Ledger) processCreatedAndConsumedAccountOutputs(stateDiff mempool.State
switch spentOutput.OutputType() {
case iotago.OutputAccount:
consumedAccount, _ := spentOutput.Output().(*iotago.AccountOutput)
accountID := consumedAccount.AccountID
if accountID == iotago.EmptyAccountID {
accountID = iotago.AccountIDFromOutputID(spentOutput.OutputID())
}

// if we transition / destroy an account output that doesn't have a block issuer feature or staking, we don't need to track the changes.
if consumedAccount.FeatureSet().BlockIssuer() == nil && consumedAccount.FeatureSet().Staking() == nil {
return true
}
consumedAccounts[consumedAccount.AccountID] = spentOutput
consumedAccounts[accountID] = spentOutput

// if we have consumed accounts that are not created in the same slot, we need to track them as destroyed
if _, exists := createdAccounts[consumedAccount.AccountID]; !exists {
destroyedAccounts.Add(consumedAccount.AccountID)
if _, exists := createdAccounts[accountID]; !exists {
destroyedAccounts.Add(accountID)
}

case iotago.OutputDelegation:
Expand Down
2 changes: 2 additions & 0 deletions pkg/protocol/engine/ledger/ledger/vm.go
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,8 @@ func (v *VM) ValidateSignatures(signedTransaction mempool.SignedTransaction, res
}

rewardInputSet[delegationID] = reward
default:
return nil, ierrors.Wrapf(iotago.ErrRewardInputInvalid, "reward input cannot point to %s", output.Output().Type())
}
}

Expand Down
3 changes: 1 addition & 2 deletions pkg/tests/accounts_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -238,10 +238,9 @@ func Test_StakeDelegateAndDelayedClaim(t *testing.T) {
mock.WithBlockIssuerFeature(iotago.BlockIssuerKeys{newAccountBlockIssuerKey}, newAccountExpirySlot),
mock.WithStakingFeature(stakedAmount, 421, 0, 10),
mock.WithAccountAmount(validatorAccountAmount),
mock.WithAccountMana(mock.MaxBlockManaCost(ts.DefaultWallet().Node.Protocol.CommittedAPI().ProtocolParameters())),
)

genesisCommitment := iotago.NewEmptyCommitment(ts.API)
genesisCommitment.ReferenceManaCost = ts.API.ProtocolParameters().CongestionControlParameters().MinReferenceManaCost
ts.SetCurrentSlot(block1Slot)
block1 := ts.IssueBasicBlockWithOptions("block1", ts.DefaultWallet(), tx1)
latestParents := ts.CommitUntilSlot(block1Slot, block1.ID())
Expand Down
259 changes: 259 additions & 0 deletions pkg/tests/reward_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,259 @@
package tests

import (
"testing"

"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/testsuite"
"github.com/iotaledger/iota-core/pkg/testsuite/mock"
"github.com/iotaledger/iota-core/pkg/utils"

iotago "github.com/iotaledger/iota.go/v4"
"github.com/iotaledger/iota.go/v4/tpkg"
)

func setupDelegationTestsuite(t *testing.T) (*testsuite.TestSuite, *mock.Node, *mock.Node) {
ts := testsuite.NewTestSuite(t,
testsuite.WithProtocolParametersOptions(
iotago.WithTimeProviderOptions(
0,
testsuite.GenesisTimeWithOffsetBySlots(1000, testsuite.DefaultSlotDurationInSeconds),
testsuite.DefaultSlotDurationInSeconds,
8,
),
iotago.WithLivenessOptions(
testsuite.DefaultLivenessThresholdLowerBoundInSeconds,
testsuite.DefaultLivenessThresholdUpperBoundInSeconds,
testsuite.DefaultMinCommittableAge,
100,
120,
),
),
)

// 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.
node2 := ts.AddNode("node2")
// Add a default block issuer to the network. This will add another block issuer account to the snapshot.
wallet := ts.AddDefaultWallet(node1)

ts.Run(true)

// Assert validator and block issuer accounts in genesis snapshot.
// Validator node account.
validatorAccountOutput := ts.AccountOutput("Genesis:1")
ts.AssertAccountData(&accounts.AccountData{
ID: node1.Validator.AccountID,
Credits: accounts.NewBlockIssuanceCredits(iotago.MaxBlockIssuanceCredits/2, 0),
OutputID: validatorAccountOutput.OutputID(),
ExpirySlot: iotago.MaxSlotIndex,
BlockIssuerKeys: node1.Validator.BlockIssuerKeys(),
StakeEndEpoch: iotago.MaxEpochIndex,
ValidatorStake: mock.MinValidatorAccountAmount(ts.API.ProtocolParameters()),
}, ts.Nodes()...)
// Default wallet block issuer account.
blockIssuerAccountOutput := ts.AccountOutput("Genesis:2")
ts.AssertAccountData(&accounts.AccountData{
ID: wallet.BlockIssuer.AccountID,
Credits: accounts.NewBlockIssuanceCredits(iotago.MaxBlockIssuanceCredits/2, 0),
OutputID: blockIssuerAccountOutput.OutputID(),
ExpirySlot: iotago.MaxSlotIndex,
BlockIssuerKeys: wallet.BlockIssuer.BlockIssuerKeys(),
}, ts.Nodes()...)

return ts, node1, node2
}

// Test that a Delegation Output which delegates to an account which does not exist / did not receive rewards
// can be destroyed.
func Test_Delegation_DestroyOutputWithoutRewards(t *testing.T) {
ts, node1, node2 := setupDelegationTestsuite(t)
defer ts.Shutdown()

// CREATE DELEGATION TO NEW ACCOUNT FROM BASIC UTXO
accountAddress := tpkg.RandAccountAddress()
var block1Slot iotago.SlotIndex = 1
ts.SetCurrentSlot(block1Slot)
tx1 := ts.DefaultWallet().CreateDelegationFromInput(
"TX1",
"Genesis:0",
mock.WithDelegatedValidatorAddress(accountAddress),
mock.WithDelegationStartEpoch(1),
)
block1 := ts.IssueBasicBlockWithOptions("block1", ts.DefaultWallet(), tx1)

latestParents := ts.CommitUntilSlot(block1Slot, block1.ID())

block2Slot := ts.CurrentSlot()
tx2 := ts.DefaultWallet().ClaimDelegatorRewards("TX2", "TX1:0")
block2 := ts.IssueBasicBlockWithOptions("block2", ts.DefaultWallet(), tx2, mock.WithStrongParents(latestParents...))

ts.CommitUntilSlot(block2Slot, block2.ID())

ts.AssertTransactionsExist([]*iotago.Transaction{tx2.Transaction}, true, node1, node2)
ts.AssertTransactionsInCacheAccepted([]*iotago.Transaction{tx2.Transaction}, true, node1, node2)
}

func Test_Delegation_DelayedClaimingDestroyOutputWithoutRewards(t *testing.T) {
ts, node1, node2 := setupDelegationTestsuite(t)
defer ts.Shutdown()

// CREATE DELEGATION TO NEW ACCOUNT FROM BASIC UTXO
accountAddress := tpkg.RandAccountAddress()
var block1_2Slot iotago.SlotIndex = 1
ts.SetCurrentSlot(block1_2Slot)
tx1 := ts.DefaultWallet().CreateDelegationFromInput(
"TX1",
"Genesis:0",
mock.WithDelegatedValidatorAddress(accountAddress),
mock.WithDelegationStartEpoch(1),
)
block1 := ts.IssueBasicBlockWithOptions("block1", ts.DefaultWallet(), tx1)

// TRANSITION TO DELAYED CLAIMING (IN THE SAME SLOT)
latestCommitment := ts.DefaultWallet().Node.Protocol.MainEngineInstance().Storage.Settings().LatestCommitment()
apiForSlot := ts.DefaultWallet().Node.Protocol.APIForSlot(block1_2Slot)

futureBoundedSlotIndex := latestCommitment.Slot() + apiForSlot.ProtocolParameters().MinCommittableAge()
futureBoundedEpochIndex := apiForSlot.TimeProvider().EpochFromSlot(futureBoundedSlotIndex)

registrationSlot := apiForSlot.TimeProvider().EpochEnd(apiForSlot.TimeProvider().EpochFromSlot(block1_2Slot))
delegationEndEpoch := futureBoundedEpochIndex
if futureBoundedSlotIndex > registrationSlot {
delegationEndEpoch = futureBoundedEpochIndex + 1
}

tx2 := ts.DefaultWallet().DelayedClaimingTransition("TX2", "TX1:0", delegationEndEpoch)
block2 := ts.IssueBasicBlockWithOptions("block2", ts.DefaultWallet(), tx2, mock.WithStrongParents(block1.ID()))
latestParents := ts.CommitUntilSlot(block1_2Slot, block2.ID())

// CLAIM ZERO REWARDS
block3Slot := ts.CurrentSlot()
tx3 := ts.DefaultWallet().ClaimDelegatorRewards("TX3", "TX2:0")
block3 := ts.IssueBasicBlockWithOptions("block3", ts.DefaultWallet(), tx3, mock.WithStrongParents(latestParents...))

ts.CommitUntilSlot(block3Slot, block3.ID())

ts.AssertTransactionsExist([]*iotago.Transaction{tx3.Transaction}, true, node1, node2)
ts.AssertTransactionsInCacheAccepted([]*iotago.Transaction{tx3.Transaction}, true, node1, node2)
}

// Test that a staking Account which did not earn rewards can remove its staking feature.
func Test_Account_RemoveStakingFeatureWithoutRewards(t *testing.T) {
ts, node1, node2 := setupDelegationTestsuite(t)
defer ts.Shutdown()

// CREATE NEW ACCOUNT WITH BLOCK ISSUER AND STAKING FEATURES FROM BASIC UTXO
var block1Slot iotago.SlotIndex = 1
ts.SetCurrentSlot(block1Slot)

// Set end epoch so the staking feature can be removed as soon as possible.
unbondingPeriod := ts.API.ProtocolParameters().StakingUnbondingPeriod()
startEpoch := ts.API.TimeProvider().EpochFromSlot(block1Slot + ts.API.ProtocolParameters().MaxCommittableAge())
endEpoch := startEpoch + unbondingPeriod
// The earliest epoch in which we can remove the staking feature and claim rewards.
claimingEpoch := endEpoch + 1
// Random fixed cost amount.
fixedCost := iotago.Mana(421)
stakedAmount := mock.MinValidatorAccountAmount(ts.API.ProtocolParameters())

blockIssuerFeatKey := utils.RandBlockIssuerKey()
// Set the expiry slot beyond the end epoch of the staking feature so we don't have to remove the feature.
blockIssuerFeatExpirySlot := ts.API.TimeProvider().EpochEnd(claimingEpoch)

tx1 := ts.DefaultWallet().CreateAccountFromInput(
"TX1",
"Genesis:0",
ts.DefaultWallet(),
mock.WithBlockIssuerFeature(iotago.BlockIssuerKeys{blockIssuerFeatKey}, blockIssuerFeatExpirySlot),
mock.WithStakingFeature(stakedAmount, fixedCost, startEpoch, endEpoch),
mock.WithAccountAmount(stakedAmount),
)

block1 := ts.IssueBasicBlockWithOptions("block1", ts.DefaultWallet(), tx1)

latestParents := ts.CommitUntilSlot(block1Slot, block1.ID())

ts.AssertTransactionsExist([]*iotago.Transaction{tx1.Transaction}, true, node1, node2)
ts.AssertTransactionsInCacheAccepted([]*iotago.Transaction{tx1.Transaction}, true, node1, node2)

// Commit until the claiming epoch.
latestParents = ts.CommitUntilSlot(ts.API.TimeProvider().EpochStart(claimingEpoch), latestParents...)

// REMOVE STAKING FEATURE AND CLAIM ZERO REWARDS
block2Slot := ts.CurrentSlot()
tx2 := ts.DefaultWallet().ClaimValidatorRewards("TX2", "TX1:0")
block2 := ts.IssueBasicBlockWithOptions("block2", ts.DefaultWallet(), tx2, mock.WithStrongParents(latestParents...))

ts.CommitUntilSlot(block2Slot, block2.ID())

ts.AssertTransactionsExist([]*iotago.Transaction{tx2.Transaction}, true, node1)
ts.AssertTransactionsInCacheAccepted([]*iotago.Transaction{tx2.Transaction}, true, node1)
accountOutput := ts.DefaultWallet().Output("TX2:0")
accountID := accountOutput.Output().(*iotago.AccountOutput).AccountID

ts.AssertAccountData(&accounts.AccountData{
ID: accountID,
Credits: &accounts.BlockIssuanceCredits{Value: 0, UpdateSlot: block1Slot},
OutputID: accountOutput.OutputID(),
ExpirySlot: blockIssuerFeatExpirySlot,
BlockIssuerKeys: iotago.BlockIssuerKeys{blockIssuerFeatKey},
StakeEndEpoch: 0,
ValidatorStake: 0,
}, ts.Nodes()...)

ts.AssertAccountDiff(accountID, block2Slot, &model.AccountDiff{
BICChange: -iotago.BlockIssuanceCredits(0),
PreviousUpdatedSlot: 0,
NewExpirySlot: blockIssuerFeatExpirySlot,
PreviousExpirySlot: blockIssuerFeatExpirySlot,
NewOutputID: accountOutput.OutputID(),
PreviousOutputID: ts.DefaultWallet().Output("TX1:0").OutputID(),
BlockIssuerKeysAdded: iotago.NewBlockIssuerKeys(),
BlockIssuerKeysRemoved: iotago.NewBlockIssuerKeys(),
ValidatorStakeChange: -int64(stakedAmount),
StakeEndEpochChange: -int64(endEpoch),
FixedCostChange: -int64(fixedCost),
DelegationStakeChange: 0,
}, false, ts.Nodes()...)
}

func Test_RewardInputCannotPointToNFTOutput(t *testing.T) {
ts, node1, node2 := setupDelegationTestsuite(t)
defer ts.Shutdown()

// CREATE NFT FROM BASIC UTXO
var block1Slot iotago.SlotIndex = 1
ts.SetCurrentSlot(block1Slot)

tx1 := ts.DefaultWallet().CreateNFTFromInput("TX1", "Genesis:0")
block1 := ts.IssueBasicBlockWithOptions("block1", ts.DefaultWallet(), tx1)

latestParents := ts.CommitUntilSlot(block1Slot, block1.ID())

ts.AssertTransactionsExist([]*iotago.Transaction{tx1.Transaction}, true, node1, node2)
ts.AssertTransactionsInCacheAccepted([]*iotago.Transaction{tx1.Transaction}, true, node1, node2)

// ATTEMPT TO POINT REWARD INPUT TO AN NFT OUTPUT
tx2 := ts.DefaultWallet().TransitionNFTWithTransactionOpts("TX2", "TX1:0",
mock.WithRewardInput(
&iotago.RewardInput{Index: 0},
0,
),
mock.WithCommitmentInput(&iotago.CommitmentInput{
CommitmentID: ts.DefaultWallet().Node.Protocol.MainEngineInstance().Storage.Settings().LatestCommitment().Commitment().MustID(),
}))

ts.IssueBasicBlockWithOptions("block2", ts.DefaultWallet(), tx2, mock.WithStrongParents(latestParents...))

ts.Wait(node1, node2)

// TODO: Assertions do not pass for node2 because the block does not get forwarded from node1.
// node2 should be added in the assertion when issue iotaledger/iota-core#580 is fixed.
ts.AssertTransactionsExist([]*iotago.Transaction{tx2.Transaction}, true, node1)
signedTx2ID := lo.PanicOnErr(tx2.ID())
ts.AssertTransactionFailure(signedTx2ID, iotago.ErrRewardInputInvalid, node1)
}
20 changes: 10 additions & 10 deletions pkg/testsuite/accounts.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,8 @@ import (
)

func (t *TestSuite) AssertAccountData(accountData *accounts.AccountData, nodes ...*mock.Node) {
t.Eventually(func() error {
for _, node := range nodes {
for _, node := range nodes {
t.Eventually(func() error {
actualAccountData, exists, err := node.Protocol.MainEngineInstance().Ledger.Account(accountData.ID, node.Protocol.MainEngineInstance().SyncManager.LatestCommitment().Slot())
if err != nil {
return ierrors.Wrap(err, "AssertAccountData: failed to load account data")
Expand Down Expand Up @@ -64,15 +64,15 @@ func (t *TestSuite) AssertAccountData(accountData *accounts.AccountData, nodes .
if accountData.LatestSupportedProtocolVersionAndHash != actualAccountData.LatestSupportedProtocolVersionAndHash {
return ierrors.Errorf("AssertAccountData: %s: accountID %s expected latest supported protocol version and hash %d, got %d", node.Name, accountData.ID, accountData.LatestSupportedProtocolVersionAndHash, actualAccountData.LatestSupportedProtocolVersionAndHash)
}
}

return nil
})
return nil
})
}
}

func (t *TestSuite) AssertAccountDiff(accountID iotago.AccountID, index iotago.SlotIndex, accountDiff *model.AccountDiff, destroyed bool, nodes ...*mock.Node) {
t.Eventually(func() error {
for _, node := range nodes {
for _, node := range nodes {
t.Eventually(func() error {

accountsDiffStorage, err := node.Protocol.MainEngineInstance().Storage.AccountDiffs(index)
if err != nil {
Expand Down Expand Up @@ -149,8 +149,8 @@ func (t *TestSuite) AssertAccountDiff(accountID iotago.AccountID, index iotago.S
if !assert.Equal(t.fakeTesting, accountDiff.NewLatestSupportedVersionAndHash, actualAccountDiff.NewLatestSupportedVersionAndHash) {
return ierrors.Errorf("AssertAccountDiff: %s: expected new latest supported protocol version change %d but actual %d for account %s at slot %d", node.Name, accountDiff.NewLatestSupportedVersionAndHash, actualAccountDiff.NewLatestSupportedVersionAndHash, accountID, index)
}
}

return nil
})
return nil
})
}
}
Loading

0 comments on commit aefe705

Please sign in to comment.