diff --git a/.circleci/config.yml b/.circleci/config.yml index 7fa6c2ea7..cc2ca76db 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -46,7 +46,7 @@ jobs: go_version: type: string environment: - CI_E2E_FILENAME: "fad05790/rel-nightly" + CI_E2E_FILENAME: "faab6dcf/rel-nightly" steps: - go/install: version: << parameters.go_version >> diff --git a/.gitignore b/.gitignore index 1bc4b793f..ee90918b7 100644 --- a/.gitignore +++ b/.gitignore @@ -17,6 +17,9 @@ tmp/ # Output of the go coverage tool, specifically when used with LiteIDE *.out +# Output of fixtures_test.go +_*.json + # Dependency directories (remove the comment below to include it) # vendor/ @@ -29,6 +32,9 @@ __pycache__ # jetbrains IDE .idea +# VS Code +.vscode + .deb_tmp .tar_tmp *.deb diff --git a/api/README.md b/api/README.md index 3340b7a04..806ef6ec4 100644 --- a/api/README.md +++ b/api/README.md @@ -5,6 +5,7 @@ We are using a documentation driven process. The API is defined using [OpenAPI v2](https://swagger.io/specification/v2/) in **indexer.oas2.yml**. ## Updating REST API + The Makefile will install our fork of **oapi-codegen**, use `make oapi-codegen` to install it directly. 1. Document your changes by editing **indexer.oas2.yml** @@ -20,3 +21,104 @@ Specifically, `uint64` types aren't strictly supported by OpenAPI. So we added a ## Why do we have indexer.oas2.yml and indexer.oas3.yml? We chose to maintain V2 and V3 versions of the spec because OpenAPI v3 doesn't seem to be widely supported. Some tools worked better with V3 and others with V2, so having both available has been useful. To reduce developer burdon, the v2 specfile is automatically converted v3 using [converter.swagger.io](http://converter.swagger.io/). + +# Fixtures Test +## What is a **Fixtures Test**? + +Currently (September 2022) [fixtures_test.go](./fixtures_test.go) is a library that allows testing Indexer's router to verify that endpoints accept parameters and respond as expected, and guard against future regressions. [app_boxes_fixtures_test.go](./app_boxes_fixtures_test.go) is an example _fixtures test_ and is the _creator_ of the fixture [boxes.json](./test_resources/boxes.json). + +A fixtures test + +1. is defined by a go-slice called a _Seed Fixture_ e.g. [var boxSeedFixture](https://github.com/algorand/indexer/blob/b5025ad640fabac0d778b4cac60d558a698ed560/api/app_boxes_fixtures_test.go#L302-L692) which contains request information for making HTTP requests against an Indexer server +2. iterates through the slice, making each of the defined requests and generating a _Live Fixture_ +3. reads a _Saved Fixture_ from a json file e.g. [boxes.json](./test_resources/boxes.json) +4. persists the _Live Fixture_ to a json file not in source control +5. asserts that the _Saved Fixture_ is equal to the _Live Fixture_ + +In reality, because we always want to save the _Live Fixture_ before making assertions that could fail the test and pre-empt saving, steps (3) and (4) happen in the opposite order. + +## What's the purpose of a Fixtures Test? + +A fixtures test should allow one to quickly stand up an end-to-end test to validate that Indexer endpoints are working as expected. After Indexer's state is programmatically set up, it's easy to add new requests and verify that the responses look exactly as expected. Once you're satisfied that the responses are correct, it's easy to _freeze_ the test and guard against future regressions. +## What does a **Fixtures Test Function** Look Like? + +[func TestBoxes](https://github.com/algorand/indexer/blob/b5025ad640fabac0d778b4cac60d558a698ed560/api/app_boxes_fixtures_test.go#L694_L704) shows the basic structure of a fixtures test. + +1. `setupIdbAndReturnShutdownFunc()` is called to set up the Indexer database + * this isn't expected to require modification +2. `setupLiveBoxes()` is used to prepare the local ledger and process blocks in order to bring Indexer into a particular state + * this will always depend on what the test is trying to achieve + * in this case, an app was used to create and modify a set of boxes which are then queried against + * it is conceivable that instead of bringing Indexer into a particular state, the responses from the DB or even the handler may be mocked, so we could have had `setupLiveBoxesMocker()` instead of `setupLiveBoxes()` +3. `setupLiveServerAndReturnShutdownFunc()` is used to bring up an instance of a real Indexer. + * this shouldn't need to be modified; however, if running in parallel and making assertions that conflict with other tests, you may need to localize the variable `fixtestListenAddr` and run on a separate port + * if running a mock server instead, a different setup function would be needed +4. `validateLiveVsSaved()` runs steps (1) through (5) defined in the previous section + * this is designed to be generic and ought not require much modification going forward + + +## Which Endpoints are Currently _Testable_ in a Fixtures Test? + +Endpoints defined in [proverRoutes](https://github.com/algorand/indexer/blob/b955a31b10d8dce7177383895ed8e57206d69f67/api/fixtures_test.go#L232-L263) are testable. + +Currently (September 2022) these are: + +* `/v2/accounts` +* `/v2/applications` +* `/v2/applications/:application-id` +* `/v2/applications/:application-id/box` +* `/v2/applications/:application-id/boxes` + +## How to Introduce a New Fixtures Test for an _Already Testable_ Endpoint? + +To set up a new test for endpoints defined above one needs to: + +### 1. Define a new _Seed Fixture_ + +For example, consider + +```go +var boxSeedFixture = fixture{ + File: "boxes.json", + Owner: "TestBoxes", + Frozen: true, + Cases: []testCase{ + // /v2/accounts - 1 case + { + Name: "What are all the accounts?", + Request: requestInfo{ + Path: "/v2/accounts", + Params: []param{}, + }, + }, + ... +``` + +A seed fixture is a `struct` with fields +* `File` (_required_) - the name in [test_resources](./test_resources/) where the fixture is read from (and written to with an `_` prefix) +* `Owner` (_recommended_) - a name to define which test "owns" the seed +* `Frozen` (_required_) - set _true_ when you need to run assertions of the _Live Fixture_ vs. the _Saved Fixture_. For tests to pass, it needs to be set _true_. +* `Cases` - the slice of `testCase`s. Each of these has the fields: + * `Name` (_required_) - an identifier for the test case + * `Request` (_required_) - a `requestInfo` struct specifying: + * `Path` (_required_) - the path to be queried + * `Params` (_required but may be empty_) - the slice of parameters (strings `name` and `value`) to be appended to the path +### 2. Define a new _Indexer State_ Setup Function + +There are many examples of setting up state that can be emulated. For example: +* [setupLiveBoxes()](https://github.com/algorand/indexer/blob/b5025ad640fabac0d778b4cac60d558a698ed560/api/app_boxes_fixtures_test.go#L43) for application boxes +* [TestApplicationHandlers()](https://github.com/algorand/indexer/blob/3a9095c2b5ee25093708f980445611a03f2cf4e2/api/handlers_e2e_test.go#L93) for applications +* [TestBlockWithTransactions()](https://github.com/algorand/indexer/blob/800cb135a0c6da0109e7282acf85cbe1961930c6/idb/postgres/postgres_integration_test.go#L339) setup state consisting of a set of basic transactions + +## How to Make a _New Endpoint_ Testable by Fixtures Tests? + +There are 2 steps: + +1. Implement a new function _witness generator_ aka [prover function](https://github.com/algorand/indexer/blob/b955a31b10d8dce7177383895ed8e57206d69f67/api/fixtures_test.go#L103) of type `func(responseInfo) (interface{}, *string)` as examplified in [this section](https://github.com/algorand/indexer/blob/b955a31b10d8dce7177383895ed8e57206d69f67/api/fixtures_test.go#L107-L200). Such a function is supposed to parse an Indexer response's body into a generated model. Currently, all provers are boilerplate, and with generics, it's expected that this step will no longer be necessary (this [POC](https://github.com/tzaffi/indexer/blob/generic-boxes/api/fixtures_test.go#L119-L155) shows how it would be done with generics). +2. Define a new route in the [proverRoutes struct](https://github.com/algorand/indexer/blob/b955a31b10d8dce7177383895ed8e57206d69f67/api/fixtures_test.go#L232_L263). This is a tree structure which is traversed by splitting a path using `/` and eventually reaching a leaf which consists of a `prover` as defined in #1. + +For example, to enable the endpoint `GET /v2/applications/{application-id}/logs` for fixtures test, one need only define a `logsProof` witness generator and have it mapped in `proverRoutes` under: + +``` +proverRoutes.parts["v2"].parts["applications"].parts[":application-id"].parts["logs"] = logsProof +``` \ No newline at end of file diff --git a/api/app_boxes_fixtures_test.go b/api/app_boxes_fixtures_test.go new file mode 100644 index 000000000..6ac030fb4 --- /dev/null +++ b/api/app_boxes_fixtures_test.go @@ -0,0 +1,718 @@ +package api + +import ( + "encoding/base32" + "encoding/base64" + "fmt" + "testing" + + "github.com/algorand/go-algorand/data/basics" + "github.com/algorand/go-algorand/data/transactions" + "github.com/algorand/go-algorand/data/transactions/logic" + "github.com/algorand/go-algorand/ledger" + "github.com/algorand/go-algorand/rpcs" + "github.com/algorand/indexer/processor" + "github.com/algorand/indexer/util/test" + "github.com/stretchr/testify/require" +) + +func goalEncode(t *testing.T, s string) string { + b1, err := logic.NewAppCallBytes(s) + require.NoError(t, err, s) + b2, err := b1.Raw() + require.NoError(t, err) + return string(b2) +} + +var goalEncodingExamples map[string]string = map[string]string{ + "str": "str", + "string": "string", + "int": "42", + "integer": "100", + "addr": basics.AppIndex(3).Address().String(), + "address": basics.AppIndex(5).Address().String(), + "b32": base32.StdEncoding.EncodeToString([]byte("b32")), + "base32": base32.StdEncoding.EncodeToString([]byte("base32")), + "byte base32": base32.StdEncoding.EncodeToString([]byte("byte base32")), + "b64": base64.StdEncoding.EncodeToString([]byte("b64")), + "base64": base64.StdEncoding.EncodeToString([]byte("base64")), + "byte base64": base64.StdEncoding.EncodeToString([]byte("byte base64")), + "abi": `(uint64,string,bool[]):[399,"pls pass",[true,false]]`, +} + +func setupLiveBoxes(t *testing.T, proc processor.Processor, l *ledger.Ledger) { + deleted := "DELETED" + + firstAppid := basics.AppIndex(1) + secondAppid := basics.AppIndex(3) + thirdAppid := basics.AppIndex(5) + + // ---- ROUND 1: create and fund the box app and another app which won't have boxes ---- // + currentRound := basics.Round(1) + + createTxn, err := test.MakeComplexCreateAppTxn(test.AccountA, test.BoxApprovalProgram, test.BoxClearProgram, 8) + require.NoError(t, err) + + payNewAppTxn := test.MakePaymentTxn(1000, 500000, 0, 0, 0, 0, test.AccountA, firstAppid.Address(), basics.Address{}, + basics.Address{}) + + createTxn2, err := test.MakeComplexCreateAppTxn(test.AccountB, test.BoxApprovalProgram, test.BoxClearProgram, 8) + require.NoError(t, err) + payNewAppTxn2 := test.MakePaymentTxn(1000, 500000, 0, 0, 0, 0, test.AccountB, secondAppid.Address(), basics.Address{}, + basics.Address{}) + + createTxn3, err := test.MakeComplexCreateAppTxn(test.AccountC, test.BoxApprovalProgram, test.BoxClearProgram, 8) + require.NoError(t, err) + payNewAppTxn3 := test.MakePaymentTxn(1000, 500000, 0, 0, 0, 0, test.AccountC, thirdAppid.Address(), basics.Address{}, + basics.Address{}) + + block, err := test.MakeBlockForTxns(test.MakeGenesisBlock().BlockHeader, &createTxn, &payNewAppTxn, &createTxn2, &payNewAppTxn2, &createTxn3, &payNewAppTxn3) + require.NoError(t, err) + + err = proc.Process(&rpcs.EncodedBlockCert{Block: block}) + require.NoError(t, err) + + // block header handoff: round 1 --> round 2 + blockHdr, err := l.BlockHdr(currentRound) + require.NoError(t, err) + + // ---- ROUND 2: create 8 boxes for appid == 1 ---- // + currentRound = basics.Round(2) + + boxNames := []string{ + "a great box", + "another great box", + "not so great box", + "disappointing box", + "don't box me in this way", + "I will be assimilated", + "I'm destined for deletion", + "box #8", + } + + expectedAppBoxes := map[basics.AppIndex]map[string]string{} + expectedAppBoxes[firstAppid] = map[string]string{} + newBoxValue := "\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" + boxTxns := make([]*transactions.SignedTxnWithAD, 0) + for _, boxName := range boxNames { + expectedAppBoxes[firstAppid][logic.MakeBoxKey(firstAppid, boxName)] = newBoxValue + args := []string{"create", boxName} + boxTxn := test.MakeAppCallTxnWithBoxes(uint64(firstAppid), test.AccountA, args, []string{boxName}) + boxTxns = append(boxTxns, &boxTxn) + } + + block, err = test.MakeBlockForTxns(blockHdr, boxTxns...) + require.NoError(t, err) + + err = proc.Process(&rpcs.EncodedBlockCert{Block: block}) + require.NoError(t, err) + + // block header handoff: round 2 --> round 3 + blockHdr, err = l.BlockHdr(currentRound) + require.NoError(t, err) + + // ---- ROUND 3: populate the boxes appropriately ---- // + currentRound = basics.Round(3) + + appBoxesToSet := map[string]string{ + "a great box": "it's a wonderful box", + "another great box": "I'm wonderful too", + "not so great box": "bummer", + "disappointing box": "RUG PULL!!!!", + "don't box me in this way": "non box-conforming", + "I will be assimilated": "THE BORG", + "I'm destined for deletion": "I'm still alive!!!", + "box #8": "eight is beautiful", + } + + boxTxns = make([]*transactions.SignedTxnWithAD, 0) + for boxName, valPrefix := range appBoxesToSet { + args := []string{"set", boxName, valPrefix} + boxTxn := test.MakeAppCallTxnWithBoxes(uint64(firstAppid), test.AccountA, args, []string{boxName}) + boxTxns = append(boxTxns, &boxTxn) + + key := logic.MakeBoxKey(firstAppid, boxName) + expectedAppBoxes[firstAppid][key] = valPrefix + newBoxValue[len(valPrefix):] + } + block, err = test.MakeBlockForTxns(blockHdr, boxTxns...) + require.NoError(t, err) + + err = proc.Process(&rpcs.EncodedBlockCert{Block: block}) + require.NoError(t, err) + + // block header handoff: round 3 --> round 4 + blockHdr, err = l.BlockHdr(currentRound) + require.NoError(t, err) + + // ---- ROUND 4: delete the unhappy boxes ---- // + currentRound = basics.Round(4) + + appBoxesToDelete := []string{ + "not so great box", + "disappointing box", + "I'm destined for deletion", + } + + boxTxns = make([]*transactions.SignedTxnWithAD, 0) + for _, boxName := range appBoxesToDelete { + args := []string{"delete", boxName} + boxTxn := test.MakeAppCallTxnWithBoxes(uint64(firstAppid), test.AccountA, args, []string{boxName}) + boxTxns = append(boxTxns, &boxTxn) + + key := logic.MakeBoxKey(firstAppid, boxName) + expectedAppBoxes[firstAppid][key] = deleted + } + block, err = test.MakeBlockForTxns(blockHdr, boxTxns...) + require.NoError(t, err) + + err = proc.Process(&rpcs.EncodedBlockCert{Block: block}) + require.NoError(t, err) + + // block header handoff: round 4 --> round 5 + blockHdr, err = l.BlockHdr(currentRound) + require.NoError(t, err) + + // ---- ROUND 5: create 4 new boxes, overwriting one of the former boxes ---- // + currentRound = basics.Round(5) + + randBoxName := []byte{0x52, 0xfd, 0xfc, 0x7, 0x21, 0x82, 0x65, 0x4f, 0x16, 0x3f, 0x5f, 0xf, 0x9a, 0x62, 0x1d, 0x72, 0x95, 0x66, 0xc7, 0x4d, 0x10, 0x3, 0x7c, 0x4d, 0x7b, 0xbb, 0x4, 0x7, 0xd1, 0xe2, 0xc6, 0x49, 0x81, 0x85, 0x5a, 0xd8, 0x68, 0x1d, 0xd, 0x86, 0xd1, 0xe9, 0x1e, 0x0, 0x16, 0x79, 0x39, 0xcb, 0x66, 0x94, 0xd2, 0xc4, 0x22, 0xac, 0xd2, 0x8, 0xa0, 0x7, 0x29, 0x39, 0x48, 0x7f, 0x69, 0x99} + appBoxesToCreate := []string{ + "fantabulous", + "disappointing box", // overwriting here + "AVM is the new EVM", + string(randBoxName), + } + boxTxns = make([]*transactions.SignedTxnWithAD, 0) + for _, boxName := range appBoxesToCreate { + args := []string{"create", boxName} + boxTxn := test.MakeAppCallTxnWithBoxes(uint64(firstAppid), test.AccountA, args, []string{boxName}) + boxTxns = append(boxTxns, &boxTxn) + + key := logic.MakeBoxKey(firstAppid, boxName) + expectedAppBoxes[firstAppid][key] = newBoxValue + } + block, err = test.MakeBlockForTxns(blockHdr, boxTxns...) + require.NoError(t, err) + + err = proc.Process(&rpcs.EncodedBlockCert{Block: block}) + require.NoError(t, err) + + // block header handoff: round 5 --> round 6 + blockHdr, err = l.BlockHdr(currentRound) + require.NoError(t, err) + + // ---- ROUND 6: populate the 4 new boxes ---- // + currentRound = basics.Round(6) + + randBoxValue := []byte{0xeb, 0x9d, 0x18, 0xa4, 0x47, 0x84, 0x4, 0x5d, 0x87, 0xf3, 0xc6, 0x7c, 0xf2, 0x27, 0x46, 0xe9, 0x95, 0xaf, 0x5a, 0x25, 0x36, 0x79, 0x51, 0xba} + appBoxesToSet = map[string]string{ + "fantabulous": "Italian food's the best!", // max char's + "disappointing box": "you made it!", + "AVM is the new EVM": "yes we can!", + string(randBoxName): string(randBoxValue), + } + boxTxns = make([]*transactions.SignedTxnWithAD, 0) + for boxName, valPrefix := range appBoxesToSet { + args := []string{"set", boxName, valPrefix} + boxTxn := test.MakeAppCallTxnWithBoxes(uint64(firstAppid), test.AccountA, args, []string{boxName}) + boxTxns = append(boxTxns, &boxTxn) + + key := logic.MakeBoxKey(firstAppid, boxName) + expectedAppBoxes[firstAppid][key] = valPrefix + newBoxValue[len(valPrefix):] + } + block, err = test.MakeBlockForTxns(blockHdr, boxTxns...) + require.NoError(t, err) + + err = proc.Process(&rpcs.EncodedBlockCert{Block: block}) + require.NoError(t, err) + + // block header handoff: round 6 --> round 7 + blockHdr, err = l.BlockHdr(currentRound) + require.NoError(t, err) + + // ---- ROUND 7: create GOAL-encoding boxes for appid == 5 ---- // + currentRound = basics.Round(7) + + encodingExamples := make(map[string]string, len(goalEncodingExamples)) + for k, v := range goalEncodingExamples { + encodingExamples[k] = goalEncode(t, k+":"+v) + } + + boxTxns = make([]*transactions.SignedTxnWithAD, 0) + expectedAppBoxes[thirdAppid] = map[string]string{} + for _, boxName := range encodingExamples { + args := []string{"create", boxName} + expectedAppBoxes[thirdAppid][logic.MakeBoxKey(thirdAppid, boxName)] = newBoxValue + boxTxn := test.MakeAppCallTxnWithBoxes(uint64(thirdAppid), test.AccountC, args, []string{boxName}) + boxTxns = append(boxTxns, &boxTxn) + } + + block, err = test.MakeBlockForTxns(blockHdr, boxTxns...) + require.NoError(t, err) + + err = proc.Process(&rpcs.EncodedBlockCert{Block: block}) + require.NoError(t, err) + + // block header handoff: round 7 --> round 8 + blockHdr, err = l.BlockHdr(currentRound) + require.NoError(t, err) + + // ---- ROUND 8: populate GOAL-encoding boxes for appid == 5 ---- // + currentRound = basics.Round(8) + + boxTxns = make([]*transactions.SignedTxnWithAD, 0) + for _, valPrefix := range encodingExamples { + require.LessOrEqual(t, len(valPrefix), 40) + args := []string{"set", valPrefix, valPrefix} + boxTxn := test.MakeAppCallTxnWithBoxes(uint64(thirdAppid), test.AccountC, args, []string{valPrefix}) + boxTxns = append(boxTxns, &boxTxn) + + key := logic.MakeBoxKey(thirdAppid, valPrefix) + expectedAppBoxes[thirdAppid][key] = valPrefix + newBoxValue[len(valPrefix):] + } + block, err = test.MakeBlockForTxns(blockHdr, boxTxns...) + require.NoError(t, err) + + err = proc.Process(&rpcs.EncodedBlockCert{Block: block}) + require.NoError(t, err) + + // block header handoff: round 8 --> round 9 + blockHdr, err = l.BlockHdr(currentRound) + require.NoError(t, err) + + // ---- ROUND 9: delete appid == 5 thus orphaning the boxes + currentRound = basics.Round(9) + + deleteTxn := test.MakeAppDestroyTxn(uint64(thirdAppid), test.AccountC) + block, err = test.MakeBlockForTxns(blockHdr, &deleteTxn) + require.NoError(t, err) + + err = proc.Process(&rpcs.EncodedBlockCert{Block: block}) + require.NoError(t, err) + + // ---- SUMMARY ---- // + + totals := map[basics.AppIndex]map[string]int{} + for appIndex, appBoxes := range expectedAppBoxes { + totals[appIndex] = map[string]int{ + "tBoxes": 0, + "tBoxBytes": 0, + } + for k, v := range appBoxes { + if v != deleted { + totals[appIndex]["tBoxes"]++ + totals[appIndex]["tBoxBytes"] += len(k) + len(v) - 11 + } + } + } + + // This is a manual sanity check only. + // Validations of db and response contents prior to server response are tested elsewhere. + // TODO: consider incorporating such stateful validations here as well. + fmt.Printf("expectedAppBoxes=%+v\n", expectedAppBoxes) + fmt.Printf("expected totals=%+v\n", totals) +} + +var boxSeedFixture = fixture{ + File: "boxes.json", + Owner: "TestBoxes", + Frozen: true, + Cases: []testCase{ + // /v2/accounts - 1 case + { + Name: "What are all the accounts?", + Request: requestInfo{ + Path: "/v2/accounts", + Params: []param{}, + }, + }, + // /v2/applications - 1 case + { + Name: "What are all the apps?", + Request: requestInfo{ + Path: "/v2/applications", + Params: []param{}, + }, + }, + // /v2/applications/:app-id - 4 cases + { + Name: "Lookup non-existing app 1337", + Request: requestInfo{ + Path: "/v2/applications/1337", + Params: []param{}, + }, + }, + { + Name: "Lookup app 3 (funded with no boxes)", + Request: requestInfo{ + Path: "/v2/applications/3", + Params: []param{}, + }, + }, + { + Name: "Lookup app 1 (funded with boxes)", + Request: requestInfo{ + Path: "/v2/applications/1", + Params: []param{}, + }, + }, + { + Name: "Lookup DELETED app 5 (funded with encoding test named boxes)", + Request: requestInfo{ + Path: "/v2/applications/5", + Params: []param{}, + }, + }, + // /v2/accounts/:account-id - 1 non-app case and 2 cases using AppIndex.Address() + { + Name: "Creator account - not an app account - no params", + Request: requestInfo{ + Path: "/v2/accounts/LMTOYRT2WPSUY6JTCW2URER6YN3GETJ5FHTQBA55EVK66JG2QOB32WPIHY", + Params: []param{}, + }, + }, + { + Name: "App 3 (as account) totals no boxes - no params", + Request: requestInfo{ + Path: "/v2/accounts/" + basics.AppIndex(3).Address().String(), + Params: []param{}, + }, + }, + { + Name: "App 1 (as account) totals with boxes - no params", + Request: requestInfo{ + Path: "/v2/accounts/" + basics.AppIndex(1).Address().String(), + Params: []param{}, + }, + }, + // /v2/applications/:app-id/boxes - 5 apps with lots of param variations + { + Name: "Boxes of a app with id == math.MaxInt64", + Request: requestInfo{ + Path: "/v2/applications/9223372036854775807/boxes", + Params: []param{}, + }, + }, + { + Name: "Boxes of a app with id == math.MaxInt64 + 1", + Request: requestInfo{ + Path: "/v2/applications/9223372036854775808/boxes", + Params: []param{}, + }, + }, + { + Name: "Boxes of a non-existing app 1337", + Request: requestInfo{ + Path: "/v2/applications/1337/boxes", + Params: []param{}, + }, + }, + { + Name: "Boxes of app 3 with no boxes: no params", + Request: requestInfo{ + Path: "/v2/applications/3/boxes", + Params: []param{}, + }, + }, + { + Name: "Boxes of DELETED app 5 with goal encoded boxes: no params", + Request: requestInfo{ + Path: "/v2/applications/5/boxes", + Params: []param{}, + }, + }, + { + Name: "Boxes of app 1 with boxes: no params", + Request: requestInfo{ + Path: "/v2/applications/1/boxes", + Params: []param{}, + }, + }, + { + Name: "Boxes of app 1 with boxes: limit 3 - page 1", + Request: requestInfo{ + Path: "/v2/applications/1/boxes", + Params: []param{ + {"limit", "3"}, + }, + }, + }, + { + Name: "Boxes of app 1 with boxes: limit 3 - page 2 - b64", + Request: requestInfo{ + Path: "/v2/applications/1/boxes", + Params: []param{ + {"limit", "3"}, + {"next", "b64:Uv38ByGCZU8WP18PmmIdcpVmx00QA3xNe7sEB9HixkmBhVrYaB0NhtHpHgAWeTnLZpTSxCKs0gigByk5SH9pmQ=="}, + }, + }, + }, + { + Name: "Boxes of app 1 with boxes: limit 3 - page 3 - b64", + Request: requestInfo{ + Path: "/v2/applications/1/boxes", + Params: []param{ + {"limit", "3"}, + {"next", "b64:Ym94ICM4"}, + }, + }, + }, + { + Name: "Boxes of app 1 with boxes: limit 3 - MISSING b64 prefix", + Request: requestInfo{ + Path: "/v2/applications/1/boxes", + Params: []param{ + {"limit", "3"}, + {"next", "Ym94ICM4"}, + }, + }, + }, + { + Name: "Boxes of app 1 with boxes: limit 3 - goal app arg encoding str", + Request: requestInfo{ + Path: "/v2/applications/1/boxes", + Params: []param{ + {"limit", "3"}, + {"next", "str:box #8"}, + }, + }, + }, + { + Name: "Boxes of app 1 with boxes: limit 3 - page 4 (empty) - b64", + Request: requestInfo{ + Path: "/v2/applications/1/boxes", + Params: []param{ + {"limit", "3"}, + {"next", "b64:ZmFudGFidWxvdXM="}, + }, + }, + }, + { + Name: "Boxes of app 1 with boxes: limit 3 - ERROR because when next param provided -even empty string- it must be goal app arg encoded", + Request: requestInfo{ + Path: "/v2/applications/1/boxes", + Params: []param{ + {"limit", "3"}, + {"next", ""}, + }, + }, + }, + // /v2/applications/:app-id/box?name=... - lots and lots + { + Name: "Boxes (with made up name param) of a app with id == math.MaxInt64", + Request: requestInfo{ + Path: "/v2/applications/9223372036854775807/box", + Params: []param{ + {"name", "string:non-existing"}, + }, + }, + }, + { + Name: "Box (with made up name param) of a app with id == math.MaxInt64 + 1", + Request: requestInfo{ + Path: "/v2/applications/9223372036854775808/box", + Params: []param{ + {"name", "string:non-existing"}, + }, + }, + }, + + { + Name: "A box attempt for a non-existing app 1337", + Request: requestInfo{ + Path: "/v2/applications/1337/box", + Params: []param{ + {"name", "string:non-existing"}, + }, + }, + }, + { + Name: "A box attempt for a non-existing app 1337 - without the required box name param", + Request: requestInfo{ + Path: "/v2/applications/1337/box", + Params: []param{}, + }, + }, + { + Name: "A box attempt for a existing app 3 - without the required box name param", + Request: requestInfo{ + Path: "/v2/applications/3/box", + Params: []param{}, + }, + }, + { + Name: "App 3 box (non-existing)", + Request: requestInfo{ + Path: "/v2/applications/3/box", + Params: []param{ + {"name", "string:non-existing"}, + }, + }, + }, + { + Name: "App 1 box (non-existing)", + Request: requestInfo{ + Path: "/v2/applications/1/box", + Params: []param{ + {"name", "string:non-existing"}, + }, + }, + }, + { + Name: "App 1 box (a great box)", + Request: requestInfo{ + Path: "/v2/applications/1/box", + Params: []param{ + {"name", "string:a great box"}, + }, + }, + }, + { + Name: "DELETED app 5 encoding (str:str) - no params", + Request: requestInfo{ + Path: "/v2/applications/5/box", + Params: []param{ + {"name", "str:str"}, + }, + }, + }, + { + Name: "DELETED app 5 encoding (integer:100) - no params", + Request: requestInfo{ + Path: "/v2/applications/5/box", + Params: []param{ + {"name", "integer:100"}, + }, + }, + }, + { + Name: "DELETED app 5 encoding (base32:MJQXGZJTGI======) - no params", + Request: requestInfo{ + Path: "/v2/applications/5/box", + Params: []param{ + {"name", "base32:MJQXGZJTGI======"}, + }, + }, + }, + { + Name: "DELETED app 5 encoding (b64:YjY0) - no params", + Request: requestInfo{ + Path: "/v2/applications/5/box", + Params: []param{ + {"name", "b64:YjY0"}, + }, + }, + }, + { + Name: "DELETED app 5 encoding (base64:YmFzZTY0) - no params", + Request: requestInfo{ + Path: "/v2/applications/5/box", + Params: []param{ + {"name", "base64:YmFzZTY0"}, + }, + }, + }, + { + Name: "DELETED app 5 encoding (string:string) - no params", + Request: requestInfo{ + Path: "/v2/applications/5/box", + Params: []param{ + {"name", "string:string"}, + }, + }, + }, + { + Name: "DELETED app 5 encoding (int:42) - no params", + Request: requestInfo{ + Path: "/v2/applications/5/box", + Params: []param{ + {"name", "int:42"}, + }, + }, + }, + { + Name: "DELETED app 5 encoding (abi:(uint64,string,bool[]):[399,\"pls pass\",[true,false]]) - no params", + Request: requestInfo{ + Path: "/v2/applications/5/box", + Params: []param{ + {"name", "abi:(uint64,string,bool[]):[399,\"pls pass\",[true,false]]"}, + }, + }, + }, + { + Name: "DELETED app 5 encoding (addr:LMTOYRT2WPSUY6JTCW2URER6YN3GETJ5FHTQBA55EVK66JG2QOB32WPIHY) - no params", + Request: requestInfo{ + Path: "/v2/applications/5/box", + Params: []param{ + {"name", "addr:LMTOYRT2WPSUY6JTCW2URER6YN3GETJ5FHTQBA55EVK66JG2QOB32WPIHY"}, + }, + }, + }, + { + Name: "DELETED app 5 encoding (address:2SYXFSCZAQCZ7YIFUCUZYOVR7G6Y3UBGSJIWT4EZ4CO3T6WVYTMHVSANOY) - no params", + Request: requestInfo{ + Path: "/v2/applications/5/box", + Params: []param{ + {"name", "address:2SYXFSCZAQCZ7YIFUCUZYOVR7G6Y3UBGSJIWT4EZ4CO3T6WVYTMHVSANOY"}, + }, + }, + }, + { + Name: "DELETED app 5 encoding (b32:MIZTE===) - no params", + Request: requestInfo{ + Path: "/v2/applications/5/box", + Params: []param{ + {"name", "b32:MIZTE==="}, + }, + }, + }, + { + Name: "DELETED app 5 encoding (byte base32:MJ4XIZJAMJQXGZJTGI======) - no params", + Request: requestInfo{ + Path: "/v2/applications/5/box", + Params: []param{ + {"name", "byte base32:MJ4XIZJAMJQXGZJTGI======"}, + }, + }, + }, + { + Name: "DELETED app 5 encoding (byte base64:Ynl0ZSBiYXNlNjQ=) - no params", + Request: requestInfo{ + Path: "/v2/applications/5/box", + Params: []param{ + {"name", "byte base64:Ynl0ZSBiYXNlNjQ="}, + }, + }, + }, + { + Name: "DELETED app 5 illegal encoding (just a plain string) - no params", + Request: requestInfo{ + Path: "/v2/applications/5/box", + Params: []param{ + {"name", "just a plain string"}, + }, + }, + }, + { + Name: "App 1337 non-existing with illegal encoding (just a plain string) - no params", + Request: requestInfo{ + Path: "/v2/applications/1337/box", + Params: []param{ + {"name", "just a plain string"}, + }, + }, + }, + }, +} + +func TestBoxes(t *testing.T) { + db, proc, l, dbShutdown := setupIdbAndReturnShutdownFunc(t) + defer dbShutdown() + + setupLiveBoxes(t, proc, l) + + serverShutdown := setupLiveServerAndReturnShutdownFunc(t, db) + defer serverShutdown() + + validateOrGenerateFixtures(t, db, boxSeedFixture, "TestBoxes") +} diff --git a/api/error_messages.go b/api/error_messages.go index 2b33b774d..692190f40 100644 --- a/api/error_messages.go +++ b/api/error_messages.go @@ -21,8 +21,12 @@ const ( errFailedSearchingAsset = "failed while searching for asset" errFailedSearchingAssetBalances = "failed while searching for asset balances" errFailedSearchingApplication = "failed while searching for application" + errFailedSearchingBoxes = "failed while searching for application boxes" errFailedLookingUpHealth = "failed while getting indexer health" errNoApplicationsFound = "no application found for application-id" + errNoBoxesFound = "no application boxes found" + errWrongAppidFound = "the wrong application-id was found, please contact us, this shouldn't happen" + errWrongBoxFound = "a box with an unexpected name was found, please contact us, this shouldn't happen" ErrNoAccountsFound = "no accounts found for address" errNoAssetsFound = "no assets found for asset-id" errNoTransactionFound = "no transaction found for transaction id" @@ -30,6 +34,8 @@ const ( errMultipleAccounts = "multiple accounts found for this address, please contact us, this shouldn't happen" errMultipleAssets = "multiple assets found for this id, please contact us, this shouldn't happen" 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" errMultiAcctRewind = "multiple accounts rewind is not supported by this server" errRewindingAccount = "error while rewinding account" errLookingUpBlockForRound = "error while looking up block for round" diff --git a/api/fixtures_test.go b/api/fixtures_test.go new file mode 100644 index 000000000..a94ecb11a --- /dev/null +++ b/api/fixtures_test.go @@ -0,0 +1,472 @@ +package api + +import ( + "context" + "encoding/json" + "fmt" + "io/ioutil" + "net/http" + "net/url" + "strings" + "testing" + "time" + + "github.com/sirupsen/logrus" + "github.com/stretchr/testify/require" + + "github.com/algorand/go-algorand/ledger" + + "github.com/algorand/indexer/api/generated/v2" + "github.com/algorand/indexer/idb/postgres" + "github.com/algorand/indexer/processor" + "github.com/algorand/indexer/util/test" +) + +/* See the README.md in this directory for more details about Fixtures Tests */ + +const fixtestListenAddr = "localhost:8999" +const fixtestBaseURL = "http://" + fixtestListenAddr +const fixtestMaxStartup time.Duration = 5 * time.Second +const fixturesDirectory = "test_resources/" + +var fixtestServerOpts = ExtraOptions{ + MaxAPIResourcesPerAccount: 1000, + + MaxTransactionsLimit: 10000, + DefaultTransactionsLimit: 1000, + + MaxAccountsLimit: 1000, + DefaultAccountsLimit: 100, + + MaxAssetsLimit: 1000, + DefaultAssetsLimit: 100, + + MaxBalancesLimit: 10000, + DefaultBalancesLimit: 1000, + + MaxApplicationsLimit: 1000, + DefaultApplicationsLimit: 100, + + MaxBoxesLimit: 10000, + DefaultBoxesLimit: 1000, + + DisabledMapConfig: MakeDisabledMapConfig(), +} + +type fixture struct { + File string `json:"file"` + Owner string `json:"owner"` + LastModified string `json:"lastModified"` + Frozen bool `json:"frozen"` + Cases []testCase `json:"cases"` +} +type testCase struct { + Name string `json:"name"` + Request requestInfo `json:"request"` + Response responseInfo `json:"response"` + Witness interface{} `json:"witness"` + WitnessError *string `json:"witnessError"` +} +type requestInfo struct { + Path string `json:"path"` + Params []param `json:"params"` + URL string `json:"url"` + Route string `json:"route"` // `Route` stores the simulated route found in `proverRoutes` +} +type param struct { + Name string `json:"name"` + Value string `json:"value"` +} +type responseInfo struct { + StatusCode int `json:"statusCode"` + Body string `json:"body"` +} +type prover func(responseInfo) (interface{}, *string) + +// ---- BEGIN provers / witness generators ---- // + +func accountsProof(resp responseInfo) (wit interface{}, errStr *string) { + accounts := generated.AccountsResponse{} + errStr = parseForProver(resp, &accounts) + if errStr != nil { + return + } + wit = struct { + Type string `json:"goType"` + Accounts generated.AccountsResponse `json:"accounts"` + }{ + Type: fmt.Sprintf("%T", accounts), + Accounts: accounts, + } + return +} +func accountInfoProof(resp responseInfo) (wit interface{}, errStr *string) { + account := generated.AccountResponse{} + errStr = parseForProver(resp, &account) + if errStr != nil { + return + } + wit = struct { + Type string `json:"goType"` + Account generated.AccountResponse `json:"account"` + }{ + Type: fmt.Sprintf("%T", account), + Account: account, + } + return +} + +func appsProof(resp responseInfo) (wit interface{}, errStr *string) { + apps := generated.ApplicationsResponse{} + errStr = parseForProver(resp, &apps) + if errStr != nil { + return + } + wit = struct { + Type string `json:"goType"` + Apps generated.ApplicationsResponse `json:"apps"` + }{ + Type: fmt.Sprintf("%T", apps), + Apps: apps, + } + return +} + +func appInfoProof(resp responseInfo) (wit interface{}, errStr *string) { + app := generated.ApplicationResponse{} + errStr = parseForProver(resp, &app) + if errStr != nil { + return + } + wit = struct { + Type string `json:"goType"` + App generated.ApplicationResponse `json:"app"` + }{ + Type: fmt.Sprintf("%T", app), + App: app, + } + return +} + +func boxProof(resp responseInfo) (wit interface{}, errStr *string) { + box := generated.BoxResponse{} + errStr = parseForProver(resp, &box) + if errStr != nil { + return + } + wit = struct { + Type string `json:"goType"` + Box generated.BoxResponse `json:"box"` + }{ + Type: fmt.Sprintf("%T", box), + Box: box, + } + return +} + +func boxesProof(resp responseInfo) (wit interface{}, errStr *string) { + boxes := generated.BoxesResponse{} + errStr = parseForProver(resp, &boxes) + if errStr != nil { + return + } + wit = struct { + Type string `json:"goType"` + Boxes generated.BoxesResponse `json:"boxes"` + }{ + Type: fmt.Sprintf("%T", boxes), + Boxes: boxes, + } + return +} + +func parseForProver(resp responseInfo, reconstructed interface{}) (errStr *string) { + if resp.StatusCode >= 300 { + s := fmt.Sprintf("%d error", resp.StatusCode) + errStr = &s + return + } + err := json.Unmarshal([]byte(resp.Body), reconstructed) + if err != nil { + s := fmt.Sprintf("unmarshal err: %s", err) + errStr = &s + return + } + return nil +} + +// ---- END provers / witness generators ---- // + +func (f *testCase) proverFromEndoint() (string, prover, error) { + path := f.Request.Path + if len(path) == 0 || path[0] != '/' { + return "", nil, fmt.Errorf("invalid endpoint [%s]", path) + } + return getProof(path[1:]) +} + +type proofPath struct { + parts map[string]proofPath + proof prover +} + +var proverRoutes = proofPath{ + parts: map[string]proofPath{ + "v2": { + parts: map[string]proofPath{ + "accounts": { + proof: accountsProof, + parts: map[string]proofPath{ + ":account-id": { + proof: accountInfoProof, + }, + }, + }, + "applications": { + proof: appsProof, + parts: map[string]proofPath{ + ":application-id": { + proof: appInfoProof, + parts: map[string]proofPath{ + "box": { + proof: boxProof, + }, + "boxes": { + proof: boxesProof, + }, + }, + }, + }, + }, + }, + }, + }, +} + +func getProof(path string) (route string, proof prover, err error) { + var impl func(string, []string, proofPath) (string, prover, error) + impl = func(prefix string, suffix []string, node proofPath) (path string, proof prover, err error) { + if len(suffix) == 0 { + return prefix, node.proof, nil + } + part := suffix[0] + next, ok := node.parts[part] + if ok { + return impl(prefix+"/"+part, suffix[1:], next) + } + // look for a wild-card part, e.g. ":application-id" + for routePart, next := range node.parts { + if routePart[0] == ':' { + return impl(prefix+"/"+routePart, suffix[1:], next) + } + } + // no wild-card, so an error + return prefix, nil, fmt.Errorf("<<>>\nfollowing sub-path (%s) cannot find part [%s]", suffix, node, prefix, part) + } + + return impl("", strings.Split(path, "/"), proverRoutes) +} + +// WARNING: receiver should not call l.Close() +func setupIdbAndReturnShutdownFunc(t *testing.T) (db *postgres.IndexerDb, proc processor.Processor, l *ledger.Ledger, shutdown func()) { + db, dbShutdown, proc, l := setupIdb(t, test.MakeGenesis()) + + shutdown = func() { + dbShutdown() + l.Close() + } + + return +} + +func setupLiveServerAndReturnShutdownFunc(t *testing.T, db *postgres.IndexerDb) (shutdown func()) { + serverCtx, shutdown := context.WithCancel(context.Background()) + go Serve(serverCtx, fixtestListenAddr, db, nil, logrus.New(), fixtestServerOpts) + + serverUp := false + for maxWait := fixtestMaxStartup; !serverUp && maxWait > 0; maxWait -= 50 * time.Millisecond { + time.Sleep(50 * time.Millisecond) + _, resp, _, reqErr, bodyErr := getRequest(t, "/health", []param{}) + if reqErr != nil || bodyErr != nil { + t.Log("waiting for server:", reqErr, bodyErr) + continue + } + if resp.StatusCode != http.StatusOK { + t.Log("waiting for server OK:", resp.StatusCode) + continue + } + serverUp = true + } + require.True(t, serverUp, "api.Serve did not start server in time") + + return +} + +func readFixture(t *testing.T, path string, seed *fixture) fixture { + fileBytes, err := ioutil.ReadFile(path + seed.File) + require.NoError(t, err) + + saved := fixture{} + err = json.Unmarshal(fileBytes, &saved) + require.NoError(t, err) + + return saved +} + +func writeFixture(t *testing.T, path string, save fixture) { + fileBytes, err := json.MarshalIndent(save, "", " ") + require.NoError(t, err) + + err = ioutil.WriteFile(path+save.File, fileBytes, 0644) + require.NoError(t, err) +} + +func getRequest(t *testing.T, endpoint string, params []param) (path string, resp *http.Response, body []byte, reqErr, bodyErr error) { + verbose := true + + path = fixtestBaseURL + endpoint + + if len(params) > 0 { + urlValues := url.Values{} + for _, param := range params { + urlValues.Add(param.Name, param.Value) + } + path += "?" + urlValues.Encode() + } + + t.Log("making HTTP request path", path) + resp, reqErr = http.Get(path) + if reqErr != nil { + reqErr = fmt.Errorf("client: error making http request: %w", reqErr) + return + } + require.NoError(t, reqErr) + defer resp.Body.Close() + + body, bodyErr = ioutil.ReadAll(resp.Body) + + if verbose { + fmt.Printf(` +resp=%+v +body=%s +reqErr=%v +bodyErr=%v`, resp, string(body), reqErr, bodyErr) + } + return +} + +func generateLiveFixture(t *testing.T, seed fixture) (live fixture) { + live = fixture{ + File: seed.File, + Owner: seed.Owner, + Frozen: seed.Frozen, + } + + for i, seedCase := range seed.Cases { + msg := fmt.Sprintf("Case %d. seedCase=%+v.", i, seedCase) + liveCase := testCase{ + Name: seedCase.Name, + Request: seedCase.Request, + } + + path, resp, body, reqErr, bodyErr := getRequest(t, seedCase.Request.Path, seedCase.Request.Params) + require.NoError(t, reqErr, msg) + + // not sure about this one!!! + require.NoError(t, bodyErr, msg) + liveCase.Request.URL = path + + liveCase.Response = responseInfo{ + StatusCode: resp.StatusCode, + Body: string(body), + } + msg += fmt.Sprintf(" newResponse=%+v", liveCase.Response) + + route, prove, err := seedCase.proverFromEndoint() + require.NoError(t, err, msg) + require.Positive(t, len(route), msg) + + liveCase.Request.Route = route + + if prove != nil { + witness, errStr := prove(liveCase.Response) + liveCase.Witness = witness + liveCase.WitnessError = errStr + } + live.Cases = append(live.Cases, liveCase) + } + + live.LastModified = time.Now().String() + writeFixture(t, fixturesDirectory+"_", live) + + return +} + +func validateLiveVsSaved(t *testing.T, seed *fixture, live *fixture) { + require.True(t, live.Frozen, "should be frozen for assertions") + saved := readFixture(t, fixturesDirectory, seed) + + require.Equal(t, saved.Owner, live.Owner, "unexpected discrepancy in Owner") + // sanity check: + require.Equal(t, seed.Owner, saved.Owner, "unexpected discrepancy in Owner") + + require.Equal(t, saved.File, live.File, "unexpected discrepancy in File") + // sanity check: + require.Equal(t, seed.File, saved.File, "unexpected discrepancy in File") + + numSeedCases, numSavedCases, numLiveCases := len(seed.Cases), len(saved.Cases), len(live.Cases) + require.Equal(t, numSavedCases, numLiveCases, "numSavedCases=%d but numLiveCases=%d", numSavedCases, numLiveCases) + // sanity check: + require.Equal(t, numSeedCases, numSavedCases, "numSeedCases=%d but numSavedCases=%d", numSeedCases, numSavedCases) + + for i, seedCase := range seed.Cases { + savedCase, liveCase := saved.Cases[i], live.Cases[i] + msg := fmt.Sprintf("(%d)[%s]. discrepency in seed=\n%+v\nsaved=\n%+v\nlive=\n%+v\n", i, seedCase.Name, seedCase, savedCase, liveCase) + + require.Equal(t, savedCase.Name, liveCase.Name, msg) + // sanity check: + require.Equal(t, seedCase.Name, savedCase.Name, msg) + + // only saved vs live: + require.Equal(t, savedCase.Request, liveCase.Request, msg) + require.Equal(t, savedCase.Response, liveCase.Response, msg) + + route, prove, err := savedCase.proverFromEndoint() + require.NoError(t, err, msg) + require.NotNil(t, prove, msg) + require.Equal(t, savedCase.Request.Route, route, msg) + + savedProof, savedErrStr := prove(savedCase.Response) + liveProof, liveErrStr := prove(liveCase.Response) + require.Equal(t, savedProof, liveProof, msg) + require.Equal(t, savedErrStr, liveErrStr, msg) + // sanity check: + require.Equal(t, savedCase.WitnessError, liveCase.WitnessError, msg) + require.Equal(t, savedCase.WitnessError == nil, savedCase.Witness != nil, msg) + } + // and the saved fixture should be frozen as well before release: + require.True(t, saved.Frozen, "Please ensure that the saved fixture is frozen before merging.") +} + +// When the provided seed has `seed.Frozen == false` assertions will be skipped. +// On the other hand, when `seed.Frozen == false` assertions are made: +// * ownerVariable == saved.Owner == live.Owner +// * saved.File == live.File +// * len(saved.Cases) == len(live.Cases) +// * for each savedCase: +// - savedCase.Name == liveCase.Name +// - savedCase.Request == liveCase.Request +// - recalculated savedCase.Witness == recalculated liveCase.Witness +// +// Regardless of `seed.Frozen`, `live` is saved to `fixturesDirectory + "_" + seed.File` +// NOTE: `live.Witness` is always recalculated via `seed.proof(live.Response)` +// NOTE: by design, the function always fails the test in the case that the seed fixture is not frozen +// as a reminder to freeze the test before merging, so that regressions may be detected going forward. +func validateOrGenerateFixtures(t *testing.T, db *postgres.IndexerDb, seed fixture, owner string) { + require.Equal(t, owner, seed.Owner, "mismatch between purported owners of fixture") + + live := generateLiveFixture(t, seed) + + require.True(t, seed.Frozen, "To guard against regressions, please ensure that the seed is frozen before merging.") + validateLiveVsSaved(t, &seed, &live) +} diff --git a/api/generated/common/routes.go b/api/generated/common/routes.go index 0bba2da8b..c13431036 100644 --- a/api/generated/common/routes.go +++ b/api/generated/common/routes.go @@ -71,179 +71,185 @@ func RegisterHandlers(router interface { // Base64 encoded, gzipped, json marshaled Swagger object var swaggerSpec = []string{ - "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", + "E/9igOtyPfv4y8f/HwAA///zL8OU5twAAA==", } // GetSwagger returns the Swagger specification corresponding to the generated code diff --git a/api/generated/common/types.go b/api/generated/common/types.go index 75c8bc799..ac3455b49 100644 --- a/api/generated/common/types.go +++ b/api/generated/common/types.go @@ -91,6 +91,12 @@ type Account struct { // The count of all assets that have been opted in, equivalent to the count of AssetHolding objects held by this account. TotalAssetsOptedIn uint64 `json:"total-assets-opted-in"` + // For app-accounts only. The total number of bytes allocated for the keys and values of boxes which belong to the associated application. + TotalBoxBytes uint64 `json:"total-box-bytes"` + + // For app-accounts only. The total number of boxes which belong to the associated application. + TotalBoxes uint64 `json:"total-boxes"` + // The count of all apps (AppParams objects) created by this account. TotalCreatedApps uint64 `json:"total-created-apps"` @@ -414,6 +420,23 @@ type BlockUpgradeVote struct { UpgradePropose *string `json:"upgrade-propose,omitempty"` } +// Box defines model for Box. +type Box struct { + + // \[name\] box name, base64 encoded + Name []byte `json:"name"` + + // \[value\] box value, base64 encoded. + Value []byte `json:"value"` +} + +// BoxDescriptor defines model for BoxDescriptor. +type BoxDescriptor struct { + + // Base64 encoded box name + Name []byte `json:"name"` +} + // EvalDelta defines model for EvalDelta. type EvalDelta struct { @@ -1009,6 +1032,9 @@ type AuthAddr string // BeforeTime defines model for before-time. type BeforeTime time.Time +// BoxName defines model for box-name. +type BoxName string + // CurrencyGreaterThan defines model for currency-greater-than. type CurrencyGreaterThan uint64 @@ -1179,6 +1205,20 @@ type AssetsResponse struct { // BlockResponse defines model for BlockResponse. type BlockResponse Block +// BoxResponse defines model for BoxResponse. +type BoxResponse Box + +// BoxesResponse defines model for BoxesResponse. +type BoxesResponse struct { + + // \[appidx\] application index. + ApplicationId uint64 `json:"application-id"` + Boxes []BoxDescriptor `json:"boxes"` + + // Used for pagination, when making another request provide this token with the next parameter. + NextToken *string `json:"next-token,omitempty"` +} + // ErrorResponse defines model for ErrorResponse. type ErrorResponse struct { Data *map[string]interface{} `json:"data,omitempty"` diff --git a/api/generated/v2/routes.go b/api/generated/v2/routes.go index e6bbfc615..173155bae 100644 --- a/api/generated/v2/routes.go +++ b/api/generated/v2/routes.go @@ -44,6 +44,12 @@ type ServerInterface interface { // (GET /v2/applications/{application-id}) LookupApplicationByID(ctx echo.Context, applicationId uint64, params LookupApplicationByIDParams) error + // Get box information for a given application. + // (GET /v2/applications/{application-id}/box) + LookupApplicationBoxByIDAndName(ctx echo.Context, applicationId uint64, params LookupApplicationBoxByIDAndNameParams) error + // Get box names for a given application. + // (GET /v2/applications/{application-id}/boxes) + SearchForApplicationBoxes(ctx echo.Context, applicationId uint64, params SearchForApplicationBoxesParams) error // (GET /v2/applications/{application-id}/logs) LookupApplicationLogsByID(ctx echo.Context, applicationId uint64, params LookupApplicationLogsByIDParams) error @@ -881,6 +887,101 @@ func (w *ServerInterfaceWrapper) LookupApplicationByID(ctx echo.Context) error { return err } +// LookupApplicationBoxByIDAndName converts echo context to params. +func (w *ServerInterfaceWrapper) LookupApplicationBoxByIDAndName(ctx echo.Context) error { + + validQueryParams := map[string]bool{ + "pretty": true, + "name": true, + } + + // Check for unknown query parameters. + for name, _ := range ctx.QueryParams() { + if _, ok := validQueryParams[name]; !ok { + return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("Unknown parameter detected: %s", name)) + } + } + + var err error + // ------------- Path parameter "application-id" ------------- + var applicationId uint64 + + err = runtime.BindStyledParameter("simple", false, "application-id", ctx.Param("application-id"), &applicationId) + if err != nil { + return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("Invalid format for parameter application-id: %s", err)) + } + + // Parameter object where we will unmarshal all parameters from the context + var params LookupApplicationBoxByIDAndNameParams + // ------------- Required query parameter "name" ------------- + if paramValue := ctx.QueryParam("name"); paramValue != "" { + + } else { + return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("Query argument name is required, but not found")) + } + + err = runtime.BindQueryParameter("form", true, true, "name", ctx.QueryParams(), ¶ms.Name) + if err != nil { + return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("Invalid format for parameter name: %s", err)) + } + + // Invoke the callback with all the unmarshalled arguments + err = w.Handler.LookupApplicationBoxByIDAndName(ctx, applicationId, params) + return err +} + +// SearchForApplicationBoxes converts echo context to params. +func (w *ServerInterfaceWrapper) SearchForApplicationBoxes(ctx echo.Context) error { + + validQueryParams := map[string]bool{ + "pretty": true, + "limit": true, + "next": true, + } + + // Check for unknown query parameters. + for name, _ := range ctx.QueryParams() { + if _, ok := validQueryParams[name]; !ok { + return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("Unknown parameter detected: %s", name)) + } + } + + var err error + // ------------- Path parameter "application-id" ------------- + var applicationId uint64 + + err = runtime.BindStyledParameter("simple", false, "application-id", ctx.Param("application-id"), &applicationId) + if err != nil { + return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("Invalid format for parameter application-id: %s", err)) + } + + // Parameter object where we will unmarshal all parameters from the context + var params SearchForApplicationBoxesParams + // ------------- Optional query parameter "limit" ------------- + if paramValue := ctx.QueryParam("limit"); paramValue != "" { + + } + + err = runtime.BindQueryParameter("form", true, false, "limit", ctx.QueryParams(), ¶ms.Limit) + if err != nil { + return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("Invalid format for parameter limit: %s", err)) + } + + // ------------- Optional query parameter "next" ------------- + if paramValue := ctx.QueryParam("next"); paramValue != "" { + + } + + err = runtime.BindQueryParameter("form", true, false, "next", ctx.QueryParams(), ¶ms.Next) + if err != nil { + return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("Invalid format for parameter next: %s", err)) + } + + // Invoke the callback with all the unmarshalled arguments + err = w.Handler.SearchForApplicationBoxes(ctx, applicationId, params) + return err +} + // LookupApplicationLogsByID converts echo context to params. func (w *ServerInterfaceWrapper) LookupApplicationLogsByID(ctx echo.Context) error { @@ -1747,6 +1848,8 @@ func RegisterHandlers(router interface { router.GET("/v2/accounts/:account-id/transactions", wrapper.LookupAccountTransactions, m...) router.GET("/v2/applications", wrapper.SearchForApplications, m...) router.GET("/v2/applications/:application-id", wrapper.LookupApplicationByID, m...) + router.GET("/v2/applications/:application-id/box", wrapper.LookupApplicationBoxByIDAndName, m...) + router.GET("/v2/applications/:application-id/boxes", wrapper.SearchForApplicationBoxes, m...) router.GET("/v2/applications/:application-id/logs", wrapper.LookupApplicationLogsByID, m...) router.GET("/v2/assets", wrapper.SearchForAssets, m...) router.GET("/v2/assets/:asset-id", wrapper.LookupAssetByID, m...) @@ -1761,214 +1864,226 @@ func RegisterHandlers(router interface { // Base64 encoded, gzipped, json marshaled Swagger object var swaggerSpec = []string{ - "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", + "5OgXh8o+/vrx/wUAAP//PMNEZwh7AQA=", } // GetSwagger returns the Swagger specification corresponding to the generated code diff --git a/api/generated/v2/types.go b/api/generated/v2/types.go index 86f949d1f..de4cc79c3 100644 --- a/api/generated/v2/types.go +++ b/api/generated/v2/types.go @@ -91,6 +91,12 @@ type Account struct { // The count of all assets that have been opted in, equivalent to the count of AssetHolding objects held by this account. TotalAssetsOptedIn uint64 `json:"total-assets-opted-in"` + // For app-accounts only. The total number of bytes allocated for the keys and values of boxes which belong to the associated application. + TotalBoxBytes uint64 `json:"total-box-bytes"` + + // For app-accounts only. The total number of boxes which belong to the associated application. + TotalBoxes uint64 `json:"total-boxes"` + // The count of all apps (AppParams objects) created by this account. TotalCreatedApps uint64 `json:"total-created-apps"` @@ -414,6 +420,23 @@ type BlockUpgradeVote struct { UpgradePropose *string `json:"upgrade-propose,omitempty"` } +// Box defines model for Box. +type Box struct { + + // \[name\] box name, base64 encoded + Name []byte `json:"name"` + + // \[value\] box value, base64 encoded. + Value []byte `json:"value"` +} + +// BoxDescriptor defines model for BoxDescriptor. +type BoxDescriptor struct { + + // Base64 encoded box name + Name []byte `json:"name"` +} + // EvalDelta defines model for EvalDelta. type EvalDelta struct { @@ -1009,6 +1032,9 @@ type AuthAddr string // BeforeTime defines model for before-time. type BeforeTime time.Time +// BoxName defines model for box-name. +type BoxName string + // CurrencyGreaterThan defines model for currency-greater-than. type CurrencyGreaterThan uint64 @@ -1179,6 +1205,20 @@ type AssetsResponse struct { // BlockResponse defines model for BlockResponse. type BlockResponse Block +// BoxResponse defines model for BoxResponse. +type BoxResponse Box + +// BoxesResponse defines model for BoxesResponse. +type BoxesResponse struct { + + // \[appidx\] application index. + ApplicationId uint64 `json:"application-id"` + Boxes []BoxDescriptor `json:"boxes"` + + // Used for pagination, when making another request provide this token with the next parameter. + NextToken *string `json:"next-token,omitempty"` +} + // ErrorResponse defines model for ErrorResponse. type ErrorResponse struct { Data *map[string]interface{} `json:"data,omitempty"` @@ -1400,6 +1440,23 @@ type LookupApplicationByIDParams struct { IncludeAll *bool `json:"include-all,omitempty"` } +// LookupApplicationBoxByIDAndNameParams defines parameters for LookupApplicationBoxByIDAndName. +type LookupApplicationBoxByIDAndNameParams struct { + + // A box name in goal-arg form 'encoding:value'. For ints, use the form 'int:1234'. For raw bytes, use the form 'b64:A=='. For printable strings, use the form 'str:hello'. For addresses, use the form 'addr:XYZ...'. + Name string `json:"name"` +} + +// SearchForApplicationBoxesParams defines parameters for SearchForApplicationBoxes. +type SearchForApplicationBoxesParams struct { + + // Maximum number of results to return. There could be additional pages even if the limit is not reached. + Limit *uint64 `json:"limit,omitempty"` + + // The next page of results. Use the next token provided by the previous results. + Next *string `json:"next,omitempty"` +} + // LookupApplicationLogsByIDParams defines parameters for LookupApplicationLogsByID. type LookupApplicationLogsByIDParams struct { diff --git a/api/handlers.go b/api/handlers.go index 5e0d79a96..190f0d63f 100644 --- a/api/handlers.go +++ b/api/handlers.go @@ -2,6 +2,8 @@ package api import ( "context" + "database/sql" + "encoding/base64" "errors" "fmt" "math" @@ -15,6 +17,7 @@ import ( log "github.com/sirupsen/logrus" "github.com/algorand/go-algorand/data/basics" + "github.com/algorand/go-algorand/data/transactions/logic" "github.com/algorand/go-algorand/protocol" "github.com/algorand/indexer/accounting" @@ -557,6 +560,140 @@ func (si *ServerImplementation) LookupApplicationByID(ctx echo.Context, applicat }) } +// LookupApplicationBoxByIDAndName returns the value of an application's box +// (GET /v2/applications/{application-id}/box) +func (si *ServerImplementation) LookupApplicationBoxByIDAndName(ctx echo.Context, applicationID uint64, params generated.LookupApplicationBoxByIDAndNameParams) error { + if err := si.verifyHandler("LookupApplicationBoxByIDAndName", ctx); err != nil { + return badRequest(ctx, err.Error()) + } + if uint64(applicationID) > math.MaxInt64 { + return notFound(ctx, errValueExceedingInt64) + } + + encodedBoxName := params.Name + boxNameBytes, err := logic.NewAppCallBytes(encodedBoxName) + if err != nil { + return badRequest(ctx, fmt.Sprintf("LookupApplicationBoxByIDAndName received illegal box name (%s): %s", encodedBoxName, err.Error())) + } + boxName, err := boxNameBytes.Raw() + if err != nil { + return badRequest(ctx, err.Error()) + } + + q := idb.ApplicationBoxQuery{ + ApplicationID: applicationID, + BoxName: boxName, + } + appid, boxes, round, err := si.fetchApplicationBoxes(ctx.Request().Context(), q) + + if err != nil { + if err == sql.ErrNoRows { + return notFound(ctx, fmt.Sprintf("%s: round=%d, appid=%d, boxName=%s", errNoApplicationsFound, round, applicationID, encodedBoxName)) + } + // sql.ErrNoRows is the only expected error condition + msg := fmt.Sprintf("%s: round=?=%d, appid=%d, boxName=%s", errFailedLookingUpBoxes, round, applicationID, encodedBoxName) + return indexerError(ctx, fmt.Errorf("%s: %w", msg, err)) + } + + if len(boxes) == 0 { // this is an unexpected situation as should have received a sql.ErrNoRows from fetchApplicationBoxes's err + msg := fmt.Sprintf("%s: round=?=%d, appid=%d, boxName=%s", errFailedLookingUpBoxes, round, applicationID, encodedBoxName) + return indexerError(ctx, fmt.Errorf(msg)) + } + + if appid != generated.ApplicationId(applicationID) { + return indexerError(ctx, fmt.Errorf("%s: round=%d, appid=%d, wrong appid=%d, boxName=%s", errWrongAppidFound, round, applicationID, appid, encodedBoxName)) + } + + if len(boxes) > 1 { + return indexerError(ctx, fmt.Errorf("%s: round=%d, appid=%d, boxName=%s", errMultipleBoxes, round, applicationID, encodedBoxName)) + } + + box := boxes[0] + if len(box.Name) == 0 && len(boxName) > 0 { + return notFound(ctx, fmt.Sprintf("%s: round=%d, appid=%d, boxName=%s", errNoBoxesFound, round, applicationID, encodedBoxName)) + } + + if string(box.Name) != string(boxName) { + return indexerError(ctx, fmt.Errorf("%s: round=%d, appid=%d, boxName=%s", errWrongBoxFound, round, applicationID, encodedBoxName)) + } + + return ctx.JSON(http.StatusOK, generated.BoxResponse(box)) +} + +// SearchForApplicationBoxes returns box names for an app +// (GET /v2/applications/{application-id}/boxes) +func (si *ServerImplementation) SearchForApplicationBoxes(ctx echo.Context, applicationID uint64, params generated.SearchForApplicationBoxesParams) error { + if err := si.verifyHandler("SearchForApplicationBoxes", ctx); err != nil { + return badRequest(ctx, err.Error()) + } + if uint64(applicationID) > math.MaxInt64 { + return notFound(ctx, errValueExceedingInt64) + } + happyResponse := generated.BoxesResponse{ApplicationId: applicationID, Boxes: []generated.BoxDescriptor{}} + + q := idb.ApplicationBoxQuery{ + ApplicationID: applicationID, + OmitValues: true, + } + if params.Limit != nil { + q.Limit = *params.Limit + } + if params.Next != nil { + encodedBoxName := *params.Next + boxNameBytes, err := logic.NewAppCallBytes(encodedBoxName) + if err != nil { + return badRequest(ctx, fmt.Sprintf("SearchForApplicationBoxes received illegal next token (%s): %s", encodedBoxName, err.Error())) + } + prevBox, err := boxNameBytes.Raw() + if err != nil { + return badRequest(ctx, err.Error()) + } + q.PrevFinalBox = []byte(prevBox) + } + + appid, boxes, round, err := si.fetchApplicationBoxes(ctx.Request().Context(), q) + + if err != nil { + if err == sql.ErrNoRows { + // NOTE: as an application may have once existed, we DO NOT error when not finding the corresponding application ID + return ctx.JSON(http.StatusOK, happyResponse) + } + // sql.ErrNoRows is the only expected error condition + msg := fmt.Sprintf("%s: round=?=%d, appid=%d", errFailedSearchingBoxes, round, applicationID) + return indexerError(ctx, fmt.Errorf("%s: %w", msg, err)) + } + + if len(boxes) == 0 { // this is an unexpected situation as should have received a sql.ErrNoRows from fetchApplicationBoxes's err + msg := fmt.Sprintf("%s: round=?=%d, appid=%d", errFailedSearchingBoxes, round, applicationID) + return indexerError(ctx, fmt.Errorf(msg)) + } + + if appid != generated.ApplicationId(applicationID) { + return indexerError(ctx, fmt.Errorf("%s: round=%d, appid=%d, wrong appid=%d", errWrongAppidFound, round, applicationID, appid)) + } + + var next *string + finalNameBytes := boxes[len(boxes)-1].Name + if finalNameBytes != nil { + encoded := base64.StdEncoding.EncodeToString(finalNameBytes) + next = strPtr(encoded) + if next != nil { + next = strPtr("b64:" + string(*next)) + } + } + happyResponse.NextToken = next + descriptors := []generated.BoxDescriptor{} + for _, box := range boxes { + if box.Name == nil { + continue + } + descriptors = append(descriptors, generated.BoxDescriptor{Name: box.Name}) + } + happyResponse.Boxes = descriptors + + return ctx.JSON(http.StatusOK, happyResponse) +} + // LookupApplicationLogsByID returns one application logs // (GET /v2/applications/{application-id}/logs) func (si *ServerImplementation) LookupApplicationLogsByID(ctx echo.Context, applicationID uint64, params generated.LookupApplicationLogsByIDParams) error { @@ -943,6 +1080,29 @@ func (si *ServerImplementation) fetchApplications(ctx context.Context, params id return apps, round, nil } +// fetchApplications fetches all results +func (si *ServerImplementation) fetchApplicationBoxes(ctx context.Context, params idb.ApplicationBoxQuery) (appid generated.ApplicationId, boxes []generated.Box, round uint64, err error) { + boxes = make([]generated.Box, 0) + + err = callWithTimeout(ctx, si.log, si.timeout, func(ctx context.Context) error { + var results <-chan idb.ApplicationBoxRow + results, round = si.db.ApplicationBoxes(ctx, params) + + for result := range results { + if result.Error != nil { + return result.Error + } + if appid == 0 { + appid = generated.ApplicationId(result.App) + } + boxes = append(boxes, result.Box) + } + + return nil + }) + return +} + // fetchAppLocalStates fetches all generated.AppLocalState from a query func (si *ServerImplementation) fetchAppLocalStates(ctx context.Context, params idb.ApplicationQuery) ([]generated.ApplicationLocalState, uint64, error) { var round uint64 diff --git a/api/handlers_e2e_test.go b/api/handlers_e2e_test.go index 310ce05fc..6ae241a71 100644 --- a/api/handlers_e2e_test.go +++ b/api/handlers_e2e_test.go @@ -24,6 +24,7 @@ import ( "github.com/algorand/go-algorand/data/basics" "github.com/algorand/go-algorand/data/bookkeeping" "github.com/algorand/go-algorand/data/transactions" + "github.com/algorand/go-algorand/data/transactions/logic" "github.com/algorand/go-algorand/ledger" "github.com/algorand/go-algorand/rpcs" "github.com/algorand/indexer/processor" @@ -54,9 +55,15 @@ var defaultOpts = ExtraOptions{ MaxApplicationsLimit: 1000, DefaultApplicationsLimit: 100, + MaxBoxesLimit: 10000, + DefaultBoxesLimit: 1000, + DisabledMapConfig: MakeDisabledMapConfig(), } +type boxTestComparator func(t *testing.T, db *postgres.IndexerDb, appBoxes map[basics.AppIndex]map[string]string, + deletedBoxes map[basics.AppIndex]map[string]bool, verifyTotals bool) + func testServerImplementation(db idb.IndexerDb) *ServerImplementation { return &ServerImplementation{db: db, timeout: 30 * time.Second, opts: defaultOpts} } @@ -1658,3 +1665,352 @@ func TestGetBlocksTransactionsLimit(t *testing.T) { }) } } + +// compareAppBoxesAgainstHandler is of type BoxTestComparator +func compareAppBoxesAgainstHandler(t *testing.T, db *postgres.IndexerDb, + appBoxes map[basics.AppIndex]map[string]string, deletedBoxes map[basics.AppIndex]map[string]bool, verifyTotals bool) { + + setupRequest := func(path, paramName, paramValue string) (echo.Context, *ServerImplementation, *httptest.ResponseRecorder) { + e := echo.New() + req := httptest.NewRequest(http.MethodGet, "/", nil) + rec := httptest.NewRecorder() + c := e.NewContext(req, rec) + c.SetPath(path) + c.SetParamNames(paramName) + c.SetParamValues(paramValue) + api := testServerImplementation(db) + return c, api, rec + } + + remainingBoxes := map[basics.AppIndex]map[string]string{} + numRequests := 0 + sumOfBoxes := 0 + sumOfBoxBytes := 0 + + caseNum := 1 + var totalBoxes, totalBoxBytes int + for appIdx, boxes := range appBoxes { + totalBoxes = 0 + totalBoxBytes = 0 + + remainingBoxes[appIdx] = map[string]string{} + + // compare expected against handler response one box at a time + for key, expectedValue := range boxes { + msg := fmt.Sprintf("caseNum=%d, appIdx=%d, key=%#v", caseNum, appIdx, key) + expectedAppIdx, boxName, err := logic.SplitBoxKey(key) + require.NoError(t, err, msg) + require.Equal(t, appIdx, expectedAppIdx, msg) + numRequests++ + + boxDeleted := false + if deletedBoxes != nil { + if _, ok := deletedBoxes[appIdx][key]; ok { + boxDeleted = true + } + } + + c, api, rec := setupRequest("/v2/applications/:appidx/box/", "appidx", strconv.Itoa(int(appIdx))) + prefixedName := fmt.Sprintf("str:%s", boxName) + params := generated.LookupApplicationBoxByIDAndNameParams{Name: prefixedName} + err = api.LookupApplicationBoxByIDAndName(c, uint64(appIdx), params) + require.NoError(t, err, msg) + require.Equal(t, http.StatusOK, rec.Code, fmt.Sprintf("msg: %s. unexpected return code, body: %s", msg, rec.Body.String())) + + var resp generated.BoxResponse + data := rec.Body.Bytes() + err = json.Decode(data, &resp) + + if !boxDeleted { + require.NoError(t, err, msg, msg) + require.Equal(t, boxName, string(resp.Name), msg) + require.Equal(t, expectedValue, string(resp.Value), msg) + + remainingBoxes[appIdx][boxName] = expectedValue + + totalBoxes++ + totalBoxBytes += len(boxName) + len(expectedValue) + } else { + require.ErrorContains(t, err, "no rows in result set", msg) + } + } + + msg := fmt.Sprintf("caseNum=%d, appIdx=%d", caseNum, appIdx) + + expectedBoxes := remainingBoxes[appIdx] + + c, api, rec := setupRequest("/v2/applications/:appidx/boxes", "appidx", strconv.Itoa(int(appIdx))) + params := generated.SearchForApplicationBoxesParams{} + + err := api.SearchForApplicationBoxes(c, uint64(appIdx), params) + require.NoError(t, err, msg) + require.Equal(t, http.StatusOK, rec.Code, fmt.Sprintf("msg: %s. unexpected return code, body: %s", msg, rec.Body.String())) + + var resp generated.BoxesResponse + data := rec.Body.Bytes() + err = json.Decode(data, &resp) + require.NoError(t, err, msg) + + require.Equal(t, uint64(appIdx), uint64(resp.ApplicationId), msg) + + boxes := resp.Boxes + require.NotNil(t, boxes, msg) + require.Len(t, boxes, len(expectedBoxes), msg) + for _, box := range boxes { + require.Contains(t, expectedBoxes, string(box.Name), msg) + } + + if verifyTotals { + // compare expected totals against handler account_data JSON fields + msg := fmt.Sprintf("caseNum=%d, appIdx=%d", caseNum, appIdx) + + appAddr := appIdx.Address().String() + c, api, rec = setupRequest("/v2/accounts/:addr", "addr", appAddr) + fmt.Printf("appIdx=%d\nappAddr=%s\npath=/v2/accounts/%s\n", appIdx, appAddr, appAddr) + tru := true + params := generated.LookupAccountByIDParams{IncludeAll: &tru} + err := api.LookupAccountByID(c, appAddr, params) + require.NoError(t, err, msg) + require.Equal(t, http.StatusOK, rec.Code, fmt.Sprintf("msg: %s. unexpected return code, body: %s", msg, rec.Body.String())) + + var resp generated.AccountResponse + data := rec.Body.Bytes() + err = json.Decode(data, &resp) + + require.NoError(t, err, msg) + require.Equal(t, uint64(totalBoxes), resp.Account.TotalBoxes, msg) + require.Equal(t, uint64(totalBoxBytes), resp.Account.TotalBoxBytes, msg) + + // sanity check of the account summary query vs. the direct box search query results: + require.Equal(t, uint64(len(boxes)), resp.Account.TotalBoxes, msg) + } + + sumOfBoxes += totalBoxes + sumOfBoxBytes += totalBoxBytes + caseNum++ + } + + fmt.Printf("compareAppBoxesAgainstHandler succeeded with %d requests, %d boxes and %d boxBytes\n", numRequests, sumOfBoxes, sumOfBoxBytes) +} + +// test runner copy/pastad/tweaked in handlers_e2e_test.go and postgres_integration_test.go +func runBoxCreateMutateDelete(t *testing.T, comparator boxTestComparator) { + start := time.Now() + + db, shutdownFunc, proc, l := setupIdb(t, test.MakeGenesis()) + defer shutdownFunc() + defer l.Close() + + appid := basics.AppIndex(1) + + // ---- ROUND 1: create and fund the box app ---- // + currentRound := basics.Round(1) + + createTxn, err := test.MakeComplexCreateAppTxn(test.AccountA, test.BoxApprovalProgram, test.BoxClearProgram, 8) + require.NoError(t, err) + + payNewAppTxn := test.MakePaymentTxn(1000, 500000, 0, 0, 0, 0, test.AccountA, appid.Address(), basics.Address{}, + basics.Address{}) + + block, err := test.MakeBlockForTxns(test.MakeGenesisBlock().BlockHeader, &createTxn, &payNewAppTxn) + require.NoError(t, err) + + err = proc.Process(&rpcs.EncodedBlockCert{Block: block}) + require.NoError(t, err) + + opts := idb.ApplicationQuery{ApplicationID: uint64(appid)} + + rowsCh, round := db.Applications(context.Background(), opts) + require.Equal(t, uint64(currentRound), round) + + row, ok := <-rowsCh + require.True(t, ok) + require.NoError(t, row.Error) + require.NotNil(t, row.Application.CreatedAtRound) + require.Equal(t, uint64(currentRound), *row.Application.CreatedAtRound) + + // block header handoff: round 1 --> round 2 + blockHdr, err := l.BlockHdr(currentRound) + require.NoError(t, err) + + // ---- ROUND 2: create 8 boxes for appid == 1 ---- // + currentRound = basics.Round(2) + + boxNames := []string{ + "a great box", + "another great box", + "not so great box", + "disappointing box", + "don't box me in this way", + "I will be assimilated", + "I'm destined for deletion", + "box #8", + } + + expectedAppBoxes := map[basics.AppIndex]map[string]string{} + + expectedAppBoxes[appid] = map[string]string{} + newBoxValue := "\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" + boxTxns := make([]*transactions.SignedTxnWithAD, 0) + for _, boxName := range boxNames { + expectedAppBoxes[appid][logic.MakeBoxKey(appid, boxName)] = newBoxValue + + args := []string{"create", boxName} + boxTxn := test.MakeAppCallTxnWithBoxes(uint64(appid), test.AccountA, args, []string{boxName}) + boxTxns = append(boxTxns, &boxTxn) + } + + block, err = test.MakeBlockForTxns(blockHdr, boxTxns...) + require.NoError(t, err) + + err = proc.Process(&rpcs.EncodedBlockCert{Block: block}) + require.NoError(t, err) + _, round = db.Applications(context.Background(), opts) + require.Equal(t, uint64(currentRound), round) + + comparator(t, db, expectedAppBoxes, nil, true) + + // block header handoff: round 2 --> round 3 + blockHdr, err = l.BlockHdr(currentRound) + require.NoError(t, err) + + // ---- ROUND 3: populate the boxes appropriately ---- // + currentRound = basics.Round(3) + + appBoxesToSet := map[string]string{ + "a great box": "it's a wonderful box", + "another great box": "I'm wonderful too", + "not so great box": "bummer", + "disappointing box": "RUG PULL!!!!", + "don't box me in this way": "non box-conforming", + "I will be assimilated": "THE BORG", + "I'm destined for deletion": "I'm still alive!!!", + "box #8": "eight is beautiful", + } + + boxTxns = make([]*transactions.SignedTxnWithAD, 0) + expectedAppBoxes[appid] = make(map[string]string) + for boxName, valPrefix := range appBoxesToSet { + args := []string{"set", boxName, valPrefix} + boxTxn := test.MakeAppCallTxnWithBoxes(uint64(appid), test.AccountA, args, []string{boxName}) + boxTxns = append(boxTxns, &boxTxn) + + key := logic.MakeBoxKey(appid, boxName) + expectedAppBoxes[appid][key] = valPrefix + newBoxValue[len(valPrefix):] + } + block, err = test.MakeBlockForTxns(blockHdr, boxTxns...) + require.NoError(t, err) + + err = proc.Process(&rpcs.EncodedBlockCert{Block: block}) + require.NoError(t, err) + _, round = db.Applications(context.Background(), opts) + require.Equal(t, uint64(currentRound), round) + + comparator(t, db, expectedAppBoxes, nil, true) + + // block header handoff: round 3 --> round 4 + blockHdr, err = l.BlockHdr(currentRound) + require.NoError(t, err) + + // ---- ROUND 4: delete the unhappy boxes ---- // + currentRound = basics.Round(4) + + appBoxesToDelete := []string{ + "not so great box", + "disappointing box", + "I'm destined for deletion", + } + + boxTxns = make([]*transactions.SignedTxnWithAD, 0) + for _, boxName := range appBoxesToDelete { + args := []string{"delete", boxName} + boxTxn := test.MakeAppCallTxnWithBoxes(uint64(appid), test.AccountA, args, []string{boxName}) + boxTxns = append(boxTxns, &boxTxn) + + key := logic.MakeBoxKey(appid, boxName) + delete(expectedAppBoxes[appid], key) + } + block, err = test.MakeBlockForTxns(blockHdr, boxTxns...) + require.NoError(t, err) + + err = proc.Process(&rpcs.EncodedBlockCert{Block: block}) + require.NoError(t, err) + _, round = db.Applications(context.Background(), opts) + require.Equal(t, uint64(currentRound), round) + + deletedBoxes := make(map[basics.AppIndex]map[string]bool) + deletedBoxes[appid] = make(map[string]bool) + for _, deletedBox := range appBoxesToDelete { + deletedBoxes[appid][deletedBox] = true + } + comparator(t, db, expectedAppBoxes, deletedBoxes, true) + + // block header handoff: round 4 --> round 5 + blockHdr, err = l.BlockHdr(currentRound) + require.NoError(t, err) + + // ---- ROUND 5: create 3 new boxes, overwriting one of the former boxes ---- // + currentRound = basics.Round(5) + + appBoxesToCreate := []string{ + "fantabulous", + "disappointing box", // overwriting here + "AVM is the new EVM", + } + boxTxns = make([]*transactions.SignedTxnWithAD, 0) + for _, boxName := range appBoxesToCreate { + args := []string{"create", boxName} + boxTxn := test.MakeAppCallTxnWithBoxes(uint64(appid), test.AccountA, args, []string{boxName}) + boxTxns = append(boxTxns, &boxTxn) + + key := logic.MakeBoxKey(appid, boxName) + expectedAppBoxes[appid][key] = newBoxValue + } + block, err = test.MakeBlockForTxns(blockHdr, boxTxns...) + require.NoError(t, err) + + err = proc.Process(&rpcs.EncodedBlockCert{Block: block}) + require.NoError(t, err) + _, round = db.Applications(context.Background(), opts) + require.Equal(t, uint64(currentRound), round) + + comparator(t, db, expectedAppBoxes, nil, true) + + // block header handoff: round 5 --> round 6 + blockHdr, err = l.BlockHdr(currentRound) + require.NoError(t, err) + + // ---- ROUND 6: populate the 3 new boxes ---- // + currentRound = basics.Round(6) + + appBoxesToSet = map[string]string{ + "fantabulous": "Italian food's the best!", // max char's + "disappointing box": "you made it!", + "AVM is the new EVM": "yes we can!", + } + boxTxns = make([]*transactions.SignedTxnWithAD, 0) + for boxName, valPrefix := range appBoxesToSet { + args := []string{"set", boxName, valPrefix} + boxTxn := test.MakeAppCallTxnWithBoxes(uint64(appid), test.AccountA, args, []string{boxName}) + boxTxns = append(boxTxns, &boxTxn) + + key := logic.MakeBoxKey(appid, boxName) + expectedAppBoxes[appid][key] = valPrefix + newBoxValue[len(valPrefix):] + } + block, err = test.MakeBlockForTxns(blockHdr, boxTxns...) + require.NoError(t, err) + + err = proc.Process(&rpcs.EncodedBlockCert{Block: block}) + require.NoError(t, err) + _, round = db.Applications(context.Background(), opts) + require.Equal(t, uint64(currentRound), round) + + comparator(t, db, expectedAppBoxes, nil, true) + + fmt.Printf("runBoxCreateMutateDelete total time: %s\n", time.Since(start)) +} + +// Test that box evolution is ingested as expected across rounds using API to compare +func TestBoxCreateMutateDeleteAgainstHandler(t *testing.T) { + runBoxCreateMutateDelete(t, compareAppBoxesAgainstHandler) +} diff --git a/api/handlers_test.go b/api/handlers_test.go index b0f572a30..4f246e0de 100644 --- a/api/handlers_test.go +++ b/api/handlers_test.go @@ -19,8 +19,6 @@ import ( "github.com/stretchr/testify/require" "github.com/algorand/go-algorand-sdk/encoding/msgpack" - "github.com/algorand/go-algorand/crypto" - "github.com/algorand/go-algorand/crypto/merklesignature" "github.com/algorand/go-algorand/data/basics" "github.com/algorand/go-algorand/data/bookkeeping" "github.com/algorand/go-algorand/data/transactions" @@ -752,47 +750,6 @@ func TestFetchAccountsRewindRoundTooLarge(t *testing.T) { assert.True(t, strings.HasPrefix(err.Error(), errRewindingAccount), err.Error()) } -// createTxn allows saving msgp-encoded canonical object to a file in order to add more test data -func createTxn(t *testing.T, target string) []byte { - defer assert.Fail(t, "this method should only be used for generating test inputs.") - addr1, err := basics.UnmarshalChecksumAddress("PT4K5LK4KYIQYYRAYPAZIEF47NVEQRDX3CPYWJVH25LKO2METIRBKRHRAE") - assert.Error(t, err) - var votePK crypto.OneTimeSignatureVerifier - votePK[0] = 1 - - var selectionPK crypto.VRFVerifier - selectionPK[0] = 1 - - var sprfkey merklesignature.Commitment - sprfkey[0] = 1 - - stxnad := transactions.SignedTxnWithAD{ - SignedTxn: transactions.SignedTxn{ - Txn: transactions.Transaction{ - Type: protocol.KeyRegistrationTx, - Header: transactions.Header{ - Sender: addr1, - }, - KeyregTxnFields: transactions.KeyregTxnFields{ - VotePK: votePK, - SelectionPK: selectionPK, - StateProofPK: sprfkey, - VoteFirst: basics.Round(0), - VoteLast: basics.Round(100), - VoteKeyDilution: 1000, - Nonparticipation: false, - }, - }, - }, - ApplyData: transactions.ApplyData{}, - } - - data := msgpack.Encode(stxnad) - err = ioutil.WriteFile(target, data, 0644) - assert.NoError(t, err) - return data -} - func TestLookupApplicationLogsByID(t *testing.T) { mockIndexer := &mocks.IndexerDb{} si := testServerImplementation(mockIndexer) diff --git a/api/indexer.oas2.json b/api/indexer.oas2.json index 5400c7949..845a99340 100644 --- a/api/indexer.oas2.json +++ b/api/indexer.oas2.json @@ -485,6 +485,94 @@ } } }, + "/v2/applications/{application-id}/boxes": { + "get": { + "description": "Given an application ID, returns the box names of that application sorted lexicographically.", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "search" + ], + "operationId": "searchForApplicationBoxes", + "summary": "Get box names for a given application.", + "parameters": [ + { + "type": "integer", + "name": "application-id", + "in": "path", + "required": true + }, + { + "$ref": "#/parameters/limit" + }, + { + "$ref": "#/parameters/next" + } + ], + "responses": { + "200": { + "$ref": "#/responses/BoxesResponse" + }, + "400": { + "$ref": "#/responses/ErrorResponse" + }, + "404": { + "$ref": "#/responses/ErrorResponse" + }, + "500": { + "$ref": "#/responses/ErrorResponse" + } + } + } + }, + "/v2/applications/{application-id}/box": { + "get": { + "description": "Given an application ID and box name, returns base64 encoded box name and value. Box names must be in the goal app call arg form 'encoding:value'. For ints, use the form 'int:1234'. For raw bytes, encode base 64 and use 'b64' prefix as in 'b64:A=='. For printable strings, use the form 'str:hello'. For addresses, use the form 'addr:XYZ...'.", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "lookup" + ], + "operationId": "lookupApplicationBoxByIDAndName", + "schemes": [ + "http" + ], + "summary": "Get box information for a given application.", + "parameters": [ + { + "type": "integer", + "name": "application-id", + "in": "path", + "required": true + }, + { + "$ref": "#/parameters/box-name" + } + ], + "responses": { + "200": { + "$ref": "#/responses/BoxResponse" + }, + "400": { + "$ref": "#/responses/ErrorResponse" + }, + "404": { + "$ref": "#/responses/ErrorResponse" + }, + "500": { + "$ref": "#/responses/ErrorResponse" + } + } + } + }, "/v2/applications/{application-id}/logs": { "get": { "description": "Lookup application logs.", @@ -942,6 +1030,8 @@ "status", "total-apps-opted-in", "total-assets-opted-in", + "total-box-bytes", + "total-boxes", "total-created-apps", "total-created-assets" ], @@ -1034,6 +1124,14 @@ "description": "The count of all assets that have been opted in, equivalent to the count of AssetHolding objects held by this account.", "type": "integer" }, + "total-box-bytes": { + "description": "For app-accounts only. The total number of bytes allocated for the keys and values of boxes which belong to the associated application.", + "type": "integer" + }, + "total-boxes": { + "description": "For app-accounts only. The total number of boxes which belong to the associated application.", + "type": "integer" + }, "total-created-apps": { "description": "The count of all apps (AppParams objects) created by this account.", "type": "integer" @@ -1616,16 +1714,38 @@ } } }, - "ParticipationUpdates": { - "description": "Participation account data that needs to be checked/acted on by the network.", + "Box": { + "description": "Box name and its content.", + "required": [ + "name", + "value" + ], "type": "object", "properties": { - "expired-participation-accounts": { - "description": "\\[partupdrmv\\] a list of online accounts that needs to be converted to offline since their participation key expired.", - "type": "array", - "items": { - "type": "string" - } + "name": { + "description": "\\[name\\] box name, base64 encoded", + "format": "byte", + "type": "string" + }, + "value": { + "description": "\\[value\\] box value, base64 encoded.", + "format": "byte", + "pattern": "^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?$", + "type": "string" + } + } + }, + "BoxDescriptor": { + "description": "Box descriptor describes an app box without a value.", + "type": "object", + "required": [ + "name" + ], + "properties": { + "name": { + "description": "Base64 encoded box name", + "format": "byte", + "type": "string" } } }, @@ -1714,6 +1834,19 @@ "delete" ] }, + "ParticipationUpdates": { + "description": "Participation account data that needs to be checked/acted on by the network.", + "type": "object", + "properties": { + "expired-participation-accounts": { + "description": "\\[partupdrmv\\] a list of online accounts that needs to be converted to offline since their participation key expired.", + "type": "array", + "items": { + "type": "string" + } + } + } + }, "StateDelta": { "description": "Application state delta.", "type": "array", @@ -2549,6 +2682,13 @@ "name": "before-time", "in": "query" }, + "box-name": { + "description": "A box name in goal-arg form 'encoding:value'. For ints, use the form 'int:1234'. For raw bytes, use the form 'b64:A=='. For printable strings, use the form 'str:hello'. For addresses, use the form 'addr:XYZ...'.", + "type": "string", + "name": "name", + "in": "query", + "required": true + }, "currency-greater-than": { "type": "integer", "description": "Results should have an amount greater than this value. MicroAlgos are the default currency unless an asset-id is provided, in which case the asset will be used.", @@ -2910,6 +3050,38 @@ } } }, + "BoxesResponse": { + "description": "Box names of an application", + "schema": { + "type": "object", + "required": [ + "application-id", + "boxes" + ], + "properties": { + "application-id": { + "description": "\\[appidx\\] application index.", + "type": "integer" + }, + "boxes": { + "type": "array", + "items": { + "$ref": "#/definitions/BoxDescriptor" + } + }, + "next-token": { + "description": "Used for pagination, when making another request provide this token with the next parameter.", + "type": "string" + } + } + } + }, + "BoxResponse": { + "description": "Box information", + "schema": { + "$ref": "#/definitions/Box" + } + }, "ErrorResponse": { "description": "Response for errors", "schema":{ diff --git a/api/indexer.oas3.yml b/api/indexer.oas3.yml index dca154548..4db926f44 100644 --- a/api/indexer.oas3.yml +++ b/api/indexer.oas3.yml @@ -81,6 +81,15 @@ }, "x-algorand-format": "RFC3339 String" }, + "box-name": { + "description": "A box name in goal-arg form 'encoding:value'. For ints, use the form 'int:1234'. For raw bytes, use the form 'b64:A=='. For printable strings, use the form 'str:hello'. For addresses, use the form 'addr:XYZ...'.", + "in": "query", + "name": "name", + "required": true, + "schema": { + "type": "string" + } + }, "currency-greater-than": { "description": "Results should have an amount greater than this value. MicroAlgos are the default currency unless an asset-id is provided, in which case the asset will be used.", "in": "query", @@ -550,6 +559,46 @@ }, "description": "(empty)" }, + "BoxResponse": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Box" + } + } + }, + "description": "Box information" + }, + "BoxesResponse": { + "content": { + "application/json": { + "schema": { + "properties": { + "application-id": { + "description": "\\[appidx\\] application index.", + "type": "integer" + }, + "boxes": { + "items": { + "$ref": "#/components/schemas/BoxDescriptor" + }, + "type": "array" + }, + "next-token": { + "description": "Used for pagination, when making another request provide this token with the next parameter.", + "type": "string" + } + }, + "required": [ + "application-id", + "boxes" + ], + "type": "object" + } + } + }, + "description": "Box names of an application" + }, "ErrorResponse": { "content": { "application/json": { @@ -746,6 +795,14 @@ "description": "The count of all assets that have been opted in, equivalent to the count of AssetHolding objects held by this account.", "type": "integer" }, + "total-box-bytes": { + "description": "For app-accounts only. The total number of bytes allocated for the keys and values of boxes which belong to the associated application.", + "type": "integer" + }, + "total-boxes": { + "description": "For app-accounts only. The total number of boxes which belong to the associated application.", + "type": "integer" + }, "total-created-apps": { "description": "The count of all apps (AppParams objects) created by this account.", "type": "integer" @@ -765,6 +822,8 @@ "status", "total-apps-opted-in", "total-assets-opted-in", + "total-box-bytes", + "total-boxes", "total-created-apps", "total-created-assets" ], @@ -1301,6 +1360,43 @@ }, "type": "object" }, + "Box": { + "description": "Box name and its content.", + "properties": { + "name": { + "description": "\\[name\\] box name, base64 encoded", + "format": "byte", + "pattern": "^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?$", + "type": "string" + }, + "value": { + "description": "\\[value\\] box value, base64 encoded.", + "format": "byte", + "pattern": "^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?$", + "type": "string" + } + }, + "required": [ + "name", + "value" + ], + "type": "object" + }, + "BoxDescriptor": { + "description": "Box descriptor describes an app box without a value.", + "properties": { + "name": { + "description": "Base64 encoded box name", + "format": "byte", + "pattern": "^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?$", + "type": "string" + } + }, + "required": [ + "name" + ], + "type": "object" + }, "EvalDelta": { "description": "Represents a TEAL value delta.", "properties": { @@ -3620,6 +3716,247 @@ ] } }, + "/v2/applications/{application-id}/box": { + "get": { + "description": "Given an application ID and box name, returns base64 encoded box name and value. Box names must be in the goal app call arg form 'encoding:value'. For ints, use the form 'int:1234'. For raw bytes, encode base 64 and use 'b64' prefix as in 'b64:A=='. For printable strings, use the form 'str:hello'. For addresses, use the form 'addr:XYZ...'.", + "operationId": "lookupApplicationBoxByIDAndName", + "parameters": [ + { + "in": "path", + "name": "application-id", + "required": true, + "schema": { + "type": "integer" + } + }, + { + "description": "A box name in goal-arg form 'encoding:value'. For ints, use the form 'int:1234'. For raw bytes, use the form 'b64:A=='. For printable strings, use the form 'str:hello'. For addresses, use the form 'addr:XYZ...'.", + "in": "query", + "name": "name", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Box" + } + } + }, + "description": "Box information" + }, + "400": { + "content": { + "application/json": { + "schema": { + "properties": { + "data": { + "properties": {}, + "type": "object" + }, + "message": { + "type": "string" + } + }, + "required": [ + "message" + ], + "type": "object" + } + } + }, + "description": "Response for errors" + }, + "404": { + "content": { + "application/json": { + "schema": { + "properties": { + "data": { + "properties": {}, + "type": "object" + }, + "message": { + "type": "string" + } + }, + "required": [ + "message" + ], + "type": "object" + } + } + }, + "description": "Response for errors" + }, + "500": { + "content": { + "application/json": { + "schema": { + "properties": { + "data": { + "properties": {}, + "type": "object" + }, + "message": { + "type": "string" + } + }, + "required": [ + "message" + ], + "type": "object" + } + } + }, + "description": "Response for errors" + } + }, + "summary": "Get box information for a given application.", + "tags": [ + "lookup" + ] + } + }, + "/v2/applications/{application-id}/boxes": { + "get": { + "description": "Given an application ID, returns the box names of that application sorted lexicographically.", + "operationId": "searchForApplicationBoxes", + "parameters": [ + { + "in": "path", + "name": "application-id", + "required": true, + "schema": { + "type": "integer" + } + }, + { + "description": "Maximum number of results to return. There could be additional pages even if the limit is not reached.", + "in": "query", + "name": "limit", + "schema": { + "type": "integer" + } + }, + { + "description": "The next page of results. Use the next token provided by the previous results.", + "in": "query", + "name": "next", + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "properties": { + "application-id": { + "description": "\\[appidx\\] application index.", + "type": "integer" + }, + "boxes": { + "items": { + "$ref": "#/components/schemas/BoxDescriptor" + }, + "type": "array" + }, + "next-token": { + "description": "Used for pagination, when making another request provide this token with the next parameter.", + "type": "string" + } + }, + "required": [ + "application-id", + "boxes" + ], + "type": "object" + } + } + }, + "description": "Box names of an application" + }, + "400": { + "content": { + "application/json": { + "schema": { + "properties": { + "data": { + "properties": {}, + "type": "object" + }, + "message": { + "type": "string" + } + }, + "required": [ + "message" + ], + "type": "object" + } + } + }, + "description": "Response for errors" + }, + "404": { + "content": { + "application/json": { + "schema": { + "properties": { + "data": { + "properties": {}, + "type": "object" + }, + "message": { + "type": "string" + } + }, + "required": [ + "message" + ], + "type": "object" + } + } + }, + "description": "Response for errors" + }, + "500": { + "content": { + "application/json": { + "schema": { + "properties": { + "data": { + "properties": {}, + "type": "object" + }, + "message": { + "type": "string" + } + }, + "required": [ + "message" + ], + "type": "object" + } + } + }, + "description": "Response for errors" + } + }, + "summary": "Get box names for a given application.", + "tags": [ + "search" + ] + } + }, "/v2/applications/{application-id}/logs": { "get": { "description": "Lookup application logs.", diff --git a/api/server.go b/api/server.go index c006d82ca..f5d9e920d 100644 --- a/api/server.go +++ b/api/server.go @@ -69,6 +69,10 @@ type ExtraOptions struct { // Applications MaxApplicationsLimit uint64 DefaultApplicationsLimit uint64 + + // Boxes + MaxBoxesLimit uint64 + DefaultBoxesLimit uint64 } func (e ExtraOptions) handlerTimeout() time.Duration { diff --git a/api/test_resources/boxes.json b/api/test_resources/boxes.json new file mode 100644 index 000000000..e9ae14f15 --- /dev/null +++ b/api/test_resources/boxes.json @@ -0,0 +1,1477 @@ +{ + "file": "boxes.json", + "owner": "TestBoxes", + "lastModified": "2022-09-23 12:04:49.038378 -0500 CDT m=+3.705634393", + "frozen": true, + "cases": [ + { + "name": "What are all the accounts?", + "request": { + "path": "/v2/accounts", + "params": [], + "url": "http://localhost:8999/v2/accounts", + "route": "/v2/accounts" + }, + "response": { + "statusCode": 200, + "body": "{\"accounts\":[{\"address\":\"GJR76Q6OXNZ2CYIVCFCDTJRBAAR6TYEJJENEII3G2U3JH546SPBQA62IFY\",\"amount\":999999499000,\"amount-without-pending-rewards\":999999499000,\"created-apps\":[{\"created-at-round\":1,\"deleted\":false,\"id\":1,\"params\":{\"approval-program\":\"CCABADEYQQB6MRtBAHU2GgCABmNyZWF0ZRJBABiBIDEbgQISQAAFSDYaAhc2GgFMuURCAE42GgCABmRlbGV0ZRJBAAg2GgG8REIANzYaAIADc2V0EkEACzYaASI2GgK7QgAgNhoAgAVjaGVjaxJBABE2GgEiNhoCFbo2GgISREIAAQCBAQ==\",\"clear-state-program\":\"CIEB\",\"creator\":\"GJR76Q6OXNZ2CYIVCFCDTJRBAAR6TYEJJENEII3G2U3JH546SPBQA62IFY\",\"global-state-schema\":{\"num-byte-slice\":0,\"num-uint\":0},\"local-state-schema\":{\"num-byte-slice\":0,\"num-uint\":0}}}],\"created-at-round\":0,\"deleted\":false,\"pending-rewards\":0,\"reward-base\":0,\"rewards\":0,\"round\":9,\"sig-type\":\"sig\",\"status\":\"Offline\",\"total-apps-opted-in\":0,\"total-assets-opted-in\":0,\"total-box-bytes\":0,\"total-boxes\":0,\"total-created-apps\":1,\"total-created-assets\":0},{\"address\":\"LMTOYRT2WPSUY6JTCW2URER6YN3GETJ5FHTQBA55EVK66JG2QOB32WPIHY\",\"amount\":500000,\"amount-without-pending-rewards\":500000,\"created-at-round\":1,\"deleted\":false,\"pending-rewards\":0,\"reward-base\":0,\"rewards\":0,\"round\":9,\"status\":\"Offline\",\"total-apps-opted-in\":0,\"total-assets-opted-in\":0,\"total-box-bytes\":0,\"total-boxes\":0,\"total-created-apps\":0,\"total-created-assets\":0},{\"address\":\"N5T74SANUWLHI6ZWYFQBEB6J2VXBTYUYZNWQB2V26DCF4ARKC7GDUW3IRU\",\"amount\":999999499000,\"amount-without-pending-rewards\":999999499000,\"created-apps\":[{\"created-at-round\":1,\"deleted\":false,\"id\":3,\"params\":{\"approval-program\":\"CCABADEYQQB6MRtBAHU2GgCABmNyZWF0ZRJBABiBIDEbgQISQAAFSDYaAhc2GgFMuURCAE42GgCABmRlbGV0ZRJBAAg2GgG8REIANzYaAIADc2V0EkEACzYaASI2GgK7QgAgNhoAgAVjaGVjaxJBABE2GgEiNhoCFbo2GgISREIAAQCBAQ==\",\"clear-state-program\":\"CIEB\",\"creator\":\"N5T74SANUWLHI6ZWYFQBEB6J2VXBTYUYZNWQB2V26DCF4ARKC7GDUW3IRU\",\"global-state-schema\":{\"num-byte-slice\":0,\"num-uint\":0},\"local-state-schema\":{\"num-byte-slice\":0,\"num-uint\":0}}}],\"created-at-round\":0,\"deleted\":false,\"pending-rewards\":0,\"reward-base\":0,\"rewards\":0,\"round\":9,\"sig-type\":\"sig\",\"status\":\"Offline\",\"total-apps-opted-in\":0,\"total-assets-opted-in\":0,\"total-box-bytes\":0,\"total-boxes\":0,\"total-created-apps\":1,\"total-created-assets\":0},{\"address\":\"OKUWMFFEKF4B4D7FRQYBVV3C2SNS54ZO4WZ2MJ3576UYKFDHM5P3AFMRWE\",\"amount\":999999499000,\"amount-without-pending-rewards\":999999499000,\"created-at-round\":0,\"deleted\":false,\"pending-rewards\":0,\"reward-base\":0,\"rewards\":0,\"round\":9,\"sig-type\":\"sig\",\"status\":\"Offline\",\"total-apps-opted-in\":0,\"total-assets-opted-in\":0,\"total-box-bytes\":0,\"total-boxes\":0,\"total-created-apps\":0,\"total-created-assets\":0},{\"address\":\"WCS6TVPJRBSARHLN2326LRU5BYVJZUKI2VJ53CAWKYYHDE455ZGKANWMGM\",\"amount\":500000,\"amount-without-pending-rewards\":500000,\"created-at-round\":1,\"deleted\":false,\"pending-rewards\":0,\"reward-base\":0,\"rewards\":0,\"round\":9,\"status\":\"Offline\",\"total-apps-opted-in\":0,\"total-assets-opted-in\":0,\"total-box-bytes\":477,\"total-boxes\":9,\"total-created-apps\":0,\"total-created-assets\":0},{\"address\":\"ZROKLZW4GVOK5WQIF2GUR6LHFVEZBMV56BIQEQD4OTIZL2BPSYYUKFBSHM\",\"amount\":3000,\"amount-without-pending-rewards\":3000,\"created-at-round\":1,\"deleted\":false,\"pending-rewards\":0,\"reward-base\":0,\"rewards\":0,\"round\":9,\"status\":\"Offline\",\"total-apps-opted-in\":0,\"total-assets-opted-in\":0,\"total-box-bytes\":0,\"total-boxes\":0,\"total-created-apps\":0,\"total-created-assets\":0},{\"address\":\"2SYXFSCZAQCZ7YIFUCUZYOVR7G6Y3UBGSJIWT4EZ4CO3T6WVYTMHVSANOY\",\"amount\":500000,\"amount-without-pending-rewards\":500000,\"created-at-round\":1,\"deleted\":false,\"pending-rewards\":0,\"reward-base\":0,\"rewards\":0,\"round\":9,\"status\":\"Offline\",\"total-apps-opted-in\":0,\"total-assets-opted-in\":0,\"total-box-bytes\":570,\"total-boxes\":13,\"total-created-apps\":0,\"total-created-assets\":0},{\"address\":\"4C3S3A5II6AYMEADSW7EVL7JAKVU2ASJMMJAGVUROIJHYMS6B24NCXVEWM\",\"amount\":100000,\"amount-without-pending-rewards\":100000,\"created-at-round\":0,\"deleted\":false,\"pending-rewards\":0,\"reward-base\":0,\"rewards\":0,\"round\":9,\"status\":\"NotParticipating\",\"total-apps-opted-in\":0,\"total-assets-opted-in\":0,\"total-box-bytes\":0,\"total-boxes\":0,\"total-created-apps\":0,\"total-created-assets\":0},{\"address\":\"6TB2ZQA2GEEDH6XTIOH5A7FUSGINXDPW5ONN6XBOBBGGUXVHRQTITAIIVI\",\"amount\":1000000000000,\"amount-without-pending-rewards\":1000000000000,\"created-at-round\":0,\"deleted\":false,\"pending-rewards\":0,\"reward-base\":0,\"rewards\":0,\"round\":9,\"status\":\"Offline\",\"total-apps-opted-in\":0,\"total-assets-opted-in\":0,\"total-box-bytes\":0,\"total-boxes\":0,\"total-created-apps\":0,\"total-created-assets\":0}],\"current-round\":9,\"next-token\":\"6TB2ZQA2GEEDH6XTIOH5A7FUSGINXDPW5ONN6XBOBBGGUXVHRQTITAIIVI\"}\n" + }, + "witness": { + "goType": "generated.AccountsResponse", + "accounts": { + "accounts": [ + { + "address": "GJR76Q6OXNZ2CYIVCFCDTJRBAAR6TYEJJENEII3G2U3JH546SPBQA62IFY", + "amount": 999999499000, + "amount-without-pending-rewards": 999999499000, + "created-apps": [ + { + "created-at-round": 1, + "deleted": false, + "id": 1, + "params": { + "approval-program": "CCABADEYQQB6MRtBAHU2GgCABmNyZWF0ZRJBABiBIDEbgQISQAAFSDYaAhc2GgFMuURCAE42GgCABmRlbGV0ZRJBAAg2GgG8REIANzYaAIADc2V0EkEACzYaASI2GgK7QgAgNhoAgAVjaGVjaxJBABE2GgEiNhoCFbo2GgISREIAAQCBAQ==", + "clear-state-program": "CIEB", + "creator": "GJR76Q6OXNZ2CYIVCFCDTJRBAAR6TYEJJENEII3G2U3JH546SPBQA62IFY", + "global-state-schema": { + "num-byte-slice": 0, + "num-uint": 0 + }, + "local-state-schema": { + "num-byte-slice": 0, + "num-uint": 0 + } + } + } + ], + "created-at-round": 0, + "deleted": false, + "pending-rewards": 0, + "reward-base": 0, + "rewards": 0, + "round": 9, + "sig-type": "sig", + "status": "Offline", + "total-apps-opted-in": 0, + "total-assets-opted-in": 0, + "total-box-bytes": 0, + "total-boxes": 0, + "total-created-apps": 1, + "total-created-assets": 0 + }, + { + "address": "LMTOYRT2WPSUY6JTCW2URER6YN3GETJ5FHTQBA55EVK66JG2QOB32WPIHY", + "amount": 500000, + "amount-without-pending-rewards": 500000, + "created-at-round": 1, + "deleted": false, + "pending-rewards": 0, + "reward-base": 0, + "rewards": 0, + "round": 9, + "status": "Offline", + "total-apps-opted-in": 0, + "total-assets-opted-in": 0, + "total-box-bytes": 0, + "total-boxes": 0, + "total-created-apps": 0, + "total-created-assets": 0 + }, + { + "address": "N5T74SANUWLHI6ZWYFQBEB6J2VXBTYUYZNWQB2V26DCF4ARKC7GDUW3IRU", + "amount": 999999499000, + "amount-without-pending-rewards": 999999499000, + "created-apps": [ + { + "created-at-round": 1, + "deleted": false, + "id": 3, + "params": { + "approval-program": "CCABADEYQQB6MRtBAHU2GgCABmNyZWF0ZRJBABiBIDEbgQISQAAFSDYaAhc2GgFMuURCAE42GgCABmRlbGV0ZRJBAAg2GgG8REIANzYaAIADc2V0EkEACzYaASI2GgK7QgAgNhoAgAVjaGVjaxJBABE2GgEiNhoCFbo2GgISREIAAQCBAQ==", + "clear-state-program": "CIEB", + "creator": "N5T74SANUWLHI6ZWYFQBEB6J2VXBTYUYZNWQB2V26DCF4ARKC7GDUW3IRU", + "global-state-schema": { + "num-byte-slice": 0, + "num-uint": 0 + }, + "local-state-schema": { + "num-byte-slice": 0, + "num-uint": 0 + } + } + } + ], + "created-at-round": 0, + "deleted": false, + "pending-rewards": 0, + "reward-base": 0, + "rewards": 0, + "round": 9, + "sig-type": "sig", + "status": "Offline", + "total-apps-opted-in": 0, + "total-assets-opted-in": 0, + "total-box-bytes": 0, + "total-boxes": 0, + "total-created-apps": 1, + "total-created-assets": 0 + }, + { + "address": "OKUWMFFEKF4B4D7FRQYBVV3C2SNS54ZO4WZ2MJ3576UYKFDHM5P3AFMRWE", + "amount": 999999499000, + "amount-without-pending-rewards": 999999499000, + "created-at-round": 0, + "deleted": false, + "pending-rewards": 0, + "reward-base": 0, + "rewards": 0, + "round": 9, + "sig-type": "sig", + "status": "Offline", + "total-apps-opted-in": 0, + "total-assets-opted-in": 0, + "total-box-bytes": 0, + "total-boxes": 0, + "total-created-apps": 0, + "total-created-assets": 0 + }, + { + "address": "WCS6TVPJRBSARHLN2326LRU5BYVJZUKI2VJ53CAWKYYHDE455ZGKANWMGM", + "amount": 500000, + "amount-without-pending-rewards": 500000, + "created-at-round": 1, + "deleted": false, + "pending-rewards": 0, + "reward-base": 0, + "rewards": 0, + "round": 9, + "status": "Offline", + "total-apps-opted-in": 0, + "total-assets-opted-in": 0, + "total-box-bytes": 477, + "total-boxes": 9, + "total-created-apps": 0, + "total-created-assets": 0 + }, + { + "address": "ZROKLZW4GVOK5WQIF2GUR6LHFVEZBMV56BIQEQD4OTIZL2BPSYYUKFBSHM", + "amount": 3000, + "amount-without-pending-rewards": 3000, + "created-at-round": 1, + "deleted": false, + "pending-rewards": 0, + "reward-base": 0, + "rewards": 0, + "round": 9, + "status": "Offline", + "total-apps-opted-in": 0, + "total-assets-opted-in": 0, + "total-box-bytes": 0, + "total-boxes": 0, + "total-created-apps": 0, + "total-created-assets": 0 + }, + { + "address": "2SYXFSCZAQCZ7YIFUCUZYOVR7G6Y3UBGSJIWT4EZ4CO3T6WVYTMHVSANOY", + "amount": 500000, + "amount-without-pending-rewards": 500000, + "created-at-round": 1, + "deleted": false, + "pending-rewards": 0, + "reward-base": 0, + "rewards": 0, + "round": 9, + "status": "Offline", + "total-apps-opted-in": 0, + "total-assets-opted-in": 0, + "total-box-bytes": 570, + "total-boxes": 13, + "total-created-apps": 0, + "total-created-assets": 0 + }, + { + "address": "4C3S3A5II6AYMEADSW7EVL7JAKVU2ASJMMJAGVUROIJHYMS6B24NCXVEWM", + "amount": 100000, + "amount-without-pending-rewards": 100000, + "created-at-round": 0, + "deleted": false, + "pending-rewards": 0, + "reward-base": 0, + "rewards": 0, + "round": 9, + "status": "NotParticipating", + "total-apps-opted-in": 0, + "total-assets-opted-in": 0, + "total-box-bytes": 0, + "total-boxes": 0, + "total-created-apps": 0, + "total-created-assets": 0 + }, + { + "address": "6TB2ZQA2GEEDH6XTIOH5A7FUSGINXDPW5ONN6XBOBBGGUXVHRQTITAIIVI", + "amount": 1000000000000, + "amount-without-pending-rewards": 1000000000000, + "created-at-round": 0, + "deleted": false, + "pending-rewards": 0, + "reward-base": 0, + "rewards": 0, + "round": 9, + "status": "Offline", + "total-apps-opted-in": 0, + "total-assets-opted-in": 0, + "total-box-bytes": 0, + "total-boxes": 0, + "total-created-apps": 0, + "total-created-assets": 0 + } + ], + "current-round": 9, + "next-token": "6TB2ZQA2GEEDH6XTIOH5A7FUSGINXDPW5ONN6XBOBBGGUXVHRQTITAIIVI" + } + }, + "witnessError": null + }, + { + "name": "What are all the apps?", + "request": { + "path": "/v2/applications", + "params": [], + "url": "http://localhost:8999/v2/applications", + "route": "/v2/applications" + }, + "response": { + "statusCode": 200, + "body": "{\"applications\":[{\"created-at-round\":1,\"deleted\":false,\"id\":1,\"params\":{\"approval-program\":\"CCABADEYQQB6MRtBAHU2GgCABmNyZWF0ZRJBABiBIDEbgQISQAAFSDYaAhc2GgFMuURCAE42GgCABmRlbGV0ZRJBAAg2GgG8REIANzYaAIADc2V0EkEACzYaASI2GgK7QgAgNhoAgAVjaGVjaxJBABE2GgEiNhoCFbo2GgISREIAAQCBAQ==\",\"clear-state-program\":\"CIEB\",\"creator\":\"GJR76Q6OXNZ2CYIVCFCDTJRBAAR6TYEJJENEII3G2U3JH546SPBQA62IFY\",\"global-state-schema\":{\"num-byte-slice\":0,\"num-uint\":0},\"local-state-schema\":{\"num-byte-slice\":0,\"num-uint\":0}}},{\"created-at-round\":1,\"deleted\":false,\"id\":3,\"params\":{\"approval-program\":\"CCABADEYQQB6MRtBAHU2GgCABmNyZWF0ZRJBABiBIDEbgQISQAAFSDYaAhc2GgFMuURCAE42GgCABmRlbGV0ZRJBAAg2GgG8REIANzYaAIADc2V0EkEACzYaASI2GgK7QgAgNhoAgAVjaGVjaxJBABE2GgEiNhoCFbo2GgISREIAAQCBAQ==\",\"clear-state-program\":\"CIEB\",\"creator\":\"N5T74SANUWLHI6ZWYFQBEB6J2VXBTYUYZNWQB2V26DCF4ARKC7GDUW3IRU\",\"global-state-schema\":{\"num-byte-slice\":0,\"num-uint\":0},\"local-state-schema\":{\"num-byte-slice\":0,\"num-uint\":0}}}],\"current-round\":9,\"next-token\":\"3\"}\n" + }, + "witness": { + "goType": "generated.ApplicationsResponse", + "apps": { + "applications": [ + { + "created-at-round": 1, + "deleted": false, + "id": 1, + "params": { + "approval-program": "CCABADEYQQB6MRtBAHU2GgCABmNyZWF0ZRJBABiBIDEbgQISQAAFSDYaAhc2GgFMuURCAE42GgCABmRlbGV0ZRJBAAg2GgG8REIANzYaAIADc2V0EkEACzYaASI2GgK7QgAgNhoAgAVjaGVjaxJBABE2GgEiNhoCFbo2GgISREIAAQCBAQ==", + "clear-state-program": "CIEB", + "creator": "GJR76Q6OXNZ2CYIVCFCDTJRBAAR6TYEJJENEII3G2U3JH546SPBQA62IFY", + "global-state-schema": { + "num-byte-slice": 0, + "num-uint": 0 + }, + "local-state-schema": { + "num-byte-slice": 0, + "num-uint": 0 + } + } + }, + { + "created-at-round": 1, + "deleted": false, + "id": 3, + "params": { + "approval-program": "CCABADEYQQB6MRtBAHU2GgCABmNyZWF0ZRJBABiBIDEbgQISQAAFSDYaAhc2GgFMuURCAE42GgCABmRlbGV0ZRJBAAg2GgG8REIANzYaAIADc2V0EkEACzYaASI2GgK7QgAgNhoAgAVjaGVjaxJBABE2GgEiNhoCFbo2GgISREIAAQCBAQ==", + "clear-state-program": "CIEB", + "creator": "N5T74SANUWLHI6ZWYFQBEB6J2VXBTYUYZNWQB2V26DCF4ARKC7GDUW3IRU", + "global-state-schema": { + "num-byte-slice": 0, + "num-uint": 0 + }, + "local-state-schema": { + "num-byte-slice": 0, + "num-uint": 0 + } + } + } + ], + "current-round": 9, + "next-token": "3" + } + }, + "witnessError": null + }, + { + "name": "Lookup non-existing app 1337", + "request": { + "path": "/v2/applications/1337", + "params": [], + "url": "http://localhost:8999/v2/applications/1337", + "route": "/v2/applications/:application-id" + }, + "response": { + "statusCode": 404, + "body": "{\"message\":\"no application found for application-id: 1337\"}\n" + }, + "witness": null, + "witnessError": "404 error" + }, + { + "name": "Lookup app 3 (funded with no boxes)", + "request": { + "path": "/v2/applications/3", + "params": [], + "url": "http://localhost:8999/v2/applications/3", + "route": "/v2/applications/:application-id" + }, + "response": { + "statusCode": 200, + "body": "{\"application\":{\"created-at-round\":1,\"deleted\":false,\"id\":3,\"params\":{\"approval-program\":\"CCABADEYQQB6MRtBAHU2GgCABmNyZWF0ZRJBABiBIDEbgQISQAAFSDYaAhc2GgFMuURCAE42GgCABmRlbGV0ZRJBAAg2GgG8REIANzYaAIADc2V0EkEACzYaASI2GgK7QgAgNhoAgAVjaGVjaxJBABE2GgEiNhoCFbo2GgISREIAAQCBAQ==\",\"clear-state-program\":\"CIEB\",\"creator\":\"N5T74SANUWLHI6ZWYFQBEB6J2VXBTYUYZNWQB2V26DCF4ARKC7GDUW3IRU\",\"global-state-schema\":{\"num-byte-slice\":0,\"num-uint\":0},\"local-state-schema\":{\"num-byte-slice\":0,\"num-uint\":0}}},\"current-round\":9}\n" + }, + "witness": { + "goType": "generated.ApplicationResponse", + "app": { + "application": { + "created-at-round": 1, + "deleted": false, + "id": 3, + "params": { + "approval-program": "CCABADEYQQB6MRtBAHU2GgCABmNyZWF0ZRJBABiBIDEbgQISQAAFSDYaAhc2GgFMuURCAE42GgCABmRlbGV0ZRJBAAg2GgG8REIANzYaAIADc2V0EkEACzYaASI2GgK7QgAgNhoAgAVjaGVjaxJBABE2GgEiNhoCFbo2GgISREIAAQCBAQ==", + "clear-state-program": "CIEB", + "creator": "N5T74SANUWLHI6ZWYFQBEB6J2VXBTYUYZNWQB2V26DCF4ARKC7GDUW3IRU", + "global-state-schema": { + "num-byte-slice": 0, + "num-uint": 0 + }, + "local-state-schema": { + "num-byte-slice": 0, + "num-uint": 0 + } + } + }, + "current-round": 9 + } + }, + "witnessError": null + }, + { + "name": "Lookup app 1 (funded with boxes)", + "request": { + "path": "/v2/applications/1", + "params": [], + "url": "http://localhost:8999/v2/applications/1", + "route": "/v2/applications/:application-id" + }, + "response": { + "statusCode": 200, + "body": "{\"application\":{\"created-at-round\":1,\"deleted\":false,\"id\":1,\"params\":{\"approval-program\":\"CCABADEYQQB6MRtBAHU2GgCABmNyZWF0ZRJBABiBIDEbgQISQAAFSDYaAhc2GgFMuURCAE42GgCABmRlbGV0ZRJBAAg2GgG8REIANzYaAIADc2V0EkEACzYaASI2GgK7QgAgNhoAgAVjaGVjaxJBABE2GgEiNhoCFbo2GgISREIAAQCBAQ==\",\"clear-state-program\":\"CIEB\",\"creator\":\"GJR76Q6OXNZ2CYIVCFCDTJRBAAR6TYEJJENEII3G2U3JH546SPBQA62IFY\",\"global-state-schema\":{\"num-byte-slice\":0,\"num-uint\":0},\"local-state-schema\":{\"num-byte-slice\":0,\"num-uint\":0}}},\"current-round\":9}\n" + }, + "witness": { + "goType": "generated.ApplicationResponse", + "app": { + "application": { + "created-at-round": 1, + "deleted": false, + "id": 1, + "params": { + "approval-program": "CCABADEYQQB6MRtBAHU2GgCABmNyZWF0ZRJBABiBIDEbgQISQAAFSDYaAhc2GgFMuURCAE42GgCABmRlbGV0ZRJBAAg2GgG8REIANzYaAIADc2V0EkEACzYaASI2GgK7QgAgNhoAgAVjaGVjaxJBABE2GgEiNhoCFbo2GgISREIAAQCBAQ==", + "clear-state-program": "CIEB", + "creator": "GJR76Q6OXNZ2CYIVCFCDTJRBAAR6TYEJJENEII3G2U3JH546SPBQA62IFY", + "global-state-schema": { + "num-byte-slice": 0, + "num-uint": 0 + }, + "local-state-schema": { + "num-byte-slice": 0, + "num-uint": 0 + } + } + }, + "current-round": 9 + } + }, + "witnessError": null + }, + { + "name": "Lookup DELETED app 5 (funded with encoding test named boxes)", + "request": { + "path": "/v2/applications/5", + "params": [], + "url": "http://localhost:8999/v2/applications/5", + "route": "/v2/applications/:application-id" + }, + "response": { + "statusCode": 404, + "body": "{\"message\":\"no application found for application-id: 5\"}\n" + }, + "witness": null, + "witnessError": "404 error" + }, + { + "name": "Creator account - not an app account - no params", + "request": { + "path": "/v2/accounts/LMTOYRT2WPSUY6JTCW2URER6YN3GETJ5FHTQBA55EVK66JG2QOB32WPIHY", + "params": [], + "url": "http://localhost:8999/v2/accounts/LMTOYRT2WPSUY6JTCW2URER6YN3GETJ5FHTQBA55EVK66JG2QOB32WPIHY", + "route": "/v2/accounts/:account-id" + }, + "response": { + "statusCode": 200, + "body": "{\"account\":{\"address\":\"LMTOYRT2WPSUY6JTCW2URER6YN3GETJ5FHTQBA55EVK66JG2QOB32WPIHY\",\"amount\":500000,\"amount-without-pending-rewards\":500000,\"created-at-round\":1,\"deleted\":false,\"pending-rewards\":0,\"reward-base\":0,\"rewards\":0,\"round\":9,\"status\":\"Offline\",\"total-apps-opted-in\":0,\"total-assets-opted-in\":0,\"total-box-bytes\":0,\"total-boxes\":0,\"total-created-apps\":0,\"total-created-assets\":0},\"current-round\":9}\n" + }, + "witness": { + "goType": "generated.AccountResponse", + "account": { + "account": { + "address": "LMTOYRT2WPSUY6JTCW2URER6YN3GETJ5FHTQBA55EVK66JG2QOB32WPIHY", + "amount": 500000, + "amount-without-pending-rewards": 500000, + "created-at-round": 1, + "deleted": false, + "pending-rewards": 0, + "reward-base": 0, + "rewards": 0, + "round": 9, + "status": "Offline", + "total-apps-opted-in": 0, + "total-assets-opted-in": 0, + "total-box-bytes": 0, + "total-boxes": 0, + "total-created-apps": 0, + "total-created-assets": 0 + }, + "current-round": 9 + } + }, + "witnessError": null + }, + { + "name": "App 3 (as account) totals no boxes - no params", + "request": { + "path": "/v2/accounts/LMTOYRT2WPSUY6JTCW2URER6YN3GETJ5FHTQBA55EVK66JG2QOB32WPIHY", + "params": [], + "url": "http://localhost:8999/v2/accounts/LMTOYRT2WPSUY6JTCW2URER6YN3GETJ5FHTQBA55EVK66JG2QOB32WPIHY", + "route": "/v2/accounts/:account-id" + }, + "response": { + "statusCode": 200, + "body": "{\"account\":{\"address\":\"LMTOYRT2WPSUY6JTCW2URER6YN3GETJ5FHTQBA55EVK66JG2QOB32WPIHY\",\"amount\":500000,\"amount-without-pending-rewards\":500000,\"created-at-round\":1,\"deleted\":false,\"pending-rewards\":0,\"reward-base\":0,\"rewards\":0,\"round\":9,\"status\":\"Offline\",\"total-apps-opted-in\":0,\"total-assets-opted-in\":0,\"total-box-bytes\":0,\"total-boxes\":0,\"total-created-apps\":0,\"total-created-assets\":0},\"current-round\":9}\n" + }, + "witness": { + "goType": "generated.AccountResponse", + "account": { + "account": { + "address": "LMTOYRT2WPSUY6JTCW2URER6YN3GETJ5FHTQBA55EVK66JG2QOB32WPIHY", + "amount": 500000, + "amount-without-pending-rewards": 500000, + "created-at-round": 1, + "deleted": false, + "pending-rewards": 0, + "reward-base": 0, + "rewards": 0, + "round": 9, + "status": "Offline", + "total-apps-opted-in": 0, + "total-assets-opted-in": 0, + "total-box-bytes": 0, + "total-boxes": 0, + "total-created-apps": 0, + "total-created-assets": 0 + }, + "current-round": 9 + } + }, + "witnessError": null + }, + { + "name": "App 1 (as account) totals with boxes - no params", + "request": { + "path": "/v2/accounts/WCS6TVPJRBSARHLN2326LRU5BYVJZUKI2VJ53CAWKYYHDE455ZGKANWMGM", + "params": [], + "url": "http://localhost:8999/v2/accounts/WCS6TVPJRBSARHLN2326LRU5BYVJZUKI2VJ53CAWKYYHDE455ZGKANWMGM", + "route": "/v2/accounts/:account-id" + }, + "response": { + "statusCode": 200, + "body": "{\"account\":{\"address\":\"WCS6TVPJRBSARHLN2326LRU5BYVJZUKI2VJ53CAWKYYHDE455ZGKANWMGM\",\"amount\":500000,\"amount-without-pending-rewards\":500000,\"created-at-round\":1,\"deleted\":false,\"pending-rewards\":0,\"reward-base\":0,\"rewards\":0,\"round\":9,\"status\":\"Offline\",\"total-apps-opted-in\":0,\"total-assets-opted-in\":0,\"total-box-bytes\":477,\"total-boxes\":9,\"total-created-apps\":0,\"total-created-assets\":0},\"current-round\":9}\n" + }, + "witness": { + "goType": "generated.AccountResponse", + "account": { + "account": { + "address": "WCS6TVPJRBSARHLN2326LRU5BYVJZUKI2VJ53CAWKYYHDE455ZGKANWMGM", + "amount": 500000, + "amount-without-pending-rewards": 500000, + "created-at-round": 1, + "deleted": false, + "pending-rewards": 0, + "reward-base": 0, + "rewards": 0, + "round": 9, + "status": "Offline", + "total-apps-opted-in": 0, + "total-assets-opted-in": 0, + "total-box-bytes": 477, + "total-boxes": 9, + "total-created-apps": 0, + "total-created-assets": 0 + }, + "current-round": 9 + } + }, + "witnessError": null + }, + { + "name": "Boxes of a app with id == math.MaxInt64", + "request": { + "path": "/v2/applications/9223372036854775807/boxes", + "params": [], + "url": "http://localhost:8999/v2/applications/9223372036854775807/boxes", + "route": "/v2/applications/:application-id/boxes" + }, + "response": { + "statusCode": 200, + "body": "{\"application-id\":9223372036854775807,\"boxes\":[]}\n" + }, + "witness": { + "goType": "generated.BoxesResponse", + "boxes": { + "application-id": 9223372036854775807, + "boxes": [] + } + }, + "witnessError": null + }, + { + "name": "Boxes of a app with id == math.MaxInt64 + 1", + "request": { + "path": "/v2/applications/9223372036854775808/boxes", + "params": [], + "url": "http://localhost:8999/v2/applications/9223372036854775808/boxes", + "route": "/v2/applications/:application-id/boxes" + }, + "response": { + "statusCode": 404, + "body": "{\"message\":\"searching by round or application-id or asset-id or filter by value greater than 9223372036854775807 is not supported\"}\n" + }, + "witness": null, + "witnessError": "404 error" + }, + { + "name": "Boxes of a non-existing app 1337", + "request": { + "path": "/v2/applications/1337/boxes", + "params": [], + "url": "http://localhost:8999/v2/applications/1337/boxes", + "route": "/v2/applications/:application-id/boxes" + }, + "response": { + "statusCode": 200, + "body": "{\"application-id\":1337,\"boxes\":[]}\n" + }, + "witness": { + "goType": "generated.BoxesResponse", + "boxes": { + "application-id": 1337, + "boxes": [] + } + }, + "witnessError": null + }, + { + "name": "Boxes of app 3 with no boxes: no params", + "request": { + "path": "/v2/applications/3/boxes", + "params": [], + "url": "http://localhost:8999/v2/applications/3/boxes", + "route": "/v2/applications/:application-id/boxes" + }, + "response": { + "statusCode": 200, + "body": "{\"application-id\":3,\"boxes\":[]}\n" + }, + "witness": { + "goType": "generated.BoxesResponse", + "boxes": { + "application-id": 3, + "boxes": [] + } + }, + "witnessError": null + }, + { + "name": "Boxes of DELETED app 5 with goal encoded boxes: no params", + "request": { + "path": "/v2/applications/5/boxes", + "params": [], + "url": "http://localhost:8999/v2/applications/5/boxes", + "route": "/v2/applications/:application-id/boxes" + }, + "response": { + "statusCode": 200, + "body": "{\"application-id\":5,\"boxes\":[{\"name\":\"AAAAAAAAACo=\"},{\"name\":\"AAAAAAAAAGQ=\"},{\"name\":\"AAAAAAAAAY8ADAAWAAhwbHMgcGFzcwACgA==\"},{\"name\":\"WybsRnqz5Ux5MxW1SJI+w3ZiTT0p5wCDvSVV7yTag4M=\"},{\"name\":\"YjMy\"},{\"name\":\"YjY0\"},{\"name\":\"YmFzZTMy\"},{\"name\":\"YmFzZTY0\"},{\"name\":\"Ynl0ZSBiYXNlMzI=\"},{\"name\":\"Ynl0ZSBiYXNlNjQ=\"},{\"name\":\"c3Ry\"},{\"name\":\"c3RyaW5n\"},{\"name\":\"1LFyyFkEBZ/hBaCpnDqx+b2N0CaSUWnwmeCdufrVxNg=\"}],\"next-token\":\"b64:1LFyyFkEBZ/hBaCpnDqx+b2N0CaSUWnwmeCdufrVxNg=\"}\n" + }, + "witness": { + "goType": "generated.BoxesResponse", + "boxes": { + "application-id": 5, + "boxes": [ + { + "name": "AAAAAAAAACo=" + }, + { + "name": "AAAAAAAAAGQ=" + }, + { + "name": "AAAAAAAAAY8ADAAWAAhwbHMgcGFzcwACgA==" + }, + { + "name": "WybsRnqz5Ux5MxW1SJI+w3ZiTT0p5wCDvSVV7yTag4M=" + }, + { + "name": "YjMy" + }, + { + "name": "YjY0" + }, + { + "name": "YmFzZTMy" + }, + { + "name": "YmFzZTY0" + }, + { + "name": "Ynl0ZSBiYXNlMzI=" + }, + { + "name": "Ynl0ZSBiYXNlNjQ=" + }, + { + "name": "c3Ry" + }, + { + "name": "c3RyaW5n" + }, + { + "name": "1LFyyFkEBZ/hBaCpnDqx+b2N0CaSUWnwmeCdufrVxNg=" + } + ], + "next-token": "b64:1LFyyFkEBZ/hBaCpnDqx+b2N0CaSUWnwmeCdufrVxNg=" + } + }, + "witnessError": null + }, + { + "name": "Boxes of app 1 with boxes: no params", + "request": { + "path": "/v2/applications/1/boxes", + "params": [], + "url": "http://localhost:8999/v2/applications/1/boxes", + "route": "/v2/applications/:application-id/boxes" + }, + "response": { + "statusCode": 200, + "body": "{\"application-id\":1,\"boxes\":[{\"name\":\"QVZNIGlzIHRoZSBuZXcgRVZN\"},{\"name\":\"SSB3aWxsIGJlIGFzc2ltaWxhdGVk\"},{\"name\":\"Uv38ByGCZU8WP18PmmIdcpVmx00QA3xNe7sEB9HixkmBhVrYaB0NhtHpHgAWeTnLZpTSxCKs0gigByk5SH9pmQ==\"},{\"name\":\"YSBncmVhdCBib3g=\"},{\"name\":\"YW5vdGhlciBncmVhdCBib3g=\"},{\"name\":\"Ym94ICM4\"},{\"name\":\"ZGlzYXBwb2ludGluZyBib3g=\"},{\"name\":\"ZG9uJ3QgYm94IG1lIGluIHRoaXMgd2F5\"},{\"name\":\"ZmFudGFidWxvdXM=\"}],\"next-token\":\"b64:ZmFudGFidWxvdXM=\"}\n" + }, + "witness": { + "goType": "generated.BoxesResponse", + "boxes": { + "application-id": 1, + "boxes": [ + { + "name": "QVZNIGlzIHRoZSBuZXcgRVZN" + }, + { + "name": "SSB3aWxsIGJlIGFzc2ltaWxhdGVk" + }, + { + "name": "Uv38ByGCZU8WP18PmmIdcpVmx00QA3xNe7sEB9HixkmBhVrYaB0NhtHpHgAWeTnLZpTSxCKs0gigByk5SH9pmQ==" + }, + { + "name": "YSBncmVhdCBib3g=" + }, + { + "name": "YW5vdGhlciBncmVhdCBib3g=" + }, + { + "name": "Ym94ICM4" + }, + { + "name": "ZGlzYXBwb2ludGluZyBib3g=" + }, + { + "name": "ZG9uJ3QgYm94IG1lIGluIHRoaXMgd2F5" + }, + { + "name": "ZmFudGFidWxvdXM=" + } + ], + "next-token": "b64:ZmFudGFidWxvdXM=" + } + }, + "witnessError": null + }, + { + "name": "Boxes of app 1 with boxes: limit 3 - page 1", + "request": { + "path": "/v2/applications/1/boxes", + "params": [ + { + "name": "limit", + "value": "3" + } + ], + "url": "http://localhost:8999/v2/applications/1/boxes?limit=3", + "route": "/v2/applications/:application-id/boxes" + }, + "response": { + "statusCode": 200, + "body": "{\"application-id\":1,\"boxes\":[{\"name\":\"QVZNIGlzIHRoZSBuZXcgRVZN\"},{\"name\":\"SSB3aWxsIGJlIGFzc2ltaWxhdGVk\"},{\"name\":\"Uv38ByGCZU8WP18PmmIdcpVmx00QA3xNe7sEB9HixkmBhVrYaB0NhtHpHgAWeTnLZpTSxCKs0gigByk5SH9pmQ==\"}],\"next-token\":\"b64:Uv38ByGCZU8WP18PmmIdcpVmx00QA3xNe7sEB9HixkmBhVrYaB0NhtHpHgAWeTnLZpTSxCKs0gigByk5SH9pmQ==\"}\n" + }, + "witness": { + "goType": "generated.BoxesResponse", + "boxes": { + "application-id": 1, + "boxes": [ + { + "name": "QVZNIGlzIHRoZSBuZXcgRVZN" + }, + { + "name": "SSB3aWxsIGJlIGFzc2ltaWxhdGVk" + }, + { + "name": "Uv38ByGCZU8WP18PmmIdcpVmx00QA3xNe7sEB9HixkmBhVrYaB0NhtHpHgAWeTnLZpTSxCKs0gigByk5SH9pmQ==" + } + ], + "next-token": "b64:Uv38ByGCZU8WP18PmmIdcpVmx00QA3xNe7sEB9HixkmBhVrYaB0NhtHpHgAWeTnLZpTSxCKs0gigByk5SH9pmQ==" + } + }, + "witnessError": null + }, + { + "name": "Boxes of app 1 with boxes: limit 3 - page 2 - b64", + "request": { + "path": "/v2/applications/1/boxes", + "params": [ + { + "name": "limit", + "value": "3" + }, + { + "name": "next", + "value": "b64:Uv38ByGCZU8WP18PmmIdcpVmx00QA3xNe7sEB9HixkmBhVrYaB0NhtHpHgAWeTnLZpTSxCKs0gigByk5SH9pmQ==" + } + ], + "url": "http://localhost:8999/v2/applications/1/boxes?limit=3\u0026next=b64%3AUv38ByGCZU8WP18PmmIdcpVmx00QA3xNe7sEB9HixkmBhVrYaB0NhtHpHgAWeTnLZpTSxCKs0gigByk5SH9pmQ%3D%3D", + "route": "/v2/applications/:application-id/boxes" + }, + "response": { + "statusCode": 200, + "body": "{\"application-id\":1,\"boxes\":[{\"name\":\"YSBncmVhdCBib3g=\"},{\"name\":\"YW5vdGhlciBncmVhdCBib3g=\"},{\"name\":\"Ym94ICM4\"}],\"next-token\":\"b64:Ym94ICM4\"}\n" + }, + "witness": { + "goType": "generated.BoxesResponse", + "boxes": { + "application-id": 1, + "boxes": [ + { + "name": "YSBncmVhdCBib3g=" + }, + { + "name": "YW5vdGhlciBncmVhdCBib3g=" + }, + { + "name": "Ym94ICM4" + } + ], + "next-token": "b64:Ym94ICM4" + } + }, + "witnessError": null + }, + { + "name": "Boxes of app 1 with boxes: limit 3 - page 3 - b64", + "request": { + "path": "/v2/applications/1/boxes", + "params": [ + { + "name": "limit", + "value": "3" + }, + { + "name": "next", + "value": "b64:Ym94ICM4" + } + ], + "url": "http://localhost:8999/v2/applications/1/boxes?limit=3\u0026next=b64%3AYm94ICM4", + "route": "/v2/applications/:application-id/boxes" + }, + "response": { + "statusCode": 200, + "body": "{\"application-id\":1,\"boxes\":[{\"name\":\"ZGlzYXBwb2ludGluZyBib3g=\"},{\"name\":\"ZG9uJ3QgYm94IG1lIGluIHRoaXMgd2F5\"},{\"name\":\"ZmFudGFidWxvdXM=\"}],\"next-token\":\"b64:ZmFudGFidWxvdXM=\"}\n" + }, + "witness": { + "goType": "generated.BoxesResponse", + "boxes": { + "application-id": 1, + "boxes": [ + { + "name": "ZGlzYXBwb2ludGluZyBib3g=" + }, + { + "name": "ZG9uJ3QgYm94IG1lIGluIHRoaXMgd2F5" + }, + { + "name": "ZmFudGFidWxvdXM=" + } + ], + "next-token": "b64:ZmFudGFidWxvdXM=" + } + }, + "witnessError": null + }, + { + "name": "Boxes of app 1 with boxes: limit 3 - MISSING b64 prefix", + "request": { + "path": "/v2/applications/1/boxes", + "params": [ + { + "name": "limit", + "value": "3" + }, + { + "name": "next", + "value": "Ym94ICM4" + } + ], + "url": "http://localhost:8999/v2/applications/1/boxes?limit=3\u0026next=Ym94ICM4", + "route": "/v2/applications/:application-id/boxes" + }, + "response": { + "statusCode": 400, + "body": "{\"message\":\"SearchForApplicationBoxes received illegal next token (Ym94ICM4): all arguments and box names should be of the form 'encoding:value'\"}\n" + }, + "witness": null, + "witnessError": "400 error" + }, + { + "name": "Boxes of app 1 with boxes: limit 3 - goal app arg encoding str", + "request": { + "path": "/v2/applications/1/boxes", + "params": [ + { + "name": "limit", + "value": "3" + }, + { + "name": "next", + "value": "str:box #8" + } + ], + "url": "http://localhost:8999/v2/applications/1/boxes?limit=3\u0026next=str%3Abox+%238", + "route": "/v2/applications/:application-id/boxes" + }, + "response": { + "statusCode": 200, + "body": "{\"application-id\":1,\"boxes\":[{\"name\":\"ZGlzYXBwb2ludGluZyBib3g=\"},{\"name\":\"ZG9uJ3QgYm94IG1lIGluIHRoaXMgd2F5\"},{\"name\":\"ZmFudGFidWxvdXM=\"}],\"next-token\":\"b64:ZmFudGFidWxvdXM=\"}\n" + }, + "witness": { + "goType": "generated.BoxesResponse", + "boxes": { + "application-id": 1, + "boxes": [ + { + "name": "ZGlzYXBwb2ludGluZyBib3g=" + }, + { + "name": "ZG9uJ3QgYm94IG1lIGluIHRoaXMgd2F5" + }, + { + "name": "ZmFudGFidWxvdXM=" + } + ], + "next-token": "b64:ZmFudGFidWxvdXM=" + } + }, + "witnessError": null + }, + { + "name": "Boxes of app 1 with boxes: limit 3 - page 4 (empty) - b64", + "request": { + "path": "/v2/applications/1/boxes", + "params": [ + { + "name": "limit", + "value": "3" + }, + { + "name": "next", + "value": "b64:ZmFudGFidWxvdXM=" + } + ], + "url": "http://localhost:8999/v2/applications/1/boxes?limit=3\u0026next=b64%3AZmFudGFidWxvdXM%3D", + "route": "/v2/applications/:application-id/boxes" + }, + "response": { + "statusCode": 200, + "body": "{\"application-id\":1,\"boxes\":[]}\n" + }, + "witness": { + "goType": "generated.BoxesResponse", + "boxes": { + "application-id": 1, + "boxes": [] + } + }, + "witnessError": null + }, + { + "name": "Boxes of app 1 with boxes: limit 3 - ERROR because when next param provided -even empty string- it must be goal app arg encoded", + "request": { + "path": "/v2/applications/1/boxes", + "params": [ + { + "name": "limit", + "value": "3" + }, + { + "name": "next", + "value": "" + } + ], + "url": "http://localhost:8999/v2/applications/1/boxes?limit=3\u0026next=", + "route": "/v2/applications/:application-id/boxes" + }, + "response": { + "statusCode": 400, + "body": "{\"message\":\"SearchForApplicationBoxes received illegal next token (): all arguments and box names should be of the form 'encoding:value'\"}\n" + }, + "witness": null, + "witnessError": "400 error" + }, + { + "name": "Boxes (with made up name param) of a app with id == math.MaxInt64", + "request": { + "path": "/v2/applications/9223372036854775807/box", + "params": [ + { + "name": "name", + "value": "string:non-existing" + } + ], + "url": "http://localhost:8999/v2/applications/9223372036854775807/box?name=string%3Anon-existing", + "route": "/v2/applications/:application-id/box" + }, + "response": { + "statusCode": 404, + "body": "{\"message\":\"no application found for application-id: round=9, appid=9223372036854775807, boxName=string:non-existing\"}\n" + }, + "witness": null, + "witnessError": "404 error" + }, + { + "name": "Box (with made up name param) of a app with id == math.MaxInt64 + 1", + "request": { + "path": "/v2/applications/9223372036854775808/box", + "params": [ + { + "name": "name", + "value": "string:non-existing" + } + ], + "url": "http://localhost:8999/v2/applications/9223372036854775808/box?name=string%3Anon-existing", + "route": "/v2/applications/:application-id/box" + }, + "response": { + "statusCode": 404, + "body": "{\"message\":\"searching by round or application-id or asset-id or filter by value greater than 9223372036854775807 is not supported\"}\n" + }, + "witness": null, + "witnessError": "404 error" + }, + { + "name": "A box attempt for a non-existing app 1337", + "request": { + "path": "/v2/applications/1337/box", + "params": [ + { + "name": "name", + "value": "string:non-existing" + } + ], + "url": "http://localhost:8999/v2/applications/1337/box?name=string%3Anon-existing", + "route": "/v2/applications/:application-id/box" + }, + "response": { + "statusCode": 404, + "body": "{\"message\":\"no application found for application-id: round=9, appid=1337, boxName=string:non-existing\"}\n" + }, + "witness": null, + "witnessError": "404 error" + }, + { + "name": "A box attempt for a non-existing app 1337 - without the required box name param", + "request": { + "path": "/v2/applications/1337/box", + "params": [], + "url": "http://localhost:8999/v2/applications/1337/box", + "route": "/v2/applications/:application-id/box" + }, + "response": { + "statusCode": 400, + "body": "{\"message\":\"Query argument name is required, but not found\"}\n" + }, + "witness": null, + "witnessError": "400 error" + }, + { + "name": "A box attempt for a existing app 3 - without the required box name param", + "request": { + "path": "/v2/applications/3/box", + "params": [], + "url": "http://localhost:8999/v2/applications/3/box", + "route": "/v2/applications/:application-id/box" + }, + "response": { + "statusCode": 400, + "body": "{\"message\":\"Query argument name is required, but not found\"}\n" + }, + "witness": null, + "witnessError": "400 error" + }, + { + "name": "App 3 box (non-existing)", + "request": { + "path": "/v2/applications/3/box", + "params": [ + { + "name": "name", + "value": "string:non-existing" + } + ], + "url": "http://localhost:8999/v2/applications/3/box?name=string%3Anon-existing", + "route": "/v2/applications/:application-id/box" + }, + "response": { + "statusCode": 404, + "body": "{\"message\":\"no application boxes found: round=9, appid=3, boxName=string:non-existing\"}\n" + }, + "witness": null, + "witnessError": "404 error" + }, + { + "name": "App 1 box (non-existing)", + "request": { + "path": "/v2/applications/1/box", + "params": [ + { + "name": "name", + "value": "string:non-existing" + } + ], + "url": "http://localhost:8999/v2/applications/1/box?name=string%3Anon-existing", + "route": "/v2/applications/:application-id/box" + }, + "response": { + "statusCode": 404, + "body": "{\"message\":\"no application boxes found: round=9, appid=1, boxName=string:non-existing\"}\n" + }, + "witness": null, + "witnessError": "404 error" + }, + { + "name": "App 1 box (a great box)", + "request": { + "path": "/v2/applications/1/box", + "params": [ + { + "name": "name", + "value": "string:a great box" + } + ], + "url": "http://localhost:8999/v2/applications/1/box?name=string%3Aa+great+box", + "route": "/v2/applications/:application-id/box" + }, + "response": { + "statusCode": 200, + "body": "{\"name\":\"YSBncmVhdCBib3g=\",\"value\":\"aXQncyBhIHdvbmRlcmZ1bCBib3gAAAAAAAAAAAAAAAA=\"}\n" + }, + "witness": { + "goType": "generated.BoxResponse", + "box": { + "name": "YSBncmVhdCBib3g=", + "value": "aXQncyBhIHdvbmRlcmZ1bCBib3gAAAAAAAAAAAAAAAA=" + } + }, + "witnessError": null + }, + { + "name": "DELETED app 5 encoding (str:str) - no params", + "request": { + "path": "/v2/applications/5/box", + "params": [ + { + "name": "name", + "value": "str:str" + } + ], + "url": "http://localhost:8999/v2/applications/5/box?name=str%3Astr", + "route": "/v2/applications/:application-id/box" + }, + "response": { + "statusCode": 200, + "body": "{\"name\":\"c3Ry\",\"value\":\"c3RyAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=\"}\n" + }, + "witness": { + "goType": "generated.BoxResponse", + "box": { + "name": "c3Ry", + "value": "c3RyAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=" + } + }, + "witnessError": null + }, + { + "name": "DELETED app 5 encoding (integer:100) - no params", + "request": { + "path": "/v2/applications/5/box", + "params": [ + { + "name": "name", + "value": "integer:100" + } + ], + "url": "http://localhost:8999/v2/applications/5/box?name=integer%3A100", + "route": "/v2/applications/:application-id/box" + }, + "response": { + "statusCode": 200, + "body": "{\"name\":\"AAAAAAAAAGQ=\",\"value\":\"AAAAAAAAAGQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=\"}\n" + }, + "witness": { + "goType": "generated.BoxResponse", + "box": { + "name": "AAAAAAAAAGQ=", + "value": "AAAAAAAAAGQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=" + } + }, + "witnessError": null + }, + { + "name": "DELETED app 5 encoding (base32:MJQXGZJTGI======) - no params", + "request": { + "path": "/v2/applications/5/box", + "params": [ + { + "name": "name", + "value": "base32:MJQXGZJTGI======" + } + ], + "url": "http://localhost:8999/v2/applications/5/box?name=base32%3AMJQXGZJTGI%3D%3D%3D%3D%3D%3D", + "route": "/v2/applications/:application-id/box" + }, + "response": { + "statusCode": 200, + "body": "{\"name\":\"YmFzZTMy\",\"value\":\"YmFzZTMyAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=\"}\n" + }, + "witness": { + "goType": "generated.BoxResponse", + "box": { + "name": "YmFzZTMy", + "value": "YmFzZTMyAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=" + } + }, + "witnessError": null + }, + { + "name": "DELETED app 5 encoding (b64:YjY0) - no params", + "request": { + "path": "/v2/applications/5/box", + "params": [ + { + "name": "name", + "value": "b64:YjY0" + } + ], + "url": "http://localhost:8999/v2/applications/5/box?name=b64%3AYjY0", + "route": "/v2/applications/:application-id/box" + }, + "response": { + "statusCode": 200, + "body": "{\"name\":\"YjY0\",\"value\":\"YjY0AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=\"}\n" + }, + "witness": { + "goType": "generated.BoxResponse", + "box": { + "name": "YjY0", + "value": "YjY0AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=" + } + }, + "witnessError": null + }, + { + "name": "DELETED app 5 encoding (base64:YmFzZTY0) - no params", + "request": { + "path": "/v2/applications/5/box", + "params": [ + { + "name": "name", + "value": "base64:YmFzZTY0" + } + ], + "url": "http://localhost:8999/v2/applications/5/box?name=base64%3AYmFzZTY0", + "route": "/v2/applications/:application-id/box" + }, + "response": { + "statusCode": 200, + "body": "{\"name\":\"YmFzZTY0\",\"value\":\"YmFzZTY0AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=\"}\n" + }, + "witness": { + "goType": "generated.BoxResponse", + "box": { + "name": "YmFzZTY0", + "value": "YmFzZTY0AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=" + } + }, + "witnessError": null + }, + { + "name": "DELETED app 5 encoding (string:string) - no params", + "request": { + "path": "/v2/applications/5/box", + "params": [ + { + "name": "name", + "value": "string:string" + } + ], + "url": "http://localhost:8999/v2/applications/5/box?name=string%3Astring", + "route": "/v2/applications/:application-id/box" + }, + "response": { + "statusCode": 200, + "body": "{\"name\":\"c3RyaW5n\",\"value\":\"c3RyaW5nAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=\"}\n" + }, + "witness": { + "goType": "generated.BoxResponse", + "box": { + "name": "c3RyaW5n", + "value": "c3RyaW5nAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=" + } + }, + "witnessError": null + }, + { + "name": "DELETED app 5 encoding (int:42) - no params", + "request": { + "path": "/v2/applications/5/box", + "params": [ + { + "name": "name", + "value": "int:42" + } + ], + "url": "http://localhost:8999/v2/applications/5/box?name=int%3A42", + "route": "/v2/applications/:application-id/box" + }, + "response": { + "statusCode": 200, + "body": "{\"name\":\"AAAAAAAAACo=\",\"value\":\"AAAAAAAAACoAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=\"}\n" + }, + "witness": { + "goType": "generated.BoxResponse", + "box": { + "name": "AAAAAAAAACo=", + "value": "AAAAAAAAACoAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=" + } + }, + "witnessError": null + }, + { + "name": "DELETED app 5 encoding (abi:(uint64,string,bool[]):[399,\"pls pass\",[true,false]]) - no params", + "request": { + "path": "/v2/applications/5/box", + "params": [ + { + "name": "name", + "value": "abi:(uint64,string,bool[]):[399,\"pls pass\",[true,false]]" + } + ], + "url": "http://localhost:8999/v2/applications/5/box?name=abi%3A%28uint64%2Cstring%2Cbool%5B%5D%29%3A%5B399%2C%22pls+pass%22%2C%5Btrue%2Cfalse%5D%5D", + "route": "/v2/applications/:application-id/box" + }, + "response": { + "statusCode": 200, + "body": "{\"name\":\"AAAAAAAAAY8ADAAWAAhwbHMgcGFzcwACgA==\",\"value\":\"AAAAAAAAAY8ADAAWAAhwbHMgcGFzcwACgAAAAAAAAAA=\"}\n" + }, + "witness": { + "goType": "generated.BoxResponse", + "box": { + "name": "AAAAAAAAAY8ADAAWAAhwbHMgcGFzcwACgA==", + "value": "AAAAAAAAAY8ADAAWAAhwbHMgcGFzcwACgAAAAAAAAAA=" + } + }, + "witnessError": null + }, + { + "name": "DELETED app 5 encoding (addr:LMTOYRT2WPSUY6JTCW2URER6YN3GETJ5FHTQBA55EVK66JG2QOB32WPIHY) - no params", + "request": { + "path": "/v2/applications/5/box", + "params": [ + { + "name": "name", + "value": "addr:LMTOYRT2WPSUY6JTCW2URER6YN3GETJ5FHTQBA55EVK66JG2QOB32WPIHY" + } + ], + "url": "http://localhost:8999/v2/applications/5/box?name=addr%3ALMTOYRT2WPSUY6JTCW2URER6YN3GETJ5FHTQBA55EVK66JG2QOB32WPIHY", + "route": "/v2/applications/:application-id/box" + }, + "response": { + "statusCode": 200, + "body": "{\"name\":\"WybsRnqz5Ux5MxW1SJI+w3ZiTT0p5wCDvSVV7yTag4M=\",\"value\":\"WybsRnqz5Ux5MxW1SJI+w3ZiTT0p5wCDvSVV7yTag4M=\"}\n" + }, + "witness": { + "goType": "generated.BoxResponse", + "box": { + "name": "WybsRnqz5Ux5MxW1SJI+w3ZiTT0p5wCDvSVV7yTag4M=", + "value": "WybsRnqz5Ux5MxW1SJI+w3ZiTT0p5wCDvSVV7yTag4M=" + } + }, + "witnessError": null + }, + { + "name": "DELETED app 5 encoding (address:2SYXFSCZAQCZ7YIFUCUZYOVR7G6Y3UBGSJIWT4EZ4CO3T6WVYTMHVSANOY) - no params", + "request": { + "path": "/v2/applications/5/box", + "params": [ + { + "name": "name", + "value": "address:2SYXFSCZAQCZ7YIFUCUZYOVR7G6Y3UBGSJIWT4EZ4CO3T6WVYTMHVSANOY" + } + ], + "url": "http://localhost:8999/v2/applications/5/box?name=address%3A2SYXFSCZAQCZ7YIFUCUZYOVR7G6Y3UBGSJIWT4EZ4CO3T6WVYTMHVSANOY", + "route": "/v2/applications/:application-id/box" + }, + "response": { + "statusCode": 200, + "body": "{\"name\":\"1LFyyFkEBZ/hBaCpnDqx+b2N0CaSUWnwmeCdufrVxNg=\",\"value\":\"1LFyyFkEBZ/hBaCpnDqx+b2N0CaSUWnwmeCdufrVxNg=\"}\n" + }, + "witness": { + "goType": "generated.BoxResponse", + "box": { + "name": "1LFyyFkEBZ/hBaCpnDqx+b2N0CaSUWnwmeCdufrVxNg=", + "value": "1LFyyFkEBZ/hBaCpnDqx+b2N0CaSUWnwmeCdufrVxNg=" + } + }, + "witnessError": null + }, + { + "name": "DELETED app 5 encoding (b32:MIZTE===) - no params", + "request": { + "path": "/v2/applications/5/box", + "params": [ + { + "name": "name", + "value": "b32:MIZTE===" + } + ], + "url": "http://localhost:8999/v2/applications/5/box?name=b32%3AMIZTE%3D%3D%3D", + "route": "/v2/applications/:application-id/box" + }, + "response": { + "statusCode": 200, + "body": "{\"name\":\"YjMy\",\"value\":\"YjMyAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=\"}\n" + }, + "witness": { + "goType": "generated.BoxResponse", + "box": { + "name": "YjMy", + "value": "YjMyAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=" + } + }, + "witnessError": null + }, + { + "name": "DELETED app 5 encoding (byte base32:MJ4XIZJAMJQXGZJTGI======) - no params", + "request": { + "path": "/v2/applications/5/box", + "params": [ + { + "name": "name", + "value": "byte base32:MJ4XIZJAMJQXGZJTGI======" + } + ], + "url": "http://localhost:8999/v2/applications/5/box?name=byte+base32%3AMJ4XIZJAMJQXGZJTGI%3D%3D%3D%3D%3D%3D", + "route": "/v2/applications/:application-id/box" + }, + "response": { + "statusCode": 200, + "body": "{\"name\":\"Ynl0ZSBiYXNlMzI=\",\"value\":\"Ynl0ZSBiYXNlMzIAAAAAAAAAAAAAAAAAAAAAAAAAAAA=\"}\n" + }, + "witness": { + "goType": "generated.BoxResponse", + "box": { + "name": "Ynl0ZSBiYXNlMzI=", + "value": "Ynl0ZSBiYXNlMzIAAAAAAAAAAAAAAAAAAAAAAAAAAAA=" + } + }, + "witnessError": null + }, + { + "name": "DELETED app 5 encoding (byte base64:Ynl0ZSBiYXNlNjQ=) - no params", + "request": { + "path": "/v2/applications/5/box", + "params": [ + { + "name": "name", + "value": "byte base64:Ynl0ZSBiYXNlNjQ=" + } + ], + "url": "http://localhost:8999/v2/applications/5/box?name=byte+base64%3AYnl0ZSBiYXNlNjQ%3D", + "route": "/v2/applications/:application-id/box" + }, + "response": { + "statusCode": 200, + "body": "{\"name\":\"Ynl0ZSBiYXNlNjQ=\",\"value\":\"Ynl0ZSBiYXNlNjQAAAAAAAAAAAAAAAAAAAAAAAAAAAA=\"}\n" + }, + "witness": { + "goType": "generated.BoxResponse", + "box": { + "name": "Ynl0ZSBiYXNlNjQ=", + "value": "Ynl0ZSBiYXNlNjQAAAAAAAAAAAAAAAAAAAAAAAAAAAA=" + } + }, + "witnessError": null + }, + { + "name": "DELETED app 5 illegal encoding (just a plain string) - no params", + "request": { + "path": "/v2/applications/5/box", + "params": [ + { + "name": "name", + "value": "just a plain string" + } + ], + "url": "http://localhost:8999/v2/applications/5/box?name=just+a+plain+string", + "route": "/v2/applications/:application-id/box" + }, + "response": { + "statusCode": 400, + "body": "{\"message\":\"LookupApplicationBoxByIDAndName received illegal box name (just a plain string): all arguments and box names should be of the form 'encoding:value'\"}\n" + }, + "witness": null, + "witnessError": "400 error" + }, + { + "name": "App 1337 non-existing with illegal encoding (just a plain string) - no params", + "request": { + "path": "/v2/applications/1337/box", + "params": [ + { + "name": "name", + "value": "just a plain string" + } + ], + "url": "http://localhost:8999/v2/applications/1337/box?name=just+a+plain+string", + "route": "/v2/applications/:application-id/box" + }, + "response": { + "statusCode": 400, + "body": "{\"message\":\"LookupApplicationBoxByIDAndName received illegal box name (just a plain string): all arguments and box names should be of the form 'encoding:value'\"}\n" + }, + "witness": null, + "witnessError": "400 error" + } + ] +} \ No newline at end of file diff --git a/cmd/algorand-indexer/daemon.go b/cmd/algorand-indexer/daemon.go index 83f36ee85..b9d728a50 100644 --- a/cmd/algorand-indexer/daemon.go +++ b/cmd/algorand-indexer/daemon.go @@ -79,6 +79,8 @@ type daemonConfig struct { defaultAccountsLimit uint32 maxAssetsLimit uint32 defaultAssetsLimit uint32 + maxBoxesLimit uint32 + defaultBoxesLimit uint32 maxBalancesLimit uint32 defaultBalancesLimit uint32 maxApplicationsLimit uint32 @@ -136,6 +138,8 @@ func DaemonCmd() *cobra.Command { cfg.flags.Uint32VarP(&cfg.defaultBalancesLimit, "default-balances-limit", "", 1000, "set the default Limit parameter for querying balances, if none is provided") cfg.flags.Uint32VarP(&cfg.maxApplicationsLimit, "max-applications-limit", "", 1000, "set the maximum allowed Limit parameter for querying applications") cfg.flags.Uint32VarP(&cfg.defaultApplicationsLimit, "default-applications-limit", "", 100, "set the default Limit parameter for querying applications, if none is provided") + cfg.flags.Uint32VarP(&cfg.maxBoxesLimit, "max-boxes-limit", "", 10000, "set the maximum allowed Limit parameter for searching an app's boxes") + cfg.flags.Uint32VarP(&cfg.defaultBoxesLimit, "default-boxes-limit", "", 1000, "set the default allowed Limit parameter for searching an app's boxes") cfg.flags.StringVarP(&cfg.indexerDataDir, "data-dir", "i", "", "path to indexer data dir, or $INDEXER_DATA") cfg.flags.BoolVar(&cfg.initLedger, "init-ledger", true, "initialize local ledger using sequential mode") @@ -459,6 +463,8 @@ func makeOptions(daemonConfig *daemonConfig) (options api.ExtraOptions) { options.DefaultBalancesLimit = uint64(daemonConfig.defaultBalancesLimit) options.MaxApplicationsLimit = uint64(daemonConfig.maxApplicationsLimit) options.DefaultApplicationsLimit = uint64(daemonConfig.defaultApplicationsLimit) + options.MaxBoxesLimit = uint64(daemonConfig.maxBoxesLimit) + options.DefaultBoxesLimit = uint64(daemonConfig.defaultBoxesLimit) if daemonConfig.enableAllParameters { options.DisabledMapConfig = api.MakeDisabledMapConfig() @@ -508,7 +514,7 @@ func blockHandler(proc processor.Processor, retryDelay time.Duration) func(conte case <-ctx.Done(): return err case <-time.After(retryDelay): - break + // NOOP } } } diff --git a/go.mod b/go.mod index 3ab1e7e4c..7776d9eaf 100644 --- a/go.mod +++ b/go.mod @@ -35,7 +35,7 @@ require ( github.com/algorand/avm-abi v0.1.0 // indirect github.com/algorand/falcon v0.0.0-20220727072124-02a2a64c4414 // indirect github.com/algorand/go-sumhash v0.1.0 // indirect - github.com/algorand/msgp v1.1.52 // indirect + github.com/algorand/msgp v1.1.53 // indirect github.com/algorand/websocket v1.4.5 // indirect github.com/aws/aws-sdk-go v1.30.19 // indirect github.com/beorn7/perks v1.0.1 // indirect diff --git a/go.sum b/go.sum index 479af940b..0298f540b 100644 --- a/go.sum +++ b/go.sum @@ -100,8 +100,8 @@ github.com/algorand/go-sumhash v0.1.0 h1:b/QRhyLuF//vOcicBIxBXYW8bERNoeLxieht/dU github.com/algorand/go-sumhash v0.1.0/go.mod h1:OOe7jdDWUhLkuP1XytkK5gnLu9entAviN5DfDZh6XAc= github.com/algorand/graphtrace v0.1.0 h1:QemP1iT0W56SExD0NfiU6rsG34/v0Je6bg5UZnptEUM= github.com/algorand/graphtrace v0.1.0/go.mod h1:HscLQrzBdH1BH+5oehs3ICd8SYcXvnSL9BjfTu8WHCc= -github.com/algorand/msgp v1.1.52 h1:Tw2OCCikKy0jaTWEIHwIfvThYHlJf9moviyKw+7PVVM= -github.com/algorand/msgp v1.1.52/go.mod h1:5K3d58/poT5fPmtiwuQft6GjgSrVEM46KoXdLrID8ZU= +github.com/algorand/msgp v1.1.53 h1:D6HKLyvLE6ltfsf8Apsrc+kqYb/CcOZEAfh1DpkPrNg= +github.com/algorand/msgp v1.1.53/go.mod h1:5K3d58/poT5fPmtiwuQft6GjgSrVEM46KoXdLrID8ZU= github.com/algorand/oapi-codegen v1.3.7 h1:TdXeGljgrnLXSCGPdeY6g6+i/G0Rr5CkjBgUJY6ht48= github.com/algorand/oapi-codegen v1.3.7/go.mod h1:UvOtAiP3hc0M2GUKBnZVTjLe3HKGDKh6y9rs3e3JyOg= github.com/algorand/websocket v1.4.5 h1:Cs6UTaCReAl02evYxmN8k57cNHmBILRcspfSxYg4AJE= diff --git a/idb/dummy/dummy.go b/idb/dummy/dummy.go index 7e7c5b842..0fa142b32 100644 --- a/idb/dummy/dummy.go +++ b/idb/dummy/dummy.go @@ -83,6 +83,11 @@ func (db *dummyIndexerDb) AppLocalState(ctx context.Context, filter idb.Applicat return nil, 0 } +// ApplicationBoxes isn't currently implemented +func (db *dummyIndexerDb) ApplicationBoxes(ctx context.Context, filter idb.ApplicationBoxQuery) (<-chan idb.ApplicationBoxRow, uint64) { + panic("not implemented") +} + // Health is part of idb.IndexerDB func (db *dummyIndexerDb) Health(ctx context.Context) (state idb.Health, err error) { return idb.Health{}, nil diff --git a/idb/idb.go b/idb/idb.go index 216cbabff..018c6c445 100644 --- a/idb/idb.go +++ b/idb/idb.go @@ -182,6 +182,7 @@ type IndexerDb interface { AssetBalances(ctx context.Context, abq AssetBalanceQuery) (<-chan AssetBalanceRow, uint64) Applications(ctx context.Context, filter ApplicationQuery) (<-chan ApplicationRow, uint64) AppLocalState(ctx context.Context, filter ApplicationQuery) (<-chan AppLocalStateRow, uint64) + ApplicationBoxes(ctx context.Context, filter ApplicationBoxQuery) (<-chan ApplicationBoxRow, uint64) Health(ctx context.Context) (status Health, err error) } @@ -381,6 +382,23 @@ type AppLocalStateRow struct { Error error } +// ApplicationBoxQuery is a parameter object used to query application boxes. +type ApplicationBoxQuery struct { + ApplicationID uint64 + BoxName []byte + OmitValues bool + Limit uint64 + PrevFinalBox []byte + // Ascending *bool - Currently, ORDER BY is hard coded to ASC +} + +// ApplicationBoxRow provides a response wrapping box information. +type ApplicationBoxRow struct { + App uint64 + Box models.Box + Error error +} + // IndexerDbOptions are the options common to all indexer backends. type IndexerDbOptions struct { ReadOnly bool diff --git a/idb/mocks/IndexerDb.go b/idb/mocks/IndexerDb.go index 55fe296c8..dbd7ca94d 100644 --- a/idb/mocks/IndexerDb.go +++ b/idb/mocks/IndexerDb.go @@ -60,6 +60,29 @@ func (_m *IndexerDb) AppLocalState(ctx context.Context, filter idb.ApplicationQu return r0, r1 } +// ApplicationBoxes provides a mock function with given fields: ctx, filter +func (_m *IndexerDb) ApplicationBoxes(ctx context.Context, filter idb.ApplicationBoxQuery) (<-chan idb.ApplicationBoxRow, uint64) { + ret := _m.Called(ctx, filter) + + var r0 <-chan idb.ApplicationBoxRow + if rf, ok := ret.Get(0).(func(context.Context, idb.ApplicationBoxQuery) <-chan idb.ApplicationBoxRow); ok { + r0 = rf(ctx, filter) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(<-chan idb.ApplicationBoxRow) + } + } + + var r1 uint64 + if rf, ok := ret.Get(1).(func(context.Context, idb.ApplicationBoxQuery) uint64); ok { + r1 = rf(ctx, filter) + } else { + r1 = ret.Get(1).(uint64) + } + + return r0, r1 +} + // Applications provides a mock function with given fields: ctx, filter func (_m *IndexerDb) Applications(ctx context.Context, filter idb.ApplicationQuery) (<-chan idb.ApplicationRow, uint64) { ret := _m.Called(ctx, filter) diff --git a/idb/postgres/internal/encoding/encoding.go b/idb/postgres/internal/encoding/encoding.go index 6f6bf35dd..659ae4c82 100644 --- a/idb/postgres/internal/encoding/encoding.go +++ b/idb/postgres/internal/encoding/encoding.go @@ -689,6 +689,8 @@ func convertTrimmedLcAccountData(ad ledgercore.AccountData) baseAccountData { TotalAssets: ad.TotalAssets, TotalAppParams: ad.TotalAppParams, TotalAppLocalStates: ad.TotalAppLocalStates, + TotalBoxes: ad.TotalBoxes, + TotalBoxBytes: ad.TotalBoxBytes, baseOnlineAccountData: baseOnlineAccountData{ VoteID: ad.VoteID, SelectionID: ad.SelectionID, @@ -711,6 +713,8 @@ func unconvertTrimmedLcAccountData(ba baseAccountData) ledgercore.AccountData { TotalAppLocalStates: ba.TotalAppLocalStates, TotalAssetParams: ba.TotalAssetParams, TotalAssets: ba.TotalAssets, + TotalBoxes: ba.TotalBoxes, + TotalBoxBytes: ba.TotalBoxBytes, }, VotingData: ledgercore.VotingData{ VoteID: ba.VoteID, diff --git a/idb/postgres/internal/encoding/encoding_test.go b/idb/postgres/internal/encoding/encoding_test.go index 2554ef78d..87e531f1c 100644 --- a/idb/postgres/internal/encoding/encoding_test.go +++ b/idb/postgres/internal/encoding/encoding_test.go @@ -2,6 +2,7 @@ package encoding import ( "math/rand" + "reflect" "testing" "github.com/algorand/go-algorand/crypto" @@ -580,6 +581,8 @@ func TestLcAccountDataEncoding(t *testing.T) { TotalAppLocalStates: 11, TotalAssetParams: 12, TotalAssets: 13, + TotalBoxes: 20, + TotalBoxBytes: 21, }, VotingData: ledgercore.VotingData{ VoteID: voteID, @@ -592,10 +595,55 @@ func TestLcAccountDataEncoding(t *testing.T) { } buf := EncodeTrimmedLcAccountData(ad) - expectedString := `{"onl":1,"sel":"DwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=","spend":"BgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=","stprf":"EwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA==","tapl":11,"tapp":10,"tas":13,"tasp":12,"teap":9,"tsch":{"nbs":8,"nui":7},"vote":"DgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=","voteFst":16,"voteKD":18,"voteLst":17}` + expectedString := `{"onl":1,"sel":"DwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=","spend":"BgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=","stprf":"EwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA==","tapl":11,"tapp":10,"tas":13,"tasp":12,"tbx":20,"tbxb":21,"teap":9,"tsch":{"nbs":8,"nui":7},"vote":"DgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=","voteFst":16,"voteKD":18,"voteLst":17}` assert.Equal(t, expectedString, string(buf)) decodedAd, err := DecodeTrimmedLcAccountData(buf) require.NoError(t, err) assert.Equal(t, ad, decodedAd) } + +// structFields recursively gets all field names in a struct +func structFields(theStruct interface{}, skip map[string]bool, names map[string]bool) { + rStruct := reflect.TypeOf(theStruct) + numFields := rStruct.NumField() + for i := 0; i < numFields; i++ { + field := rStruct.Field(i) + name := field.Name + if totalSkip, nameSkip := skip[name]; nameSkip { + if totalSkip { + continue + } + } else { + names[name] = true + } + if field.Type.Kind() == reflect.Struct { + structFields(reflect.New(field.Type).Elem().Interface(), skip, names) + } + } +} + +// Test that all fields in go-algorand's AccountBaseData are either in local baseAccountData +// or are accounted for explicitly in "skip" +func TestBaseAccountDataVersusAccountBaseDataParity(t *testing.T) { + + skip := map[string]bool{ + "_struct": true, + "MicroAlgos": true, + "RewardsBase": true, + "RewardedMicroAlgos": true, + "MicroAlgosWithRewards": true, + "VotingData": false, // skip the name, but continue with the recursion + } + + goalNames := map[string]bool{} + structFields(ledgercore.AccountBaseData{}, skip, goalNames) + structFields(ledgercore.OnlineAccountData{}, skip, goalNames) + + indexerNames := map[string]bool{} + structFields(baseAccountData{}, skip, indexerNames) + + for name := range goalNames { + require.Contains(t, indexerNames, name) + } +} diff --git a/idb/postgres/internal/encoding/types.go b/idb/postgres/internal/encoding/types.go index b8fe96131..a04684dda 100644 --- a/idb/postgres/internal/encoding/types.go +++ b/idb/postgres/internal/encoding/types.go @@ -130,6 +130,8 @@ type baseAccountData struct { TotalAssets uint64 `codec:"tas"` TotalAppParams uint64 `codec:"tapp"` TotalAppLocalStates uint64 `codec:"tapl"` + TotalBoxes uint64 `codec:"tbx"` + TotalBoxBytes uint64 `codec:"tbxb"` baseOnlineAccountData } diff --git a/idb/postgres/internal/schema/setup_postgres.sql b/idb/postgres/internal/schema/setup_postgres.sql index 9337c0e10..bd2e3242e 100644 --- a/idb/postgres/internal/schema/setup_postgres.sql +++ b/idb/postgres/internal/schema/setup_postgres.sql @@ -117,3 +117,11 @@ CREATE TABLE IF NOT EXISTS account_app ( -- For looking up existing app local states by account CREATE INDEX IF NOT EXISTS account_app_by_addr_partial ON account_app(addr) WHERE NOT deleted; + +-- For looking up app box storage +CREATE TABLE IF NOT EXISTS app_box ( + app bigint NOT NULL, + name bytea NOT NULL, + value bytea NOT NULL, -- upon creation 'value' is 0x000...000 with length being the box'es size + PRIMARY KEY (app, name) +); diff --git a/idb/postgres/internal/schema/setup_postgres_sql.go b/idb/postgres/internal/schema/setup_postgres_sql.go index 3d5fb0a2a..7636e9329 100644 --- a/idb/postgres/internal/schema/setup_postgres_sql.go +++ b/idb/postgres/internal/schema/setup_postgres_sql.go @@ -121,4 +121,12 @@ CREATE TABLE IF NOT EXISTS account_app ( -- For looking up existing app local states by account CREATE INDEX IF NOT EXISTS account_app_by_addr_partial ON account_app(addr) WHERE NOT deleted; + +-- For looking up app box storage +CREATE TABLE IF NOT EXISTS app_box ( + app bigint NOT NULL, + name bytea NOT NULL, + value bytea NOT NULL, -- upon creation 'value' is 0x000...000 with length being the box'es size + PRIMARY KEY (app, name) +); ` diff --git a/idb/postgres/internal/writer/util.go b/idb/postgres/internal/writer/util.go index 1372b234a..61f2f5234 100644 --- a/idb/postgres/internal/writer/util.go +++ b/idb/postgres/internal/writer/util.go @@ -8,7 +8,6 @@ import ( type copyFromChannelStruct struct { ch chan []interface{} next []interface{} - err error } func (c *copyFromChannelStruct) Next() bool { diff --git a/idb/postgres/internal/writer/writer.go b/idb/postgres/internal/writer/writer.go index 48a71e5b6..08f1428c9 100644 --- a/idb/postgres/internal/writer/writer.go +++ b/idb/postgres/internal/writer/writer.go @@ -9,6 +9,7 @@ import ( "github.com/algorand/go-algorand/data/basics" "github.com/algorand/go-algorand/data/bookkeeping" "github.com/algorand/go-algorand/data/transactions" + "github.com/algorand/go-algorand/data/transactions/logic" "github.com/algorand/go-algorand/ledger/ledgercore" "github.com/algorand/go-algorand/protocol" "github.com/jackc/pgx/v4" @@ -33,6 +34,8 @@ const ( deleteAccountAssetStmtName = "delete_account_asset" deleteAppStmtName = "delete_app" deleteAccountAppStmtName = "delete_account_app" + upsertAppBoxStmtName = "upsert_app_box" + deleteAppBoxStmtName = "delete_app_box" updateAccountTotalsStmtName = "update_account_totals" ) @@ -105,6 +108,12 @@ var statements = map[string]string{ (addr, app, localstate, deleted, created_at, closed_at) VALUES($1, $2, 'null'::jsonb, TRUE, $3, $3) ON CONFLICT (addr, app) DO UPDATE SET localstate = EXCLUDED.localstate, deleted = TRUE, closed_at = EXCLUDED.closed_at`, + upsertAppBoxStmtName: `INSERT INTO app_box AS ab + (app, name, value) + VALUES ($1, $2, $3) + ON CONFLICT (app, name) DO UPDATE SET + value = EXCLUDED.value`, + deleteAppBoxStmtName: `DELETE FROM app_box WHERE app = $1 and name = $2`, updateAccountTotalsStmtName: `UPDATE metastate SET v = $1 WHERE k = '` + schema.AccountTotals + `'`, } @@ -123,7 +132,7 @@ func MakeWriter(tx pgx.Tx) (Writer, error) { for name, query := range statements { _, err := tx.Prepare(context.Background(), name, query) if err != nil { - return Writer{}, fmt.Errorf("MakeWriter() prepare statement err: %w", err) + return Writer{}, fmt.Errorf("MakeWriter() prepare statement for name '%s' err: %w", name, err) } } @@ -291,6 +300,28 @@ func writeAccountDeltas(round basics.Round, accountDeltas *ledgercore.AccountDel writeAppResource(round, &appResources[i], batch) } } + +} + +func writeBoxMods(kvMods map[string]ledgercore.KvValueDelta, batch *pgx.Batch) error { + // INSERT INTO / UPDATE / DELETE FROM `app_box` + // WARNING: kvMods can in theory support more general storage types than app boxes. + // However, here we assume that all the provided kvMods represent app boxes. + // If a non-box is encountered inside kvMods, an error will be returned and + // AddBlock() will fail with the import getting stuck at the corresponding round. + for key, valueDelta := range kvMods { + app, name, err := logic.SplitBoxKey(key) + if err != nil { + return fmt.Errorf("writeBoxMods() err: %w", err) + } + if valueDelta.Data != nil { + batch.Queue(upsertAppBoxStmtName, app, []byte(name), []byte(valueDelta.Data)) + } else { + batch.Queue(deleteAppBoxStmtName, app, []byte(name)) + } + } + + return nil } // AddBlock0 writes block 0 to the database. @@ -310,12 +341,12 @@ func (w *Writer) AddBlock0(block *bookkeeping.Block) error { _, err := results.Exec() if err != nil { results.Close() - return fmt.Errorf("AddBlock() exec err: %w", err) + return fmt.Errorf("AddBlock0() exec err: %w", err) } } err := results.Close() if err != nil { - return fmt.Errorf("AddBlock() close results err: %w", err) + return fmt.Errorf("AddBlock0() close results err: %w", err) } return nil @@ -340,6 +371,12 @@ func (w *Writer) AddBlock(block *bookkeeping.Block, modifiedTxns []transactions. } writeAccountDeltas(block.Round(), &delta.Accts, sigTypeDeltas, &batch) } + { + err := writeBoxMods(delta.KvMods, &batch) + if err != nil { + return fmt.Errorf("AddBlock() err on boxes: %w", err) + } + } batch.Queue(updateAccountTotalsStmtName, encoding.EncodeAccountTotals(&delta.Totals)) results := w.tx.SendBatch(context.Background(), &batch) diff --git a/idb/postgres/internal/writer/writer_test.go b/idb/postgres/internal/writer/writer_test.go index 3b0902c0e..f0df67f04 100644 --- a/idb/postgres/internal/writer/writer_test.go +++ b/idb/postgres/internal/writer/writer_test.go @@ -11,6 +11,7 @@ import ( "github.com/algorand/go-algorand/data/basics" "github.com/algorand/go-algorand/data/bookkeeping" "github.com/algorand/go-algorand/data/transactions" + "github.com/algorand/go-algorand/data/transactions/logic" "github.com/algorand/go-algorand/ledger/ledgercore" "github.com/algorand/go-algorand/protocol" "github.com/jackc/pgx/v4" @@ -1564,3 +1565,274 @@ func TestWriterAddBlock0(t *testing.T) { assert.Equal(t, expected, accounts) } } +func getNameAndAccountPointer(t *testing.T, value ledgercore.KvValueDelta, fullKey string, accts map[basics.Address]*ledgercore.AccountData) (basics.Address, string, *ledgercore.AccountData) { + require.NotNil(t, value, "cannot handle a nil value for box stats modification") + appIdx, name, err := logic.SplitBoxKey(fullKey) + account := appIdx.Address() + require.NoError(t, err) + acctData, ok := accts[account] + if !ok { + acctData = &ledgercore.AccountData{ + AccountBaseData: ledgercore.AccountBaseData{}, + } + accts[account] = acctData + } + return account, name, acctData +} + +func addBoxInfoToStats(t *testing.T, fullKey string, value ledgercore.KvValueDelta, + accts map[basics.Address]*ledgercore.AccountData, boxTotals map[basics.Address]basics.AccountData) { + addr, name, acctData := getNameAndAccountPointer(t, value, fullKey, accts) + + acctData.TotalBoxes++ + acctData.TotalBoxBytes += uint64(len(name) + len(value.Data)) + + boxTotals[addr] = basics.AccountData{ + TotalBoxes: acctData.TotalBoxes, + TotalBoxBytes: acctData.TotalBoxBytes, + } +} + +func subtractBoxInfoToStats(t *testing.T, fullKey string, value ledgercore.KvValueDelta, + accts map[basics.Address]*ledgercore.AccountData, boxTotals map[basics.Address]basics.AccountData) { + addr, name, acctData := getNameAndAccountPointer(t, value, fullKey, accts) + + prevBoxBytes := uint64(len(name) + len(value.Data)) + require.GreaterOrEqual(t, acctData.TotalBoxes, uint64(0)) + require.GreaterOrEqual(t, acctData.TotalBoxBytes, prevBoxBytes) + + acctData.TotalBoxes-- + acctData.TotalBoxBytes -= prevBoxBytes + + boxTotals[addr] = basics.AccountData{ + TotalBoxes: acctData.TotalBoxes, + TotalBoxBytes: acctData.TotalBoxBytes, + } +} + +// buildAccountDeltasFromKvsAndMods simulates keeping track of the evolution of the box statistics +func buildAccountDeltasFromKvsAndMods(t *testing.T, kvOriginals, kvMods map[string]ledgercore.KvValueDelta) ( + ledgercore.StateDelta, map[string]ledgercore.KvValueDelta, map[basics.Address]basics.AccountData) { + kvUpdated := map[string]ledgercore.KvValueDelta{} + boxTotals := map[basics.Address]basics.AccountData{} + accts := map[basics.Address]*ledgercore.AccountData{} + /* + 1. fill the accts and kvUpdated using kvOriginals + 2. for each (fullKey, value) in kvMod: + * (A) if the key is not present in kvOriginals just add the info as in #1 + * (B) else (fullKey present): + * (i) if the value is nil + ==> remove the box info from the stats and kvUpdated with assertions + * (ii) else (value is NOT nil): + ==> reset kvUpdated and assert that the box hasn't changed shapes + */ + + /* 1. */ + for fullKey, value := range kvOriginals { + addBoxInfoToStats(t, fullKey, value, accts, boxTotals) + kvUpdated[fullKey] = value + } + + /* 2. */ + for fullKey, value := range kvMods { + prevValue, ok := kvOriginals[fullKey] + if !ok { + /* 2A. */ + addBoxInfoToStats(t, fullKey, value, accts, boxTotals) + kvUpdated[fullKey] = value + continue + } + /* 2B. */ + if value.Data == nil { + /* 2Bi. */ + subtractBoxInfoToStats(t, fullKey, prevValue, accts, boxTotals) + delete(kvUpdated, fullKey) + continue + } + /* 2Bii. */ + require.Contains(t, kvUpdated, fullKey) + kvUpdated[fullKey] = value + } + + var delta ledgercore.StateDelta + for acct, acctData := range accts { + delta.Accts.Upsert(acct, *acctData) + } + return delta, kvUpdated, boxTotals +} + +// Simulate a scenario where app boxes are created, mutated and deleted in consecutive rounds. +func TestWriterAppBoxTableInsertMutateDelete(t *testing.T) { + /* Simulation details: + Box 1: inserted and then untouched + Box 2: inserted and mutated + Box 3: inserted and deleted + Box 4: inserted, mutated and deleted + Box 5: inserted, deleted and re-inserted + Box 6: inserted after Box 2 is set + */ + + db, _, shutdownFunc := pgtest.SetupPostgresWithSchema(t) + defer shutdownFunc() + + var block bookkeeping.Block + block.BlockHeader.Round = basics.Round(1) + delta := ledgercore.StateDelta{} + + addNewBlock := func(tx pgx.Tx) error { + w, err := writer.MakeWriter(tx) + require.NoError(t, err) + + err = w.AddBlock(&block, block.Payset, delta) + require.NoError(t, err) + + w.Close() + return nil + } + + appID := basics.AppIndex(3) + notPresent := "NOT PRESENT" + + // ---- ROUND 1: create 5 boxes ---- // + n1, v1 := "box1", "inserted" + n2, v2 := "box2", "inserted" + n3, v3 := "box3", "inserted" + n4, v4 := "box4", "inserted" + n5, v5 := "box5", "inserted" + + k1 := logic.MakeBoxKey(appID, n1) + k2 := logic.MakeBoxKey(appID, n2) + k3 := logic.MakeBoxKey(appID, n3) + k4 := logic.MakeBoxKey(appID, n4) + k5 := logic.MakeBoxKey(appID, n5) + + delta.KvMods = map[string]ledgercore.KvValueDelta{} + delta.KvMods[k1] = ledgercore.KvValueDelta{Data: []byte(v1)} + delta.KvMods[k2] = ledgercore.KvValueDelta{Data: []byte(v2)} + delta.KvMods[k3] = ledgercore.KvValueDelta{Data: []byte(v3)} + delta.KvMods[k4] = ledgercore.KvValueDelta{Data: []byte(v4)} + delta.KvMods[k5] = ledgercore.KvValueDelta{Data: []byte(v5)} + + delta2, newKvMods, accts := buildAccountDeltasFromKvsAndMods(t, map[string]ledgercore.KvValueDelta{}, delta.KvMods) + delta.Accts = delta2.Accts + + err := pgutil.TxWithRetry(db, serializable, addNewBlock, nil) + require.NoError(t, err) + + validateRow := func(expectedName string, expectedValue string) { + appBoxSQL := `SELECT app, name, value FROM app_box WHERE app = $1 AND name = $2` + var app basics.AppIndex + var name, value []byte + + row := db.QueryRow(context.Background(), appBoxSQL, appID, []byte(expectedName)) + err := row.Scan(&app, &name, &value) + + if expectedValue == notPresent { + require.ErrorContains(t, err, "no rows in result set") + return + } + + require.NoError(t, err) + require.Equal(t, appID, app) + require.Equal(t, expectedName, string(name)) + require.Equal(t, expectedValue, string(value)) + } + + validateTotals := func() { + acctDataSQL := "SELECT account_data FROM account WHERE addr = $1" + for addr, acctInfo := range accts { + row := db.QueryRow(context.Background(), acctDataSQL, addr[:]) + + var buf []byte + err := row.Scan(&buf) + require.NoError(t, err) + + ret, err := encoding.DecodeTrimmedLcAccountData(buf) + require.NoError(t, err) + require.Equal(t, acctInfo.TotalBoxes, ret.TotalBoxes) + require.Equal(t, acctInfo.TotalBoxBytes, ret.TotalBoxBytes) + } + } + + validateRow(n1, v1) + validateRow(n2, v2) + validateRow(n3, v3) + validateRow(n4, v4) + validateRow(n5, v5) + + validateTotals() + + // ---- ROUND 2: mutate 2, delete 3, mutate 4, delete 5, create 6 ---- // + oldV2 := v2 + v2 = "mutated" + // v3 is "deleted" + oldV4 := v4 + v4 = "mutated" + // v5 is "deleted" + n6, v6 := "box6", "inserted" + + k6 := logic.MakeBoxKey(appID, n6) + + delta.KvMods = map[string]ledgercore.KvValueDelta{} + delta.KvMods[k2] = ledgercore.KvValueDelta{Data: []byte(v2), OldData: []byte(oldV2)} + delta.KvMods[k3] = ledgercore.KvValueDelta{Data: nil} + delta.KvMods[k4] = ledgercore.KvValueDelta{Data: []byte(v4), OldData: []byte(oldV4)} + delta.KvMods[k5] = ledgercore.KvValueDelta{Data: nil} + delta.KvMods[k6] = ledgercore.KvValueDelta{Data: []byte(v6)} + + delta2, newKvMods, accts = buildAccountDeltasFromKvsAndMods(t, newKvMods, delta.KvMods) + delta.Accts = delta2.Accts + + err = pgutil.TxWithRetry(db, serializable, addNewBlock, nil) + require.NoError(t, err) + + validateRow(n1, v1) // untouched + validateRow(n2, v2) // new v2 + validateRow(n3, notPresent) + validateRow(n4, v4) // new v4 + validateRow(n5, notPresent) + validateRow(n6, v6) + + validateTotals() + + // ---- ROUND 3: delete 4, insert 5 ---- // + + // v4 is "deleted" + v5 = "re-inserted" + + delta.KvMods = map[string]ledgercore.KvValueDelta{} + delta.KvMods[k4] = ledgercore.KvValueDelta{Data: nil} + delta.KvMods[k5] = ledgercore.KvValueDelta{Data: []byte(v5)} + + delta2, newKvMods, accts = buildAccountDeltasFromKvsAndMods(t, newKvMods, delta.KvMods) + delta.Accts = delta2.Accts + + err = pgutil.TxWithRetry(db, serializable, addNewBlock, nil) + require.NoError(t, err) + + validateRow(n1, v1) // untouched + validateRow(n2, v2) // untouched + validateRow(n3, notPresent) // still deleted + validateRow(n4, notPresent) // deleted + validateRow(n5, v5) // re-inserted + validateRow(n6, v6) // untouched + + validateTotals() + + /*** FOURTH ROUND - NOOP ***/ + delta.KvMods = map[string]ledgercore.KvValueDelta{} + delta2, _, accts = buildAccountDeltasFromKvsAndMods(t, newKvMods, delta.KvMods) + delta.Accts = delta2.Accts + + err = pgutil.TxWithRetry(db, serializable, addNewBlock, nil) + require.NoError(t, err) + + validateRow(n1, v1) // untouched + validateRow(n2, v2) // untouched + validateRow(n3, notPresent) // still deleted + validateRow(n4, notPresent) // still deleted + validateRow(n5, v5) // untouched + validateRow(n6, v6) // untouched + + validateTotals() +} diff --git a/idb/postgres/postgres.go b/idb/postgres/postgres.go index 59626030a..95deb4fbd 100644 --- a/idb/postgres/postgres.go +++ b/idb/postgres/postgres.go @@ -48,7 +48,7 @@ func OpenPostgres(connection string, opts idb.IndexerDbOptions, log *log.Logger) postgresConfig, err := pgxpool.ParseConfig(connection) if err != nil { - return nil, nil, fmt.Errorf("Couldn't parse config: %v", err) + return nil, nil, fmt.Errorf("couldn't parse config: %v", err) } if opts.MaxConn != 0 { @@ -1035,58 +1035,61 @@ func (db *IndexerDb) yieldAccountsThread(req *getAccountsRequest) { } { - var ad ledgercore.AccountData - ad, err = encoding.DecodeTrimmedLcAccountData(accountDataJSONStr) + var accountData ledgercore.AccountData + accountData, err = encoding.DecodeTrimmedLcAccountData(accountDataJSONStr) if err != nil { err = fmt.Errorf("account decode err (%s) %v", accountDataJSONStr, err) req.out <- idb.AccountRow{Error: err} break } - account.Status = statusStrings[ad.Status] - hasSel := !allZero(ad.SelectionID[:]) - hasVote := !allZero(ad.VoteID[:]) - hasStateProofkey := !allZero(ad.StateProofID[:]) + account.Status = statusStrings[accountData.Status] + hasSel := !allZero(accountData.SelectionID[:]) + hasVote := !allZero(accountData.VoteID[:]) + hasStateProofkey := !allZero(accountData.StateProofID[:]) if hasSel || hasVote || hasStateProofkey { part := new(models.AccountParticipation) if hasSel { - part.SelectionParticipationKey = ad.SelectionID[:] + part.SelectionParticipationKey = accountData.SelectionID[:] } if hasVote { - part.VoteParticipationKey = ad.VoteID[:] + part.VoteParticipationKey = accountData.VoteID[:] } if hasStateProofkey { - part.StateProofKey = byteSlicePtr(ad.StateProofID[:]) + part.StateProofKey = byteSlicePtr(accountData.StateProofID[:]) } - part.VoteFirstValid = uint64(ad.VoteFirstValid) - part.VoteLastValid = uint64(ad.VoteLastValid) - part.VoteKeyDilution = ad.VoteKeyDilution + part.VoteFirstValid = uint64(accountData.VoteFirstValid) + part.VoteLastValid = uint64(accountData.VoteLastValid) + part.VoteKeyDilution = accountData.VoteKeyDilution account.Participation = part } - if !ad.AuthAddr.IsZero() { + if !accountData.AuthAddr.IsZero() { var spendingkey basics.Address - copy(spendingkey[:], ad.AuthAddr[:]) + copy(spendingkey[:], accountData.AuthAddr[:]) account.AuthAddr = stringPtr(spendingkey.String()) } { totalSchema := models.ApplicationStateSchema{ - NumByteSlice: ad.TotalAppSchema.NumByteSlice, - NumUint: ad.TotalAppSchema.NumUint, + NumByteSlice: accountData.TotalAppSchema.NumByteSlice, + NumUint: accountData.TotalAppSchema.NumUint, } if totalSchema != (models.ApplicationStateSchema{}) { account.AppsTotalSchema = &totalSchema } } - if ad.TotalExtraAppPages != 0 { - account.AppsTotalExtraPages = uint64Ptr(uint64(ad.TotalExtraAppPages)) + if accountData.TotalExtraAppPages != 0 { + account.AppsTotalExtraPages = uint64Ptr(uint64(accountData.TotalExtraAppPages)) } - account.TotalAppsOptedIn = ad.TotalAppLocalStates - account.TotalCreatedApps = ad.TotalAppParams - account.TotalAssetsOptedIn = ad.TotalAssets - account.TotalCreatedAssets = ad.TotalAssetParams + account.TotalAppsOptedIn = accountData.TotalAppLocalStates + account.TotalCreatedApps = accountData.TotalAppParams + account.TotalAssetsOptedIn = accountData.TotalAssets + account.TotalCreatedAssets = accountData.TotalAssetParams + + account.TotalBoxes = accountData.TotalBoxes + account.TotalBoxBytes = accountData.TotalBoxBytes } if account.Status == "NotParticipating" { @@ -2292,6 +2295,123 @@ func (db *IndexerDb) yieldApplicationsThread(rows pgx.Rows, out chan idb.Applica } } +// ApplicationBoxes is part of interface idb.IndexerDB. The most complex query formed looks like: +// +// WITH apps AS (SELECT index AS app FROM app WHERE index = $1) +// SELECT a.app, ab.name, ab.value +// FROM apps a +// LEFT OUTER JOIN app_box ab ON ab.app = a.app AND name [= or >] $2 ORDER BY ab.name ASC LIMIT {queryOpts.Limit} +// +// where the binary operator in the last line is `=` for the box lookup and `>` for boxes search +// with query substitutions: +// $1 <-- queryOpts.ApplicationID +// $2 <-- queryOpts.BoxName +// $3 <-- queryOpts.PrevFinalBox +func (db *IndexerDb) ApplicationBoxes(ctx context.Context, queryOpts idb.ApplicationBoxQuery) (<-chan idb.ApplicationBoxRow, uint64) { + out := make(chan idb.ApplicationBoxRow, 1) + + columns := `a.app, ab.name` + if !queryOpts.OmitValues { + columns += `, ab.value` + } + query := fmt.Sprintf(`WITH apps AS (SELECT index AS app FROM app WHERE index = $1) +SELECT %s +FROM apps a +LEFT OUTER JOIN app_box ab ON ab.app = a.app`, columns) + + whereArgs := []interface{}{queryOpts.ApplicationID} + if queryOpts.BoxName != nil { + query += " AND name = $2" + whereArgs = append(whereArgs, queryOpts.BoxName) + } else if queryOpts.PrevFinalBox != nil { + // when enabling ORDER BY DESC, will need to filter by "name < $2": + query += " AND name > $2" + whereArgs = append(whereArgs, queryOpts.PrevFinalBox) + } + + // To enable ORDER BY DESC, consider re-introducing and exposing queryOpts.Ascending + query += " ORDER BY ab.name ASC" + + if queryOpts.Limit != 0 { + query += fmt.Sprintf(" LIMIT %d", queryOpts.Limit) + } + + tx, err := db.db.BeginTx(ctx, readonlyRepeatableRead) + if err != nil { + out <- idb.ApplicationBoxRow{Error: err} + close(out) + return out, 0 + } + + round, err := db.getMaxRoundAccounted(ctx, tx) + if err != nil { + out <- idb.ApplicationBoxRow{Error: err} + close(out) + if rerr := tx.Rollback(ctx); rerr != nil { + db.log.Printf("rollback error: %s", rerr) + } + return out, round + } + + rows, err := tx.Query(ctx, query, whereArgs...) + if err != nil { + out <- idb.ApplicationBoxRow{Error: err} + close(out) + if rerr := tx.Rollback(ctx); rerr != nil { + db.log.Printf("rollback error: %s", rerr) + } + return out, round + } + + go func() { + db.yieldApplicationBoxThread(queryOpts.OmitValues, rows, out) + // Because we return a channel into a "callWithTimeout" function, + // We need to make sure that rollback is called before close() + // otherwise we can end up with a situation where "callWithTimeout" + // will cancel our context, resulting in connection pool churn + if rerr := tx.Rollback(ctx); rerr != nil { + db.log.Printf("rollback error: %s", rerr) + } + close(out) + }() + return out, round +} + +func (db *IndexerDb) yieldApplicationBoxThread(omitValues bool, rows pgx.Rows, out chan idb.ApplicationBoxRow) { + defer rows.Close() + + gotRows := false + for rows.Next() { + gotRows = true + var app uint64 + var name []byte + var value []byte + var err error + + if omitValues { + err = rows.Scan(&app, &name) + } else { + err = rows.Scan(&app, &name, &value) + } + if err != nil { + out <- idb.ApplicationBoxRow{Error: err} + break + } + + box := models.Box{ + Name: name, + Value: value, // is nil when omitValues + } + + out <- idb.ApplicationBoxRow{App: app, Box: box} + } + if err := rows.Err(); err != nil { + out <- idb.ApplicationBoxRow{Error: err} + } else if !gotRows { + out <- idb.ApplicationBoxRow{Error: sql.ErrNoRows} + } +} + // AppLocalState is part of idb.IndexerDB func (db *IndexerDb) AppLocalState(ctx context.Context, filter idb.ApplicationQuery) (<-chan idb.AppLocalStateRow, uint64) { out := make(chan idb.AppLocalStateRow, 1) diff --git a/idb/postgres/postgres_boxes_test.go b/idb/postgres/postgres_boxes_test.go new file mode 100644 index 000000000..666bc384b --- /dev/null +++ b/idb/postgres/postgres_boxes_test.go @@ -0,0 +1,463 @@ +package postgres + +import ( + "context" + "fmt" + "math/rand" + "testing" + "time" + + "github.com/jackc/pgx/v4" + "github.com/stretchr/testify/require" + + "github.com/algorand/go-algorand/config" + "github.com/algorand/go-algorand/data/basics" + "github.com/algorand/go-algorand/data/bookkeeping" + "github.com/algorand/go-algorand/data/transactions" + "github.com/algorand/go-algorand/data/transactions/logic" + "github.com/algorand/go-algorand/ledger/ledgercore" + "github.com/algorand/go-algorand/protocol" + "github.com/algorand/go-algorand/rpcs" + + "github.com/algorand/indexer/idb" + "github.com/algorand/indexer/idb/postgres/internal/encoding" + "github.com/algorand/indexer/idb/postgres/internal/writer" + "github.com/algorand/indexer/util/test" +) + +type boxTestComparator func(t *testing.T, db *IndexerDb, appBoxes map[basics.AppIndex]map[string]string, + deletedBoxes map[basics.AppIndex]map[string]bool, verifyTotals bool) + +// compareAppBoxesAgainstDB is of type testing.BoxTestComparator +func compareAppBoxesAgainstDB(t *testing.T, db *IndexerDb, + appBoxes map[basics.AppIndex]map[string]string, + deletedBoxes map[basics.AppIndex]map[string]bool, verifyTotals bool) { + + numQueries := 0 + sumOfBoxes := 0 + sumOfBoxBytes := 0 + + appBoxSQL := `SELECT app, name, value FROM app_box WHERE app = $1 AND name = $2` + acctDataSQL := `SELECT account_data FROM account WHERE addr = $1` + + caseNum := 1 + var totalBoxes, totalBoxBytes int + for appIdx, boxes := range appBoxes { + totalBoxes = 0 + totalBoxBytes = 0 + + // compare expected against db contents one box at a time + for key, expectedValue := range boxes { + msg := fmt.Sprintf("caseNum=%d, appIdx=%d, key=%#v", caseNum, appIdx, key) + expectedAppIdx, boxName, err := logic.SplitBoxKey(key) + require.NoError(t, err, msg) + require.Equal(t, appIdx, expectedAppIdx, msg) + + row := db.db.QueryRow(context.Background(), appBoxSQL, appIdx, []byte(boxName)) + numQueries++ + + boxDeleted := false + if deletedBoxes != nil { + if _, ok := deletedBoxes[appIdx][key]; ok { + boxDeleted = true + } + } + + var app basics.AppIndex + var name, value []byte + err = row.Scan(&app, &name, &value) + if !boxDeleted { + require.NoError(t, err, msg) + require.Equal(t, expectedAppIdx, app, msg) + require.Equal(t, boxName, string(name), msg) + require.Equal(t, expectedValue, string(value), msg) + + totalBoxes++ + totalBoxBytes += len(boxName) + len(expectedValue) + } else { + require.ErrorContains(t, err, "no rows in result set", msg) + } + } + if verifyTotals { + addr := appIdx.Address() + msg := fmt.Sprintf("caseNum=%d, appIdx=%d", caseNum, appIdx) + + row := db.db.QueryRow(context.Background(), acctDataSQL, addr[:]) + + var buf []byte + err := row.Scan(&buf) + require.NoError(t, err, msg) + + ret, err := encoding.DecodeTrimmedLcAccountData(buf) + require.NoError(t, err, msg) + require.Equal(t, uint64(totalBoxes), ret.TotalBoxes, msg) + require.Equal(t, uint64(totalBoxBytes), ret.TotalBoxBytes, msg) + } + + sumOfBoxes += totalBoxes + sumOfBoxBytes += totalBoxBytes + caseNum++ + } + + fmt.Printf("compareAppBoxesAgainstDB succeeded with %d queries, %d boxes and %d boxBytes\n", numQueries, sumOfBoxes, sumOfBoxBytes) +} + +// test runner copy/pastad/tweaked in handlers_e2e_test.go and postgres_integration_test.go +func runBoxCreateMutateDelete(t *testing.T, comparator boxTestComparator) { + start := time.Now() + + db, shutdownFunc, proc, l := setupIdb(t, test.MakeGenesis()) + defer shutdownFunc() + + defer l.Close() + + appid := basics.AppIndex(1) + + // ---- ROUND 1: create and fund the box app ---- // + currentRound := basics.Round(1) + + createTxn, err := test.MakeComplexCreateAppTxn(test.AccountA, test.BoxApprovalProgram, test.BoxClearProgram, 8) + require.NoError(t, err) + + payNewAppTxn := test.MakePaymentTxn(1000, 500000, 0, 0, 0, 0, test.AccountA, appid.Address(), basics.Address{}, + basics.Address{}) + + block, err := test.MakeBlockForTxns(test.MakeGenesisBlock().BlockHeader, &createTxn, &payNewAppTxn) + require.NoError(t, err) + + err = proc.Process(&rpcs.EncodedBlockCert{Block: block}) + require.NoError(t, err) + + opts := idb.ApplicationQuery{ApplicationID: uint64(appid)} + + rowsCh, round := db.Applications(context.Background(), opts) + require.Equal(t, uint64(currentRound), round) + + row, ok := <-rowsCh + require.True(t, ok) + require.NoError(t, row.Error) + require.NotNil(t, row.Application.CreatedAtRound) + require.Equal(t, uint64(currentRound), *row.Application.CreatedAtRound) + + // block header handoff: round 1 --> round 2 + blockHdr, err := l.BlockHdr(currentRound) + require.NoError(t, err) + + // ---- ROUND 2: create 8 boxes for appid == 1 ---- // + currentRound = basics.Round(2) + + boxNames := []string{ + "a great box", + "another great box", + "not so great box", + "disappointing box", + "don't box me in this way", + "I will be assimilated", + "I'm destined for deletion", + "box #8", + } + + expectedAppBoxes := map[basics.AppIndex]map[string]string{} + + expectedAppBoxes[appid] = map[string]string{} + newBoxValue := "\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" + boxTxns := make([]*transactions.SignedTxnWithAD, 0) + for _, boxName := range boxNames { + expectedAppBoxes[appid][logic.MakeBoxKey(appid, boxName)] = newBoxValue + + args := []string{"create", boxName} + boxTxn := test.MakeAppCallTxnWithBoxes(uint64(appid), test.AccountA, args, []string{boxName}) + boxTxns = append(boxTxns, &boxTxn) + } + + block, err = test.MakeBlockForTxns(blockHdr, boxTxns...) + require.NoError(t, err) + + err = proc.Process(&rpcs.EncodedBlockCert{Block: block}) + require.NoError(t, err) + _, round = db.Applications(context.Background(), opts) + require.Equal(t, uint64(currentRound), round) + + comparator(t, db, expectedAppBoxes, nil, true) + + // block header handoff: round 2 --> round 3 + blockHdr, err = l.BlockHdr(currentRound) + require.NoError(t, err) + + // ---- ROUND 3: populate the boxes appropriately ---- // + currentRound = basics.Round(3) + + appBoxesToSet := map[string]string{ + "a great box": "it's a wonderful box", + "another great box": "I'm wonderful too", + "not so great box": "bummer", + "disappointing box": "RUG PULL!!!!", + "don't box me in this way": "non box-conforming", + "I will be assimilated": "THE BORG", + "I'm destined for deletion": "I'm still alive!!!", + "box #8": "eight is beautiful", + } + + boxTxns = make([]*transactions.SignedTxnWithAD, 0) + expectedAppBoxes[appid] = make(map[string]string) + for boxName, valPrefix := range appBoxesToSet { + args := []string{"set", boxName, valPrefix} + boxTxn := test.MakeAppCallTxnWithBoxes(uint64(appid), test.AccountA, args, []string{boxName}) + boxTxns = append(boxTxns, &boxTxn) + + key := logic.MakeBoxKey(appid, boxName) + expectedAppBoxes[appid][key] = valPrefix + newBoxValue[len(valPrefix):] + } + block, err = test.MakeBlockForTxns(blockHdr, boxTxns...) + require.NoError(t, err) + + err = proc.Process(&rpcs.EncodedBlockCert{Block: block}) + require.NoError(t, err) + _, round = db.Applications(context.Background(), opts) + require.Equal(t, uint64(currentRound), round) + + comparator(t, db, expectedAppBoxes, nil, true) + + // block header handoff: round 3 --> round 4 + blockHdr, err = l.BlockHdr(currentRound) + require.NoError(t, err) + + // ---- ROUND 4: delete the unhappy boxes ---- // + currentRound = basics.Round(4) + + appBoxesToDelete := []string{ + "not so great box", + "disappointing box", + "I'm destined for deletion", + } + + boxTxns = make([]*transactions.SignedTxnWithAD, 0) + for _, boxName := range appBoxesToDelete { + args := []string{"delete", boxName} + boxTxn := test.MakeAppCallTxnWithBoxes(uint64(appid), test.AccountA, args, []string{boxName}) + boxTxns = append(boxTxns, &boxTxn) + + key := logic.MakeBoxKey(appid, boxName) + delete(expectedAppBoxes[appid], key) + } + block, err = test.MakeBlockForTxns(blockHdr, boxTxns...) + require.NoError(t, err) + + err = proc.Process(&rpcs.EncodedBlockCert{Block: block}) + require.NoError(t, err) + _, round = db.Applications(context.Background(), opts) + require.Equal(t, uint64(currentRound), round) + + deletedBoxes := make(map[basics.AppIndex]map[string]bool) + deletedBoxes[appid] = make(map[string]bool) + for _, deletedBox := range appBoxesToDelete { + deletedBoxes[appid][deletedBox] = true + } + comparator(t, db, expectedAppBoxes, deletedBoxes, true) + + // block header handoff: round 4 --> round 5 + blockHdr, err = l.BlockHdr(currentRound) + require.NoError(t, err) + + // ---- ROUND 5: create 3 new boxes, overwriting one of the former boxes ---- // + currentRound = basics.Round(5) + + appBoxesToCreate := []string{ + "fantabulous", + "disappointing box", // overwriting here + "AVM is the new EVM", + } + boxTxns = make([]*transactions.SignedTxnWithAD, 0) + for _, boxName := range appBoxesToCreate { + args := []string{"create", boxName} + boxTxn := test.MakeAppCallTxnWithBoxes(uint64(appid), test.AccountA, args, []string{boxName}) + boxTxns = append(boxTxns, &boxTxn) + + key := logic.MakeBoxKey(appid, boxName) + expectedAppBoxes[appid][key] = newBoxValue + } + block, err = test.MakeBlockForTxns(blockHdr, boxTxns...) + require.NoError(t, err) + + err = proc.Process(&rpcs.EncodedBlockCert{Block: block}) + require.NoError(t, err) + _, round = db.Applications(context.Background(), opts) + require.Equal(t, uint64(currentRound), round) + + comparator(t, db, expectedAppBoxes, nil, true) + + // block header handoff: round 5 --> round 6 + blockHdr, err = l.BlockHdr(currentRound) + require.NoError(t, err) + + // ---- ROUND 6: populate the 3 new boxes ---- // + currentRound = basics.Round(6) + + appBoxesToSet = map[string]string{ + "fantabulous": "Italian food's the best!", // max char's + "disappointing box": "you made it!", + "AVM is the new EVM": "yes we can!", + } + boxTxns = make([]*transactions.SignedTxnWithAD, 0) + for boxName, valPrefix := range appBoxesToSet { + args := []string{"set", boxName, valPrefix} + boxTxn := test.MakeAppCallTxnWithBoxes(uint64(appid), test.AccountA, args, []string{boxName}) + boxTxns = append(boxTxns, &boxTxn) + + key := logic.MakeBoxKey(appid, boxName) + expectedAppBoxes[appid][key] = valPrefix + newBoxValue[len(valPrefix):] + } + block, err = test.MakeBlockForTxns(blockHdr, boxTxns...) + require.NoError(t, err) + + err = proc.Process(&rpcs.EncodedBlockCert{Block: block}) + require.NoError(t, err) + _, round = db.Applications(context.Background(), opts) + require.Equal(t, uint64(currentRound), round) + + comparator(t, db, expectedAppBoxes, nil, true) + + fmt.Printf("runBoxCreateMutateDelete total time: %s\n", time.Since(start)) +} + +// generateRandomBoxes generates a random slice of box keys and values for an app using future consensus params for guidance. +// NOTE: no attempt is made to adhere to the constraints BytesPerBoxReference etc. +func generateRandomBoxes(t *testing.T, appIdx basics.AppIndex, maxBoxes int) map[string]string { + future := config.Consensus[protocol.ConsensusFuture] + + numBoxes := rand.Intn(maxBoxes + 1) + boxes := make(map[string]string) + for i := 0; i < numBoxes; i++ { + nameLen := rand.Intn(future.MaxAppKeyLen + 1) + size := rand.Intn(int(future.MaxBoxSize) + 1) + + nameBytes := make([]byte, nameLen) + _, err := rand.Read(nameBytes) + require.NoError(t, err) + key := logic.MakeBoxKey(appIdx, string(nameBytes)) + + require.Positive(t, len(key)) + + valueBytes := make([]byte, size) + _, err = rand.Read(valueBytes) + require.NoError(t, err) + + boxes[key] = string(valueBytes) + } + return boxes +} + +func createRandomBoxesWithDelta(t *testing.T, numApps, maxBoxes int) (map[basics.AppIndex]map[string]string, ledgercore.StateDelta) { + appBoxes := make(map[basics.AppIndex]map[string]string) + + delta := ledgercore.StateDelta{ + KvMods: map[string]ledgercore.KvValueDelta{}, + Accts: ledgercore.MakeAccountDeltas(numApps), + } + + for i := 0; i < numApps; i++ { + appIndex := basics.AppIndex(rand.Int63()) + boxes := generateRandomBoxes(t, appIndex, maxBoxes) + appBoxes[appIndex] = boxes + + for key, value := range boxes { + embeddedAppIdx, _, err := logic.SplitBoxKey(key) + require.NoError(t, err) + require.Equal(t, appIndex, embeddedAppIdx) + + val := string([]byte(value)[:]) + delta.KvMods[key] = ledgercore.KvValueDelta{Data: []byte(val)} + } + + } + return appBoxes, delta +} + +func randomMutateSomeBoxesWithDelta(t *testing.T, appBoxes map[basics.AppIndex]map[string]string) ledgercore.StateDelta { + var delta ledgercore.StateDelta + delta.KvMods = make(map[string]ledgercore.KvValueDelta) + + for _, boxes := range appBoxes { + for key, value := range boxes { + if rand.Intn(2) == 0 { + continue + } + valueBytes := make([]byte, len(value)) + _, err := rand.Read(valueBytes) + require.NoError(t, err) + boxes[key] = string(valueBytes) + + val := string([]byte(boxes[key])[:]) + delta.KvMods[key] = ledgercore.KvValueDelta{Data: []byte(val)} + } + } + + return delta +} + +func deleteSomeBoxesWithDelta(t *testing.T, appBoxes map[basics.AppIndex]map[string]string) (map[basics.AppIndex]map[string]bool, ledgercore.StateDelta) { + deletedBoxes := make(map[basics.AppIndex]map[string]bool, len(appBoxes)) + + var delta ledgercore.StateDelta + delta.KvMods = make(map[string]ledgercore.KvValueDelta) + + for appIndex, boxes := range appBoxes { + deletedBoxes[appIndex] = map[string]bool{} + for key := range boxes { + if rand.Intn(2) == 0 { + continue + } + deletedBoxes[appIndex][key] = true + delta.KvMods[key] = ledgercore.KvValueDelta{Data: nil} + } + } + + return deletedBoxes, delta +} + +func addAppBoxesBlock(t *testing.T, db *IndexerDb, delta ledgercore.StateDelta) { + f := func(tx pgx.Tx) error { + w, err := writer.MakeWriter(tx) + require.NoError(t, err) + + err = w.AddBlock(&bookkeeping.Block{}, transactions.Payset{}, delta) + require.NoError(t, err) + + w.Close() + return nil + } + err := db.txWithRetry(serializable, f) + require.NoError(t, err) +} + +// Integration test for validating that box evolution is ingested as expected across rounds using database to compare +func TestBoxCreateMutateDeleteAgainstDB(t *testing.T) { + runBoxCreateMutateDelete(t, compareAppBoxesAgainstDB) +} + +// Write random apps with random box names and values, then read them from indexer DB and compare. +// NOTE: this does not populate TotalBoxes nor TotalBoxBytes deep under StateDeltas.Accts and therefore +// no query is taken to compare the summary box information in `account.account_data` +// Mutate some boxes and repeat the comparison. +// Delete some boxes and repeat the comparison. +func TestRandomWriteReadBoxes(t *testing.T) { + start := time.Now() + + db, shutdownFunc, _, ld := setupIdb(t, test.MakeGenesis()) + defer shutdownFunc() + defer ld.Close() + + appBoxes, delta := createRandomBoxesWithDelta(t, 10, 2500) + addAppBoxesBlock(t, db, delta) + compareAppBoxesAgainstDB(t, db, appBoxes, nil, false) + + delta = randomMutateSomeBoxesWithDelta(t, appBoxes) + addAppBoxesBlock(t, db, delta) + compareAppBoxesAgainstDB(t, db, appBoxes, nil, false) + + deletedBoxes, delta := deleteSomeBoxesWithDelta(t, appBoxes) + addAppBoxesBlock(t, db, delta) + compareAppBoxesAgainstDB(t, db, appBoxes, deletedBoxes, false) + + fmt.Printf("TestRandomWriteReadBoxes total time: %s\n", time.Since(start)) +} diff --git a/idb/postgres/postgres_migrations.go b/idb/postgres/postgres_migrations.go index 0aa92bdbf..44e798ade 100644 --- a/idb/postgres/postgres_migrations.go +++ b/idb/postgres/postgres_migrations.go @@ -50,6 +50,9 @@ func init() { {upgradeNotSupported, true, "notify the user that upgrade is not supported"}, {dropTxnBytesColumn, true, "drop txnbytes column"}, {convertAccountData, true, "convert account.account_data column"}, + + // Migration for app box support + {createAppBoxTable, true, "add new table app_box for application boxes"}, } } @@ -249,3 +252,13 @@ func convertAccountData(db *IndexerDb, migrationState *types.MigrationState, opt *migrationState = newMigrationState return nil } + +func createAppBoxTable(db *IndexerDb, migrationState *types.MigrationState, opts *idb.IndexerDbOptions) error { + return sqlMigration( + db, migrationState, []string{`CREATE TABLE IF NOT EXISTS app_box ( + app bigint NOT NULL, + name bytea NOT NULL, + value bytea NOT NULL, -- upon creation 'value' is 0x000...000 with length being the box'es size + PRIMARY KEY (app, name) + )`}) +} diff --git a/idb/postgres/postgres_migrations_test.go b/idb/postgres/postgres_migrations_test.go index d6369729f..ebe9e5b19 100644 --- a/idb/postgres/postgres_migrations_test.go +++ b/idb/postgres/postgres_migrations_test.go @@ -7,6 +7,8 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + "github.com/algorand/go-algorand/data/basics" + pgtest "github.com/algorand/indexer/idb/postgres/internal/testing" "github.com/algorand/indexer/idb/postgres/internal/types" ) @@ -32,3 +34,34 @@ func TestConvertAccountDataIncrementsMigrationNumber(t *testing.T) { assert.Equal(t, types.MigrationState{NextMigration: 6}, migrationState) } + +func TestCreateAppBoxTable(t *testing.T) { + pdb, _, shutdownFunc := pgtest.SetupPostgresWithSchema(t) + defer shutdownFunc() + + db := IndexerDb{db: pdb} + defer db.Close() + + migrationState := types.MigrationState{ + NextMigration: 19, + } + err := db.setMigrationState(nil, &migrationState) + require.NoError(t, err) + + err = createAppBoxTable(&db, &migrationState, nil) + require.NoError(t, err) + + migrationState, err = db.getMigrationState(context.Background(), nil) + require.NoError(t, err) + + appBoxSQL := `SELECT app, name, value FROM app_box WHERE app = $1 AND name = $2` + appIdx := basics.AppIndex(42) + boxName := "I do not exist" + var app basics.AppIndex + var name, value []byte + row := db.db.QueryRow(context.Background(), appBoxSQL, appIdx, []byte(boxName)) + err = row.Scan(&app, &name, &value) + require.ErrorContains(t, err, "no rows in result set") + + assert.Equal(t, types.MigrationState{NextMigration: 20}, migrationState) +} diff --git a/idb/sig_type.go b/idb/sig_type.go index 64f9390fe..a6978fa7e 100644 --- a/idb/sig_type.go +++ b/idb/sig_type.go @@ -59,5 +59,5 @@ func SignatureType(stxn *transactions.SignedTxn) (SigType, error) { return Lsig, nil } - return "", fmt.Errorf("unable to determine the signature type") + return "", fmt.Errorf("unable to determine the signature type for stxn=%+v", stxn) } diff --git a/misc/parity/reports/algod2indexer_full.yml b/misc/parity/reports/algod2indexer_full.yml index 2f6721ad4..c1458b71c 100644 --- a/misc/parity/reports/algod2indexer_full.yml +++ b/misc/parity/reports/algod2indexer_full.yml @@ -17,6 +17,16 @@ definitions: description: - INDEXER: '"Indicates what type of signature is used by this account, must be one of:\n* sig\n* msig\n* lsig\n* or null if unknown"' - ALGOD: '"Indicates what type of signature is used by this account, must be one of:\n* sig\n* msig\n* lsig"' + total-box-bytes: + - INDEXER: '{"description":"For app-accounts only. The total n...' + - ALGOD: null + total-boxes: + - INDEXER: '{"description":"For app-accounts only. The total n...' + - ALGOD: null required: + - - INDEXER: '"total-boxes"' + - ALGOD: null + - - INDEXER: '"total-box-bytes"' + - ALGOD: null - - INDEXER: null - ALGOD: '"min-balance"' diff --git a/misc/parity/test_indexer_v_algod.py b/misc/parity/test_indexer_v_algod.py index e5e6b87a1..28dc4f6d0 100644 --- a/misc/parity/test_indexer_v_algod.py +++ b/misc/parity/test_indexer_v_algod.py @@ -162,7 +162,7 @@ def test_parity(reports: List[str] = ASSERTIONS, save_new: bool = True): old_diff = yaml.safe_load(f) new_diff = generate_diff(algod_swgr, indexer_swgr, excludes, diff_type) - diff_of_diffs = deep_diff(old_diff, new_diff) + diff_of_diffs = deep_diff(old_diff, new_diff, arraysets=True) assert ( diff_of_diffs is None ), f"""UNEXPECTED CHANGE IN {ypath}. Differences are: diff --git a/processor/eval/ledger_for_evaluator.go b/processor/eval/ledger_for_evaluator.go index 0a89d9e29..e800f1603 100644 --- a/processor/eval/ledger_for_evaluator.go +++ b/processor/eval/ledger_for_evaluator.go @@ -1,7 +1,6 @@ // Package eval implements the 'ledger.indexerLedgerForEval' interface for // generating StateDelta's and updating ApplyData with a custom protocol. // -// // TODO: Expose private functions in go-algorand to allow code reuse. // This interface is designed to allow sourcing initial state data from // postgres. Since we are not sourcing initial states from the ledger there @@ -143,6 +142,12 @@ func (l LedgerForEvaluator) GetAppCreator(indices map[basics.AppIndex]struct{}) return res, nil } +// LookupKv is part of go-algorand's indexerLedgerForEval interface. +func (l LedgerForEvaluator) LookupKv(round basics.Round, key string) ([]byte, error) { + // a simple pass-thru to the go-algorand ledger + return l.Ledger.LookupKv(round, key) +} + // LatestTotals is part of go-algorand's indexerLedgerForEval interface. func (l LedgerForEvaluator) LatestTotals() (ledgercore.AccountTotals, error) { _, totals, err := l.Ledger.LatestTotals() diff --git a/processor/eval/ledger_for_evaluator_test.go b/processor/eval/ledger_for_evaluator_test.go index 0a50f2446..106d373f9 100644 --- a/processor/eval/ledger_for_evaluator_test.go +++ b/processor/eval/ledger_for_evaluator_test.go @@ -2,6 +2,7 @@ package eval_test import ( "crypto/rand" + "fmt" "testing" test2 "github.com/sirupsen/logrus/hooks/test" @@ -10,12 +11,14 @@ import ( "github.com/algorand/go-algorand/agreement" "github.com/algorand/go-algorand/data/basics" + "github.com/algorand/go-algorand/data/transactions" + "github.com/algorand/go-algorand/data/transactions/logic" "github.com/algorand/go-algorand/ledger" "github.com/algorand/go-algorand/ledger/ledgercore" "github.com/algorand/go-algorand/rpcs" block_processor "github.com/algorand/indexer/processor/blockprocessor" - indxLeder "github.com/algorand/indexer/processor/eval" + indxLedger "github.com/algorand/indexer/processor/eval" "github.com/algorand/indexer/util/test" ) @@ -39,8 +42,7 @@ func TestLedgerForEvaluatorLatestBlockHdr(t *testing.T) { err = pr.Process(&rawBlock) assert.Nil(t, err) - ld := indxLeder.MakeLedgerForEvaluator(l) - require.NoError(t, err) + ld := indxLedger.MakeLedgerForEvaluator(l) defer ld.Close() ret, err := ld.LatestBlockHdr() @@ -57,8 +59,7 @@ func TestLedgerForEvaluatorAccountDataBasic(t *testing.T) { accountData, _, err := l.LookupWithoutRewards(0, test.AccountB) require.NoError(t, err) - ld := indxLeder.MakeLedgerForEvaluator(l) - require.NoError(t, err) + ld := indxLedger.MakeLedgerForEvaluator(l) defer ld.Close() ret, err := @@ -72,12 +73,12 @@ func TestLedgerForEvaluatorAccountDataBasic(t *testing.T) { func TestLedgerForEvaluatorAccountDataMissingAccount(t *testing.T) { l := makeTestLedger(t) - ld := indxLeder.MakeLedgerForEvaluator(l) + ld := indxLedger.MakeLedgerForEvaluator(l) defer l.Close() defer ld.Close() var addr basics.Address - _, err := rand.Read(addr[:]) + rand.Read(addr[:]) ret, err := ld.LookupWithoutRewards(map[basics.Address]struct{}{addr: {}}) require.NoError(t, err) @@ -104,8 +105,7 @@ func TestLedgerForEvaluatorAsset(t *testing.T) { err = pr.Process(&rawBlock) assert.Nil(t, err) - ld := indxLeder.MakeLedgerForEvaluator(l) - require.NoError(t, err) + ld := indxLedger.MakeLedgerForEvaluator(l) defer ld.Close() ret, err := @@ -158,12 +158,12 @@ func TestLedgerForEvaluatorApp(t *testing.T) { logger, _ := test2.NewNullLogger() pr, _ := block_processor.MakeProcessorWithLedger(logger, l, nil) - txn0 := test.MakeAppCallTxn(0, test.AccountA) + txn0 := test.MakeSimpleAppCallTxn(0, test.AccountA) txn1 := test.MakeAppCallTxnWithLogs(0, test.AccountA, []string{"testing"}) txn2 := test.MakeAppCallWithInnerTxn(test.AccountA, test.AccountA, test.AccountB, basics.Address{}, basics.Address{}) txn3 := test.MakeAppCallWithMultiLogs(test.AccountA) txn4 := test.MakeAppDestroyTxn(1, test.AccountA) - txn5 := test.MakeAppCallTxn(0, test.AccountB) + txn5 := test.MakeSimpleAppCallTxn(0, test.AccountB) block, err := test.MakeBlockForTxns(test.MakeGenesisBlock().BlockHeader, &txn0, &txn1, &txn2, &txn3, &txn4, &txn5) assert.Nil(t, err) @@ -171,8 +171,7 @@ func TestLedgerForEvaluatorApp(t *testing.T) { err = pr.Process(&rawBlock) assert.Nil(t, err) - ld := indxLeder.MakeLedgerForEvaluator(l) - require.NoError(t, err) + ld := indxLedger.MakeLedgerForEvaluator(l) defer ld.Close() ret, err := @@ -241,7 +240,7 @@ func TestLedgerForEvaluatorFetchAllResourceTypes(t *testing.T) { logger, _ := test2.NewNullLogger() pr, _ := block_processor.MakeProcessorWithLedger(logger, l, nil) - txn0 := test.MakeAppCallTxn(0, test.AccountA) + txn0 := test.MakeSimpleAppCallTxn(0, test.AccountA) txn1 := test.MakeAssetConfigTxn(0, 2, 0, false, "", "", "", test.AccountA) block, err := test.MakeBlockForTxns(test.MakeGenesisBlock().BlockHeader, &txn0, &txn1) @@ -250,8 +249,7 @@ func TestLedgerForEvaluatorFetchAllResourceTypes(t *testing.T) { err = pr.Process(&rawBlock) assert.Nil(t, err) - ld := indxLeder.MakeLedgerForEvaluator(l) - require.NoError(t, err) + ld := indxLedger.MakeLedgerForEvaluator(l) defer ld.Close() ret, err := @@ -302,7 +300,7 @@ func TestLedgerForEvaluatorLookupMultipleAccounts(t *testing.T) { addressesMap[test.FeeAddr] = struct{}{} addressesMap[test.RewardAddr] = struct{}{} - ld := indxLeder.MakeLedgerForEvaluator(l) + ld := indxLedger.MakeLedgerForEvaluator(l) defer ld.Close() ret, err := @@ -310,7 +308,7 @@ func TestLedgerForEvaluatorLookupMultipleAccounts(t *testing.T) { require.NoError(t, err) for _, address := range addresses { - accountData, _ := ret[address] + accountData := ret[address] require.NotNil(t, accountData) } } @@ -329,8 +327,7 @@ func TestLedgerForEvaluatorAssetCreatorBasic(t *testing.T) { err = pr.Process(&rawBlock) assert.Nil(t, err) - ld := indxLeder.MakeLedgerForEvaluator(l) - require.NoError(t, err) + ld := indxLedger.MakeLedgerForEvaluator(l) defer ld.Close() ret, err := ld.GetAssetCreator( @@ -362,8 +359,7 @@ func TestLedgerForEvaluatorAssetCreatorDeleted(t *testing.T) { err = pr.Process(&rawBlock) assert.Nil(t, err) - ld := indxLeder.MakeLedgerForEvaluator(l) - require.NoError(t, err) + ld := indxLedger.MakeLedgerForEvaluator(l) defer ld.Close() ret, err := ld.GetAssetCreator( @@ -394,8 +390,7 @@ func TestLedgerForEvaluatorAssetCreatorMultiple(t *testing.T) { err = pr.Process(&rawBlock) assert.Nil(t, err) - ld := indxLeder.MakeLedgerForEvaluator(l) - require.NoError(t, err) + ld := indxLedger.MakeLedgerForEvaluator(l) defer ld.Close() indices := map[basics.AssetIndex]struct{}{ @@ -438,7 +433,7 @@ func TestLedgerForEvaluatorAppCreatorBasic(t *testing.T) { logger, _ := test2.NewNullLogger() pr, _ := block_processor.MakeProcessorWithLedger(logger, l, nil) - txn0 := test.MakeAppCallTxn(0, test.AccountA) + txn0 := test.MakeSimpleAppCallTxn(0, test.AccountA) block, err := test.MakeBlockForTxns(test.MakeGenesisBlock().BlockHeader, &txn0) assert.Nil(t, err) @@ -446,7 +441,7 @@ func TestLedgerForEvaluatorAppCreatorBasic(t *testing.T) { err = pr.Process(&rawBlock) assert.Nil(t, err) - ld := indxLeder.MakeLedgerForEvaluator(l) + ld := indxLedger.MakeLedgerForEvaluator(l) require.NoError(t, err) defer ld.Close() @@ -471,7 +466,7 @@ func TestLedgerForEvaluatorAppCreatorDeleted(t *testing.T) { logger, _ := test2.NewNullLogger() pr, _ := block_processor.MakeProcessorWithLedger(logger, l, nil) - txn0 := test.MakeAppCallTxn(0, test.AccountA) + txn0 := test.MakeSimpleAppCallTxn(0, test.AccountA) txn1 := test.MakeAppDestroyTxn(1, test.AccountA) block, err := test.MakeBlockForTxns(test.MakeGenesisBlock().BlockHeader, &txn0, &txn1) @@ -480,8 +475,7 @@ func TestLedgerForEvaluatorAppCreatorDeleted(t *testing.T) { err = pr.Process(&rawBlock) assert.Nil(t, err) - ld := indxLeder.MakeLedgerForEvaluator(l) - require.NoError(t, err) + ld := indxLedger.MakeLedgerForEvaluator(l) defer ld.Close() ret, err := ld.GetAppCreator( @@ -501,10 +495,10 @@ func TestLedgerForEvaluatorAppCreatorMultiple(t *testing.T) { logger, _ := test2.NewNullLogger() pr, _ := block_processor.MakeProcessorWithLedger(logger, l, nil) - txn0 := test.MakeAppCallTxn(0, test.AccountA) - txn1 := test.MakeAppCallTxn(0, test.AccountB) - txn2 := test.MakeAppCallTxn(0, test.AccountC) - txn3 := test.MakeAppCallTxn(0, test.AccountD) + txn0 := test.MakeSimpleAppCallTxn(0, test.AccountA) + txn1 := test.MakeSimpleAppCallTxn(0, test.AccountB) + txn2 := test.MakeSimpleAppCallTxn(0, test.AccountC) + txn3 := test.MakeSimpleAppCallTxn(0, test.AccountD) block, err := test.MakeBlockForTxns(test.MakeGenesisBlock().BlockHeader, &txn0, &txn1, &txn2, &txn3) assert.Nil(t, err) @@ -512,8 +506,7 @@ func TestLedgerForEvaluatorAppCreatorMultiple(t *testing.T) { err = pr.Process(&rawBlock) assert.Nil(t, err) - ld := indxLeder.MakeLedgerForEvaluator(l) - require.NoError(t, err) + ld := indxLedger.MakeLedgerForEvaluator(l) defer ld.Close() creatorsMap := map[basics.AppIndex]basics.Address{ @@ -551,11 +544,255 @@ func TestLedgerForEvaluatorAppCreatorMultiple(t *testing.T) { } } +// compareAppBoxesAgainstLedger uses LedgerForEvaluator to assert that provided app boxes can be retrieved as expected +func compareAppBoxesAgainstLedger(t *testing.T, ld indxLedger.LedgerForEvaluator, round basics.Round, + appBoxes map[basics.AppIndex]map[string]string, extras ...map[basics.AppIndex]map[string]bool) { + require.LessOrEqual(t, len(extras), 1) + var deletedBoxes map[basics.AppIndex]map[string]bool + if len(extras) == 1 { + deletedBoxes = extras[0] + } + + caseNum := 1 + for appIdx, boxes := range appBoxes { + for key, expectedValue := range boxes { + msg := fmt.Sprintf("caseNum=%d, appIdx=%d, key=%#v", caseNum, appIdx, key) + expectedAppIdx, _, err := logic.SplitBoxKey(key) + require.NoError(t, err, msg) + require.Equal(t, appIdx, expectedAppIdx, msg) + + boxDeleted := false + if deletedBoxes != nil { + if _, ok := deletedBoxes[appIdx][key]; ok { + boxDeleted = true + } + } + + value, err := ld.LookupKv(round, key) + require.NoError(t, err, msg) + if !boxDeleted { + require.Equal(t, []byte(expectedValue), value, msg) + } else { + require.Nil(t, value, msg) + } + } + caseNum++ + } +} + +// Test the functionality of `func (l LedgerForEvaluator) LookupKv()`. +// This is done by handing off a pointer to Struct `processor/eval/ledger_for_evaluator.go::LedgerForEvaluator` +// to `compareAppBoxesAgainstLedger()` which then asserts using `LookupKv()` +func TestLedgerForEvaluatorLookupKv(t *testing.T) { + logger, _ := test2.NewNullLogger() + l := makeTestLedger(t) + pr, _ := block_processor.MakeProcessorWithLedger(logger, l, nil) + ld := indxLedger.MakeLedgerForEvaluator(l) + defer l.Close() + defer ld.Close() + + // ---- ROUND 1: create and fund the box app ---- // + + appid := basics.AppIndex(1) + currentRound := basics.Round(1) + + createTxn, err := test.MakeComplexCreateAppTxn(test.AccountA, test.BoxApprovalProgram, test.BoxClearProgram, 8) + require.NoError(t, err) + + payNewAppTxn := test.MakePaymentTxn(1000, 500000, 0, 0, 0, 0, test.AccountA, appid.Address(), basics.Address{}, + basics.Address{}) + + block, err := test.MakeBlockForTxns(test.MakeGenesisBlock().BlockHeader, &createTxn, &payNewAppTxn) + require.NoError(t, err) + + rawBlock := rpcs.EncodedBlockCert{Block: block, Certificate: agreement.Certificate{}} + err = pr.Process(&rawBlock) + require.NoError(t, err) + + ret, err := ld.LookupKv(currentRound, "sanity check") + require.NoError(t, err) + require.Nil(t, ret) // nothing found isn't considered an error + + // block header handoff: round 1 --> round 2 + blockHdr, err := l.BlockHdr(currentRound) + require.NoError(t, err) + + // ---- ROUND 2: create 8 boxes of appid == 1 ---- // + currentRound = basics.Round(2) + + boxNames := []string{ + "a great box", + "another great box", + "not so great box", + "disappointing box", + "don't box me in this way", + "I will be assimilated", + "I'm destined for deletion", + "box #8", + } + + expectedAppBoxes := map[basics.AppIndex]map[string]string{} + expectedAppBoxes[appid] = map[string]string{} + newBoxValue := "\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" + boxTxns := make([]*transactions.SignedTxnWithAD, 0) + for _, boxName := range boxNames { + expectedAppBoxes[appid][logic.MakeBoxKey(appid, boxName)] = newBoxValue + + args := []string{"create", boxName} + boxTxn := test.MakeAppCallTxnWithBoxes(uint64(appid), test.AccountA, args, []string{boxName}) + boxTxns = append(boxTxns, &boxTxn) + + } + + block, err = test.MakeBlockForTxns(blockHdr, boxTxns...) + require.NoError(t, err) + + rawBlock = rpcs.EncodedBlockCert{Block: block, Certificate: agreement.Certificate{}} + err = pr.Process(&rawBlock) + require.NoError(t, err) + + compareAppBoxesAgainstLedger(t, ld, currentRound, expectedAppBoxes) + + // block header handoff: round 2 --> round 3 + blockHdr, err = l.BlockHdr(currentRound) + require.NoError(t, err) + + // ---- ROUND 3: populate the boxes appropriately ---- // + currentRound = basics.Round(3) + + appBoxesToSet := map[string]string{ + "a great box": "it's a wonderful box", + "another great box": "I'm wonderful too", + "not so great box": "bummer", + "disappointing box": "RUG PULL!!!!", + "don't box me in this way": "non box-conforming", + "I will be assimilated": "THE BORG", + "I'm destined for deletion": "I'm still alive!!!", + "box #8": "eight is beautiful", + } + + boxTxns = make([]*transactions.SignedTxnWithAD, 0) + expectedAppBoxes[appid] = make(map[string]string) + for boxName, valPrefix := range appBoxesToSet { + args := []string{"set", boxName, valPrefix} + boxTxn := test.MakeAppCallTxnWithBoxes(uint64(appid), test.AccountA, args, []string{boxName}) + boxTxns = append(boxTxns, &boxTxn) + + key := logic.MakeBoxKey(appid, boxName) + expectedAppBoxes[appid][key] = valPrefix + newBoxValue[len(valPrefix):] + } + block, err = test.MakeBlockForTxns(blockHdr, boxTxns...) + require.NoError(t, err) + + rawBlock = rpcs.EncodedBlockCert{Block: block, Certificate: agreement.Certificate{}} + err = pr.Process(&rawBlock) + require.NoError(t, err) + + compareAppBoxesAgainstLedger(t, ld, currentRound, expectedAppBoxes) + + // block header handoff: round 3 --> round 4 + blockHdr, err = l.BlockHdr(currentRound) + require.NoError(t, err) + + // ---- ROUND 4: delete the unhappy boxes ---- // + currentRound = basics.Round(4) + + appBoxesToDelete := []string{ + "not so great box", + "disappointing box", + "I'm destined for deletion", + } + + boxTxns = make([]*transactions.SignedTxnWithAD, 0) + for _, boxName := range appBoxesToDelete { + args := []string{"delete", boxName} + boxTxn := test.MakeAppCallTxnWithBoxes(uint64(appid), test.AccountA, args, []string{boxName}) + boxTxns = append(boxTxns, &boxTxn) + + key := logic.MakeBoxKey(appid, boxName) + delete(expectedAppBoxes[appid], key) + } + block, err = test.MakeBlockForTxns(blockHdr, boxTxns...) + require.NoError(t, err) + + rawBlock = rpcs.EncodedBlockCert{Block: block, Certificate: agreement.Certificate{}} + err = pr.Process(&rawBlock) + require.NoError(t, err) + + deletedBoxes := make(map[basics.AppIndex]map[string]bool) + deletedBoxes[appid] = make(map[string]bool) + for _, deletedBox := range appBoxesToDelete { + deletedBoxes[appid][deletedBox] = true + } + compareAppBoxesAgainstLedger(t, ld, currentRound, expectedAppBoxes, deletedBoxes) + + // block header handoff: round 4 --> round 5 + blockHdr, err = l.BlockHdr(currentRound) + require.NoError(t, err) + + // ---- ROUND 5: create 3 new boxes, overwriting one of the former boxes ---- // + currentRound = basics.Round(5) + + appBoxesToCreate := []string{ + "fantabulous", + "disappointing box", // overwriting here + "AVM is the new EVM", + } + boxTxns = make([]*transactions.SignedTxnWithAD, 0) + for _, boxName := range appBoxesToCreate { + args := []string{"create", boxName} + boxTxn := test.MakeAppCallTxnWithBoxes(uint64(appid), test.AccountA, args, []string{boxName}) + boxTxns = append(boxTxns, &boxTxn) + + key := logic.MakeBoxKey(appid, boxName) + expectedAppBoxes[appid] = make(map[string]string) + expectedAppBoxes[appid][key] = newBoxValue + } + block, err = test.MakeBlockForTxns(blockHdr, boxTxns...) + require.NoError(t, err) + + rawBlock = rpcs.EncodedBlockCert{Block: block, Certificate: agreement.Certificate{}} + err = pr.Process(&rawBlock) + require.NoError(t, err) + + compareAppBoxesAgainstLedger(t, ld, currentRound, expectedAppBoxes) + + // block header handoff: round 5 --> round 6 + blockHdr, err = l.BlockHdr(currentRound) + require.NoError(t, err) + + // ---- ROUND 6: populate the 3 new boxes ---- // + currentRound = basics.Round(6) + + appBoxesToSet = map[string]string{ + "fantabulous": "Italian food's the best!", // max char's + "disappointing box": "you made it!", + "AVM is the new EVM": "yes we can!", + } + boxTxns = make([]*transactions.SignedTxnWithAD, 0) + for boxName, valPrefix := range appBoxesToSet { + args := []string{"set", boxName, valPrefix} + boxTxn := test.MakeAppCallTxnWithBoxes(uint64(appid), test.AccountA, args, []string{boxName}) + boxTxns = append(boxTxns, &boxTxn) + + key := logic.MakeBoxKey(appid, boxName) + expectedAppBoxes[appid][key] = valPrefix + newBoxValue[len(valPrefix):] + } + block, err = test.MakeBlockForTxns(blockHdr, boxTxns...) + require.NoError(t, err) + + rawBlock = rpcs.EncodedBlockCert{Block: block, Certificate: agreement.Certificate{}} + err = pr.Process(&rawBlock) + require.NoError(t, err) + + compareAppBoxesAgainstLedger(t, ld, currentRound, expectedAppBoxes, deletedBoxes) +} + func TestLedgerForEvaluatorAccountTotals(t *testing.T) { l := makeTestLedger(t) defer l.Close() - ld := indxLeder.MakeLedgerForEvaluator(l) + ld := indxLedger.MakeLedgerForEvaluator(l) defer ld.Close() accountTotalsRead, err := ld.LatestTotals() diff --git a/third_party/go-algorand b/third_party/go-algorand index afd8b05b9..15efc02d8 160000 --- a/third_party/go-algorand +++ b/third_party/go-algorand @@ -1 +1 @@ -Subproject commit afd8b05b9a8fa9b49a37159d975fc2367da72fcd +Subproject commit 15efc02d8dde1fe223cd6daf353a595d4d4c91c5 diff --git a/util/test/account_testutil.go b/util/test/account_testutil.go index 563c151fb..2b101847f 100644 --- a/util/test/account_testutil.go +++ b/util/test/account_testutil.go @@ -9,6 +9,7 @@ import ( "github.com/algorand/go-algorand/data/basics" "github.com/algorand/go-algorand/data/bookkeeping" "github.com/algorand/go-algorand/data/transactions" + "github.com/algorand/go-algorand/data/transactions/logic" "github.com/algorand/go-algorand/protocol" ) @@ -212,6 +213,36 @@ func MakeCreateAppTxn(sender basics.Address) transactions.SignedTxnWithAD { } } +// MakeComplexCreateAppTxn makes a transaction that creates an arbitrary app. When assemblerVersion is set to 0, use the AssemblerDefaultVersion. +func MakeComplexCreateAppTxn(sender basics.Address, approval, clear string, assemblerVersion uint64) (transactions.SignedTxnWithAD, error) { + // Create a transaction with ExtraProgramPages field set to 1 + approvalOps, err := logic.AssembleStringWithVersion(approval, assemblerVersion) + if err != nil { + return transactions.SignedTxnWithAD{}, err + } + clearOps, err := logic.AssembleString(clear) + if err != nil { + return transactions.SignedTxnWithAD{}, err + } + + return transactions.SignedTxnWithAD{ + SignedTxn: transactions.SignedTxn{ + Txn: transactions.Transaction{ + Type: "appl", + Header: transactions.Header{ + Sender: sender, + GenesisHash: GenesisHash, + }, + ApplicationCallTxnFields: transactions.ApplicationCallTxnFields{ + ApprovalProgram: approvalOps.Program, + ClearStateProgram: clearOps.Program, + }, + }, + Sig: Signature, + }, + }, nil +} + // MakeAppDestroyTxn makes a transaction that destroys an app. func MakeAppDestroyTxn(appid uint64, sender basics.Address) transactions.SignedTxnWithAD { return transactions.SignedTxnWithAD{ @@ -277,8 +308,8 @@ func MakeAppOptOutTxn(appid uint64, sender basics.Address) transactions.SignedTx } } -// MakeAppCallTxn makes an appl transaction with a NoOp upon completion. -func MakeAppCallTxn(appid uint64, sender basics.Address) transactions.SignedTxnWithAD { +// MakeSimpleAppCallTxn makes an appl transaction with a NoOp upon completion. +func MakeSimpleAppCallTxn(appid uint64, sender basics.Address) transactions.SignedTxnWithAD { return transactions.SignedTxnWithAD{ SignedTxn: transactions.SignedTxn{ Txn: transactions.Transaction{ @@ -300,9 +331,40 @@ func MakeAppCallTxn(appid uint64, sender basics.Address) transactions.SignedTxnW } } +// MakeAppCallTxnWithBoxes makes an appl transaction with a NoOp upon completion. +func MakeAppCallTxnWithBoxes(appid uint64, sender basics.Address, appArgs []string, boxNames []string) transactions.SignedTxnWithAD { + appArgBytes := [][]byte{} + for _, appArg := range appArgs { + appArgBytes = append(appArgBytes, []byte(appArg)) + } + appBoxes := []transactions.BoxRef{} + for _, boxName := range boxNames { + // hard-coding box reference to current app + appBoxes = append(appBoxes, transactions.BoxRef{Index: uint64(0), Name: []byte(boxName)}) + } + return transactions.SignedTxnWithAD{ + SignedTxn: transactions.SignedTxn{ + Txn: transactions.Transaction{ + Type: "appl", + Header: transactions.Header{ + Sender: sender, + GenesisHash: GenesisHash, + }, + ApplicationCallTxnFields: transactions.ApplicationCallTxnFields{ + ApplicationID: basics.AppIndex(appid), + OnCompletion: transactions.NoOpOC, + ApplicationArgs: appArgBytes, + Boxes: appBoxes, + }, + }, + Sig: Signature, + }, + } +} + // MakeAppCallTxnWithLogs makes an appl NoOp transaction with initialized logs. func MakeAppCallTxnWithLogs(appid uint64, sender basics.Address, logs []string) (txn transactions.SignedTxnWithAD) { - txn = MakeAppCallTxn(appid, sender) + txn = MakeSimpleAppCallTxn(appid, sender) txn.ApplyData.EvalDelta.Logs = logs return } @@ -373,7 +435,7 @@ func MakeAppCallWithInnerTxn(appSender, paymentSender, paymentReceiver, assetSen }, }, // Inner application call - MakeAppCallTxn(789, assetSender), + MakeSimpleAppCallTxn(789, assetSender), }, }, }, @@ -421,7 +483,7 @@ func MakeAppCallWithMultiLogs(appSender basics.Address) transactions.SignedTxnWi EvalDelta: transactions.EvalDelta{ InnerTxns: []transactions.SignedTxnWithAD{ // Inner application call - MakeAppCallTxn(789, appSender), + MakeSimpleAppCallTxn(789, appSender), }, Logs: []string{ "testing inner log", diff --git a/util/test/boxes.teal b/util/test/boxes.teal new file mode 100644 index 000000000..7939fc151 --- /dev/null +++ b/util/test/boxes.teal @@ -0,0 +1,60 @@ +#pragma version 8 + txn ApplicationID + bz end // go create the app + txn NumAppArgs + bz end // approve when no args (so can delete with no questions asked) + txn ApplicationArgs 0 // [arg[0]] // fails if no args && app already exists + byte "create" // [arg[0], "create"] // create box named arg[1] + == // [arg[0]=?="create"] + bz del // "create" ? continue : goto del + int 32 // [32] + txn NumAppArgs // [32, NumAppArgs] + int 2 // [32, NumAppArgs, 2] + == // [32, NumAppArgs=?=2] + bnz default // WARNING: Assumes that when "create" provided, NumAppArgs >= 3 + pop // get rid of 32 // NumAppArgs != 2 + txn ApplicationArgs 2 // [arg[2]] // ERROR when NumAppArgs == 1 + btoi // [btoi(arg[2])] +default: // [32] // NumAppArgs >= 3 + txn ApplicationArgs 1 // [32, arg[1]] + swap // [arg[1], 32] + box_create // [] // boxes: arg[1] -> [32]byte + assert + b end +del: // delete box arg[1] + txn ApplicationArgs 0 // [arg[0]] + byte "delete" // [arg[0], "delete"] + == // [arg[0]=?="delete"] + bz set // "delete" ? continue : goto set + txn ApplicationArgs 1 // [arg[1]] + box_del // del boxes[arg[1]] + assert + b end +set: // put arg[1] at start of box arg[0] ... so actually a _partial_ "set" + txn ApplicationArgs 0 // [arg[0]] + byte "set" // [arg[0], "set"] + == // [arg[0]=?="set"] + bz test // "set" ? continue : goto test + txn ApplicationArgs 1 // [arg[1]] + int 0 // [arg[1], 0] + txn ApplicationArgs 2 // [arg[1], 0, arg[2]] + box_replace // [] // boxes: arg[1] -> replace(boxes[arg[1]], 0, arg[2]) + b end +test: // fail unless arg[2] is the prefix of box arg[1] + txn ApplicationArgs 0 // [arg[0]] + byte "check" // [arg[0], "check"] + == // [arg[0]=?="check"] + bz bad // "check" ? continue : goto bad + txn ApplicationArgs 1 // [arg[1]] + int 0 // [arg[1], 0] + txn ApplicationArgs 2 // [arg[1], 0, arg[2]] + len // [arg[1], 0, len(arg[2])] + box_extract // [ boxes[arg[1]][0:len(arg[2])] ] + txn ApplicationArgs 2 // [ boxes[arg[1]][0:len(arg[2])], arg[2] ] + == // [ boxes[arg[1]][0:len(arg[2])]=?=arg[2] ] + assert // boxes[arg[1]].startwith(arg[2]) ? pop : ERROR + b end +bad: // arg[0] ∉ {"create", "delete", "set", "check"} + err +end: + int 1 \ No newline at end of file diff --git a/util/test/programs.go b/util/test/programs.go new file mode 100644 index 000000000..65eb3929d --- /dev/null +++ b/util/test/programs.go @@ -0,0 +1,13 @@ +package test + +import _ "embed" // embed teal programs as string variables + +// BoxApprovalProgram is a TEAL program which allows for testing box functionality +// +//go:embed boxes.teal +var BoxApprovalProgram string + +// BoxClearProgram is a vanilla TEAL clear state program +const BoxClearProgram string = `#pragma version 8 +int 1 +`