Skip to content

Commit 0845773

Browse files
authored
Rewind: Re-enable Rewinding (#1645)
* Revert "Disable rewinding and reject the query param on account lookups and searches. (#1630)" This reverts commit 249016c. The synthetic transaction implementation of payouts in the indexer should allow balances retrieved via rewind to calculate as before. * GCI lint warning fix. * Add HeartbeatTxn to be ignored during rewinding.
1 parent f8b161f commit 0845773

File tree

12 files changed

+605
-309
lines changed

12 files changed

+605
-309
lines changed

accounting/rewind.go

Lines changed: 189 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,189 @@
1+
package accounting
2+
3+
import (
4+
"context"
5+
"fmt"
6+
7+
models "github.com/algorand/indexer/v3/api/generated/v2"
8+
"github.com/algorand/indexer/v3/idb"
9+
"github.com/algorand/indexer/v3/types"
10+
11+
sdk "github.com/algorand/go-algorand-sdk/v2/types"
12+
)
13+
14+
// ConsistencyError is returned when the database returns inconsistent (stale) results.
15+
type ConsistencyError struct {
16+
msg string
17+
}
18+
19+
func (e ConsistencyError) Error() string {
20+
return e.msg
21+
}
22+
23+
func assetUpdate(account *models.Account, assetid uint64, add, sub uint64) {
24+
if account.Assets == nil {
25+
account.Assets = new([]models.AssetHolding)
26+
}
27+
assets := *account.Assets
28+
for i, ah := range assets {
29+
if ah.AssetId == assetid {
30+
ah.Amount += add
31+
ah.Amount -= sub
32+
assets[i] = ah
33+
// found and updated asset, done
34+
return
35+
}
36+
}
37+
// add asset to list
38+
assets = append(assets, models.AssetHolding{
39+
Amount: add - sub,
40+
AssetId: assetid,
41+
//Creator: base32 addr string of asset creator, TODO
42+
//IsFrozen: leave nil? // TODO: on close record frozen state for rewind
43+
})
44+
*account.Assets = assets
45+
}
46+
47+
// SpecialAccountRewindError indicates that an attempt was made to rewind one of the special accounts.
48+
type SpecialAccountRewindError struct {
49+
account string
50+
}
51+
52+
// MakeSpecialAccountRewindError helper to initialize a SpecialAccountRewindError.
53+
func MakeSpecialAccountRewindError(account string) *SpecialAccountRewindError {
54+
return &SpecialAccountRewindError{account: account}
55+
}
56+
57+
// Error is part of the error interface.
58+
func (sare *SpecialAccountRewindError) Error() string {
59+
return fmt.Sprintf("unable to rewind the %s", sare.account)
60+
}
61+
62+
var specialAccounts *types.SpecialAddresses
63+
64+
// AccountAtRound queries the idb.IndexerDb object for transactions and rewinds most fields of the account back to
65+
// their values at the requested round.
66+
// `round` must be <= `account.Round`
67+
func AccountAtRound(ctx context.Context, account models.Account, round uint64, db idb.IndexerDb) (acct models.Account, err error) {
68+
// Make sure special accounts cache has been initialized.
69+
if specialAccounts == nil {
70+
var accounts types.SpecialAddresses
71+
accounts, err = db.GetSpecialAccounts(ctx)
72+
if err != nil {
73+
return models.Account{}, fmt.Errorf("unable to get special accounts: %v", err)
74+
}
75+
specialAccounts = &accounts
76+
}
77+
78+
acct = account
79+
var addr sdk.Address
80+
addr, err = sdk.DecodeAddress(account.Address)
81+
if err != nil {
82+
return
83+
}
84+
85+
// ensure that the don't attempt to rewind a special account.
86+
if specialAccounts.FeeSink == addr {
87+
err = MakeSpecialAccountRewindError("FeeSink")
88+
return
89+
}
90+
if specialAccounts.RewardsPool == addr {
91+
err = MakeSpecialAccountRewindError("RewardsPool")
92+
return
93+
}
94+
95+
// Get transactions and rewind account.
96+
tf := idb.TransactionFilter{
97+
Address: addr[:],
98+
MinRound: round + 1,
99+
MaxRound: account.Round,
100+
}
101+
ctx2, cf := context.WithCancel(ctx)
102+
// In case of a panic before the next defer, call cf() here.
103+
defer cf()
104+
txns, r := db.Transactions(ctx2, tf)
105+
// In case of an error, make sure the context is cancelled, and the channel is cleaned up.
106+
defer func() {
107+
cf()
108+
for range txns {
109+
}
110+
}()
111+
if r < account.Round {
112+
err = ConsistencyError{fmt.Sprintf("queried round r: %d < account.Round: %d", r, account.Round)}
113+
return
114+
}
115+
txcount := 0
116+
for txnrow := range txns {
117+
if txnrow.Error != nil {
118+
err = txnrow.Error
119+
return
120+
}
121+
txcount++
122+
stxn := txnrow.Txn
123+
if stxn == nil {
124+
return models.Account{},
125+
fmt.Errorf("rewinding past inner transactions is not supported")
126+
}
127+
if addr == stxn.Txn.Sender {
128+
acct.AmountWithoutPendingRewards += uint64(stxn.Txn.Fee)
129+
acct.AmountWithoutPendingRewards -= uint64(stxn.SenderRewards)
130+
}
131+
switch stxn.Txn.Type {
132+
case sdk.PaymentTx:
133+
if addr == stxn.Txn.Sender {
134+
acct.AmountWithoutPendingRewards += uint64(stxn.Txn.Amount)
135+
}
136+
if addr == stxn.Txn.Receiver {
137+
acct.AmountWithoutPendingRewards -= uint64(stxn.Txn.Amount)
138+
acct.AmountWithoutPendingRewards -= uint64(stxn.ReceiverRewards)
139+
}
140+
if addr == stxn.Txn.CloseRemainderTo {
141+
// unwind receiving a close-to
142+
acct.AmountWithoutPendingRewards -= uint64(stxn.ClosingAmount)
143+
acct.AmountWithoutPendingRewards -= uint64(stxn.CloseRewards)
144+
} else if !stxn.Txn.CloseRemainderTo.IsZero() {
145+
// unwind sending a close-to
146+
acct.AmountWithoutPendingRewards += uint64(stxn.ClosingAmount)
147+
}
148+
case sdk.KeyRegistrationTx:
149+
// TODO: keyreg does not rewind. workaround: query for txns on an account with typeenum=2 to find previous values it was set to.
150+
case sdk.AssetConfigTx:
151+
if stxn.Txn.ConfigAsset == 0 {
152+
// create asset, unwind the application of the value
153+
assetUpdate(&acct, txnrow.AssetID, 0, stxn.Txn.AssetParams.Total)
154+
}
155+
case sdk.AssetTransferTx:
156+
if addr == stxn.Txn.AssetSender || addr == stxn.Txn.Sender {
157+
assetUpdate(&acct, uint64(stxn.Txn.XferAsset), stxn.Txn.AssetAmount+txnrow.Extra.AssetCloseAmount, 0)
158+
}
159+
if addr == stxn.Txn.AssetReceiver {
160+
assetUpdate(&acct, uint64(stxn.Txn.XferAsset), 0, stxn.Txn.AssetAmount)
161+
}
162+
if addr == stxn.Txn.AssetCloseTo {
163+
assetUpdate(&acct, uint64(stxn.Txn.XferAsset), 0, txnrow.Extra.AssetCloseAmount)
164+
}
165+
case sdk.AssetFreezeTx:
166+
case sdk.HeartbeatTx:
167+
default:
168+
err = fmt.Errorf("%s[%d,%d]: rewinding past txn type %s is not currently supported", account.Address, txnrow.Round, txnrow.Intra, stxn.Txn.Type)
169+
return
170+
}
171+
}
172+
173+
acct.Round = round
174+
175+
// Due to accounts being closed and re-opened, we cannot always rewind Rewards. So clear it out.
176+
acct.Rewards = 0
177+
178+
// Computing pending rewards is not supported.
179+
acct.PendingRewards = 0
180+
acct.Amount = acct.AmountWithoutPendingRewards
181+
182+
// MinBalance is not supported.
183+
acct.MinBalance = 0
184+
185+
// TODO: Clear out the closed-at field as well. Like Rewards we cannot know this value for all accounts.
186+
//acct.ClosedAt = 0
187+
188+
return
189+
}

accounting/rewind_test.go

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
package accounting
2+
3+
import (
4+
"context"
5+
"errors"
6+
"testing"
7+
8+
"github.com/stretchr/testify/assert"
9+
"github.com/stretchr/testify/mock"
10+
11+
models "github.com/algorand/indexer/v3/api/generated/v2"
12+
"github.com/algorand/indexer/v3/idb"
13+
"github.com/algorand/indexer/v3/idb/mocks"
14+
"github.com/algorand/indexer/v3/types"
15+
16+
sdk "github.com/algorand/go-algorand-sdk/v2/types"
17+
)
18+
19+
func TestBasic(t *testing.T) {
20+
var a sdk.Address
21+
a[0] = 'a'
22+
23+
account := models.Account{
24+
Address: a.String(),
25+
Amount: 100,
26+
AmountWithoutPendingRewards: 100,
27+
Round: 8,
28+
}
29+
30+
txnRow := idb.TxnRow{
31+
Round: 7,
32+
Txn: &sdk.SignedTxnWithAD{
33+
SignedTxn: sdk.SignedTxn{
34+
Txn: sdk.Transaction{
35+
Type: sdk.PaymentTx,
36+
PaymentTxnFields: sdk.PaymentTxnFields{
37+
Receiver: a,
38+
Amount: sdk.MicroAlgos(2),
39+
},
40+
},
41+
},
42+
},
43+
}
44+
45+
ch := make(chan idb.TxnRow, 1)
46+
ch <- txnRow
47+
close(ch)
48+
var outCh <-chan idb.TxnRow = ch
49+
50+
db := &mocks.IndexerDb{}
51+
db.On("GetSpecialAccounts", mock.Anything).Return(types.SpecialAddresses{}, nil)
52+
db.On("Transactions", mock.Anything, mock.Anything).Return(outCh, uint64(8))
53+
54+
account, err := AccountAtRound(context.Background(), account, 6, db)
55+
assert.NoError(t, err)
56+
57+
assert.Equal(t, uint64(98), account.Amount)
58+
}
59+
60+
// Test that when idb.Transactions() returns stale data the first time, we return an error.
61+
func TestStaleTransactions1(t *testing.T) {
62+
var a sdk.Address
63+
a[0] = 'a'
64+
65+
account := models.Account{
66+
Address: a.String(),
67+
Round: 8,
68+
}
69+
70+
ch := make(chan idb.TxnRow)
71+
var outCh <-chan idb.TxnRow = ch
72+
close(ch)
73+
74+
db := &mocks.IndexerDb{}
75+
db.On("GetSpecialAccounts", mock.Anything).Return(types.SpecialAddresses{}, nil)
76+
db.On("Transactions", mock.Anything, mock.Anything).Return(outCh, uint64(7)).Once()
77+
78+
account, err := AccountAtRound(context.Background(), account, 6, db)
79+
assert.True(t, errors.As(err, &ConsistencyError{}), "err: %v", err)
80+
}

api/error_messages.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,8 @@ const (
3737
errMultipleApplications = "multiple applications found for this id, please contact us, this shouldn't happen"
3838
ErrMultipleBoxes = "multiple application boxes found for this app id and box name, please contact us, this shouldn't happen"
3939
ErrFailedLookingUpBoxes = "failed while looking up application boxes"
40-
errRewindingAccountNotSupported = "rewinding account is no longer supported, please remove the `round=` query parameter and try again"
40+
errMultiAcctRewind = "multiple accounts rewind is not supported by this server"
41+
errRewindingAccount = "error while rewinding account"
4142
errLookingUpBlockForRound = "error while looking up block for round"
4243
errBlockHeaderSearch = "error while searching for block headers"
4344
errTransactionSearch = "error while searching for transaction"

0 commit comments

Comments
 (0)