Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Inner Transaction Search #721

Merged
merged 41 commits into from
Oct 26, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
41 commits
Select commit Hold shift + click to select a range
1df3fe1
Bump timeout to 59 seconds
bricerisingalgorand Sep 17, 2021
dd5736b
Bump version to 2.6.2
bricerisingalgorand Sep 17, 2021
d944718
Merge branch 'hotfix/2.6.2'
bricerisingalgorand Sep 17, 2021
3e36474
Merge branch 'release/2.7.0-beta'
bricerisingalgorand Sep 22, 2021
34d0598
Merge branch 'release/2.7.1-beta'
egieseke Sep 27, 2021
4accb38
Merge branch 'release/2.7.1'
algobarb Sep 29, 2021
6635cba
WIP
winder Sep 29, 2021
9cfaa5a
root intra instead of root txid
winder Oct 1, 2021
e97639d
WIP - starting to add query.
winder Oct 2, 2021
8828fa5
Filter out duplicates.
winder Oct 6, 2021
b4cee77
Fix root txn deduplication between result sets.
winder Oct 7, 2021
26e9b08
Fix integration test.
winder Oct 7, 2021
31181d3
Fix test.
winder Oct 7, 2021
4f648d4
Add e2e rest API test
winder Oct 7, 2021
5c58cb1
Add e2e rest API test for paging deduplication.
winder Oct 7, 2021
9ec7a32
Add e2e rest API test to check deduplication while paging.
winder Oct 8, 2021
adaf69f
Cleanup and comments.
winder Oct 8, 2021
54febfb
make fmt and make lint
winder Oct 8, 2021
381c3dd
Fix tests.
winder Oct 8, 2021
2ae438a
Add some comments, paging on round 1 condition.
winder Oct 20, 2021
09c7e56
Add comment about more pages even if limit is not reached.
winder Oct 20, 2021
10f8070
Merge remote-tracking branch 'origin/develop' into will/search-inner-…
winder Oct 20, 2021
cd9950c
Merge remote-tracking branch 'origin/master' into will/search-inner-tx-2
winder Oct 21, 2021
453455f
Move deduplication into fetchTransactions.
winder Oct 21, 2021
59c6e8f
Merge thirdparty
winder Oct 21, 2021
a83c4e1
Revert "Merge thirdparty"
winder Oct 21, 2021
111639e
Fix go-algorand version.
winder Oct 21, 2021
5e7af6d
make fmt
winder Oct 21, 2021
6b4fd4c
Update go-algorand
winder Oct 21, 2021
ef0febc
Merge branch 'develop' into will/search-inner-tx-2
winder Oct 21, 2021
42be115
Use declaration order for idb test helpers.
winder Oct 22, 2021
d05ce6f
Update intra rather than encoding the root txid.
winder Oct 25, 2021
d52832c
Merge remote-tracking branch 'origin/develop' into will/search-inner-…
winder Oct 25, 2021
0029594
Update go-algorand also.
winder Oct 25, 2021
d32e86f
Fix 'Next' test
winder Oct 25, 2021
fd0ee3c
Make sure inner transactions are skipped when matching the root trans…
winder Oct 25, 2021
9cba1d1
Fix next token when there are no results.
winder Oct 25, 2021
0668f08
Merge branch 'develop' into will/search-inner-tx-2
winder Oct 25, 2021
e599c71
PR Feedback.
winder Oct 25, 2021
869cce1
Revert "Update go-algorand also."
winder Oct 26, 2021
cb4c7aa
Merge branch 'develop' into will/search-inner-tx-2
winder Oct 26, 2021
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 19 additions & 2 deletions api/converter_utils.go
Original file line number Diff line number Diff line change
Expand Up @@ -246,13 +246,23 @@ type rowData struct {
AssetCloseAmount uint64
}

// txnRowToTransaction parses the idb.TxnRow and generates the appropriate generated.Transaction object.
// If the idb.TxnRow represents an inner transaction, the root transaction is returned.
func txnRowToTransaction(row idb.TxnRow) (generated.Transaction, error) {
if row.Error != nil {
return generated.Transaction{}, row.Error
}

var bytes []byte
if row.TxnBytes != nil {
bytes = row.TxnBytes
} else if row.RootTxnBytes != nil {
bytes = row.RootTxnBytes
} else {
return generated.Transaction{}, fmt.Errorf("%d:%d transaction bytes missing", row.Round, row.Intra)
}
var stxn transactions.SignedTxnWithAD
err := protocol.Decode(row.TxnBytes, &stxn)
err := protocol.Decode(bytes, &stxn)
if err != nil {
return generated.Transaction{}, fmt.Errorf("%s: %s", errUnableToDecodeTransaction, err.Error())
}
Expand All @@ -265,9 +275,16 @@ func txnRowToTransaction(row idb.TxnRow) (generated.Transaction, error) {
AssetCloseAmount: row.Extra.AssetCloseAmount,
}

if row.Extra.RootIntra != "" {
extra.Intra, err = strconv.Atoi(row.Extra.RootIntra)
if err != nil {
return generated.Transaction{}, fmt.Errorf("txnRowToTransaction(): failed to parse root-intra (%s): %w", row.Extra.RootIntra, err)
}
}

txn, err := signedTxnWithAdToTransaction(&stxn, extra)
if err != nil {
return generated.Transaction{}, err
return generated.Transaction{}, fmt.Errorf("txnRowToTransaction(): failure converting signed transaction to response: %w", err)
}

sig := generated.TransactionSignature{
Expand Down
267 changes: 134 additions & 133 deletions api/generated/common/routes.go

Large diffs are not rendered by default.

317 changes: 159 additions & 158 deletions api/generated/v2/routes.go

Large diffs are not rendered by default.

16 changes: 8 additions & 8 deletions api/generated/v2/types.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

30 changes: 26 additions & 4 deletions api/handlers.go
Original file line number Diff line number Diff line change
Expand Up @@ -676,7 +676,6 @@ func (si *ServerImplementation) fetchBlock(ctx context.Context, round uint64) (g
return generated.Block{}, err
}
results = append(results, tx)
txrow.Next()
}

ret.Transactions = &results
Expand Down Expand Up @@ -727,19 +726,42 @@ func (si *ServerImplementation) fetchAccounts(ctx context.Context, options idb.A

// fetchTransactions is used to query the backend for transactions, and compute the next token
func (si *ServerImplementation) fetchTransactions(ctx context.Context, filter idb.TransactionFilter) ([]generated.Transaction, string, uint64 /*round*/, error) {
// Used for filtering duplicates after getting results.
tolikzinovyev marked this conversation as resolved.
Show resolved Hide resolved
rootTxnDedupeMap := make(map[string]struct{})

results := make([]generated.Transaction, 0)
txchan, round := si.db.Transactions(ctx, filter)
nextToken := ""
for txrow := range txchan {
var txrow idb.TxnRow
for txrow = range txchan {
winder marked this conversation as resolved.
Show resolved Hide resolved
tx, err := txnRowToTransaction(txrow)
tolikzinovyev marked this conversation as resolved.
Show resolved Hide resolved
if err != nil {
return nil, "", round, err
}

// Do not return inner transactions.
if tx.Id == nil {
continue
}

// The root txn has already been added.
if _, ok := rootTxnDedupeMap[*tx.Id]; ok {
continue
}

rootTxnDedupeMap[*tx.Id] = struct{}{}
results = append(results, tx)
nextToken = txrow.Next()
}

return results, nextToken, round, nil
// No next token if there were no results.
if len(results) == 0 {
return results, "", round, nil
}

// The sort order depends on whether the address filter is used.
nextToken, err := txrow.Next(filter.Address == nil)
winder marked this conversation as resolved.
Show resolved Hide resolved

return results, nextToken, round, err
}

//////////////////////
Expand Down
169 changes: 169 additions & 0 deletions api/handlers_e2e_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,10 @@ import (
"github.com/stretchr/testify/require"

"github.com/algorand/go-algorand-sdk/encoding/json"
"github.com/algorand/go-algorand/data/basics"
"github.com/algorand/go-algorand/data/bookkeeping"
"github.com/algorand/go-algorand/data/transactions"

"github.com/algorand/indexer/api/generated/v2"
"github.com/algorand/indexer/idb"
"github.com/algorand/indexer/idb/postgres"
Expand Down Expand Up @@ -130,3 +132,170 @@ func TestBlockNotFound(t *testing.T) {
require.Equal(t, http.StatusNotFound, rec.Code)
require.Equal(t, "{\"message\":\"error while looking up block for round '100': block not found\"}\n", rec.Body.String())
}

// TestInnerTxn runs queries that return one or more root/inner transactions,
// and verifies that only a single root transaction is returned.
func TestInnerTxn(t *testing.T) {
var appAddr basics.Address
appAddr[1] = 99
appAddrStr := appAddr.String()

pay := "pay"
axfer := "axfer"
testcases := []struct {
name string
filter generated.SearchForTransactionsParams
}{
{
name: "match on root",
filter: generated.SearchForTransactionsParams{Address: &appAddrStr, TxType: &pay},
},
{
name: "match on inner",
filter: generated.SearchForTransactionsParams{Address: &appAddrStr, TxType: &pay},
},
{
name: "match on inner-inner",
filter: generated.SearchForTransactionsParams{Address: &appAddrStr, TxType: &axfer},
},
{
name: "match all",
filter: generated.SearchForTransactionsParams{Address: &appAddrStr},
},
}

db, shutdownFunc := setupIdb(t, test.MakeGenesis(), test.MakeGenesisBlock())
defer shutdownFunc()

///////////
// Given // a DB with some inner txns in it.
///////////
appCall := test.MakeAppCallWithInnerTxn(test.AccountA, appAddr, test.AccountB, appAddr, test.AccountC)
expectedID := appCall.Txn.ID().String()

block, err := test.MakeBlockForTxns(test.MakeGenesisBlock().BlockHeader, &appCall)
require.NoError(t, err)

err = db.AddBlock(&block)
require.NoError(t, err, "failed to commit")

for _, tc := range testcases {
t.Run(tc.name, func(t *testing.T) {
//////////
// When // we run a query that matches the Root Txn and/or Inner Txns
//////////
e := echo.New()
req := httptest.NewRequest(http.MethodGet, "/", nil)
rec := httptest.NewRecorder()
c := e.NewContext(req, rec)
c.SetPath("/v2/transactions/")

api := &ServerImplementation{db: db}
err = api.SearchForTransactions(c, tc.filter)
require.NoError(t, err)

//////////
// Then // The only result is the root transaction.
//////////
require.Equal(t, http.StatusOK, rec.Code)
var response generated.TransactionsResponse
json.Decode(rec.Body.Bytes(), &response)

require.Len(t, response.Transactions, 1)
require.Equal(t, expectedID, *(response.Transactions[0].Id))
})
}
}

// TestPagingRootTxnDeduplication checks that paging in the middle of an inner
// transaction group does not allow the root transaction to be returned on both
// pages.
func TestPagingRootTxnDeduplication(t *testing.T) {
db, shutdownFunc := setupIdb(t, test.MakeGenesis(), test.MakeGenesisBlock())
defer shutdownFunc()

///////////
// Given // a DB with some inner txns in it.
///////////
var appAddr basics.Address
appAddr[1] = 99
appAddrStr := appAddr.String()

appCall := test.MakeAppCallWithInnerTxn(test.AccountA, appAddr, test.AccountB, appAddr, test.AccountC)
expectedID := appCall.Txn.ID().String()

block, err := test.MakeBlockForTxns(test.MakeGenesisBlock().BlockHeader, &appCall)
require.NoError(t, err)

err = db.AddBlock(&block)
require.NoError(t, err, "failed to commit")

testcases := []struct {
name string
params generated.SearchForTransactionsParams
}{
{
name: "descending transaction search, middle of inner txns",
params: generated.SearchForTransactionsParams{Address: &appAddrStr, Limit: uint64Ptr(1)},
},
{
name: "ascending transaction search, middle of inner txns",
params: generated.SearchForTransactionsParams{Limit: uint64Ptr(2)},
},
{
name: "ascending transaction search, match root skip over inner txns",
params: generated.SearchForTransactionsParams{Limit: uint64Ptr(1)},
},
}

for _, tc := range testcases {
t.Run(tc.name, func(t *testing.T) {
//////////
// When // we match the first inner transaction and page to the next.
//////////
e := echo.New()
req := httptest.NewRequest(http.MethodGet, "/", nil)
rec1 := httptest.NewRecorder()
c := e.NewContext(req, rec1)
c.SetPath("/v2/transactions/")

// Get first page with limit 1.
// Address filter causes results to return newest to oldest.
api := &ServerImplementation{db: db}
err = api.SearchForTransactions(c, tc.params)
require.NoError(t, err)

require.Equal(t, http.StatusOK, rec1.Code)
var response generated.TransactionsResponse
json.Decode(rec1.Body.Bytes(), &response)
require.Len(t, response.Transactions, 1)
require.Equal(t, expectedID, *(response.Transactions[0].Id))
pageOneNextToken := *response.NextToken

// Second page, using "NextToken" from first page.
req = httptest.NewRequest(http.MethodGet, "/", nil)
rec2 := httptest.NewRecorder()
c = e.NewContext(req, rec2)
c.SetPath("/v2/transactions/")

// Set the next token
tc.params.Next = &pageOneNextToken
// In the debugger I see the internal call returning the inner tx + root tx
err = api.SearchForTransactions(c, tc.params)
require.NoError(t, err)

//////////
// Then // There are no new results on the next page.
//////////
var response2 generated.TransactionsResponse
require.Equal(t, http.StatusOK, rec2.Code)
json.Decode(rec2.Body.Bytes(), &response2)

require.Len(t, response2.Transactions, 0)
// The fact that NextToken changes indicates that the search results were different.
if response2.NextToken != nil {
require.NotEqual(t, pageOneNextToken, *response2.NextToken)
}
})
}
}
2 changes: 1 addition & 1 deletion api/indexer.oas2.json
Original file line number Diff line number Diff line change
Expand Up @@ -2094,7 +2094,7 @@
},
"limit": {
"type": "integer",
"description": "Maximum number of results to return.",
"description": "Maximum number of results to return. There could be additional pages even if the limit is not reached.",
"name": "limit",
"in": "query"
},
Expand Down
Loading