Skip to content
New issue

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

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

Already on GitHub? # to your account

Update tx indexer to include tx action outputs #1597

Merged
merged 2 commits into from
Sep 27, 2024
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 8 additions & 1 deletion api/indexer/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import (
"github.com/ava-labs/avalanchego/trace"

"github.com/ava-labs/hypersdk/api"
"github.com/ava-labs/hypersdk/codec"
"github.com/ava-labs/hypersdk/fees"
)

Expand Down Expand Up @@ -52,6 +53,7 @@ type GetTxResponse struct {
Success bool `json:"success"`
Units fees.Dimensions `json:"units"`
Fee uint64 `json:"fee"`
Outputs []codec.Bytes `json:"result"`
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Uses wrapped codec.Bytes to provide hex format

}

type Server struct {
Expand All @@ -63,7 +65,7 @@ func (s *Server) GetTx(req *http.Request, args *GetTxRequest, reply *GetTxRespon
_, span := s.tracer.Start(req.Context(), "Indexer.GetTx")
defer span.End()

found, t, success, units, fee, err := s.indexer.GetTransaction(args.TxID)
found, t, success, units, fee, outputs, err := s.indexer.GetTransaction(args.TxID)
if err != nil {
return err
}
Expand All @@ -75,5 +77,10 @@ func (s *Server) GetTx(req *http.Request, args *GetTxRequest, reply *GetTxRespon
reply.Success = success
reply.Units = units
reply.Fee = fee
wrappedOutputs := make([]codec.Bytes, len(outputs))
for i, output := range outputs {
wrappedOutputs[i] = codec.Bytes(output)
}
reply.Outputs = wrappedOutputs
return nil
}
64 changes: 40 additions & 24 deletions api/indexer/tx_indexer.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,14 @@
package indexer

import (
"encoding/binary"
"errors"
"path/filepath"

"github.com/ava-labs/avalanchego/database"
"github.com/ava-labs/avalanchego/ids"

"github.com/ava-labs/hypersdk/chain"
"github.com/ava-labs/hypersdk/codec"
"github.com/ava-labs/hypersdk/consts"
"github.com/ava-labs/hypersdk/event"
"github.com/ava-labs/hypersdk/fees"
Expand All @@ -25,9 +25,6 @@ const (
)

var (
failureByte = byte(0x0)
successByte = byte(0x1)

_ event.SubscriptionFactory[*chain.StatefulBlock] = (*subscriptionFactory)(nil)
_ event.Subscription[*chain.StatefulBlock] = (*txDBIndexer)(nil)
)
Expand Down Expand Up @@ -103,6 +100,7 @@ func (t *txDBIndexer) Accept(blk *chain.StatefulBlock) error {
result.Success,
result.Units,
result.Fee,
result.Outputs,
); err != nil {
return err
}
Expand All @@ -122,36 +120,54 @@ func (*txDBIndexer) storeTransaction(
success bool,
units fees.Dimensions,
fee uint64,
outputs [][]byte,
) error {
v := make([]byte, consts.Uint64Len+1+fees.DimensionsLen+consts.Uint64Len)
binary.BigEndian.PutUint64(v, uint64(timestamp))
if success {
v[consts.Uint64Len] = successByte
} else {
v[consts.Uint64Len] = failureByte
outputLength := 1 // Single byte containing number of outputs
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note: GetMaxActionsPerTx() returns a uint8, which enforces the maximum possible number of actions and corresponding results included in a transaction.

for _, output := range outputs {
outputLength += consts.Uint32Len + len(output)
}
txResultLength := consts.Uint64Len + 1 + fees.DimensionsLen + consts.Uint64Len + outputLength
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note: we use an exact txResultLength as the maximum here. Calling this out because it means if we calculate a number that's too low here, the packer error below will be treated as fatal since this is on the accept path.

Alternative would be to use consts.MaxNetworkLen or similar to make sure this never causes an issue vs. keep it as is to immediately surface any miscalculation here.


writer := codec.NewWriter(txResultLength, txResultLength)
writer.PackUint64(uint64(timestamp))
writer.PackBool(success)
writer.PackFixedBytes(units.Bytes())
writer.PackUint64(fee)
writer.PackByte(byte(len(outputs)))
for _, output := range outputs {
writer.PackBytes(output)
}
copy(v[consts.Uint64Len+1:], units.Bytes())
binary.BigEndian.PutUint64(v[consts.Uint64Len+1+fees.DimensionsLen:], fee)
return batch.Put(txID[:], v)
if err := writer.Err(); err != nil {
return err
}
return batch.Put(txID[:], writer.Bytes())
}

func (t *txDBIndexer) GetTransaction(txID ids.ID) (bool, int64, bool, fees.Dimensions, uint64, error) {
func (t *txDBIndexer) GetTransaction(txID ids.ID) (bool, int64, bool, fees.Dimensions, uint64, [][]byte, error) {
v, err := t.db.Get(txID[:])
if errors.Is(err, database.ErrNotFound) {
return false, 0, false, fees.Dimensions{}, 0, nil
return false, 0, false, fees.Dimensions{}, 0, nil, nil
}
if err != nil {
return false, 0, false, fees.Dimensions{}, 0, err
return false, 0, false, fees.Dimensions{}, 0, nil, err
}
reader := codec.NewReader(v, consts.NetworkSizeLimit)
timestamp := reader.UnpackUint64(true)
success := reader.UnpackBool()
dimensionsBytes := make([]byte, fees.DimensionsLen)
reader.UnpackFixedBytes(fees.DimensionsLen, &dimensionsBytes)
fee := reader.UnpackUint64(true)
numOutputs := int(reader.UnpackByte())
outputs := make([][]byte, numOutputs)
for i := range outputs {
outputs[i] = reader.UnpackLimitedBytes(consts.NetworkSizeLimit)
}
timestamp := int64(binary.BigEndian.Uint64(v))
success := true
if v[consts.Uint64Len] == failureByte {
success = false
if err := reader.Err(); err != nil {
return false, 0, false, fees.Dimensions{}, 0, nil, err
}
d, err := fees.UnpackDimensions(v[consts.Uint64Len+1 : consts.Uint64Len+1+fees.DimensionsLen])
dimensions, err := fees.UnpackDimensions(dimensionsBytes)
if err != nil {
return false, 0, false, fees.Dimensions{}, 0, err
return false, 0, false, fees.Dimensions{}, 0, nil, err
}
fee := binary.BigEndian.Uint64(v[consts.Uint64Len+1+fees.DimensionsLen:])
return true, timestamp, success, d, fee, nil
return true, int64(timestamp), success, dimensions, fee, outputs, nil
}
8 changes: 4 additions & 4 deletions codec/address.go
Original file line number Diff line number Diff line change
Expand Up @@ -41,18 +41,18 @@ func ToAddress(b []byte) (Address, error) {
// StringToAddress uses copy, which copies the minimum of
// either AddressLen or the length of the hex decoded string.
func StringToAddress(s string) (Address, error) {
b, err := hex.DecodeString(s)
var a Address
b, err := LoadHex(s, AddressLen)
if err != nil {
return Address{}, fmt.Errorf("failed to convert hex string to address: %w", err)
return a, err
}
var a Address
copy(a[:], b)
return a, nil
}

// String implements fmt.Stringer.
func (a Address) String() string {
return hex.EncodeToString(a[:])
return ToHex(a[:])
}

// MarshalText returns the hex representation of a.
Expand Down
17 changes: 17 additions & 0 deletions codec/hex.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,3 +22,20 @@ func LoadHex(s string, expectedSize int) ([]byte, error) {
}
return bytes, nil
}

type Bytes []byte

// MarshalText returns the hex representation of b.
func (b Bytes) MarshalText() ([]byte, error) {
return []byte(ToHex(b)), nil
}

// UnmarshalText sets b to the bytes represented by text.
func (b *Bytes) UnmarshalText(text []byte) error {
bytes, err := LoadHex(string(text), -1)
if err != nil {
return err
}
*b = bytes
return nil
}
31 changes: 31 additions & 0 deletions codec/hex_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
// Copyright (C) 2024, Ava Labs, Inc. All rights reserved.
// See the file LICENSE for licensing terms.

package codec

import (
"encoding/json"
"testing"

"github.com/stretchr/testify/require"
)

func TestBytesHex(t *testing.T) {
require := require.New(t)
b := []byte{1, 2, 3, 4, 5}
wrappedBytes := Bytes(b)

marshalledBytes, err := wrappedBytes.MarshalText()
require.NoError(err)

var unmarshalledBytes Bytes
require.NoError(unmarshalledBytes.UnmarshalText(marshalledBytes))
require.Equal(b, []byte(unmarshalledBytes))

jsonMarshalledBytes, err := json.Marshal(wrappedBytes)
require.NoError(err)

var jsonUnmarshalledBytes Bytes
require.NoError(json.Unmarshal(jsonMarshalledBytes, &jsonUnmarshalledBytes))
require.Equal(b, []byte(jsonUnmarshalledBytes))
}
46 changes: 27 additions & 19 deletions examples/morpheusvm/tests/workload/workload.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import (
"github.com/ava-labs/hypersdk/crypto/ed25519"
"github.com/ava-labs/hypersdk/crypto/secp256r1"
"github.com/ava-labs/hypersdk/examples/morpheusvm/actions"
"github.com/ava-labs/hypersdk/examples/morpheusvm/consts"
"github.com/ava-labs/hypersdk/examples/morpheusvm/vm"
"github.com/ava-labs/hypersdk/fees"
"github.com/ava-labs/hypersdk/genesis"
Expand Down Expand Up @@ -134,15 +135,7 @@ func (g *simpleTxWorkload) GenerateTxWithAssertion(ctx context.Context) (*chain.
}

return tx, func(ctx context.Context, require *require.Assertions, uri string) {
indexerCli := indexer.NewClient(uri)
success, _, err := indexerCli.WaitForTransaction(ctx, txCheckInterval, tx.ID())
require.NoError(err)
require.True(success)
lcli := vm.NewJSONRPCClient(uri)
balance, err := lcli.Balance(ctx, aother)
require.NoError(err)
require.Equal(uint64(1), balance)
// TODO: check transaction output (not currently available via API)
confirmTx(ctx, require, uri, tx.ID(), aother, 1)
}, nil
}

Expand Down Expand Up @@ -231,15 +224,30 @@ func (g *mixedAuthWorkload) GenerateTxWithAssertion(ctx context.Context) (*chain
g.balance = expectedBalance

return tx, func(ctx context.Context, require *require.Assertions, uri string) {
indexerCli := indexer.NewClient(uri)
success, _, err := indexerCli.WaitForTransaction(ctx, txCheckInterval, tx.ID())
require.NoError(err)
require.True(success)
lcli := vm.NewJSONRPCClient(uri)
balance, err := lcli.Balance(ctx, receiver.address)
require.NoError(err)
require.Equal(expectedBalance, balance)
// TODO: check tx fee + units (not currently available via API)
// TODO: check transaction output (not currently available via API)
confirmTx(ctx, require, uri, tx.ID(), receiver.address, expectedBalance)
}, nil
}

func confirmTx(ctx context.Context, require *require.Assertions, uri string, txID ids.ID, receiverAddr codec.Address, receiverExpectedBalance uint64) {
indexerCli := indexer.NewClient(uri)
success, _, err := indexerCli.WaitForTransaction(ctx, txCheckInterval, txID)
require.NoError(err)
require.True(success)
lcli := vm.NewJSONRPCClient(uri)
balance, err := lcli.Balance(ctx, receiverAddr)
require.NoError(err)
require.Equal(receiverExpectedBalance, balance)
txRes, _, err := indexerCli.GetTx(ctx, txID)
require.NoError(err)
// TODO: perform exact expected fee, units check, and output check
require.NotZero(txRes.Fee)
require.Len(txRes.Outputs, 1)
transferOutputBytes := []byte(txRes.Outputs[0])
require.Equal(consts.TransferID, transferOutputBytes[0])
reader := codec.NewReader(transferOutputBytes, len(transferOutputBytes))
transferOutputTyped, err := vm.OutputParser.Unmarshal(reader)
require.NoError(err)
transferOutput, ok := transferOutputTyped.(*actions.TransferResult)
require.True(ok)
require.Equal(receiverExpectedBalance, transferOutput.ReceiverBalance)
}
Loading