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 19 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 may only point to a delegation or account output")
}
}

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
233 changes: 233 additions & 0 deletions pkg/tests/reward_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,233 @@
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/protocol/engine/utxoledger"
"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/builder"
"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)
}

// 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)

input := ts.DefaultWallet().Output("Genesis:0")
nftOutput := builder.NewNFTOutputBuilder(ts.DefaultWallet().Address(), input.BaseTokenAmount()).MustBuild()
tx1 := ts.DefaultWallet().CreateSignedTransactionWithOptions(
"TX1",
mock.WithInputs(utxoledger.Outputs{input}),
mock.WithOutputs(iotago.Outputs[iotago.Output]{nftOutput}),
mock.WithAllotAllManaToAccount(ts.CurrentSlot(), ts.DefaultWallet().BlockIssuer.AccountID),
)

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
inputNFT := ts.DefaultWallet().Output("TX1:0")
prevNFT := inputNFT.Output().Clone().(*iotago.NFTOutput)
nftOutput = builder.NewNFTOutputBuilderFromPrevious(prevNFT).NFTID(iotago.NFTIDFromOutputID(inputNFT.OutputID())).MustBuild()

tx2 := ts.DefaultWallet().CreateSignedTransactionWithOptions(
"TX2",
mock.WithInputs(utxoledger.Outputs{inputNFT}),
mock.WithRewardInput(
&iotago.RewardInput{Index: 0},
0,
),
mock.WithCommitmentInput(&iotago.CommitmentInput{
CommitmentID: ts.DefaultWallet().Node.Protocol.MainEngineInstance().Storage.Settings().LatestCommitment().Commitment().MustID(),
}),
mock.WithOutputs(iotago.Outputs[iotago.Output]{nftOutput}),
mock.WithAllotAllManaToAccount(ts.CurrentSlot(), ts.DefaultWallet().BlockIssuer.AccountID),
)

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

ts.Wait(node1, node2)

// TODO: Assertions do not pass for node2 - why?
PhilippGackstatter marked this conversation as resolved.
Show resolved Hide resolved
ts.AssertTransactionsExist([]*iotago.Transaction{tx2.Transaction}, true, node1)
signedTx3ID := lo.PanicOnErr(tx2.ID())
ts.AssertTransactionFailure(signedTx3ID, iotago.ErrRewardInputInvalid, node1)
}
58 changes: 54 additions & 4 deletions pkg/testsuite/mock/node.go
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,11 @@
idAliases = make(map[peer.ID]string)
}

type InvalidSignedTransactionEvent struct {
Metadata mempool.SignedTransactionMetadata
Error error
}

type Node struct {
Testing *testing.T

Expand All @@ -69,10 +74,11 @@
candidateEngineActivatedCount atomic.Uint32
mainEngineSwitchedCount atomic.Uint32

mutex syncutils.RWMutex
attachedBlocks []*blocks.Block
currentSlot iotago.SlotIndex
mutex syncutils.RWMutex
attachedBlocks []*blocks.Block
currentSlot iotago.SlotIndex
filteredBlockEvents []*postsolidfilter.BlockFilteredEvent

Check failure on line 80 in pkg/testsuite/mock/node.go

View workflow job for this annotation

GitHub Actions / GolangCI-Lint

File is not `gofmt`-ed with `-s` (gofmt)
invalidTransactionEvents map[iotago.SignedTransactionID]InvalidSignedTransactionEvent
}

func NewNode(t *testing.T, net *Network, partition string, name string, validator bool) *Node {
Expand Down Expand Up @@ -107,7 +113,8 @@
Endpoint: net.JoinWithEndpointID(peerID, partition),
Workers: workerpool.NewGroup(name),

attachedBlocks: make([]*blocks.Block, 0),
attachedBlocks: make([]*blocks.Block, 0),
invalidTransactionEvents: make(map[iotago.SignedTransactionID]InvalidSignedTransactionEvent),
}
}

Expand Down Expand Up @@ -162,6 +169,33 @@

n.filteredBlockEvents = append(n.filteredBlockEvents, event)
})

n.Protocol.MainEngineInstance().Ledger.MemPool().OnSignedTransactionAttached(
func(signedTransactionMetadata mempool.SignedTransactionMetadata) {
signedTxID := signedTransactionMetadata.ID()

signedTransactionMetadata.OnSignaturesInvalid(func(err error) {
n.mutex.Lock()
defer n.mutex.Unlock()

n.invalidTransactionEvents[signedTxID] = InvalidSignedTransactionEvent{
Metadata: signedTransactionMetadata,
Error: err,
}
})

transactionMetadata := signedTransactionMetadata.TransactionMetadata()

transactionMetadata.OnInvalid(func(err error) {
n.mutex.Lock()
defer n.mutex.Unlock()

n.invalidTransactionEvents[signedTxID] = InvalidSignedTransactionEvent{
Metadata: signedTransactionMetadata,
Error: err,
}
})
})
}

func (n *Node) hookLogging(failOnBlockFiltered bool) {
Expand Down Expand Up @@ -419,6 +453,14 @@
fmt.Printf("%s > [%s] ConflictDAG.ConflictAccepted: %s\n", n.Name, engineName, conflictID)
})

instance.Ledger.MemPool().OnSignedTransactionAttached(
func(signedTransactionMetadata mempool.SignedTransactionMetadata) {
signedTransactionMetadata.OnSignaturesInvalid(func(err error) {
fmt.Printf("%s > [%s] MemPool.SignedTransactionSignaturesInvalid(%s): %s\n", n.Name, engineName, err, signedTransactionMetadata.ID())
})
},
)

instance.Ledger.OnTransactionAttached(func(transactionMetadata mempool.TransactionMetadata) {
fmt.Printf("%s > [%s] Ledger.TransactionAttached: %s\n", n.Name, engineName, transactionMetadata.ID())

Expand Down Expand Up @@ -531,6 +573,14 @@
return n.filteredBlockEvents
}

func (n *Node) TransactionFailure(txID iotago.SignedTransactionID) (InvalidSignedTransactionEvent, bool) {
n.mutex.RLock()
defer n.mutex.RUnlock()
event, exists := n.invalidTransactionEvents[txID]

return event, exists
}

func (n *Node) MainEngineSwitchedCount() int {
return int(n.mainEngineSwitchedCount.Load())
}
Expand Down
Loading
Loading