Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
21 changes: 19 additions & 2 deletions accounting/rewind.go
Original file line number Diff line number Diff line change
Expand Up @@ -147,6 +147,18 @@ func AccountAtRound(ctx context.Context, account models.Account, round uint64, d
}
case sdk.KeyRegistrationTx:
// TODO: keyreg does not rewind. workaround: query for txns on an account with typeenum=2 to find previous values it was set to.
// We can't rewind keyreg transactions, so null it out.
acct.Participation = nil
case sdk.ApplicationCallTx:
// We can't rewind app calls, so null out application state.
acct.AppsLocalState = nil
acct.AppsTotalExtraPages = nil
acct.AppsTotalSchema = nil
acct.CreatedApps = nil
acct.TotalAppsOptedIn = 0
acct.TotalBoxBytes = 0
acct.TotalBoxes = 0
acct.TotalCreatedApps = 0
case sdk.AssetConfigTx:
if stxn.Txn.ConfigAsset == 0 {
// create asset, unwind the application of the value
Expand Down Expand Up @@ -182,8 +194,13 @@ func AccountAtRound(ctx context.Context, account models.Account, round uint64, d
// MinBalance is not supported.
acct.MinBalance = 0

// TODO: Clear out the closed-at field as well. Like Rewards we cannot know this value for all accounts.
//acct.ClosedAt = 0
// Clear out the closed-at field as well. Like Rewards we cannot know this value for all accounts.
acct.ClosedAtRound = nil

// Clear out incentives fields, they change according to block header data, not transactions
acct.IncentiveEligible = nil
acct.LastHeartbeat = nil
acct.LastProposed = nil

return
}
143 changes: 143 additions & 0 deletions accounting/rewind_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -78,3 +78,146 @@ func TestStaleTransactions1(t *testing.T) {
account, err := AccountAtRound(context.Background(), account, 6, db)
assert.True(t, errors.As(err, &ConsistencyError{}), "err: %v", err)
}

// TestAllClearedFields verifies that all fields that should be reset during a rewind are properly cleared
func TestAllClearedFields(t *testing.T) {
var a sdk.Address
a[0] = 'a'

// Helper functions for pointer values
uint64Ptr := func(v uint64) *uint64 { return &v }
boolPtr := func(v bool) *bool { return &v }
bytesPtr := func(v []byte) *[]byte { return &v }
stringPtr := func(v string) *string { return &v }

// Create an account with ALL fields populated
account := models.Account{
Address: a.String(),
Amount: 1000,
AmountWithoutPendingRewards: 980,
PendingRewards: 20,
Rewards: 100,
Round: 10,
Status: "Online",
MinBalance: 200,

// Keyreg-related fields
Participation: &models.AccountParticipation{
VoteFirstValid: 100,
VoteLastValid: 200,
VoteKeyDilution: 10000,
VoteParticipationKey: []byte("votepk"),
SelectionParticipationKey: []byte("selpk"),
StateProofKey: bytesPtr([]byte("stpk")),
},

// App-related fields
AppsLocalState: &[]models.ApplicationLocalState{{Id: 123}},
AppsTotalExtraPages: uint64Ptr(2),
AppsTotalSchema: &models.ApplicationStateSchema{NumByteSlice: 10, NumUint: 10},
CreatedApps: &[]models.Application{{Id: 456}},
TotalAppsOptedIn: 5,
TotalBoxBytes: 1000,
TotalBoxes: 10,
TotalCreatedApps: 3,

// Asset-related fields
Assets: &[]models.AssetHolding{{AssetId: 789, Amount: 50}},
CreatedAssets: &[]models.Asset{{Index: 999}},
TotalAssetsOptedIn: 2,
TotalCreatedAssets: 1,

// Fields set at account creation/deletion
ClosedAtRound: uint64Ptr(500),
CreatedAtRound: uint64Ptr(1),
Deleted: boolPtr(false),

// Incentive fields
IncentiveEligible: boolPtr(true),
LastHeartbeat: uint64Ptr(7),
LastProposed: uint64Ptr(6),

// Auth fields
AuthAddr: stringPtr("authaddr"),
SigType: (*models.AccountSigType)(stringPtr(string(models.AccountSigTypeSig))),
}

// Create various transaction types for testing
txns := []idb.TxnRow{
{ // Application call
Round: 10,
Txn: &sdk.SignedTxnWithAD{SignedTxn: sdk.SignedTxn{
Txn: sdk.Transaction{Type: sdk.ApplicationCallTx, Header: sdk.Header{Sender: a}},
}},
},
{ // Key registration
Round: 9,
Txn: &sdk.SignedTxnWithAD{SignedTxn: sdk.SignedTxn{
Txn: sdk.Transaction{Type: sdk.KeyRegistrationTx, Header: sdk.Header{Sender: a}},
}},
},
{ // Payment
Round: 8,
Txn: &sdk.SignedTxnWithAD{SignedTxn: sdk.SignedTxn{
Txn: sdk.Transaction{
Type: sdk.PaymentTx,
Header: sdk.Header{Sender: a},
PaymentTxnFields: sdk.PaymentTxnFields{Amount: 10},
},
}},
},
}

// Set up mock DB
ch := make(chan idb.TxnRow, len(txns))
for _, txn := range txns {
ch <- txn
}
close(ch)
var outCh <-chan idb.TxnRow = ch

db := &mocks.IndexerDb{}
db.On("GetSpecialAccounts", mock.Anything).Return(types.SpecialAddresses{}, nil)
db.On("Transactions", mock.Anything, mock.Anything).Return(outCh, uint64(10))

// Run the rewind
result, err := AccountAtRound(context.Background(), account, 5, db)
assert.NoError(t, err)

// Verify all fields that should be reset or zeroed out

// Fields that should be preserved/changed correctly
assert.Equal(t, a.String(), result.Address, "Address should be preserved")

// Fields that are explicitly zeroed out
assert.Equal(t, uint64(0), result.Rewards, "Rewards should be 0")
assert.Equal(t, uint64(0), result.PendingRewards, "PendingRewards should be 0")
assert.Equal(t, uint64(0), result.MinBalance, "MinBalance should be 0")

// Fields that are explicitly set to nil
assert.Nil(t, result.ClosedAtRound, "ClosedAtRound should be nil")

// Fields nulled out by KeyRegistrationTx
assert.Nil(t, result.Participation, "Participation should be nil")

// Fields nulled out by ApplicationCallTx
assert.Nil(t, result.AppsLocalState, "AppsLocalState should be nil")
assert.Nil(t, result.AppsTotalExtraPages, "AppsTotalExtraPages should be nil")
assert.Nil(t, result.AppsTotalSchema, "AppsTotalSchema should be nil")
assert.Nil(t, result.CreatedApps, "CreatedApps should be nil")
assert.Equal(t, uint64(0), result.TotalAppsOptedIn, "TotalAppsOptedIn should be 0")
assert.Equal(t, uint64(0), result.TotalBoxBytes, "TotalBoxBytes should be 0")
assert.Equal(t, uint64(0), result.TotalBoxes, "TotalBoxes should be 0")
assert.Equal(t, uint64(0), result.TotalCreatedApps, "TotalCreatedApps should be 0")

// Incentive fields explicitly set to nil
assert.Nil(t, result.IncentiveEligible, "IncentiveEligible should be nil")
assert.Nil(t, result.LastHeartbeat, "LastHeartbeat should be nil")
assert.Nil(t, result.LastProposed, "LastProposed should be nil")

// Assets should be preserved (although updated with AssetConfigTx/AssetTransferTx)
assert.NotNil(t, result.Assets, "Assets should not be nil")

// Round should be set to the target round
assert.Equal(t, uint64(5), result.Round, "Round should be set to target round")
}
Loading