Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add Reward Input tests #551

Merged
merged 31 commits into from
Nov 30, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
2eae7a8
Add DO with empty reward can be destroyed test
PhilippGackstatter Nov 23, 2023
81a3c66
Export Create Signed TX func
PhilippGackstatter Nov 23, 2023
bcc7faf
Add reward input pointing to disallowed output tst
PhilippGackstatter Nov 23, 2023
6063f0b
Make transaction failure assertion more generic
PhilippGackstatter Nov 23, 2023
bddf697
Use transaction failure listener in testsuite
PhilippGackstatter Nov 23, 2023
d4891b6
Use pre-computed tx id
PhilippGackstatter Nov 23, 2023
18aa7b1
Merge remote-tracking branch 'origin/develop' into reward-tests
PhilippGackstatter Nov 24, 2023
fa05af8
Merge remote-tracking branch 'origin/develop' into reward-tests
PhilippGackstatter Nov 24, 2023
d796ac9
Fix post merge issues
PhilippGackstatter Nov 24, 2023
3c4dcc8
Remove unnecessary genesis commitment
PhilippGackstatter Nov 24, 2023
364df7e
Simplify test setup
PhilippGackstatter Nov 24, 2023
04f2ee3
Fix account ID when zero
PhilippGackstatter Nov 28, 2023
4f294bc
Finish claim validator reward implementation
PhilippGackstatter Nov 28, 2023
e90a71b
Test staking feat with 0 rewards can be removed
PhilippGackstatter Nov 28, 2023
baf447b
Merge remote-tracking branch 'origin/develop' into reward-tests
PhilippGackstatter Nov 28, 2023
7d8b626
Fix post merge issue
PhilippGackstatter Nov 28, 2023
3683907
Log signatures invalid
PhilippGackstatter Nov 28, 2023
afd3cd8
Update to latest iota.go
PhilippGackstatter Nov 28, 2023
5cdba0d
Merge remote-tracking branch 'origin/develop' into reward-tests
PhilippGackstatter Nov 29, 2023
a6c4517
Go fmt
PhilippGackstatter Nov 29, 2023
9587170
Create dedicated functions for NFT test
PhilippGackstatter Nov 29, 2023
82a474d
Make `createSignedTransactionWithOptions` private
PhilippGackstatter Nov 29, 2023
493f5b6
Take Potential Mana into account in reward funcs
PhilippGackstatter Nov 29, 2023
1e288b2
Decay stored Mana, fix lints
PhilippGackstatter Nov 29, 2023
6c9be22
Improve reward input pointing to wrong output err
PhilippGackstatter Nov 30, 2023
1abd80b
Add delayed claiming test
PhilippGackstatter Nov 30, 2023
138ff4b
Transition to delayed claiming in same slot
PhilippGackstatter Nov 30, 2023
2c3b32f
Simplify potential mana helper and add stored mana
PhilippGackstatter Nov 30, 2023
0169f63
Add note on outstanding test issue
PhilippGackstatter Nov 30, 2023
ba9d463
Loop over nodes outside `t.Eventually`
PhilippGackstatter Nov 30, 2023
80e4cb3
Fix the empty line before return lint
PhilippGackstatter Nov 30, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading