From 4c3d2d385f703f14dc0732de7785df080288dd80 Mon Sep 17 00:00:00 2001 From: Agustin Godnic Date: Sun, 22 Dec 2024 13:50:15 -0300 Subject: [PATCH] Enable account rewinding --- accounting/rewind.go | 177 +++++++++++++++++++++++++++++++++ api/error_messages.go | 3 +- api/generated/v2/routes.go | 91 ++++++++--------- api/generated/v2/types.go | 4 +- api/handlers.go | 51 ++++++++-- api/handlers_test.go | 47 --------- api/indexer.oas2.json | 7 +- api/indexer.oas3.yml | 4 +- api/server.go | 16 +-- cmd/algorand-indexer/daemon.go | 3 +- 10 files changed, 283 insertions(+), 120 deletions(-) create mode 100644 accounting/rewind.go diff --git a/accounting/rewind.go b/accounting/rewind.go new file mode 100644 index 00000000..040b0ee9 --- /dev/null +++ b/accounting/rewind.go @@ -0,0 +1,177 @@ +package accounting + +import ( + "context" + "fmt" + + sdk "github.com/algorand/go-algorand-sdk/v2/types" + models "github.com/algorand/indexer/v3/api/generated/v2" + "github.com/algorand/indexer/v3/idb" + "github.com/algorand/indexer/v3/types" +) + +// ConsistencyError is returned when the database returns inconsistent (stale) results. +type ConsistencyError struct { + msg string +} + +func (e ConsistencyError) Error() string { + return e.msg +} +func assetUpdate(account *models.Account, assetid uint64, add, sub uint64) { + if account.Assets == nil { + account.Assets = new([]models.AssetHolding) + } + assets := *account.Assets + for i, ah := range assets { + if ah.AssetId == assetid { + ah.Amount += add + ah.Amount -= sub + assets[i] = ah + // found and updated asset, done + return + } + } + // add asset to list + assets = append(assets, models.AssetHolding{ + Amount: add - sub, + AssetId: assetid, + //Creator: base32 addr string of asset creator, TODO + //IsFrozen: leave nil? // TODO: on close record frozen state for rewind + }) + *account.Assets = assets +} + +// SpecialAccountRewindError indicates that an attempt was made to rewind one of the special accounts. +type SpecialAccountRewindError struct { + account string +} + +// MakeSpecialAccountRewindError helper to initialize a SpecialAccountRewindError. +func MakeSpecialAccountRewindError(account string) *SpecialAccountRewindError { + return &SpecialAccountRewindError{account: account} +} + +// Error is part of the error interface. +func (sare *SpecialAccountRewindError) Error() string { + return fmt.Sprintf("unable to rewind the %s", sare.account) +} + +var specialAccounts *types.SpecialAddresses + +// AccountAtRound queries the idb.IndexerDb object for transactions and rewinds most fields of the account back to +// their values at the requested round. +// `round` must be <= `account.Round` +func AccountAtRound(ctx context.Context, account models.Account, round uint64, db idb.IndexerDb) (acct models.Account, err error) { + // Make sure special accounts cache has been initialized. + if specialAccounts == nil { + var accounts types.SpecialAddresses + accounts, err = db.GetSpecialAccounts(ctx) + if err != nil { + return models.Account{}, fmt.Errorf("unable to get special accounts: %v", err) + } + specialAccounts = &accounts + } + acct = account + var addr sdk.Address + addr, err = sdk.DecodeAddress(account.Address) + if err != nil { + return + } + // ensure that the don't attempt to rewind a special account. + if specialAccounts.FeeSink == addr { + err = MakeSpecialAccountRewindError("FeeSink") + return + } + if specialAccounts.RewardsPool == addr { + err = MakeSpecialAccountRewindError("RewardsPool") + return + } + // Get transactions and rewind account. + tf := idb.TransactionFilter{ + Address: addr[:], + MinRound: round + 1, + MaxRound: account.Round, + } + ctx2, cf := context.WithCancel(ctx) + // In case of a panic before the next defer, call cf() here. + defer cf() + txns, r := db.Transactions(ctx2, tf) + // In case of an error, make sure the context is cancelled, and the channel is cleaned up. + defer func() { + cf() + for range txns { + } + }() + if r < account.Round { + err = ConsistencyError{fmt.Sprintf("queried round r: %d < account.Round: %d", r, account.Round)} + return + } + txcount := 0 + for txnrow := range txns { + if txnrow.Error != nil { + err = txnrow.Error + return + } + txcount++ + stxn := txnrow.Txn + if stxn == nil { + return models.Account{}, + fmt.Errorf("rewinding past inner transactions is not supported") + } + if addr == stxn.Txn.Sender { + acct.AmountWithoutPendingRewards += uint64(stxn.Txn.Fee) + acct.AmountWithoutPendingRewards -= uint64(stxn.SenderRewards) + } + switch stxn.Txn.Type { + case sdk.PaymentTx: + if addr == stxn.Txn.Sender { + acct.AmountWithoutPendingRewards += uint64(stxn.Txn.Amount) + } + if addr == stxn.Txn.Receiver { + acct.AmountWithoutPendingRewards -= uint64(stxn.Txn.Amount) + acct.AmountWithoutPendingRewards -= uint64(stxn.ReceiverRewards) + } + if addr == stxn.Txn.CloseRemainderTo { + // unwind receiving a close-to + acct.AmountWithoutPendingRewards -= uint64(stxn.ClosingAmount) + acct.AmountWithoutPendingRewards -= uint64(stxn.CloseRewards) + } else if !stxn.Txn.CloseRemainderTo.IsZero() { + // unwind sending a close-to + acct.AmountWithoutPendingRewards += uint64(stxn.ClosingAmount) + } + 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. + case sdk.AssetConfigTx: + if stxn.Txn.ConfigAsset == 0 { + // create asset, unwind the application of the value + assetUpdate(&acct, txnrow.AssetID, 0, stxn.Txn.AssetParams.Total) + } + case sdk.AssetTransferTx: + if addr == stxn.Txn.AssetSender || addr == stxn.Txn.Sender { + assetUpdate(&acct, uint64(stxn.Txn.XferAsset), stxn.Txn.AssetAmount+txnrow.Extra.AssetCloseAmount, 0) + } + if addr == stxn.Txn.AssetReceiver { + assetUpdate(&acct, uint64(stxn.Txn.XferAsset), 0, stxn.Txn.AssetAmount) + } + if addr == stxn.Txn.AssetCloseTo { + assetUpdate(&acct, uint64(stxn.Txn.XferAsset), 0, txnrow.Extra.AssetCloseAmount) + } + case sdk.AssetFreezeTx: + default: + err = fmt.Errorf("%s[%d,%d]: rewinding past txn type %s is not currently supported", account.Address, txnrow.Round, txnrow.Intra, stxn.Txn.Type) + return + } + } + acct.Round = round + // Due to accounts being closed and re-opened, we cannot always rewind Rewards. So clear it out. + acct.Rewards = 0 + // Computing pending rewards is not supported. + acct.PendingRewards = 0 + acct.Amount = acct.AmountWithoutPendingRewards + // 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 + return +} diff --git a/api/error_messages.go b/api/error_messages.go index 0bc4fb34..0060e521 100644 --- a/api/error_messages.go +++ b/api/error_messages.go @@ -38,7 +38,8 @@ const ( errMultipleApplications = "multiple applications found for this id, please contact us, this shouldn't happen" ErrMultipleBoxes = "multiple application boxes found for this app id and box name, please contact us, this shouldn't happen" ErrFailedLookingUpBoxes = "failed while looking up application boxes" - errRewindingAccountNotSupported = "rewinding account is no longer supported, please remove the `round=` query parameter and try again" + errMultiAcctRewind = "multiple accounts rewind is not supported by this server" + errRewindingAccount = "error while rewinding account" errLookingUpBlockForRound = "error while looking up block for round" errBlockHeaderSearch = "error while searching for block headers" errTransactionSearch = "error while searching for transaction" diff --git a/api/generated/v2/routes.go b/api/generated/v2/routes.go index cc1e6c38..4d398412 100644 --- a/api/generated/v2/routes.go +++ b/api/generated/v2/routes.go @@ -1615,51 +1615,52 @@ var swaggerSpec = []string{ "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", - "ulgwldiZEeYnJ48nn/5fAAAA///7f6MiAcABAA==", + "kXU6C8j+bC+hyxgX5RpVPo5+arfXm3735z7wPZ/xL4veT8GFlGLZ/YIpGFKk8JqugVp4UzXivHenzLim", + "sxxzcIMdquGLB/wB5KCmC2rofTfnOdwhOEXkfZidpvJfEJklTAkXNWMnL6GXHXq2JQF5aQyzYwTYgIos", + "ovMGXPBqhp+kSFynFRV0YWG0qGs5bBhqhy4HuKtg2wyRdxdKVhW3D8DCMEl2v1DS9sTcMcOvVjnHujVA", + "bZ48fuzlR2dfD0Y7/adGTbAesD+C5ZCw2RgR8kVLd+Y3qUrRN04B5aZVUZp+77iNSUBa6Y78s3aMoqAL", + "LpxPKZzsil6gUI8B1M6l21Mon87GikDVc6QTmtytGWA8ruXS5gb8GpX3m5A/BNfOR3aBX1/rHHsLFvUX", + "DmqtwzccAvY7h4AYloIFjz5NJ9987kuwSE0XGirkgd4x+fVTS5s5/dPHVPDsU69q81rKi7KoHkWCMqZd", + "DQfbunv1bAtEYqeGUz21eL4DJAWKzNQUpQJyEu6RUSU7SF4fyoWOSDFHOXmUk29HTr4RVnoAA71Bhhln", + "UiOPmnz9+OuRzd4fNpsD89vDZk87FGAf3xWBh2KbjsoCyW2+9RZ0HxyJ/os7uPPTooCcNWCV1veJTx9d", + "zfirsuXR0HslQ++RWWnrvh+gntaz1Dd1VFaDkM/Wxo4SwSgRfI4SQRVgfidygFdN7g//v5FXz5Hnjzz/", + "1nh+daOHMfqwfvDI3z1/r4woI1MfmfrnxtQjOewPY/HeWhk3Zl6L5T/HoZ+GoI36/ygLjLLAzej/DQJw", + "qOo/CgSRHE+jWDCKBZ+3WHC4zl8JBK230KOIAqMRYGT8I+O/cyPAyOxH7X9k858/mw8j04Y61jUTjX1o", + "lNtUzJFtlhHB1vayGUlkbpnRHg4fDrSPwY984ziRQUENQDvLnG8cdfaZtlxN9dqHW0jDsGRELxSQdwUG", + "O9hxHyPo+/z2q69/Rif21Q3CSY9cnSG2hXwBwY7eQf+fduc8NpZ1jpDKd9PX9KiCY6HehuYLklSpGuwv", + "K/wJwn/f84X9KcefIPEAhl3H9kHzRf9GaOi2wn/seIMW6ShAsJBmzoXZ1knw8XOJi7/30gHWT0kNhF/M", + "MTIunHrFRbJz+qrBUUCYsbl0oUABDHSzBwbf4NDIiRvVZvzKgjUtuKXCUOKfvHFEhwry7uVz8tVXX/2N", + "4OW32g2iS9+CcUisfxQCVxGPjJrq8xBS9O7lcwDgfeXXOqjV3kOtMOpYK4cR79/C/8JBp3/JyL+7DJDA", + "VTszhNMssSDcblGlKhu302pxXG37L6IlTydt1eL6FWBb2lJzJ1sTjoFg/1LK65DH6TC1RfMFpi+7xQHv", + "yjf/1ouxuqg/NGrVVJcOJYYqXLfOtBcl6NjsaoL3aHYezQfje/Nf8b35XzqcONin0z+bxHp/WHFQsLLP", + "kFk3iYcUx0TiNsvYKxb/5V4Nb4zsHEhsbi9y9JpPSeM7zGciynaI0OlMbnoJ0X+B+Ge1/4YsCtdwJjfE", + "3qupE190Kxlt1QBaO5vDM/dbXabcGfkX0pVmTC0loWqBhegfwGBcLM5ggAeYBocDNSmdHIINuTBnXz75", + "6mvXRNE1mW0N01MHD0BHvv0aoLFdH8y+/fqBf4KgkGTe/nT29Lvv3BiF4sLQWc6chaEzpzbqbMnyXLoO", + "Tj5mnYb2w9l//8//npycPBhCyuXGUvOnIvuJrtjtE/Wn9dlxAUeTHPVEmu1ua9OjAiju73DD0HU5wy7i", + "/0xuYtfd3pkgs8j4dj/yjOPxDF2uVlRtLa1nBq59gGrOZQ6NAC1p9MrMhulD2U3NYaAKU8VCINkrbUqB", + "WiorYeZsw1O5ULRYcstRtieDbDLPALxbp7ejceB+GQf6S8YXPNucn//aQDkuMraJ6+8Vug+yNDyTmxdu", + "ShktS/w5mAPwNuDChxCmZ+F1bl79kdONnO4mOR2i3QAed5BV5zSXC32AaYfY9gOUgtdyoe/GxjOyp+O4", + "vt2xS9Nf1L8IKi9VD/Wh66hjd1xX5bR2v29hq6D05M2kCL7/Ys2NvnnkcpF4jnF4LqDFC9v1s5adrmGK", + "3WUE3B1VFb5kQ8tdCtOgiKjxYXdkjgdwq4YvAqb9vkUvhP2z29H3WBGPOl8puOmbz36b3H7I4BgDNsaA", + "jarpbXoPwCGf/umv536PAbjmQ9KQ24bDtcmaPIy+AjfsKwBkbigtvMXM0jDlSG5GY979dnVoU8zTGc2p", + "SNleixyK3tqAGdrXolkvJRAUlxQfCMxOiuonG3WjUTcaq+mNgU1DA5uOJnQdVxoJiecgLe0NF3xM2Rnj", + "erOaNYwq219JADkk30XjeQJssY4+7Up6gakuLEvF9Bc7db4x5cWY8mJMeTGmvBhTXtzhk/SYnGJMTjHq", + "cP/aySmGuJ24l0wLqBQM/ZkbjVEG6BVFbtoTpbOo53I144LVWpBfQV1+2kh7UNBoSU3Fh31DI4muXA32", + "rCtRMu/hr+CJA5pxyvgl/HeuGPuDJYYqK2EP4beN1XgAoUhmMH9YJfOgtVnJGK1uxCcF8XWq1QoS0poq", + "ay2hxK9kaoXlrSzJGi5Lzi+gv6uwaTd9RSwSt6p+G0mMKntfqF33BODZm35kehuvQGMmlTGTyphJ5S9g", + "EpnlMr1IloxmYGbY74AGHYjrcEKehX82TR/csv6UCXg4AVQiUmVMRcwlQhpPZCo1W5amKM0OTzeY+gcH", + "+WgtuRVryagjjjriX1RH/NCU6TjqNjmnQEPsBvxmCb/UTOnf6oZTwg1hG/s7hkiyDU0N0XTFyLwUKVIY", + "brZDy7n7SeL13A/UMHbXZu96Kvq39xVVFygce3A82Q75wwPYMMNTXuCDdllk8JhNWptJ05QVFpmsFLai", + "RDP7DWJF/au/DyQ/dJv0PdkntiksP79v2+TAuiebRGeaCXPf9gihumd4ZNnuvdwtN9X92q6p3ywIobi/", + "RCuc+BZ28MgOCXYXD4ioruX30ROh8kTAPZyOOZ/+hV3H64sS1baBzjCRFZIL0xA07fj/9f0HUg+ClswZ", + "W9JLLkEq5RkThqc036c6j0rzqDSPSvOoNI9K86g0j0rzqDSPSvOoNI9K86g0j0rzqDTfW6X59E842wR1", + "0r0x19Cpz+ce7tI+JRivDE4Xr2IUAnRN9xu82ESKfEvmOV2ckH/YKwR3BDIhGe9LNK3f2ZEOZ5KhXu38", + "1dvearpHcUD6ndgpb9ZZZy9VG6/n5+xIMiiSJvAjGVo1th1A4x1L4w6OXIPjaNu9tNK7D6tHW9nLxsCc", + "MTBnDMy534E5IQWZbclCybIgr144DQTQokIdPK3EFUPAGHywoK2pyvTUF0tIl1TRFLYOXH3/fUpOoe13", + "1Ug/v3vth+lZMgCS7Iz/uSbCjYFKY23esTbvaKUfw5/G8Kcx/GkMf/pXD3+6y5Cl6Y0Xgh2DosagqNGW", + "daem5vBoT/+0OtH+5J7EqtN5g0P22Z1DrBuS4dMpZbdXB+0WSUiwXQdd1uGXc8yDOZKX+2Iq/zSdaKYu", + "/V0vVT45myyNKfTZ6Snb0FWRs5NUrk7hldn1/7OS++VqBYyq+sWNHPziSJntvkmk4pb35ole08WCqcTO", + "jDA/OXk8+fT/AgAA//+iEaM6j8ABAA==", } // GetSwagger returns the content of the embedded swagger specification file diff --git a/api/generated/v2/types.go b/api/generated/v2/types.go index dca1136c..2b373c4c 100644 --- a/api/generated/v2/types.go +++ b/api/generated/v2/types.go @@ -1656,7 +1656,7 @@ type SearchForAccountsParams struct { // AuthAddr Include accounts configured to use this spending key. AuthAddr *string `form:"auth-addr,omitempty" json:"auth-addr,omitempty"` - // Round Deprecated and disallowed. This parameter used to include results for a specified round. Requests with this parameter set are now rejected. + // Round Include results for the specified round. For performance reasons, this parameter may be disabled on some configurations. Using application-id or asset-id filters will return both creator and opt-in accounts. Filtering by include-all will return creator and opt-in accounts for deleted assets and accounts. Non-opt-in managers are not included in the results when asset-id is used. Round *uint64 `form:"round,omitempty" json:"round,omitempty"` // ApplicationId Application ID @@ -1668,7 +1668,7 @@ type SearchForAccountsParamsExclude string // LookupAccountByIDParams defines parameters for LookupAccountByID. type LookupAccountByIDParams struct { - // Round Deprecated and disallowed. This parameter used to include results for a specified round. Requests with this parameter set are now rejected. + // Round Include results for the specified round. Round *uint64 `form:"round,omitempty" json:"round,omitempty"` // IncludeAll Include all items including closed accounts, deleted applications, destroyed assets, opted-out asset holdings, and closed-out application localstates. diff --git a/api/handlers.go b/api/handlers.go index 121e9960..f1839912 100644 --- a/api/handlers.go +++ b/api/handlers.go @@ -17,6 +17,7 @@ import ( log "github.com/sirupsen/logrus" "github.com/algorand/avm-abi/apps" + "github.com/algorand/indexer/v3/accounting" "github.com/algorand/indexer/v3/api/generated/common" "github.com/algorand/indexer/v3/api/generated/v2" "github.com/algorand/indexer/v3/idb" @@ -28,6 +29,14 @@ import ( // ServerImplementation implements the handler interface used by the generated route definitions. type ServerImplementation struct { + + // EnableAddressSearchRoundRewind is allows configuring whether or not the + // 'accounts' endpoint allows specifying a round number. This is done for + // performance reasons, because requesting many accounts at a particular + // round could put a lot of strain on the system (especially if the round + // is from long ago). + EnableAddressSearchRoundRewind bool + db idb.IndexerDb dataError func() error @@ -209,9 +218,8 @@ func (si *ServerImplementation) LookupAccountByID(ctx echo.Context, accountID st if err := si.verifyHandler("LookupAccountByID", ctx); err != nil { return badRequest(ctx, err.Error()) } - // The Round parameter is no longer supported (as it was used to request account rewinding) - if params.Round != nil { - return badRequest(ctx, errRewindingAccountNotSupported) + if params.Round != nil && uint64(*params.Round) > math.MaxInt64 { + return notFound(ctx, errValueExceedingInt64) } addr, err := sdk.DecodeAddress(accountID) @@ -241,7 +249,7 @@ func (si *ServerImplementation) LookupAccountByID(ctx echo.Context, accountID st } } - accounts, round, err := si.fetchAccounts(ctx.Request().Context(), options) + accounts, round, err := si.fetchAccounts(ctx.Request().Context(), options, params.Round) if err != nil { var maxErr idb.MaxAPIResourcesPerAccountError if errors.As(err, &maxErr) { @@ -399,13 +407,13 @@ func (si *ServerImplementation) SearchForAccounts(ctx echo.Context, params gener return badRequest(ctx, err.Error()) } if (params.AssetId != nil && uint64(*params.AssetId) > math.MaxInt64) || - (params.ApplicationId != nil && uint64(*params.ApplicationId) > math.MaxInt64) { + (params.ApplicationId != nil && uint64(*params.ApplicationId) > math.MaxInt64) || + (params.Round != nil && uint64(*params.Round) > math.MaxInt64) { return notFound(ctx, errValueExceedingInt64) } - // The Round parameter is no longer supported (as it was used to request account rewinding) - if params.Round != nil { - return badRequest(ctx, errRewindingAccountNotSupported) + if !si.EnableAddressSearchRoundRewind && params.Round != nil { + return badRequest(ctx, errMultiAcctRewind) } var spendingAddrBytes []byte @@ -458,7 +466,7 @@ func (si *ServerImplementation) SearchForAccounts(ctx echo.Context, params gener options.GreaterThanAddress = addr[:] } - accounts, round, err := si.fetchAccounts(ctx.Request().Context(), options) + accounts, round, err := si.fetchAccounts(ctx.Request().Context(), options, params.Round) if err != nil { var maxErr idb.MaxAPIResourcesPerAccountError if errors.As(err, &maxErr) { @@ -1489,7 +1497,7 @@ func (si *ServerImplementation) fetchBlock(ctx context.Context, round uint64, op // fetchAccounts queries for accounts and converts them into generated.Account // objects, optionally rewinding their value back to a particular round. -func (si *ServerImplementation) fetchAccounts(ctx context.Context, options idb.AccountQueryOptions) ([]generated.Account, uint64 /*round*/, error) { +func (si *ServerImplementation) fetchAccounts(ctx context.Context, options idb.AccountQueryOptions, atRound *uint64) ([]generated.Account, uint64 /*round*/, error) { var round uint64 accounts := make([]generated.Account, 0) err := callWithTimeout(ctx, si.log, si.timeout, func(ctx context.Context) error { @@ -1502,12 +1510,33 @@ func (si *ServerImplementation) fetchAccounts(ctx context.Context, options idb.A } }() + if (atRound != nil) && (*atRound > round) { + return fmt.Errorf("%s: the requested round %d > the current round %d", + errRewindingAccount, *atRound, round) + } + for row := range accountchan { if row.Error != nil { return row.Error } - account := row.Account + // Compute for a given round if requested. + var account generated.Account + if atRound != nil { + acct, err := accounting.AccountAtRound(ctx, row.Account, *atRound, si.db) + if err != nil { + // Ignore the error if this is an account search rewind error + _, isSpecialAccountRewindError := err.(*accounting.SpecialAccountRewindError) + if len(options.EqualToAddress) != 0 || !isSpecialAccountRewindError { + return fmt.Errorf("%s: %v", errRewindingAccount, err) + } + // If we didn't return, continue to the next account + continue + } + account = acct + } else { + account = row.Account + } // match the algod equivalent which includes pending rewards account.Rewards += account.PendingRewards diff --git a/api/handlers_test.go b/api/handlers_test.go index b290835d..6f413460 100644 --- a/api/handlers_test.go +++ b/api/handlers_test.go @@ -1206,53 +1206,6 @@ func TestBigNumbers(t *testing.T) { } } -func TestRewindRoundParameterRejected(t *testing.T) { - testcases := []struct { - name string - errString string - callHandler func(ctx echo.Context, si ServerImplementation) error - }{ - { - name: "SearchForAccountInvalidRound", - errString: errRewindingAccountNotSupported, - callHandler: func(ctx echo.Context, si ServerImplementation) error { - return si.SearchForAccounts(ctx, generated.SearchForAccountsParams{Round: uint64Ptr(uint64(math.MaxInt64 + 1))}) - }, - }, - { - name: "LookupAccountByID", - errString: errRewindingAccountNotSupported, - callHandler: func(ctx echo.Context, si ServerImplementation) error { - return si.LookupAccountByID(ctx, - "PBH2JQNVP5SBXLTOWNHHPGU6FUMBVS4ZDITPK5RA5FG2YIIFS6UYEMFM2Y", - generated.LookupAccountByIDParams{Round: uint64Ptr(uint64(math.MaxInt64 + 1))}) - }, - }, - } - - for _, tc := range testcases { - t.Run(tc.name, func(t *testing.T) { - - // Make a mock indexer. - mockIndexer := &mocks.IndexerDb{} - - si := testServerImplementation(mockIndexer) - - // Setup context... - e := echo.New() - req := httptest.NewRequest(http.MethodGet, "/", nil) - rec1 := httptest.NewRecorder() - c := e.NewContext(req, rec1) - - // call handler - require.NoError(t, tc.callHandler(c, *si)) - assert.Equal(t, http.StatusBadRequest, rec1.Code) - bodyStr := rec1.Body.String() - require.Contains(t, bodyStr, tc.errString) - }) - } -} - func TestFetchBlock(t *testing.T) { testcases := []struct { name string diff --git a/api/indexer.oas2.json b/api/indexer.oas2.json index 3fbcb603..fa84254f 100644 --- a/api/indexer.oas2.json +++ b/api/indexer.oas2.json @@ -80,7 +80,7 @@ }, { "type": "integer", - "description": "Deprecated and disallowed. This parameter used to include results for a specified round. Requests with this parameter set are now rejected.", + "description": "Include results for the specified round. For performance reasons, this parameter may be disabled on some configurations. Using application-id or asset-id filters will return both creator and opt-in accounts. Filtering by include-all will return creator and opt-in accounts for deleted assets and accounts. Non-opt-in managers are not included in the results when asset-id is used.", "name": "round", "in": "query" }, @@ -119,10 +119,7 @@ "$ref": "#/parameters/account-id" }, { - "type": "integer", - "description": "Deprecated and disallowed. This parameter used to include results for a specified round. Requests with this parameter set are now rejected.", - "name": "round", - "in": "query" + "$ref": "#/parameters/round" }, { "$ref": "#/parameters/include-all" diff --git a/api/indexer.oas3.yml b/api/indexer.oas3.yml index b7abcbc2..aec70e73 100644 --- a/api/indexer.oas3.yml +++ b/api/indexer.oas3.yml @@ -2810,7 +2810,7 @@ "x-algorand-format": "Address" }, { - "description": "Deprecated and disallowed. This parameter used to include results for a specified round. Requests with this parameter set are now rejected.", + "description": "Include results for the specified round. For performance reasons, this parameter may be disabled on some configurations. Using application-id or asset-id filters will return both creator and opt-in accounts. Filtering by include-all will return creator and opt-in accounts for deleted assets and accounts. Non-opt-in managers are not included in the results when asset-id is used.", "in": "query", "name": "round", "schema": { @@ -2922,7 +2922,7 @@ } }, { - "description": "Deprecated and disallowed. This parameter used to include results for a specified round. Requests with this parameter set are now rejected.", + "description": "Include results for the specified round.", "in": "query", "name": "round", "schema": { diff --git a/api/server.go b/api/server.go index 0096878d..1e4decba 100644 --- a/api/server.go +++ b/api/server.go @@ -23,6 +23,9 @@ type ExtraOptions struct { // Tokens are the access tokens which can access the API. Tokens []string + // DeveloperMode turns on features like AddressSearchRoundRewind + DeveloperMode bool + // Respond to Private Network Access preflight requests sent to the indexer. EnablePrivateNetworkAccessHeader bool @@ -146,12 +149,13 @@ func Serve(ctx context.Context, serveAddr string, db idb.IndexerDb, dataError fu } api := ServerImplementation{ - db: db, - dataError: dataError, - timeout: options.handlerTimeout(), - log: log, - disabledParams: disabledMap, - opts: options, + EnableAddressSearchRoundRewind: options.DeveloperMode, + db: db, + dataError: dataError, + timeout: options.handlerTimeout(), + log: log, + disabledParams: disabledMap, + opts: options, } generated.RegisterHandlers(e, &api, middleware...) diff --git a/cmd/algorand-indexer/daemon.go b/cmd/algorand-indexer/daemon.go index 213f2189..faf36608 100644 --- a/cmd/algorand-indexer/daemon.go +++ b/cmd/algorand-indexer/daemon.go @@ -74,7 +74,7 @@ func DaemonCmd() *cobra.Command { cfg.flags = daemonCmd.Flags() cfg.flags.StringVarP(&cfg.daemonServerAddr, "server", "S", ":8980", "host:port to serve API on (default :8980)") cfg.flags.StringVarP(&cfg.tokenString, "token", "t", "", "an optional auth token, when set REST calls must use this token in a bearer format, or in a 'X-Indexer-API-Token' header") - cfg.flags.BoolVarP(&cfg.developerMode, "dev-mode", "", false, "has no effect currently, reserved for future performance intensive operations") + cfg.flags.BoolVarP(&cfg.developerMode, "dev-mode", "", false, "allow performance intensive operations like searching for accounts at a particular round") cfg.flags.BoolVarP(&cfg.enablePrivateNetworkAccessHeader, "enable-private-network-access-header", "", false, "respond to Private Network Access preflight requests") cfg.flags.StringVarP(&cfg.metricsMode, "metrics-mode", "", "OFF", "configure the /metrics endpoint to [ON, OFF, VERBOSE]") cfg.flags.DurationVarP(&cfg.writeTimeout, "write-timeout", "", 30*time.Second, "set the maximum duration to wait before timing out writes to a http response, breaking connection") @@ -309,6 +309,7 @@ func runDaemon(daemonConfig *daemonConfig) error { // makeOptions converts CLI options to server options func makeOptions(daemonConfig *daemonConfig) (options api.ExtraOptions) { options.EnablePrivateNetworkAccessHeader = daemonConfig.enablePrivateNetworkAccessHeader + options.DeveloperMode = daemonConfig.developerMode if daemonConfig.tokenString != "" { options.Tokens = append(options.Tokens, daemonConfig.tokenString) }