From d6160648f58bf5d48d93c94e487596b1ad69ea2e Mon Sep 17 00:00:00 2001 From: Aaron Buchwald Date: Fri, 27 Sep 2024 12:13:53 -0400 Subject: [PATCH 1/2] Update tx indexer to include tx action outputs --- api/indexer/api.go | 9 ++- api/indexer/tx_indexer.go | 64 ++++++++++++------- codec/address.go | 8 +-- codec/hex.go | 17 +++++ codec/hex_test.go | 31 +++++++++ .../morpheusvm/tests/workload/workload.go | 46 +++++++------ 6 files changed, 127 insertions(+), 48 deletions(-) create mode 100644 codec/hex_test.go diff --git a/api/indexer/api.go b/api/indexer/api.go index 118b774d25..b7ea28bb3b 100644 --- a/api/indexer/api.go +++ b/api/indexer/api.go @@ -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" ) @@ -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"` } type Server struct { @@ -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 } @@ -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 } diff --git a/api/indexer/tx_indexer.go b/api/indexer/tx_indexer.go index ba498444dc..ebf7a5d527 100644 --- a/api/indexer/tx_indexer.go +++ b/api/indexer/tx_indexer.go @@ -4,7 +4,6 @@ package indexer import ( - "encoding/binary" "errors" "path/filepath" @@ -12,6 +11,7 @@ import ( "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" @@ -25,9 +25,6 @@ const ( ) var ( - failureByte = byte(0x0) - successByte = byte(0x1) - _ event.SubscriptionFactory[*chain.StatefulBlock] = (*subscriptionFactory)(nil) _ event.Subscription[*chain.StatefulBlock] = (*txDBIndexer)(nil) ) @@ -103,6 +100,7 @@ func (t *txDBIndexer) Accept(blk *chain.StatefulBlock) error { result.Success, result.Units, result.Fee, + result.Outputs, ); err != nil { return err } @@ -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 + for _, output := range outputs { + outputLength += consts.Uint32Len + len(output) + } + txResultLength := consts.Uint64Len + 1 + fees.DimensionsLen + consts.Uint64Len + outputLength + + 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 } diff --git a/codec/address.go b/codec/address.go index 3a0af71b2d..f0029bac3e 100644 --- a/codec/address.go +++ b/codec/address.go @@ -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. diff --git a/codec/hex.go b/codec/hex.go index 00bb6d84b3..8faf69bb99 100644 --- a/codec/hex.go +++ b/codec/hex.go @@ -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 +} diff --git a/codec/hex_test.go b/codec/hex_test.go new file mode 100644 index 0000000000..a610df8dc4 --- /dev/null +++ b/codec/hex_test.go @@ -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)) +} diff --git a/examples/morpheusvm/tests/workload/workload.go b/examples/morpheusvm/tests/workload/workload.go index 3e8aca007b..77a821ce5d 100644 --- a/examples/morpheusvm/tests/workload/workload.go +++ b/examples/morpheusvm/tests/workload/workload.go @@ -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" @@ -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 } @@ -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) +} From 6d5c1b0b7e648437c6f915db4737461fa89fd79e Mon Sep 17 00:00:00 2001 From: Aaron Buchwald Date: Fri, 27 Sep 2024 12:23:03 -0400 Subject: [PATCH 2/2] use constants for byteLen and boolLen in tx storage --- api/indexer/tx_indexer.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/api/indexer/tx_indexer.go b/api/indexer/tx_indexer.go index ebf7a5d527..a6d654c0e9 100644 --- a/api/indexer/tx_indexer.go +++ b/api/indexer/tx_indexer.go @@ -122,11 +122,11 @@ func (*txDBIndexer) storeTransaction( fee uint64, outputs [][]byte, ) error { - outputLength := 1 // Single byte containing number of outputs + outputLength := consts.ByteLen // Single byte containing number of outputs for _, output := range outputs { outputLength += consts.Uint32Len + len(output) } - txResultLength := consts.Uint64Len + 1 + fees.DimensionsLen + consts.Uint64Len + outputLength + txResultLength := consts.Uint64Len + consts.BoolLen + fees.DimensionsLen + consts.Uint64Len + outputLength writer := codec.NewWriter(txResultLength, txResultLength) writer.PackUint64(uint64(timestamp))