diff --git a/access/api.go b/access/api.go index 7595371835a..30f55705e43 100644 --- a/access/api.go +++ b/access/api.go @@ -59,6 +59,9 @@ type TransactionsAPI interface { GetSystemTransaction(ctx context.Context, txID flow.Identifier, blockID flow.Identifier) (*flow.TransactionBody, error) GetSystemTransactionResult(ctx context.Context, txID flow.Identifier, blockID flow.Identifier, encodingVersion entities.EventEncodingVersion) (*accessmodel.TransactionResult, error) + + GetScheduledTransaction(ctx context.Context, scheduledTxID uint64) (*flow.TransactionBody, error) + GetScheduledTransactionResult(ctx context.Context, scheduledTxID uint64, encodingVersion entities.EventEncodingVersion) (*accessmodel.TransactionResult, error) } type TransactionStreamAPI interface { diff --git a/access/mock/api.go b/access/mock/api.go index 158945f9a55..cfbc16f4bd4 100644 --- a/access/mock/api.go +++ b/access/mock/api.go @@ -916,6 +916,66 @@ func (_m *API) GetProtocolStateSnapshotByHeight(ctx context.Context, blockHeight return r0, r1 } +// GetScheduledTransaction provides a mock function with given fields: ctx, scheduledTxID +func (_m *API) GetScheduledTransaction(ctx context.Context, scheduledTxID uint64) (*flow.TransactionBody, error) { + ret := _m.Called(ctx, scheduledTxID) + + if len(ret) == 0 { + panic("no return value specified for GetScheduledTransaction") + } + + var r0 *flow.TransactionBody + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, uint64) (*flow.TransactionBody, error)); ok { + return rf(ctx, scheduledTxID) + } + if rf, ok := ret.Get(0).(func(context.Context, uint64) *flow.TransactionBody); ok { + r0 = rf(ctx, scheduledTxID) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*flow.TransactionBody) + } + } + + if rf, ok := ret.Get(1).(func(context.Context, uint64) error); ok { + r1 = rf(ctx, scheduledTxID) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// GetScheduledTransactionResult provides a mock function with given fields: ctx, scheduledTxID, encodingVersion +func (_m *API) GetScheduledTransactionResult(ctx context.Context, scheduledTxID uint64, encodingVersion entities.EventEncodingVersion) (*modelaccess.TransactionResult, error) { + ret := _m.Called(ctx, scheduledTxID, encodingVersion) + + if len(ret) == 0 { + panic("no return value specified for GetScheduledTransactionResult") + } + + var r0 *modelaccess.TransactionResult + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, uint64, entities.EventEncodingVersion) (*modelaccess.TransactionResult, error)); ok { + return rf(ctx, scheduledTxID, encodingVersion) + } + if rf, ok := ret.Get(0).(func(context.Context, uint64, entities.EventEncodingVersion) *modelaccess.TransactionResult); ok { + r0 = rf(ctx, scheduledTxID, encodingVersion) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*modelaccess.TransactionResult) + } + } + + if rf, ok := ret.Get(1).(func(context.Context, uint64, entities.EventEncodingVersion) error); ok { + r1 = rf(ctx, scheduledTxID, encodingVersion) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + // GetSystemTransaction provides a mock function with given fields: ctx, txID, blockID func (_m *API) GetSystemTransaction(ctx context.Context, txID flow.Identifier, blockID flow.Identifier) (*flow.TransactionBody, error) { ret := _m.Called(ctx, txID, blockID) diff --git a/access/mock/transactions_api.go b/access/mock/transactions_api.go index d3bef687ab1..957bcd51be1 100644 --- a/access/mock/transactions_api.go +++ b/access/mock/transactions_api.go @@ -18,6 +18,66 @@ type TransactionsAPI struct { mock.Mock } +// GetScheduledTransaction provides a mock function with given fields: ctx, scheduledTxID +func (_m *TransactionsAPI) GetScheduledTransaction(ctx context.Context, scheduledTxID uint64) (*flow.TransactionBody, error) { + ret := _m.Called(ctx, scheduledTxID) + + if len(ret) == 0 { + panic("no return value specified for GetScheduledTransaction") + } + + var r0 *flow.TransactionBody + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, uint64) (*flow.TransactionBody, error)); ok { + return rf(ctx, scheduledTxID) + } + if rf, ok := ret.Get(0).(func(context.Context, uint64) *flow.TransactionBody); ok { + r0 = rf(ctx, scheduledTxID) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*flow.TransactionBody) + } + } + + if rf, ok := ret.Get(1).(func(context.Context, uint64) error); ok { + r1 = rf(ctx, scheduledTxID) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// GetScheduledTransactionResult provides a mock function with given fields: ctx, scheduledTxID, encodingVersion +func (_m *TransactionsAPI) GetScheduledTransactionResult(ctx context.Context, scheduledTxID uint64, encodingVersion entities.EventEncodingVersion) (*modelaccess.TransactionResult, error) { + ret := _m.Called(ctx, scheduledTxID, encodingVersion) + + if len(ret) == 0 { + panic("no return value specified for GetScheduledTransactionResult") + } + + var r0 *modelaccess.TransactionResult + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, uint64, entities.EventEncodingVersion) (*modelaccess.TransactionResult, error)); ok { + return rf(ctx, scheduledTxID, encodingVersion) + } + if rf, ok := ret.Get(0).(func(context.Context, uint64, entities.EventEncodingVersion) *modelaccess.TransactionResult); ok { + r0 = rf(ctx, scheduledTxID, encodingVersion) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*modelaccess.TransactionResult) + } + } + + if rf, ok := ret.Get(1).(func(context.Context, uint64, entities.EventEncodingVersion) error); ok { + r1 = rf(ctx, scheduledTxID, encodingVersion) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + // GetSystemTransaction provides a mock function with given fields: ctx, txID, blockID func (_m *TransactionsAPI) GetSystemTransaction(ctx context.Context, txID flow.Identifier, blockID flow.Identifier) (*flow.TransactionBody, error) { ret := _m.Called(ctx, txID, blockID) diff --git a/access/unimplemented.go b/access/unimplemented.go new file mode 100644 index 00000000000..309eebb3f81 --- /dev/null +++ b/access/unimplemented.go @@ -0,0 +1,310 @@ +package access + +import ( + "context" + + "google.golang.org/grpc/codes" + "google.golang.org/grpc/status" + + "github.com/onflow/flow/protobuf/go/flow/entities" + + "github.com/onflow/flow-go/engine/access/subscription" + accessmodel "github.com/onflow/flow-go/model/access" + "github.com/onflow/flow-go/model/flow" +) + +// UnimplementedAPI provides an implementation of the access.API interface where all methods return +// unimplemented errors. +type UnimplementedAPI struct{} + +var _ API = (*UnimplementedAPI)(nil) + +// NewUnimplementedAPI creates a new UnimplementedAPI instance. +func NewUnimplementedAPI() *UnimplementedAPI { + return &UnimplementedAPI{} +} + +// GetAccount returns an unimplemented error. +func (u *UnimplementedAPI) GetAccount(ctx context.Context, address flow.Address) (*flow.Account, error) { + return nil, status.Error(codes.Unimplemented, "method GetAccount not implemented") +} + +// GetAccountAtLatestBlock returns an unimplemented error. +func (u *UnimplementedAPI) GetAccountAtLatestBlock(ctx context.Context, address flow.Address) (*flow.Account, error) { + return nil, status.Error(codes.Unimplemented, "method GetAccountAtLatestBlock not implemented") +} + +// GetAccountAtBlockHeight returns an unimplemented error. +func (u *UnimplementedAPI) GetAccountAtBlockHeight(ctx context.Context, address flow.Address, height uint64) (*flow.Account, error) { + return nil, status.Error(codes.Unimplemented, "method GetAccountAtBlockHeight not implemented") +} + +// GetAccountBalanceAtLatestBlock returns an unimplemented error. +func (u *UnimplementedAPI) GetAccountBalanceAtLatestBlock(ctx context.Context, address flow.Address) (uint64, error) { + return 0, status.Error(codes.Unimplemented, "method GetAccountBalanceAtLatestBlock not implemented") +} + +// GetAccountBalanceAtBlockHeight returns an unimplemented error. +func (u *UnimplementedAPI) GetAccountBalanceAtBlockHeight(ctx context.Context, address flow.Address, height uint64) (uint64, error) { + return 0, status.Error(codes.Unimplemented, "method GetAccountBalanceAtBlockHeight not implemented") +} + +// GetAccountKeyAtLatestBlock returns an unimplemented error. +func (u *UnimplementedAPI) GetAccountKeyAtLatestBlock(ctx context.Context, address flow.Address, keyIndex uint32) (*flow.AccountPublicKey, error) { + return nil, status.Error(codes.Unimplemented, "method GetAccountKeyAtLatestBlock not implemented") +} + +// GetAccountKeyAtBlockHeight returns an unimplemented error. +func (u *UnimplementedAPI) GetAccountKeyAtBlockHeight(ctx context.Context, address flow.Address, keyIndex uint32, height uint64) (*flow.AccountPublicKey, error) { + return nil, status.Error(codes.Unimplemented, "method GetAccountKeyAtBlockHeight not implemented") +} + +// GetAccountKeysAtLatestBlock returns an unimplemented error. +func (u *UnimplementedAPI) GetAccountKeysAtLatestBlock(ctx context.Context, address flow.Address) ([]flow.AccountPublicKey, error) { + return nil, status.Error(codes.Unimplemented, "method GetAccountKeysAtLatestBlock not implemented") +} + +// GetAccountKeysAtBlockHeight returns an unimplemented error. +func (u *UnimplementedAPI) GetAccountKeysAtBlockHeight(ctx context.Context, address flow.Address, height uint64) ([]flow.AccountPublicKey, error) { + return nil, status.Error(codes.Unimplemented, "method GetAccountKeysAtBlockHeight not implemented") +} + +// GetEventsForHeightRange returns an unimplemented error. +func (u *UnimplementedAPI) GetEventsForHeightRange( + ctx context.Context, + eventType string, + startHeight, + endHeight uint64, + requiredEventEncodingVersion entities.EventEncodingVersion, +) ([]flow.BlockEvents, error) { + return nil, status.Error(codes.Unimplemented, "method GetEventsForHeightRange not implemented") +} + +// GetEventsForBlockIDs returns an unimplemented error. +func (u *UnimplementedAPI) GetEventsForBlockIDs( + ctx context.Context, + eventType string, + blockIDs []flow.Identifier, + requiredEventEncodingVersion entities.EventEncodingVersion, +) ([]flow.BlockEvents, error) { + return nil, status.Error(codes.Unimplemented, "method GetEventsForBlockIDs not implemented") +} + +// ExecuteScriptAtLatestBlock returns an unimplemented error. +func (u *UnimplementedAPI) ExecuteScriptAtLatestBlock(ctx context.Context, script []byte, arguments [][]byte) ([]byte, error) { + return nil, status.Error(codes.Unimplemented, "method ExecuteScriptAtLatestBlock not implemented") +} + +// ExecuteScriptAtBlockHeight returns an unimplemented error. +func (u *UnimplementedAPI) ExecuteScriptAtBlockHeight(ctx context.Context, blockHeight uint64, script []byte, arguments [][]byte) ([]byte, error) { + return nil, status.Error(codes.Unimplemented, "method ExecuteScriptAtBlockHeight not implemented") +} + +// ExecuteScriptAtBlockID returns an unimplemented error. +func (u *UnimplementedAPI) ExecuteScriptAtBlockID(ctx context.Context, blockID flow.Identifier, script []byte, arguments [][]byte) ([]byte, error) { + return nil, status.Error(codes.Unimplemented, "method ExecuteScriptAtBlockID not implemented") +} + +// SendTransaction returns an unimplemented error. +func (u *UnimplementedAPI) SendTransaction(ctx context.Context, tx *flow.TransactionBody) error { + return status.Error(codes.Unimplemented, "method SendTransaction not implemented") +} + +// GetTransaction returns an unimplemented error. +func (u *UnimplementedAPI) GetTransaction(ctx context.Context, id flow.Identifier) (*flow.TransactionBody, error) { + return nil, status.Error(codes.Unimplemented, "method GetTransaction not implemented") +} + +// GetTransactionsByBlockID returns an unimplemented error. +func (u *UnimplementedAPI) GetTransactionsByBlockID(ctx context.Context, blockID flow.Identifier) ([]*flow.TransactionBody, error) { + return nil, status.Error(codes.Unimplemented, "method GetTransactionsByBlockID not implemented") +} + +// GetTransactionResult returns an unimplemented error. +func (u *UnimplementedAPI) GetTransactionResult(ctx context.Context, txID flow.Identifier, blockID flow.Identifier, collectionID flow.Identifier, encodingVersion entities.EventEncodingVersion) (*accessmodel.TransactionResult, error) { + return nil, status.Error(codes.Unimplemented, "method GetTransactionResult not implemented") +} + +// GetTransactionResultByIndex returns an unimplemented error. +func (u *UnimplementedAPI) GetTransactionResultByIndex(ctx context.Context, blockID flow.Identifier, index uint32, encodingVersion entities.EventEncodingVersion) (*accessmodel.TransactionResult, error) { + return nil, status.Error(codes.Unimplemented, "method GetTransactionResultByIndex not implemented") +} + +// GetTransactionResultsByBlockID returns an unimplemented error. +func (u *UnimplementedAPI) GetTransactionResultsByBlockID(ctx context.Context, blockID flow.Identifier, encodingVersion entities.EventEncodingVersion) ([]*accessmodel.TransactionResult, error) { + return nil, status.Error(codes.Unimplemented, "method GetTransactionResultsByBlockID not implemented") +} + +// GetSystemTransaction returns an unimplemented error. +func (u *UnimplementedAPI) GetSystemTransaction(ctx context.Context, txID flow.Identifier, blockID flow.Identifier) (*flow.TransactionBody, error) { + return nil, status.Error(codes.Unimplemented, "method GetSystemTransaction not implemented") +} + +// GetSystemTransactionResult returns an unimplemented error. +func (u *UnimplementedAPI) GetSystemTransactionResult(ctx context.Context, txID flow.Identifier, blockID flow.Identifier, encodingVersion entities.EventEncodingVersion) (*accessmodel.TransactionResult, error) { + return nil, status.Error(codes.Unimplemented, "method GetSystemTransactionResult not implemented") +} + +// GetScheduledTransaction returns an unimplemented error. +func (u *UnimplementedAPI) GetScheduledTransaction(ctx context.Context, scheduledTxID uint64) (*flow.TransactionBody, error) { + return nil, status.Error(codes.Unimplemented, "method GetScheduledTransaction not implemented") +} + +// GetScheduledTransactionResult returns an unimplemented error. +func (u *UnimplementedAPI) GetScheduledTransactionResult(ctx context.Context, scheduledTxID uint64, encodingVersion entities.EventEncodingVersion) (*accessmodel.TransactionResult, error) { + return nil, status.Error(codes.Unimplemented, "method GetScheduledTransactionResult not implemented") +} + +// SubscribeTransactionStatuses returns a failed subscription. +func (u *UnimplementedAPI) SubscribeTransactionStatuses( + ctx context.Context, + txID flow.Identifier, + requiredEventEncodingVersion entities.EventEncodingVersion, +) subscription.Subscription { + msg := "method SubscribeTransactionStatuses not implemented" + return subscription.NewFailedSubscription(status.Error(codes.Unimplemented, msg), msg) +} + +// SendAndSubscribeTransactionStatuses returns a failed subscription. +func (u *UnimplementedAPI) SendAndSubscribeTransactionStatuses( + ctx context.Context, + tx *flow.TransactionBody, + requiredEventEncodingVersion entities.EventEncodingVersion, +) subscription.Subscription { + msg := "method SendAndSubscribeTransactionStatuses not implemented" + return subscription.NewFailedSubscription(status.Error(codes.Unimplemented, msg), msg) +} + +// Ping returns an unimplemented error. +func (u *UnimplementedAPI) Ping(ctx context.Context) error { + return status.Error(codes.Unimplemented, "method Ping not implemented") +} + +// GetNetworkParameters returns an unimplemented error. +func (u *UnimplementedAPI) GetNetworkParameters(ctx context.Context) accessmodel.NetworkParameters { + return accessmodel.NetworkParameters{} +} + +// GetNodeVersionInfo returns an unimplemented error. +func (u *UnimplementedAPI) GetNodeVersionInfo(ctx context.Context) (*accessmodel.NodeVersionInfo, error) { + return nil, status.Error(codes.Unimplemented, "method GetNodeVersionInfo not implemented") +} + +// GetLatestBlockHeader returns an unimplemented error. +func (u *UnimplementedAPI) GetLatestBlockHeader(ctx context.Context, isSealed bool) (*flow.Header, flow.BlockStatus, error) { + return nil, flow.BlockStatusUnknown, status.Error(codes.Unimplemented, "method GetLatestBlockHeader not implemented") +} + +// GetBlockHeaderByHeight returns an unimplemented error. +func (u *UnimplementedAPI) GetBlockHeaderByHeight(ctx context.Context, height uint64) (*flow.Header, flow.BlockStatus, error) { + return nil, flow.BlockStatusUnknown, status.Error(codes.Unimplemented, "method GetBlockHeaderByHeight not implemented") +} + +// GetBlockHeaderByID returns an unimplemented error. +func (u *UnimplementedAPI) GetBlockHeaderByID(ctx context.Context, id flow.Identifier) (*flow.Header, flow.BlockStatus, error) { + return nil, flow.BlockStatusUnknown, status.Error(codes.Unimplemented, "method GetBlockHeaderByID not implemented") +} + +// GetLatestBlock returns an unimplemented error. +func (u *UnimplementedAPI) GetLatestBlock(ctx context.Context, isSealed bool) (*flow.Block, flow.BlockStatus, error) { + return nil, flow.BlockStatusUnknown, status.Error(codes.Unimplemented, "method GetLatestBlock not implemented") +} + +// GetBlockByHeight returns an unimplemented error. +func (u *UnimplementedAPI) GetBlockByHeight(ctx context.Context, height uint64) (*flow.Block, flow.BlockStatus, error) { + return nil, flow.BlockStatusUnknown, status.Error(codes.Unimplemented, "method GetBlockByHeight not implemented") +} + +// GetBlockByID returns an unimplemented error. +func (u *UnimplementedAPI) GetBlockByID(ctx context.Context, id flow.Identifier) (*flow.Block, flow.BlockStatus, error) { + return nil, flow.BlockStatusUnknown, status.Error(codes.Unimplemented, "method GetBlockByID not implemented") +} + +// GetCollectionByID returns an unimplemented error. +func (u *UnimplementedAPI) GetCollectionByID(ctx context.Context, id flow.Identifier) (*flow.LightCollection, error) { + return nil, status.Error(codes.Unimplemented, "method GetCollectionByID not implemented") +} + +// GetFullCollectionByID returns an unimplemented error. +func (u *UnimplementedAPI) GetFullCollectionByID(ctx context.Context, id flow.Identifier) (*flow.Collection, error) { + return nil, status.Error(codes.Unimplemented, "method GetFullCollectionByID not implemented") +} + +// GetLatestProtocolStateSnapshot returns an unimplemented error. +func (u *UnimplementedAPI) GetLatestProtocolStateSnapshot(ctx context.Context) ([]byte, error) { + return nil, status.Error(codes.Unimplemented, "method GetLatestProtocolStateSnapshot not implemented") +} + +// GetProtocolStateSnapshotByBlockID returns an unimplemented error. +func (u *UnimplementedAPI) GetProtocolStateSnapshotByBlockID(ctx context.Context, blockID flow.Identifier) ([]byte, error) { + return nil, status.Error(codes.Unimplemented, "method GetProtocolStateSnapshotByBlockID not implemented") +} + +// GetProtocolStateSnapshotByHeight returns an unimplemented error. +func (u *UnimplementedAPI) GetProtocolStateSnapshotByHeight(ctx context.Context, blockHeight uint64) ([]byte, error) { + return nil, status.Error(codes.Unimplemented, "method GetProtocolStateSnapshotByHeight not implemented") +} + +// GetExecutionResultForBlockID returns an unimplemented error. +func (u *UnimplementedAPI) GetExecutionResultForBlockID(ctx context.Context, blockID flow.Identifier) (*flow.ExecutionResult, error) { + return nil, status.Error(codes.Unimplemented, "method GetExecutionResultForBlockID not implemented") +} + +// GetExecutionResultByID returns an unimplemented error. +func (u *UnimplementedAPI) GetExecutionResultByID(ctx context.Context, id flow.Identifier) (*flow.ExecutionResult, error) { + return nil, status.Error(codes.Unimplemented, "method GetExecutionResultByID not implemented") +} + +// SubscribeBlocksFromStartBlockID returns a failed subscription. +func (u *UnimplementedAPI) SubscribeBlocksFromStartBlockID(ctx context.Context, startBlockID flow.Identifier, blockStatus flow.BlockStatus) subscription.Subscription { + msg := "method SubscribeBlocksFromStartBlockID not implemented" + return subscription.NewFailedSubscription(status.Error(codes.Unimplemented, msg), msg) +} + +// SubscribeBlocksFromStartHeight returns a failed subscription. +func (u *UnimplementedAPI) SubscribeBlocksFromStartHeight(ctx context.Context, startHeight uint64, blockStatus flow.BlockStatus) subscription.Subscription { + msg := "method SubscribeBlocksFromStartHeight not implemented" + return subscription.NewFailedSubscription(status.Error(codes.Unimplemented, msg), msg) +} + +// SubscribeBlocksFromLatest returns a failed subscription. +func (u *UnimplementedAPI) SubscribeBlocksFromLatest(ctx context.Context, blockStatus flow.BlockStatus) subscription.Subscription { + msg := "method SubscribeBlocksFromLatest not implemented" + return subscription.NewFailedSubscription(status.Error(codes.Unimplemented, msg), msg) +} + +// SubscribeBlockHeadersFromStartBlockID returns a failed subscription. +func (u *UnimplementedAPI) SubscribeBlockHeadersFromStartBlockID(ctx context.Context, startBlockID flow.Identifier, blockStatus flow.BlockStatus) subscription.Subscription { + msg := "method SubscribeBlockHeadersFromStartBlockID not implemented" + return subscription.NewFailedSubscription(status.Error(codes.Unimplemented, msg), msg) +} + +// SubscribeBlockHeadersFromStartHeight returns a failed subscription. +func (u *UnimplementedAPI) SubscribeBlockHeadersFromStartHeight(ctx context.Context, startHeight uint64, blockStatus flow.BlockStatus) subscription.Subscription { + msg := "method SubscribeBlockHeadersFromStartHeight not implemented" + return subscription.NewFailedSubscription(status.Error(codes.Unimplemented, msg), msg) +} + +// SubscribeBlockHeadersFromLatest returns a failed subscription. +func (u *UnimplementedAPI) SubscribeBlockHeadersFromLatest(ctx context.Context, blockStatus flow.BlockStatus) subscription.Subscription { + msg := "method SubscribeBlockHeadersFromLatest not implemented" + return subscription.NewFailedSubscription(status.Error(codes.Unimplemented, msg), msg) +} + +// SubscribeBlockDigestsFromStartBlockID returns a failed subscription. +func (u *UnimplementedAPI) SubscribeBlockDigestsFromStartBlockID(ctx context.Context, startBlockID flow.Identifier, blockStatus flow.BlockStatus) subscription.Subscription { + msg := "method SubscribeBlockDigestsFromStartBlockID not implemented" + return subscription.NewFailedSubscription(status.Error(codes.Unimplemented, msg), msg) +} + +// SubscribeBlockDigestsFromStartHeight returns a failed subscription. +func (u *UnimplementedAPI) SubscribeBlockDigestsFromStartHeight(ctx context.Context, startHeight uint64, blockStatus flow.BlockStatus) subscription.Subscription { + msg := "method SubscribeBlockDigestsFromStartHeight not implemented" + return subscription.NewFailedSubscription(status.Error(codes.Unimplemented, msg), msg) +} + +// SubscribeBlockDigestsFromLatest returns a failed subscription. +func (u *UnimplementedAPI) SubscribeBlockDigestsFromLatest(ctx context.Context, blockStatus flow.BlockStatus) subscription.Subscription { + msg := "method SubscribeBlockDigestsFromLatest not implemented" + return subscription.NewFailedSubscription(status.Error(codes.Unimplemented, msg), msg) +} diff --git a/cmd/access/node_builder/access_node_builder.go b/cmd/access/node_builder/access_node_builder.go index 5c2dce02ebb..22bcc33b91b 100644 --- a/cmd/access/node_builder/access_node_builder.go +++ b/cmd/access/node_builder/access_node_builder.go @@ -2127,6 +2127,7 @@ func (builder *FlowAccessNodeBuilder) Build() (cmd.Node, error) { ExecutionReceipts: node.Storage.Receipts, ExecutionResults: node.Storage.Results, TxResultErrorMessages: builder.transactionResultErrorMessages, // might be nil + ScheduledTransactions: builder.scheduledTransactions, // might be nil ChainID: node.RootChainID, AccessMetrics: notNil(builder.AccessMetrics), ConnFactory: connFactory, diff --git a/cmd/observer/node_builder/observer_builder.go b/cmd/observer/node_builder/observer_builder.go index e2958d9fefa..94fbe778399 100644 --- a/cmd/observer/node_builder/observer_builder.go +++ b/cmd/observer/node_builder/observer_builder.go @@ -1963,25 +1963,26 @@ func (builder *ObserverServiceBuilder) enqueueRPCServer() { ) backendParams := backend.Params{ - State: node.State, - Blocks: node.Storage.Blocks, - Headers: node.Storage.Headers, - Collections: node.Storage.Collections, - Transactions: node.Storage.Transactions, - ExecutionReceipts: node.Storage.Receipts, - ExecutionResults: node.Storage.Results, - ChainID: node.RootChainID, - AccessMetrics: accessMetrics, - ConnFactory: connFactory, - RetryEnabled: false, - MaxHeightRange: backendConfig.MaxHeightRange, - Log: node.Logger, - SnapshotHistoryLimit: backend.DefaultSnapshotHistoryLimit, - Communicator: node_communicator.NewNodeCommunicator(backendConfig.CircuitBreakerConfig.Enabled), - BlockTracker: blockTracker, - ScriptExecutionMode: scriptExecMode, - EventQueryMode: eventQueryMode, - TxResultQueryMode: txResultQueryMode, + State: node.State, + Blocks: node.Storage.Blocks, + Headers: node.Storage.Headers, + Collections: node.Storage.Collections, + Transactions: node.Storage.Transactions, + ExecutionReceipts: node.Storage.Receipts, + ExecutionResults: node.Storage.Results, + ScheduledTransactions: builder.scheduledTransactions, + ChainID: node.RootChainID, + AccessMetrics: accessMetrics, + ConnFactory: connFactory, + RetryEnabled: false, + MaxHeightRange: backendConfig.MaxHeightRange, + Log: node.Logger, + SnapshotHistoryLimit: backend.DefaultSnapshotHistoryLimit, + Communicator: node_communicator.NewNodeCommunicator(backendConfig.CircuitBreakerConfig.Enabled), + BlockTracker: blockTracker, + ScriptExecutionMode: scriptExecMode, + EventQueryMode: eventQueryMode, + TxResultQueryMode: txResultQueryMode, SubscriptionHandler: subscription.NewSubscriptionHandler( builder.Logger, broadcaster, diff --git a/cmd/util/cmd/run-script/cmd.go b/cmd/util/cmd/run-script/cmd.go index 520440f1601..31a1f1f2fc4 100644 --- a/cmd/util/cmd/run-script/cmd.go +++ b/cmd/util/cmd/run-script/cmd.go @@ -2,13 +2,11 @@ package run_script import ( "context" - "errors" "fmt" "io" "os" jsoncdc "github.com/onflow/cadence/encoding/json" - "github.com/onflow/flow/protobuf/go/flow/entities" "github.com/rs/zerolog/log" "github.com/spf13/cobra" @@ -18,7 +16,6 @@ import ( "github.com/onflow/flow-go/engine/access/rest" "github.com/onflow/flow-go/engine/access/rest/websockets" "github.com/onflow/flow-go/engine/access/state_stream/backend" - "github.com/onflow/flow-go/engine/access/subscription" "github.com/onflow/flow-go/engine/execution/computation" "github.com/onflow/flow-go/fvm" "github.com/onflow/flow-go/fvm/storage/snapshot" @@ -30,8 +27,6 @@ import ( modutil "github.com/onflow/flow-go/module/util" ) -var ErrNotImplemented = errors.New("not implemented") - var ( flagPayloads string flagState string @@ -246,6 +241,7 @@ func runScript( } type api struct { + access.UnimplementedAPI chainID flow.ChainID vm *fvm.VirtualMachine ctx fvm.Context @@ -264,154 +260,6 @@ func (a *api) GetNetworkParameters(_ context.Context) accessmodel.NetworkParamet } } -func (*api) GetNodeVersionInfo(_ context.Context) (*accessmodel.NodeVersionInfo, error) { - return nil, errors.New("unimplemented") -} - -func (*api) GetLatestBlockHeader(_ context.Context, _ bool) (*flow.Header, flow.BlockStatus, error) { - return nil, flow.BlockStatusUnknown, errors.New("unimplemented") -} - -func (*api) GetBlockHeaderByHeight(_ context.Context, _ uint64) (*flow.Header, flow.BlockStatus, error) { - return nil, flow.BlockStatusUnknown, errors.New("unimplemented") -} - -func (*api) GetBlockHeaderByID(_ context.Context, _ flow.Identifier) (*flow.Header, flow.BlockStatus, error) { - return nil, flow.BlockStatusUnknown, errors.New("unimplemented") -} - -func (*api) GetLatestBlock(_ context.Context, _ bool) (*flow.Block, flow.BlockStatus, error) { - return nil, flow.BlockStatusUnknown, errors.New("unimplemented") -} - -func (*api) GetBlockByHeight(_ context.Context, _ uint64) (*flow.Block, flow.BlockStatus, error) { - return nil, flow.BlockStatusUnknown, errors.New("unimplemented") -} - -func (*api) GetBlockByID(_ context.Context, _ flow.Identifier) (*flow.Block, flow.BlockStatus, error) { - return nil, flow.BlockStatusUnknown, errors.New("unimplemented") -} - -func (*api) GetCollectionByID(_ context.Context, _ flow.Identifier) (*flow.LightCollection, error) { - return nil, errors.New("unimplemented") -} - -func (*api) GetFullCollectionByID(_ context.Context, _ flow.Identifier) (*flow.Collection, error) { - return nil, errors.New("unimplemented") -} - -func (*api) SendTransaction(_ context.Context, _ *flow.TransactionBody) error { - return errors.New("unimplemented") -} - -func (*api) GetTransaction(_ context.Context, _ flow.Identifier) (*flow.TransactionBody, error) { - return nil, errors.New("unimplemented") -} - -func (*api) GetTransactionsByBlockID(_ context.Context, _ flow.Identifier) ([]*flow.TransactionBody, error) { - return nil, errors.New("unimplemented") -} - -func (*api) GetTransactionResult( - _ context.Context, - _ flow.Identifier, - _ flow.Identifier, - _ flow.Identifier, - _ entities.EventEncodingVersion, -) (*accessmodel.TransactionResult, error) { - return nil, errors.New("unimplemented") -} - -func (*api) GetTransactionResultByIndex( - _ context.Context, - _ flow.Identifier, - _ uint32, - _ entities.EventEncodingVersion, -) (*accessmodel.TransactionResult, error) { - return nil, errors.New("unimplemented") -} - -func (*api) GetTransactionResultsByBlockID( - _ context.Context, - _ flow.Identifier, - _ entities.EventEncodingVersion, -) ([]*accessmodel.TransactionResult, error) { - return nil, errors.New("unimplemented") -} - -func (*api) GetSystemTransaction( - _ context.Context, - _ flow.Identifier, - _ flow.Identifier, -) (*flow.TransactionBody, error) { - return nil, errors.New("unimplemented") -} - -func (*api) GetSystemTransactionResult( - _ context.Context, - _ flow.Identifier, - _ flow.Identifier, - _ entities.EventEncodingVersion, -) (*accessmodel.TransactionResult, error) { - return nil, errors.New("unimplemented") -} - -func (*api) GetAccount(_ context.Context, _ flow.Address) (*flow.Account, error) { - return nil, errors.New("unimplemented") -} - -func (*api) GetAccountAtLatestBlock(_ context.Context, _ flow.Address) (*flow.Account, error) { - return nil, errors.New("unimplemented") -} - -func (*api) GetAccountAtBlockHeight(_ context.Context, _ flow.Address, _ uint64) (*flow.Account, error) { - return nil, errors.New("unimplemented") -} - -func (*api) GetAccountBalanceAtLatestBlock(_ context.Context, _ flow.Address) (uint64, error) { - return 0, errors.New("unimplemented") -} - -func (*api) GetAccountBalanceAtBlockHeight( - _ context.Context, - _ flow.Address, - _ uint64, -) (uint64, error) { - return 0, errors.New("unimplemented") -} - -func (*api) GetAccountKeyAtLatestBlock( - _ context.Context, - _ flow.Address, - _ uint32, -) (*flow.AccountPublicKey, error) { - return nil, errors.New("unimplemented") -} - -func (*api) GetAccountKeyAtBlockHeight( - _ context.Context, - _ flow.Address, - _ uint32, - _ uint64, -) (*flow.AccountPublicKey, error) { - return nil, errors.New("unimplemented") -} - -func (*api) GetAccountKeysAtLatestBlock( - _ context.Context, - _ flow.Address, -) ([]flow.AccountPublicKey, error) { - return nil, errors.New("unimplemented") -} - -func (*api) GetAccountKeysAtBlockHeight( - _ context.Context, - _ flow.Address, - _ uint64, -) ([]flow.AccountPublicKey, error) { - return nil, errors.New("unimplemented") -} - func (a *api) ExecuteScriptAtLatestBlock( _ context.Context, script []byte, @@ -425,144 +273,3 @@ func (a *api) ExecuteScriptAtLatestBlock( arguments, ) } - -func (*api) ExecuteScriptAtBlockHeight( - _ context.Context, - _ uint64, - _ []byte, - _ [][]byte, -) ([]byte, error) { - return nil, errors.New("unimplemented") -} - -func (*api) ExecuteScriptAtBlockID( - _ context.Context, - _ flow.Identifier, - _ []byte, - _ [][]byte, -) ([]byte, error) { - return nil, errors.New("unimplemented") -} - -func (a *api) GetEventsForHeightRange( - _ context.Context, - _ string, - _, _ uint64, - _ entities.EventEncodingVersion, -) ([]flow.BlockEvents, error) { - return nil, errors.New("unimplemented") -} - -func (a *api) GetEventsForBlockIDs( - _ context.Context, - _ string, - _ []flow.Identifier, - _ entities.EventEncodingVersion, -) ([]flow.BlockEvents, error) { - return nil, errors.New("unimplemented") -} - -func (*api) GetLatestProtocolStateSnapshot(_ context.Context) ([]byte, error) { - return nil, errors.New("unimplemented") -} - -func (*api) GetProtocolStateSnapshotByBlockID(_ context.Context, _ flow.Identifier) ([]byte, error) { - return nil, errors.New("unimplemented") -} - -func (*api) GetProtocolStateSnapshotByHeight(_ context.Context, _ uint64) ([]byte, error) { - return nil, errors.New("unimplemented") -} - -func (*api) GetExecutionResultForBlockID(_ context.Context, _ flow.Identifier) (*flow.ExecutionResult, error) { - return nil, errors.New("unimplemented") -} - -func (*api) GetExecutionResultByID(_ context.Context, _ flow.Identifier) (*flow.ExecutionResult, error) { - return nil, errors.New("unimplemented") -} - -func (*api) SubscribeBlocksFromStartBlockID( - _ context.Context, - _ flow.Identifier, - _ flow.BlockStatus, -) subscription.Subscription { - return nil -} - -func (*api) SubscribeBlocksFromStartHeight( - _ context.Context, - _ uint64, - _ flow.BlockStatus, -) subscription.Subscription { - return nil -} - -func (*api) SubscribeBlocksFromLatest( - _ context.Context, - _ flow.BlockStatus, -) subscription.Subscription { - return nil -} - -func (*api) SubscribeBlockHeadersFromStartBlockID( - _ context.Context, - _ flow.Identifier, - _ flow.BlockStatus, -) subscription.Subscription { - return nil -} - -func (*api) SubscribeBlockHeadersFromStartHeight( - _ context.Context, - _ uint64, - _ flow.BlockStatus, -) subscription.Subscription { - return nil -} - -func (*api) SubscribeBlockHeadersFromLatest( - _ context.Context, - _ flow.BlockStatus, -) subscription.Subscription { - return nil -} - -func (*api) SubscribeBlockDigestsFromStartBlockID( - _ context.Context, - _ flow.Identifier, - _ flow.BlockStatus, -) subscription.Subscription { - return nil -} - -func (*api) SubscribeBlockDigestsFromStartHeight( - _ context.Context, - _ uint64, - _ flow.BlockStatus, -) subscription.Subscription { - return nil -} - -func (*api) SubscribeBlockDigestsFromLatest( - _ context.Context, - _ flow.BlockStatus, -) subscription.Subscription { - return nil -} - -func (a *api) SubscribeTransactionStatuses( - _ context.Context, - _ flow.Identifier, - _ entities.EventEncodingVersion, -) subscription.Subscription { - return subscription.NewFailedSubscription(ErrNotImplemented, "failed to call SubscribeTransactionStatuses") -} - -func (a *api) SendAndSubscribeTransactionStatuses( - _ context.Context, - _ *flow.TransactionBody, - _ entities.EventEncodingVersion, -) subscription.Subscription { - return subscription.NewFailedSubscription(ErrNotImplemented, "failed to call SendAndSubscribeTransactionStatuses") -} diff --git a/engine/access/access_test.go b/engine/access/access_test.go index f5620fafe66..16ce5150d9e 100644 --- a/engine/access/access_test.go +++ b/engine/access/access_test.go @@ -979,6 +979,10 @@ func (suite *Suite) TestGetTransactionResult() { ctx := irrecoverable.NewMockSignalerContext(suite.T(), background) ingestEng.Start(ctx) <-ingestEng.Ready() + defer func() { + cancel() + <-ingestEng.Done() + }() processExecutionReceipts := func( block *flow.Block, @@ -1042,7 +1046,7 @@ func (suite *Suite) TestGetTransactionResult() { getReq := &accessproto.GetTransactionRequest{ Id: txId[:], } - resp, err := handler.GetTransactionResult(context.Background(), getReq) + resp, err := handler.GetTransactionResult(ctx, getReq) assertTransactionResult(resp, err) }) @@ -1052,28 +1056,18 @@ func (suite *Suite) TestGetTransactionResult() { Id: txId[:], BlockId: blockId[:], } - resp, err := handler.GetTransactionResult(context.Background(), getReq) + resp, err := handler.GetTransactionResult(ctx, getReq) assertTransactionResult(resp, err) }) - suite.Run("Get transaction result with wrong transaction ID and correct block ID", func() { - getReq := &accessproto.GetTransactionRequest{ - Id: txIdNegative[:], - BlockId: blockId[:], - } - resp, err := handler.GetTransactionResult(context.Background(), getReq) - require.Error(suite.T(), err) - require.Nil(suite.T(), resp) - }) - suite.Run("Get transaction result with wrong block ID and correct transaction ID", func() { getReq := &accessproto.GetTransactionRequest{ Id: txId[:], BlockId: blockNegativeId[:], } - resp, err := handler.GetTransactionResult(context.Background(), getReq) + resp, err := handler.GetTransactionResult(ctx, getReq) require.Error(suite.T(), err) - require.Contains(suite.T(), err.Error(), "failed to find: transaction not in block") + require.Contains(suite.T(), err.Error(), "transaction found in block") require.Nil(suite.T(), resp) }) @@ -1083,7 +1077,7 @@ func (suite *Suite) TestGetTransactionResult() { Id: txId[:], CollectionId: collectionId[:], } - resp, err := handler.GetTransactionResult(context.Background(), getReq) + resp, err := handler.GetTransactionResult(ctx, getReq) assertTransactionResult(resp, err) }) @@ -1092,7 +1086,7 @@ func (suite *Suite) TestGetTransactionResult() { Id: txId[:], CollectionId: collectionIdNegative[:], } - resp, err := handler.GetTransactionResult(context.Background(), getReq) + resp, err := handler.GetTransactionResult(ctx, getReq) require.Error(suite.T(), err) require.Nil(suite.T(), resp) }) @@ -1102,7 +1096,7 @@ func (suite *Suite) TestGetTransactionResult() { Id: txIdNegative[:], CollectionId: collectionId[:], } - resp, err := handler.GetTransactionResult(context.Background(), getReq) + resp, err := handler.GetTransactionResult(ctx, getReq) require.Error(suite.T(), err) require.Nil(suite.T(), resp) }) @@ -1114,7 +1108,7 @@ func (suite *Suite) TestGetTransactionResult() { BlockId: blockId[:], CollectionId: collectionId[:], } - resp, err := handler.GetTransactionResult(context.Background(), getReq) + resp, err := handler.GetTransactionResult(ctx, getReq) assertTransactionResult(resp, err) }) @@ -1124,7 +1118,7 @@ func (suite *Suite) TestGetTransactionResult() { BlockId: blockId[:], CollectionId: collectionIdNegative[:], } - resp, err := handler.GetTransactionResult(context.Background(), getReq) + resp, err := handler.GetTransactionResult(ctx, getReq) require.Error(suite.T(), err) require.Nil(suite.T(), resp) }) diff --git a/engine/access/apiproxy/access_api_proxy.go b/engine/access/apiproxy/access_api_proxy.go index 79d7dc15fdd..b57ff64fa63 100644 --- a/engine/access/apiproxy/access_api_proxy.go +++ b/engine/access/apiproxy/access_api_proxy.go @@ -30,6 +30,8 @@ type FlowAccessAPIRouter struct { useIndex bool } +var _ access.AccessAPIServer = (*FlowAccessAPIRouter)(nil) + type Params struct { Log zerolog.Logger Metrics *metrics.ObserverCollector @@ -214,6 +216,30 @@ func (h *FlowAccessAPIRouter) GetSystemTransactionResult(context context.Context return res, err } +func (h *FlowAccessAPIRouter) GetScheduledTransaction(context context.Context, req *access.GetScheduledTransactionRequest) (*access.TransactionResponse, error) { + if h.useIndex { + res, err := h.local.GetScheduledTransaction(context, req) + h.log(LocalApiService, "GetScheduledTransaction", err) + return res, err + } + + res, err := h.upstream.GetScheduledTransaction(context, req) + h.log(UpstreamApiService, "GetScheduledTransaction", err) + return res, err +} + +func (h *FlowAccessAPIRouter) GetScheduledTransactionResult(context context.Context, req *access.GetScheduledTransactionResultRequest) (*access.TransactionResultResponse, error) { + if h.useIndex { + res, err := h.local.GetScheduledTransactionResult(context, req) + h.log(LocalApiService, "GetScheduledTransactionResult", err) + return res, err + } + + res, err := h.upstream.GetScheduledTransactionResult(context, req) + h.log(UpstreamApiService, "GetScheduledTransactionResult", err) + return res, err +} + func (h *FlowAccessAPIRouter) GetAccount(context context.Context, req *access.GetAccountRequest) (*access.GetAccountResponse, error) { if h.useIndex { res, err := h.local.GetAccount(context, req) @@ -488,6 +514,8 @@ type FlowAccessAPIForwarder struct { *forwarder.Forwarder } +var _ access.AccessAPIServer = (*FlowAccessAPIForwarder)(nil) + func NewFlowAccessAPIForwarder(identities flow.IdentitySkeletonList, connectionFactory connection.ConnectionFactory) (*FlowAccessAPIForwarder, error) { forwarder, err := forwarder.NewForwarder(identities, connectionFactory) if err != nil { @@ -641,6 +669,26 @@ func (h *FlowAccessAPIForwarder) GetSystemTransaction(context context.Context, r return upstream.GetSystemTransaction(context, req) } +func (h *FlowAccessAPIForwarder) GetScheduledTransaction(context context.Context, req *access.GetScheduledTransactionRequest) (*access.TransactionResponse, error) { + // This is a passthrough request + upstream, closer, err := h.FaultTolerantClient() + if err != nil { + return nil, err + } + defer closer.Close() + return upstream.GetScheduledTransaction(context, req) +} + +func (h *FlowAccessAPIForwarder) GetScheduledTransactionResult(context context.Context, req *access.GetScheduledTransactionResultRequest) (*access.TransactionResultResponse, error) { + // This is a passthrough request + upstream, closer, err := h.FaultTolerantClient() + if err != nil { + return nil, err + } + defer closer.Close() + return upstream.GetScheduledTransactionResult(context, req) +} + func (h *FlowAccessAPIForwarder) GetSystemTransactionResult(context context.Context, req *access.GetSystemTransactionResultRequest) (*access.TransactionResultResponse, error) { // This is a passthrough request upstream, closer, err := h.FaultTolerantClient() @@ -840,6 +888,26 @@ func (h *FlowAccessAPIForwarder) GetLatestProtocolStateSnapshot(context context. return upstream.GetLatestProtocolStateSnapshot(context, req) } +func (h *FlowAccessAPIForwarder) GetProtocolStateSnapshotByBlockID(context context.Context, req *access.GetProtocolStateSnapshotByBlockIDRequest) (*access.ProtocolStateSnapshotResponse, error) { + // This is a passthrough request + upstream, closer, err := h.FaultTolerantClient() + if err != nil { + return nil, err + } + defer closer.Close() + return upstream.GetProtocolStateSnapshotByBlockID(context, req) +} + +func (h *FlowAccessAPIForwarder) GetProtocolStateSnapshotByHeight(context context.Context, req *access.GetProtocolStateSnapshotByHeightRequest) (*access.ProtocolStateSnapshotResponse, error) { + // This is a passthrough request + upstream, closer, err := h.FaultTolerantClient() + if err != nil { + return nil, err + } + defer closer.Close() + return upstream.GetProtocolStateSnapshotByHeight(context, req) +} + func (h *FlowAccessAPIForwarder) GetExecutionResultForBlockID(context context.Context, req *access.GetExecutionResultForBlockIDRequest) (*access.ExecutionResultForBlockIDResponse, error) { // This is a passthrough request upstream, closer, err := h.FaultTolerantClient() @@ -859,3 +927,43 @@ func (h *FlowAccessAPIForwarder) GetExecutionResultByID(context context.Context, defer closer.Close() return upstream.GetExecutionResultByID(context, req) } + +func (h *FlowAccessAPIForwarder) SendAndSubscribeTransactionStatuses(req *access.SendAndSubscribeTransactionStatusesRequest, server access.AccessAPI_SendAndSubscribeTransactionStatusesServer) error { + return status.Errorf(codes.Unimplemented, "method SendAndSubscribeTransactionStatuses not implemented") +} + +func (h *FlowAccessAPIForwarder) SubscribeBlockDigestsFromLatest(req *access.SubscribeBlockDigestsFromLatestRequest, server access.AccessAPI_SubscribeBlockDigestsFromLatestServer) error { + return status.Errorf(codes.Unimplemented, "method SubscribeBlockDigestsFromLatest not implemented") +} + +func (h *FlowAccessAPIForwarder) SubscribeBlockDigestsFromStartBlockID(req *access.SubscribeBlockDigestsFromStartBlockIDRequest, server access.AccessAPI_SubscribeBlockDigestsFromStartBlockIDServer) error { + return status.Errorf(codes.Unimplemented, "method SubscribeBlockDigestsFromStartBlockID not implemented") +} + +func (h *FlowAccessAPIForwarder) SubscribeBlockDigestsFromStartHeight(req *access.SubscribeBlockDigestsFromStartHeightRequest, server access.AccessAPI_SubscribeBlockDigestsFromStartHeightServer) error { + return status.Errorf(codes.Unimplemented, "method SubscribeBlockDigestsFromStartHeight not implemented") +} + +func (h *FlowAccessAPIForwarder) SubscribeBlockHeadersFromLatest(req *access.SubscribeBlockHeadersFromLatestRequest, server access.AccessAPI_SubscribeBlockHeadersFromLatestServer) error { + return status.Errorf(codes.Unimplemented, "method SubscribeBlockHeadersFromLatest not implemented") +} + +func (h *FlowAccessAPIForwarder) SubscribeBlockHeadersFromStartBlockID(req *access.SubscribeBlockHeadersFromStartBlockIDRequest, server access.AccessAPI_SubscribeBlockHeadersFromStartBlockIDServer) error { + return status.Errorf(codes.Unimplemented, "method SubscribeBlockHeadersFromStartBlockID not implemented") +} + +func (h *FlowAccessAPIForwarder) SubscribeBlockHeadersFromStartHeight(req *access.SubscribeBlockHeadersFromStartHeightRequest, server access.AccessAPI_SubscribeBlockHeadersFromStartHeightServer) error { + return status.Errorf(codes.Unimplemented, "method SubscribeBlockHeadersFromStartHeight not implemented") +} + +func (h *FlowAccessAPIForwarder) SubscribeBlocksFromLatest(req *access.SubscribeBlocksFromLatestRequest, server access.AccessAPI_SubscribeBlocksFromLatestServer) error { + return status.Errorf(codes.Unimplemented, "method SubscribeBlocksFromLatest not implemented") +} + +func (h *FlowAccessAPIForwarder) SubscribeBlocksFromStartBlockID(req *access.SubscribeBlocksFromStartBlockIDRequest, server access.AccessAPI_SubscribeBlocksFromStartBlockIDServer) error { + return status.Errorf(codes.Unimplemented, "method SubscribeBlocksFromStartBlockID not implemented") +} + +func (h *FlowAccessAPIForwarder) SubscribeBlocksFromStartHeight(req *access.SubscribeBlocksFromStartHeightRequest, server access.AccessAPI_SubscribeBlocksFromStartHeightServer) error { + return status.Errorf(codes.Unimplemented, "method SubscribeBlocksFromStartHeight not implemented") +} diff --git a/engine/access/mock/access_api_client.go b/engine/access/mock/access_api_client.go index ee70f212d21..e0365a924cf 100644 --- a/engine/access/mock/access_api_client.go +++ b/engine/access/mock/access_api_client.go @@ -1090,6 +1090,80 @@ func (_m *AccessAPIClient) GetProtocolStateSnapshotByHeight(ctx context.Context, return r0, r1 } +// GetScheduledTransaction provides a mock function with given fields: ctx, in, opts +func (_m *AccessAPIClient) GetScheduledTransaction(ctx context.Context, in *access.GetScheduledTransactionRequest, opts ...grpc.CallOption) (*access.TransactionResponse, error) { + _va := make([]interface{}, len(opts)) + for _i := range opts { + _va[_i] = opts[_i] + } + var _ca []interface{} + _ca = append(_ca, ctx, in) + _ca = append(_ca, _va...) + ret := _m.Called(_ca...) + + if len(ret) == 0 { + panic("no return value specified for GetScheduledTransaction") + } + + var r0 *access.TransactionResponse + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, *access.GetScheduledTransactionRequest, ...grpc.CallOption) (*access.TransactionResponse, error)); ok { + return rf(ctx, in, opts...) + } + if rf, ok := ret.Get(0).(func(context.Context, *access.GetScheduledTransactionRequest, ...grpc.CallOption) *access.TransactionResponse); ok { + r0 = rf(ctx, in, opts...) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*access.TransactionResponse) + } + } + + if rf, ok := ret.Get(1).(func(context.Context, *access.GetScheduledTransactionRequest, ...grpc.CallOption) error); ok { + r1 = rf(ctx, in, opts...) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// GetScheduledTransactionResult provides a mock function with given fields: ctx, in, opts +func (_m *AccessAPIClient) GetScheduledTransactionResult(ctx context.Context, in *access.GetScheduledTransactionResultRequest, opts ...grpc.CallOption) (*access.TransactionResultResponse, error) { + _va := make([]interface{}, len(opts)) + for _i := range opts { + _va[_i] = opts[_i] + } + var _ca []interface{} + _ca = append(_ca, ctx, in) + _ca = append(_ca, _va...) + ret := _m.Called(_ca...) + + if len(ret) == 0 { + panic("no return value specified for GetScheduledTransactionResult") + } + + var r0 *access.TransactionResultResponse + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, *access.GetScheduledTransactionResultRequest, ...grpc.CallOption) (*access.TransactionResultResponse, error)); ok { + return rf(ctx, in, opts...) + } + if rf, ok := ret.Get(0).(func(context.Context, *access.GetScheduledTransactionResultRequest, ...grpc.CallOption) *access.TransactionResultResponse); ok { + r0 = rf(ctx, in, opts...) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*access.TransactionResultResponse) + } + } + + if rf, ok := ret.Get(1).(func(context.Context, *access.GetScheduledTransactionResultRequest, ...grpc.CallOption) error); ok { + r1 = rf(ctx, in, opts...) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + // GetSystemTransaction provides a mock function with given fields: ctx, in, opts func (_m *AccessAPIClient) GetSystemTransaction(ctx context.Context, in *access.GetSystemTransactionRequest, opts ...grpc.CallOption) (*access.TransactionResponse, error) { _va := make([]interface{}, len(opts)) diff --git a/engine/access/mock/access_api_server.go b/engine/access/mock/access_api_server.go index c7af30bdae4..e77f00374bc 100644 --- a/engine/access/mock/access_api_server.go +++ b/engine/access/mock/access_api_server.go @@ -885,6 +885,66 @@ func (_m *AccessAPIServer) GetProtocolStateSnapshotByHeight(_a0 context.Context, return r0, r1 } +// GetScheduledTransaction provides a mock function with given fields: _a0, _a1 +func (_m *AccessAPIServer) GetScheduledTransaction(_a0 context.Context, _a1 *access.GetScheduledTransactionRequest) (*access.TransactionResponse, error) { + ret := _m.Called(_a0, _a1) + + if len(ret) == 0 { + panic("no return value specified for GetScheduledTransaction") + } + + var r0 *access.TransactionResponse + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, *access.GetScheduledTransactionRequest) (*access.TransactionResponse, error)); ok { + return rf(_a0, _a1) + } + if rf, ok := ret.Get(0).(func(context.Context, *access.GetScheduledTransactionRequest) *access.TransactionResponse); ok { + r0 = rf(_a0, _a1) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*access.TransactionResponse) + } + } + + if rf, ok := ret.Get(1).(func(context.Context, *access.GetScheduledTransactionRequest) error); ok { + r1 = rf(_a0, _a1) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// GetScheduledTransactionResult provides a mock function with given fields: _a0, _a1 +func (_m *AccessAPIServer) GetScheduledTransactionResult(_a0 context.Context, _a1 *access.GetScheduledTransactionResultRequest) (*access.TransactionResultResponse, error) { + ret := _m.Called(_a0, _a1) + + if len(ret) == 0 { + panic("no return value specified for GetScheduledTransactionResult") + } + + var r0 *access.TransactionResultResponse + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, *access.GetScheduledTransactionResultRequest) (*access.TransactionResultResponse, error)); ok { + return rf(_a0, _a1) + } + if rf, ok := ret.Get(0).(func(context.Context, *access.GetScheduledTransactionResultRequest) *access.TransactionResultResponse); ok { + r0 = rf(_a0, _a1) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*access.TransactionResultResponse) + } + } + + if rf, ok := ret.Get(1).(func(context.Context, *access.GetScheduledTransactionResultRequest) error); ok { + r1 = rf(_a0, _a1) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + // GetSystemTransaction provides a mock function with given fields: _a0, _a1 func (_m *AccessAPIServer) GetSystemTransaction(_a0 context.Context, _a1 *access.GetSystemTransactionRequest) (*access.TransactionResponse, error) { ret := _m.Called(_a0, _a1) diff --git a/engine/access/rpc/backend/backend.go b/engine/access/rpc/backend/backend.go index 2c00ae9bc28..80dfd1876e9 100644 --- a/engine/access/rpc/backend/backend.go +++ b/engine/access/rpc/backend/backend.go @@ -25,12 +25,12 @@ import ( "github.com/onflow/flow-go/engine/access/rpc/backend/transactions/provider" "github.com/onflow/flow-go/engine/access/rpc/backend/transactions/status" txstream "github.com/onflow/flow-go/engine/access/rpc/backend/transactions/stream" + "github.com/onflow/flow-go/engine/access/rpc/backend/transactions/system" "github.com/onflow/flow-go/engine/access/rpc/connection" "github.com/onflow/flow-go/engine/access/subscription" "github.com/onflow/flow-go/engine/access/subscription/tracker" "github.com/onflow/flow-go/engine/common/rpc" "github.com/onflow/flow-go/engine/common/version" - "github.com/onflow/flow-go/fvm/blueprints" accessmodel "github.com/onflow/flow-go/model/access" "github.com/onflow/flow-go/model/flow" "github.com/onflow/flow-go/module" @@ -93,6 +93,7 @@ type Params struct { ExecutionReceipts storage.ExecutionReceipts ExecutionResults storage.ExecutionResults TxResultErrorMessages storage.TransactionResultErrorMessages + ScheduledTransactions storage.ScheduledTransactionsReader ChainID flow.ChainID AccessMetrics module.AccessMetrics ConnFactory connection.ConnectionFactory @@ -130,7 +131,7 @@ func New(params Params) (*Backend, error) { return nil, fmt.Errorf("failed to initialize script logging cache: %w", err) } - var txResCache *lru.Cache[flow.Identifier, *accessmodel.TransactionResult] + var txResCache transactions.TxResultCache = transactions.NewNoopTxResultCache() if params.TxResultCacheSize > 0 { txResCache, err = lru.New[flow.Identifier, *accessmodel.TransactionResult](int(params.TxResultCacheSize)) if err != nil { @@ -138,12 +139,10 @@ func New(params Params) (*Backend, error) { } } - // the system tx is hardcoded and never changes during runtime - systemTx, err := blueprints.SystemChunkTransaction(params.ChainID.Chain()) + systemCollection, err := system.DefaultSystemCollection(params.ChainID, params.ScheduledCallbacksEnabled) if err != nil { - return nil, fmt.Errorf("failed to create system chunk transaction: %w", err) + return nil, fmt.Errorf("failed to construct system collection: %w", err) } - systemTxID := systemTx.ID() accountsBackend, err := accounts.NewAccountsBackend( params.Log, @@ -222,7 +221,7 @@ func New(params Params) (*Backend, error) { params.EventsIndex, params.TxResultsIndex, params.TxErrorMessageProvider, - systemTxID, + systemCollection, txStatusDeriver, params.ChainID, params.ScheduledCallbacksEnabled, @@ -235,34 +234,35 @@ func New(params Params) (*Backend, error) { params.Communicator, params.ExecNodeIdentitiesProvider, txStatusDeriver, - systemTxID, + systemCollection, params.ChainID, params.ScheduledCallbacksEnabled, ) failoverTxProvider := provider.NewFailoverTransactionProvider(localTxProvider, execNodeTxProvider) txParams := transactions.Params{ - Log: params.Log, - Metrics: params.AccessMetrics, - State: params.State, - ChainID: params.ChainID, - SystemTxID: systemTxID, - StaticCollectionRPCClient: params.CollectionRPC, - HistoricalAccessNodeClients: params.HistoricalAccessNodes, - NodeCommunicator: params.Communicator, - ConnFactory: params.ConnFactory, - EnableRetries: params.RetryEnabled, - NodeProvider: params.ExecNodeIdentitiesProvider, - Blocks: params.Blocks, - Collections: params.Collections, - Transactions: params.Transactions, - TxErrorMessageProvider: params.TxErrorMessageProvider, - TxResultCache: txResCache, - TxValidator: txValidator, - TxStatusDeriver: txStatusDeriver, - EventsIndex: params.EventsIndex, - TxResultsIndex: params.TxResultsIndex, - ScheduledCallbacksEnabled: params.ScheduledCallbacksEnabled, + Log: params.Log, + Metrics: params.AccessMetrics, + State: params.State, + ChainID: params.ChainID, + SystemCollection: systemCollection, + StaticCollectionRPCClient: params.CollectionRPC, + HistoricalAccessNodeClients: params.HistoricalAccessNodes, + NodeCommunicator: params.Communicator, + ConnFactory: params.ConnFactory, + EnableRetries: params.RetryEnabled, + NodeProvider: params.ExecNodeIdentitiesProvider, + Blocks: params.Blocks, + Collections: params.Collections, + Transactions: params.Transactions, + TxErrorMessageProvider: params.TxErrorMessageProvider, + ScheduledTransactions: params.ScheduledTransactions, + TxResultCache: txResCache, + TxValidator: txValidator, + TxStatusDeriver: txStatusDeriver, + EventsIndex: params.EventsIndex, + TxResultsIndex: params.TxResultsIndex, + ScheduledTransactionsEnabled: params.ScheduledCallbacksEnabled, } switch params.TxResultQueryMode { diff --git a/engine/access/rpc/backend/backend_test.go b/engine/access/rpc/backend/backend_test.go index acee00c278d..abe3983006d 100644 --- a/engine/access/rpc/backend/backend_test.go +++ b/engine/access/rpc/backend/backend_test.go @@ -10,7 +10,6 @@ import ( "time" "github.com/coreos/go-semver/semver" - accessproto "github.com/onflow/flow/protobuf/go/flow/access" "github.com/onflow/flow/protobuf/go/flow/entities" entitiesproto "github.com/onflow/flow/protobuf/go/flow/entities" @@ -53,6 +52,7 @@ import ( "github.com/onflow/flow-go/storage/operation/pebbleimpl" "github.com/onflow/flow-go/storage/store" "github.com/onflow/flow-go/utils/unittest" + "github.com/onflow/flow-go/utils/unittest/fixtures" "github.com/onflow/flow-go/utils/unittest/mocks" ) @@ -66,19 +66,21 @@ var eventEncodingVersions = []entitiesproto.EventEncodingVersion{ type Suite struct { suite.Suite + g *fixtures.GeneratorSuite state *protocol.State snapshot *protocol.Snapshot log zerolog.Logger - blocks *storagemock.Blocks - headers *storagemock.Headers - collections *storagemock.Collections - transactions *storagemock.Transactions - receipts *storagemock.ExecutionReceipts - results *storagemock.ExecutionResults - transactionResults *storagemock.LightTransactionResults - events *storagemock.Events - txErrorMessages *storagemock.TransactionResultErrorMessages + blocks *storagemock.Blocks + headers *storagemock.Headers + collections *storagemock.Collections + transactions *storagemock.Transactions + receipts *storagemock.ExecutionReceipts + results *storagemock.ExecutionResults + transactionResults *storagemock.LightTransactionResults + events *storagemock.Events + scheduledTransactions *storagemock.ScheduledTransactions + txErrorMessages *storagemock.TransactionResultErrorMessages db storage.DB dbDir string @@ -104,6 +106,8 @@ func TestHandler(t *testing.T) { } func (suite *Suite) SetupTest() { + suite.g = fixtures.NewGeneratorSuite() + suite.log = unittest.Logger() suite.state = new(protocol.State) suite.snapshot = new(protocol.Snapshot) @@ -126,6 +130,7 @@ func (suite *Suite) SetupTest() { suite.execClient = new(accessmock.ExecutionAPIClient) suite.transactionResults = storagemock.NewLightTransactionResults(suite.T()) suite.events = storagemock.NewEvents(suite.T()) + suite.scheduledTransactions = storagemock.NewScheduledTransactions(suite.T()) suite.chainID = flow.Testnet suite.historicalAccessClient = new(accessmock.AccessAPIClient) suite.connectionFactory = connectionmock.NewConnectionFactory(suite.T()) @@ -1073,7 +1078,6 @@ func (suite *Suite) TestTransactionStatusTransition() { }, nil) light := collection.Light() - suite.collections.On("LightByID", collection.ID()).Return(light, nil) // transaction storage returns the corresponding transaction suite.transactions. @@ -1309,7 +1313,7 @@ func (suite *Suite) TestTransactionPendingToFinalizedStatusTransition() { On("Head"). Return(headBlock.ToHeader(), nil) - snapshotAtBlock := new(protocol.Snapshot) + snapshotAtBlock := protocol.NewSnapshot(suite.T()) snapshotAtBlock.On("Head").Return(refBlock.ToHeader(), nil) _, enIDs := suite.setupReceipts(block) @@ -1332,22 +1336,12 @@ func (suite *Suite) TestTransactionPendingToFinalizedStatusTransition() { // collection storage returns a not found error if tx is pending, else it returns the collection light reference suite.collections. On("LightByTransactionID", txID). - Return(func(txID flow.Identifier) *flow.LightCollection { + Return(func(txID flow.Identifier) (*flow.LightCollection, error) { if currentState == flow.TransactionStatusPending { - return nil + return nil, storage.ErrNotFound } - collLight := collection.Light() - return collLight - }, - func(txID flow.Identifier) error { - if currentState == flow.TransactionStatusPending { - return storage.ErrNotFound - } - return nil - }) - - light := collection.Light() - suite.collections.On("LightByID", mock.Anything).Return(light, nil) + return collection.Light(), nil + }) // refBlock storage returns the corresponding refBlock suite.blocks. @@ -1417,6 +1411,10 @@ func (suite *Suite) TestTransactionResultUnknown() { On("ByID", txID). Return(nil, storage.ErrNotFound) + suite.collections. + On("LightByTransactionID", txID). + Return(nil, storage.ErrNotFound) + params := suite.defaultBackendParams() backend, err := New(params) @@ -1760,45 +1758,45 @@ func (suite *Suite) TestGetNetworkParameters() { // TestGetTransactionResultEventEncodingVersion tests the GetTransactionResult function with different event encoding versions. func (suite *Suite) TestGetTransactionResultEventEncodingVersion() { - suite.state.On("Sealed").Return(suite.snapshot, nil).Maybe() + refBlock := suite.g.Blocks().Fixture(fixtures.Block.WithHeight(2)) - ctx := context.Background() + tx := suite.g.Transactions().Fixture(fixtures.Transaction.WithReferenceBlockID(refBlock.ID())) + txID := tx.ID() - collection := unittest.CollectionFixture(1) - transactionBody := collection.Transactions[0] - // block which will eventually contain the transaction - block := unittest.BlockFixture( - unittest.Block.WithPayload(unittest.PayloadFixture( - unittest.WithGuarantees( - unittest.CollectionGuaranteesWithCollectionIDFixture([]*flow.Collection{&collection})...), - )), - ) - blockId := block.ID() + collection := suite.g.Collections().Fixture(fixtures.Collection.WithTransactions(tx)) + light := collection.Light() + collectionID := collection.ID() - // reference block to which the transaction points to - refBlock := unittest.BlockFixture( - unittest.Block.WithHeight(2), - ) - transactionBody.ReferenceBlockID = refBlock.ID() - txId := transactionBody.ID() + guarantee := suite.g.Guarantees().Fixture(fixtures.Guarantee.WithCollectionID(collectionID)) + payload := suite.g.Payloads().Fixture(fixtures.Payload.WithGuarantees(guarantee)) + block := suite.g.Blocks().Fixture(fixtures.Block.WithPayload(payload)) + blockID := block.ID() + + ctx := context.Background() // transaction storage returns the corresponding transaction suite.transactions. - On("ByID", txId). - Return(transactionBody, nil) + On("ByID", txID). + Return(tx, nil) - light := collection.Light() - suite.collections.On("LightByID", mock.Anything).Return(light, nil) + suite.collections. + On("LightByTransactionID", txID). + Return(light, nil) + + suite.blocks. + On("ByCollectionID", collectionID). + Return(block, nil) suite.snapshot.On("Head").Return(block.ToHeader(), nil) + suite.state.On("Sealed").Return(suite.snapshot, nil) // block storage returns the corresponding block suite.blocks. - On("ByID", blockId). + On("ByID", blockID). Return(block, nil) _, fixedENIDs := suite.setupReceipts(block) - suite.state.On("Final").Return(suite.snapshot, nil).Maybe() + suite.state.On("Final").Return(suite.snapshot, nil) suite.snapshot.On("Identities", mock.Anything).Return(fixedENIDs, nil) suite.fixedExecutionNodeIDs = fixedENIDs.NodeIDs() @@ -1822,13 +1820,13 @@ func (suite *Suite) TestGetTransactionResultEventEncodingVersion() { suite.execClient. On("GetTransactionResult", ctx, &execproto.GetTransactionResultRequest{ - BlockId: blockId[:], - TransactionId: txId[:], + BlockId: blockID[:], + TransactionId: txID[:], }). Return(exeEventResp, nil). Once() - result, err := backend.GetTransactionResult(ctx, txId, blockId, flow.ZeroID, version) + result, err := backend.GetTransactionResult(ctx, txID, blockID, flow.ZeroID, version) suite.Require().NoError(err) suite.Require().NotNil(result) diff --git a/engine/access/rpc/backend/historical_access_test.go b/engine/access/rpc/backend/historical_access_test.go index 4c6a076bde0..fd3a6b5694b 100644 --- a/engine/access/rpc/backend/historical_access_test.go +++ b/engine/access/rpc/backend/historical_access_test.go @@ -5,11 +5,10 @@ import ( accessproto "github.com/onflow/flow/protobuf/go/flow/access" "github.com/onflow/flow/protobuf/go/flow/entities" - "google.golang.org/grpc/codes" - "google.golang.org/grpc/status" "github.com/onflow/flow-go/engine/common/rpc/convert" "github.com/onflow/flow-go/model/flow" + "github.com/onflow/flow-go/storage" "github.com/onflow/flow-go/utils/unittest" ) @@ -18,12 +17,16 @@ func (suite *Suite) TestHistoricalTransactionResult() { ctx := context.Background() collection := unittest.CollectionFixture(1) transactionBody := collection.Transactions[0] - txID := transactionBody.ID() + + suite.collections. + On("LightByTransactionID", txID). + Return(nil, storage.ErrNotFound) + // transaction storage returns the corresponding transaction suite.transactions. On("ByID", txID). - Return(nil, status.Errorf(codes.NotFound, "not found on main node")) + Return(nil, storage.ErrNotFound) accessEventReq := accessproto.GetTransactionRequest{ Id: txID[:], @@ -74,7 +77,7 @@ func (suite *Suite) TestHistoricalTransaction() { // transaction storage returns the corresponding transaction suite.transactions. On("ByID", txID). - Return(nil, status.Errorf(codes.NotFound, "not found on main node")) + Return(nil, storage.ErrNotFound) accessEventReq := accessproto.GetTransactionRequest{ Id: txID[:], diff --git a/engine/access/rpc/backend/transactions/cache.go b/engine/access/rpc/backend/transactions/cache.go new file mode 100644 index 00000000000..1a42bfd3a51 --- /dev/null +++ b/engine/access/rpc/backend/transactions/cache.go @@ -0,0 +1,44 @@ +package transactions + +import ( + accessmodel "github.com/onflow/flow-go/model/access" + "github.com/onflow/flow-go/model/flow" +) + +// TxResultCache is a cache for transaction results used by the API to avoid unnecessary lookups on +// historical access nodes. +type TxResultCache interface { + // Get retrieves a cached transaction result by transaction ID, returns true if it exists in the + // cache, otherwise false. + Get(flow.Identifier) (*accessmodel.TransactionResult, bool) + + // Add adds a transaction result to the cache keyed by its transaction ID, and returns true if + // an entry was evicted in the process. + Add(flow.Identifier, *accessmodel.TransactionResult) bool +} + +// NoopTxResultCache is a no-op implementation of the TxResultCache interface. +type NoopTxResultCache struct{} + +var _ TxResultCache = (*NoopTxResultCache)(nil) + +// NewNoopTxResultCache creates a new no-op implementation of the TxResultCache interface. +func NewNoopTxResultCache() *NoopTxResultCache { + return &NoopTxResultCache{} +} + +// Get retrieves a cached transaction result by transaction ID, returns true if it exists in the +// cache, otherwise false. +// +// This is a no-op implementation and always returns nil and false +func (n *NoopTxResultCache) Get(flow.Identifier) (*accessmodel.TransactionResult, bool) { + return nil, false +} + +// Add adds a transaction result to the cache keyed by its transaction ID, and returns true if +// an entry was evicted in the process. +// +// This is a no-op implementation which simply ignores the inputs and returns false. +func (n *NoopTxResultCache) Add(flow.Identifier, *accessmodel.TransactionResult) bool { + return false +} diff --git a/engine/access/rpc/backend/transactions/provider/execution_node.go b/engine/access/rpc/backend/transactions/provider/execution_node.go index 1031e66de60..0df02af833a 100644 --- a/engine/access/rpc/backend/transactions/provider/execution_node.go +++ b/engine/access/rpc/backend/transactions/provider/execution_node.go @@ -14,6 +14,7 @@ import ( "github.com/onflow/flow-go/engine/access/rpc/backend/common" "github.com/onflow/flow-go/engine/access/rpc/backend/node_communicator" txstatus "github.com/onflow/flow-go/engine/access/rpc/backend/transactions/status" + "github.com/onflow/flow-go/engine/access/rpc/backend/transactions/system" "github.com/onflow/flow-go/engine/access/rpc/connection" "github.com/onflow/flow-go/engine/common/rpc" "github.com/onflow/flow-go/engine/common/rpc/convert" @@ -39,9 +40,9 @@ type ENTransactionProvider struct { txStatusDeriver *txstatus.TxStatusDeriver - systemTxID flow.Identifier - scheduledCallbacksEnabled bool - processScheduledCallbackEventType flow.EventType + systemCollection *system.SystemCollection + scheduledTransactionsEnabled bool + processScheduledTransactionEventType flow.EventType } var _ TransactionProvider = (*ENTransactionProvider)(nil) @@ -54,23 +55,23 @@ func NewENTransactionProvider( nodeCommunicator node_communicator.Communicator, execNodeIdentitiesProvider *rpc.ExecutionNodeIdentitiesProvider, txStatusDeriver *txstatus.TxStatusDeriver, - systemTxID flow.Identifier, + systemCollection *system.SystemCollection, chainID flow.ChainID, - scheduledCallbacksEnabled bool, + scheduledTransactionsEnabled bool, ) *ENTransactionProvider { env := systemcontracts.SystemContractsForChain(chainID).AsTemplateEnv() return &ENTransactionProvider{ - log: log.With().Str("transaction_provider", "execution_node").Logger(), - state: state, - collections: collections, - connFactory: connFactory, - nodeCommunicator: nodeCommunicator, - nodeProvider: execNodeIdentitiesProvider, - txStatusDeriver: txStatusDeriver, - systemTxID: systemTxID, - chainID: chainID, - scheduledCallbacksEnabled: scheduledCallbacksEnabled, - processScheduledCallbackEventType: blueprints.PendingExecutionEventType(env), + log: log.With().Str("transaction_provider", "execution_node").Logger(), + state: state, + collections: collections, + connFactory: connFactory, + nodeCommunicator: nodeCommunicator, + nodeProvider: execNodeIdentitiesProvider, + txStatusDeriver: txStatusDeriver, + systemCollection: systemCollection, + chainID: chainID, + scheduledTransactionsEnabled: scheduledTransactionsEnabled, + processScheduledTransactionEventType: blueprints.PendingExecutionEventType(env), } } @@ -78,6 +79,7 @@ func (e *ENTransactionProvider) TransactionResult( ctx context.Context, block *flow.Header, transactionID flow.Identifier, + collectionID flow.Identifier, requiredEventEncodingVersion entities.EventEncodingVersion, ) (*accessmodel.TransactionResult, error) { blockID := block.ID() @@ -125,6 +127,7 @@ func (e *ENTransactionProvider) TransactionResult( ErrorMessage: resp.GetErrorMessage(), BlockID: blockID, BlockHeight: block.Height, + CollectionID: collectionID, }, nil } @@ -148,16 +151,11 @@ func (e *ENTransactionProvider) TransactionsByBlockID( // system transactions // TODO: implement system that allows this endpoint to dynamically determine if scheduled // transactions were enabled for this block. See https://github.com/onflow/flow-go/issues/7873 - if !e.scheduledCallbacksEnabled { - systemTx, err := blueprints.SystemChunkTransaction(e.chainID.Chain()) - if err != nil { - return nil, fmt.Errorf("failed to construct system chunk transaction: %w", err) - } - - return append(transactions, systemTx), nil + if !e.scheduledTransactionsEnabled { + return append(transactions, e.systemCollection.Transactions()...), nil } - events, err := e.getBlockEvents(ctx, blockID, e.processScheduledCallbackEventType) + events, err := e.getBlockEvents(ctx, blockID, e.processScheduledTransactionEventType) if err != nil { return nil, rpc.ConvertError(err, "failed to retrieve events from any execution node", codes.Internal) } @@ -174,6 +172,7 @@ func (e *ENTransactionProvider) TransactionResultByIndex( ctx context.Context, block *flow.Block, index uint32, + collectionID flow.Identifier, encodingVersion entities.EventEncodingVersion, ) (*accessmodel.TransactionResult, error) { blockID := block.ID() @@ -183,6 +182,13 @@ func (e *ENTransactionProvider) TransactionResultByIndex( Index: index, } + // first, try to find the transaction ID within the locally available data. + // if it's not found, then it's unlikely the EN will have the data either. + txID, err := e.getTransactionIDByIndex(ctx, block, index) + if err != nil { + return nil, err + } + execNodes, err := e.nodeProvider.ExecutionNodesForBlockID( ctx, blockID, @@ -213,12 +219,14 @@ func (e *ENTransactionProvider) TransactionResultByIndex( // convert to response, cache and return return &accessmodel.TransactionResult{ - Status: txStatus, - StatusCode: uint(resp.GetStatusCode()), - Events: events, - ErrorMessage: resp.GetErrorMessage(), - BlockID: blockID, - BlockHeight: block.Height, + Status: txStatus, + StatusCode: uint(resp.GetStatusCode()), + Events: events, + ErrorMessage: resp.GetErrorMessage(), + BlockID: blockID, + BlockHeight: block.Height, + TransactionID: txID, + CollectionID: collectionID, }, nil } @@ -255,7 +263,6 @@ func (e *ENTransactionProvider) TransactionResultsByBlockID( } userTxResults, err := e.userTransactionResults( - ctx, executionResponse, block, blockID, @@ -293,58 +300,27 @@ func (e *ENTransactionProvider) TransactionResultsByBlockID( return append(userTxResults, systemTxResults...), nil } -func (e *ENTransactionProvider) SystemTransaction( +// ScheduledTransactionsByBlockID constructs the scheduled transaction bodies using events from the +// execution node response. +// +// Expected error returns during normal operation: +// - [codes.Internal]: if the scheduled transactions cannot be constructed +// - [status.Error]: for any error returned by the execution node +func (e *ENTransactionProvider) ScheduledTransactionsByBlockID( ctx context.Context, - block *flow.Block, - txID flow.Identifier, -) (*flow.TransactionBody, error) { - blockID := block.ID() - - if txID == e.systemTxID || !e.scheduledCallbacksEnabled { - systemTx, err := blueprints.SystemChunkTransaction(e.chainID.Chain()) - if err != nil { - return nil, status.Errorf(codes.Internal, "failed to construct system chunk transaction: %v", err) - } - - if txID == systemTx.ID() { - return systemTx, nil - } - return nil, fmt.Errorf("transaction %s not found in block %s", txID, blockID) - } - - events, err := e.getBlockEvents(ctx, blockID, e.processScheduledCallbackEventType) + header *flow.Header, +) ([]*flow.TransactionBody, error) { + events, err := e.getBlockEvents(ctx, header.ID(), e.processScheduledTransactionEventType) if err != nil { return nil, rpc.ConvertError(err, "failed to retrieve events from any execution node", codes.Internal) } - sysCollection, err := blueprints.SystemCollection(e.chainID.Chain(), events) + txs, err := blueprints.ExecuteCallbacksTransactions(e.chainID.Chain(), events) if err != nil { - return nil, status.Errorf(codes.Internal, "could not construct system collection: %v", err) - } - - for _, tx := range sysCollection.Transactions { - if tx.ID() == txID { - return tx, nil - } - } - - return nil, status.Errorf(codes.NotFound, "system transaction not found") -} - -func (e *ENTransactionProvider) SystemTransactionResult( - ctx context.Context, - block *flow.Block, - txID flow.Identifier, - requiredEventEncodingVersion entities.EventEncodingVersion, -) (*accessmodel.TransactionResult, error) { - // make sure the request is for a system transaction - if txID != e.systemTxID { - if _, err := e.SystemTransaction(ctx, block, txID); err != nil { - return nil, status.Errorf(codes.NotFound, "system transaction not found") - } + return nil, status.Errorf(codes.Internal, "could not construct scheduled transactions: %v", err) } - return e.TransactionResult(ctx, block.ToHeader(), txID, requiredEventEncodingVersion) + return txs, nil } // userTransactionResults constructs the user transaction results from the execution node response. @@ -352,7 +328,6 @@ func (e *ENTransactionProvider) SystemTransactionResult( // It does so by iterating through all user collections (without system collection) in the block // and constructing the transaction results. func (e *ENTransactionProvider) userTransactionResults( - ctx context.Context, resp *execproto.GetTransactionResultsResponse, block *flow.Block, blockID flow.Identifier, @@ -460,7 +435,7 @@ func (e *ENTransactionProvider) systemTransactionIDs( // TODO: implement system that allows this endpoint to dynamically determine if scheduled // transactions were enabled for this block. See https://github.com/onflow/flow-go/issues/7873 if len(systemTxResults) == 1 { - return []flow.Identifier{e.systemTxID}, nil + return []flow.Identifier{e.systemCollection.SystemTxID()}, nil } // if scheduled callbacks are enabled, the first transaction will always be the "process" transaction @@ -750,3 +725,55 @@ func (e *ENTransactionProvider) tryGetBlockEventsByBlockIDs( return resp, nil } + +// getTransactionIDByIndex returns the transaction ID for the given transaction index in the block. +// This is found by searching guarantees and then the system collection until the transaction is found. +// +// Expected error returns during normal operation: +// - [codes.NotFound]: if the transaction is not found in the block. +// - [codes.Internal]: if the system collection cannot be constructed. +func (e *ENTransactionProvider) getTransactionIDByIndex(ctx context.Context, block *flow.Block, index uint32) (flow.Identifier, error) { + i := uint32(0) + + // first search the user transactions within the guarantees + for _, guarantee := range block.Payload.Guarantees { + collection, err := e.collections.LightByID(guarantee.CollectionID) + if err != nil { + return flow.ZeroID, rpc.ConvertStorageError(err) + } + + for _, txID := range collection.Transactions { + if i == index { + return txID, nil + } + i++ + } + } + + // then search the system collection + if !e.scheduledTransactionsEnabled { + if i == index { + return e.systemCollection.SystemTxID(), nil + } + return flow.ZeroID, status.Errorf(codes.NotFound, "transaction with index %d not found in block", index) + } + + events, err := e.getBlockEvents(ctx, block.ID(), e.processScheduledTransactionEventType) + if err != nil { + return flow.ZeroID, rpc.ConvertError(err, "failed to retrieve events from any execution node", codes.Internal) + } + + systemCollection, err := blueprints.SystemCollection(e.chainID.Chain(), events) + if err != nil { + return flow.ZeroID, status.Errorf(codes.Internal, "could not construct system collection: %v", err) + } + + for _, tx := range systemCollection.Transactions { + if i == index { + return tx.ID(), nil + } + i++ + } + + return flow.ZeroID, status.Errorf(codes.NotFound, "transaction with index %d not found in block", index) +} diff --git a/engine/access/rpc/backend/transactions/provider/failover.go b/engine/access/rpc/backend/transactions/provider/failover.go index d20b1a2c37d..0eff62be9f3 100644 --- a/engine/access/rpc/backend/transactions/provider/failover.go +++ b/engine/access/rpc/backend/transactions/provider/failover.go @@ -27,30 +27,30 @@ func (f *FailoverTransactionProvider) TransactionResult( ctx context.Context, header *flow.Header, txID flow.Identifier, + collectionID flow.Identifier, encodingVersion entities.EventEncodingVersion, ) (*accessmodel.TransactionResult, error) { - localResult, localErr := f.localProvider.TransactionResult(ctx, header, txID, encodingVersion) + localResult, localErr := f.localProvider.TransactionResult(ctx, header, txID, collectionID, encodingVersion) if localErr == nil { return localResult, nil } - execNodeResult, execNodeErr := f.execNodeProvider.TransactionResult(ctx, header, txID, encodingVersion) - return execNodeResult, execNodeErr + return f.execNodeProvider.TransactionResult(ctx, header, txID, collectionID, encodingVersion) } func (f *FailoverTransactionProvider) TransactionResultByIndex( ctx context.Context, block *flow.Block, index uint32, + collectionID flow.Identifier, encodingVersion entities.EventEncodingVersion, ) (*accessmodel.TransactionResult, error) { - localResult, localErr := f.localProvider.TransactionResultByIndex(ctx, block, index, encodingVersion) + localResult, localErr := f.localProvider.TransactionResultByIndex(ctx, block, index, collectionID, encodingVersion) if localErr == nil { return localResult, nil } - execNodeResult, execNodeErr := f.execNodeProvider.TransactionResultByIndex(ctx, block, index, encodingVersion) - return execNodeResult, execNodeErr + return f.execNodeProvider.TransactionResultByIndex(ctx, block, index, collectionID, encodingVersion) } func (f *FailoverTransactionProvider) TransactionResultsByBlockID( @@ -63,8 +63,7 @@ func (f *FailoverTransactionProvider) TransactionResultsByBlockID( return localResults, nil } - execNodeResults, execNodeErr := f.execNodeProvider.TransactionResultsByBlockID(ctx, block, encodingVersion) - return execNodeResults, execNodeErr + return f.execNodeProvider.TransactionResultsByBlockID(ctx, block, encodingVersion) } func (f *FailoverTransactionProvider) TransactionsByBlockID( @@ -76,35 +75,17 @@ func (f *FailoverTransactionProvider) TransactionsByBlockID( return localResults, nil } - execNodeResults, execNodeErr := f.execNodeProvider.TransactionsByBlockID(ctx, block) - return execNodeResults, execNodeErr + return f.execNodeProvider.TransactionsByBlockID(ctx, block) } -func (f *FailoverTransactionProvider) SystemTransaction( +func (f *FailoverTransactionProvider) ScheduledTransactionsByBlockID( ctx context.Context, - block *flow.Block, - txID flow.Identifier, -) (*flow.TransactionBody, error) { - localResult, localErr := f.localProvider.SystemTransaction(ctx, block, txID) - if localErr == nil { - return localResult, nil - } - - execNodeResult, execNodeErr := f.execNodeProvider.SystemTransaction(ctx, block, txID) - return execNodeResult, execNodeErr -} - -func (f *FailoverTransactionProvider) SystemTransactionResult( - ctx context.Context, - block *flow.Block, - txID flow.Identifier, - requiredEventEncodingVersion entities.EventEncodingVersion, -) (*accessmodel.TransactionResult, error) { - localResult, localErr := f.localProvider.SystemTransactionResult(ctx, block, txID, requiredEventEncodingVersion) + header *flow.Header, +) ([]*flow.TransactionBody, error) { + localResults, localErr := f.localProvider.ScheduledTransactionsByBlockID(ctx, header) if localErr == nil { - return localResult, nil + return localResults, nil } - execNodeResult, execNodeErr := f.execNodeProvider.SystemTransactionResult(ctx, block, txID, requiredEventEncodingVersion) - return execNodeResult, execNodeErr + return f.execNodeProvider.ScheduledTransactionsByBlockID(ctx, header) } diff --git a/engine/access/rpc/backend/transactions/provider/local.go b/engine/access/rpc/backend/transactions/provider/local.go index 9fa1ebec785..95a84214d35 100644 --- a/engine/access/rpc/backend/transactions/provider/local.go +++ b/engine/access/rpc/backend/transactions/provider/local.go @@ -13,6 +13,7 @@ import ( "github.com/onflow/flow-go/engine/access/index" "github.com/onflow/flow-go/engine/access/rpc/backend/transactions/error_messages" txstatus "github.com/onflow/flow-go/engine/access/rpc/backend/transactions/status" + "github.com/onflow/flow-go/engine/access/rpc/backend/transactions/system" "github.com/onflow/flow-go/engine/common/rpc" "github.com/onflow/flow-go/engine/common/rpc/convert" "github.com/onflow/flow-go/fvm/blueprints" @@ -29,16 +30,16 @@ var ErrTransactionNotInBlock = errors.New("transaction not in block") // LocalTransactionProvider provides functionality for retrieving transaction results and error messages from local storages type LocalTransactionProvider struct { - state protocol.State - collections storage.Collections - blocks storage.Blocks - eventsIndex *index.EventsIndex - txResultsIndex *index.TransactionResultsIndex - txErrorMessages error_messages.Provider - systemTxID flow.Identifier - txStatusDeriver *txstatus.TxStatusDeriver - scheduledCallbacksEnabled bool - chainID flow.ChainID + state protocol.State + collections storage.Collections + blocks storage.Blocks + eventsIndex *index.EventsIndex + txResultsIndex *index.TransactionResultsIndex + txErrorMessages error_messages.Provider + defaultSystemCollection *system.SystemCollection + txStatusDeriver *txstatus.TxStatusDeriver + scheduledTransactionsEnabled bool + chainID flow.ChainID } var _ TransactionProvider = (*LocalTransactionProvider)(nil) @@ -50,22 +51,22 @@ func NewLocalTransactionProvider( eventsIndex *index.EventsIndex, txResultsIndex *index.TransactionResultsIndex, txErrorMessages error_messages.Provider, - systemTxID flow.Identifier, + systemCollection *system.SystemCollection, txStatusDeriver *txstatus.TxStatusDeriver, chainID flow.ChainID, - scheduledCallbacksEnabled bool, + scheduledTransactionsEnabled bool, ) *LocalTransactionProvider { return &LocalTransactionProvider{ - state: state, - collections: collections, - blocks: blocks, - eventsIndex: eventsIndex, - txResultsIndex: txResultsIndex, - txErrorMessages: txErrorMessages, - systemTxID: systemTxID, - txStatusDeriver: txStatusDeriver, - scheduledCallbacksEnabled: scheduledCallbacksEnabled, - chainID: chainID, + state: state, + collections: collections, + blocks: blocks, + eventsIndex: eventsIndex, + txResultsIndex: txResultsIndex, + txErrorMessages: txErrorMessages, + defaultSystemCollection: systemCollection, + txStatusDeriver: txStatusDeriver, + scheduledTransactionsEnabled: scheduledTransactionsEnabled, + chainID: chainID, } } @@ -80,20 +81,21 @@ func NewLocalTransactionProvider( // getter or when deriving transaction status. func (t *LocalTransactionProvider) TransactionResult( ctx context.Context, - block *flow.Header, + header *flow.Header, transactionID flow.Identifier, + collectionID flow.Identifier, encodingVersion entities.EventEncodingVersion, ) (*accessmodel.TransactionResult, error) { - blockID := block.ID() - txResult, err := t.txResultsIndex.ByBlockIDTransactionID(blockID, block.Height, transactionID) + blockID := header.ID() + txResult, err := t.txResultsIndex.ByBlockIDTransactionID(blockID, header.Height, transactionID) if err != nil { - return nil, rpc.ConvertIndexError(err, block.Height, "failed to get transaction result") + return nil, rpc.ConvertIndexError(err, header.Height, "failed to get transaction result") } var txErrorMessage string var txStatusCode uint = 0 if txResult.Failed { - txErrorMessage, err = t.txErrorMessages.ErrorMessageByTransactionID(ctx, blockID, block.Height, transactionID) + txErrorMessage, err = t.txErrorMessages.ErrorMessageByTransactionID(ctx, blockID, header.Height, transactionID) if err != nil { return nil, err } @@ -110,7 +112,7 @@ func (t *LocalTransactionProvider) TransactionResult( txStatusCode = 1 // statusCode of 1 indicates an error and 0 indicates no error, the same as on EN } - txStatus, err := t.txStatusDeriver.DeriveTransactionStatus(block.Height, true) + txStatus, err := t.txStatusDeriver.DeriveTransactionStatus(header.Height, true) if err != nil { if !errors.Is(err, state.ErrUnknownSnapshotReference) { irrecoverable.Throw(ctx, err) @@ -118,9 +120,9 @@ func (t *LocalTransactionProvider) TransactionResult( return nil, rpc.ConvertStorageError(err) } - events, err := t.eventsIndex.ByBlockIDTransactionID(blockID, block.Height, transactionID) + events, err := t.eventsIndex.ByBlockIDTransactionID(blockID, header.Height, transactionID) if err != nil { - return nil, rpc.ConvertIndexError(err, block.Height, "failed to get events") + return nil, rpc.ConvertIndexError(err, header.Height, "failed to get events") } // events are encoded in CCF format in storage. convert to JSON-CDC if requested @@ -138,7 +140,8 @@ func (t *LocalTransactionProvider) TransactionResult( Events: events, ErrorMessage: txErrorMessage, BlockID: blockID, - BlockHeight: block.Height, + BlockHeight: header.Height, + CollectionID: collectionID, }, nil } @@ -155,6 +158,7 @@ func (t *LocalTransactionProvider) TransactionResultByIndex( ctx context.Context, block *flow.Block, index uint32, + collectionID flow.Identifier, eventEncoding entities.EventEncodingVersion, ) (*accessmodel.TransactionResult, error) { blockID := block.ID() @@ -199,11 +203,6 @@ func (t *LocalTransactionProvider) TransactionResultByIndex( } } - collectionID, err := t.lookupCollectionIDInBlock(block, txResult.TransactionID) - if err != nil { - return nil, err - } - return &accessmodel.TransactionResult{ TransactionID: txResult.TransactionID, Status: txStatus, @@ -241,15 +240,11 @@ func (t *LocalTransactionProvider) TransactionsByBlockID( transactions = append(transactions, collection.Transactions...) } - if !t.scheduledCallbacksEnabled { - systemTx, err := blueprints.SystemChunkTransaction(t.chainID.Chain()) - if err != nil { - return nil, fmt.Errorf("failed to construct system chunk transaction: %w", err) - } - - return append(transactions, systemTx), nil + if !t.scheduledTransactionsEnabled { + return append(transactions, t.defaultSystemCollection.Transactions()...), nil } + // generate the system collection which includes scheduled transactions events, err := t.eventsIndex.ByBlockID(blockID, block.Height) if err != nil { return nil, rpc.ConvertIndexError(err, block.Height, "failed to get events") @@ -269,9 +264,6 @@ func (t *LocalTransactionProvider) TransactionsByBlockID( // - codes.Internal when event payload conversion failed. // - indexer.ErrIndexNotInitialized when txResultsIndex not initialized // - storage.ErrHeightNotIndexed when data is unavailable -// -// All other errors are considered as state corruption (fatal) or internal errors in the transaction error message -// getter or when deriving transaction status. func (t *LocalTransactionProvider) TransactionResultsByBlockID( ctx context.Context, block *flow.Block, @@ -358,79 +350,29 @@ func (t *LocalTransactionProvider) TransactionResultsByBlockID( return results, nil } -// SystemTransaction rebuilds the system transaction from storage -func (t *LocalTransactionProvider) SystemTransaction( +// ScheduledTransactionsByBlockID constructs the scheduled transaction bodies using events from the +// local storage. +// +// Expected error returns during normal operation: +// - [codes.NotFound]: if the events are not found for the block ID. +// - [codes.OutOfRange]: if the events are not available for the block height. +// - [codes.FailedPrecondition]: if the events index is not initialized. +// - [codes.Internal]: if the scheduled transactions cannot be constructed. +func (t *LocalTransactionProvider) ScheduledTransactionsByBlockID( ctx context.Context, - block *flow.Block, - txID flow.Identifier, -) (*flow.TransactionBody, error) { - blockID := block.ID() - - if txID == t.systemTxID || !t.scheduledCallbacksEnabled { - systemTx, err := blueprints.SystemChunkTransaction(t.chainID.Chain()) - if err != nil { - return nil, status.Errorf(codes.Internal, "failed to construct system chunk transaction: %v", err) - } - - if txID == systemTx.ID() { - return systemTx, nil - } - return nil, fmt.Errorf("transaction %s not found in block %s", txID, blockID) - } - - events, err := t.eventsIndex.ByBlockID(blockID, block.Height) + header *flow.Header, +) ([]*flow.TransactionBody, error) { + events, err := t.eventsIndex.ByBlockID(header.ID(), header.Height) if err != nil { - return nil, rpc.ConvertIndexError(err, block.Height, "failed to get events") + return nil, rpc.ConvertIndexError(err, header.Height, "failed to get events to reconstruct scheduled transactions") } - sysCollection, err := blueprints.SystemCollection(t.chainID.Chain(), events) + txs, err := blueprints.ExecuteCallbacksTransactions(t.chainID.Chain(), events) if err != nil { - return nil, status.Errorf(codes.Internal, "could not construct system collection: %v", err) - } - - for _, tx := range sysCollection.Transactions { - if tx.ID() == txID { - return tx, nil - } - } - - return nil, status.Errorf(codes.NotFound, "system transaction not found") -} - -func (t *LocalTransactionProvider) SystemTransactionResult( - ctx context.Context, - block *flow.Block, - txID flow.Identifier, - requiredEventEncodingVersion entities.EventEncodingVersion, -) (*accessmodel.TransactionResult, error) { - // make sure the request is for a system transaction - if txID != t.systemTxID { - if _, err := t.SystemTransaction(ctx, block, txID); err != nil { - return nil, status.Errorf(codes.NotFound, "system transaction not found") - } + return nil, status.Errorf(codes.Internal, "could not construct scheduled transactions: %v", err) } - return t.TransactionResult(ctx, block.ToHeader(), txID, requiredEventEncodingVersion) -} -// lookupCollectionIDInBlock returns the collection ID based on the transaction ID. -// The lookup is performed in block collections. -func (t *LocalTransactionProvider) lookupCollectionIDInBlock( - block *flow.Block, - txID flow.Identifier, -) (flow.Identifier, error) { - for _, guarantee := range block.Payload.Guarantees { - collection, err := t.collections.LightByID(guarantee.CollectionID) - if err != nil { - return flow.ZeroID, fmt.Errorf("failed to get collection %s in indexed block: %w", guarantee.CollectionID, err) - } - - for _, collectionTxID := range collection.Transactions { - if collectionTxID == txID { - return guarantee.CollectionID, nil - } - } - } - return flow.ZeroID, ErrTransactionNotInBlock + return txs, nil } // buildTxIDToCollectionIDMapping returns a map of transaction ID to collection ID based on the provided block. diff --git a/engine/access/rpc/backend/transactions/provider/mock/transaction_provider.go b/engine/access/rpc/backend/transactions/provider/mock/transaction_provider.go index 31d2714821c..1a762a038de 100644 --- a/engine/access/rpc/backend/transactions/provider/mock/transaction_provider.go +++ b/engine/access/rpc/backend/transactions/provider/mock/transaction_provider.go @@ -19,59 +19,29 @@ type TransactionProvider struct { mock.Mock } -// SystemTransaction provides a mock function with given fields: ctx, block, txID -func (_m *TransactionProvider) SystemTransaction(ctx context.Context, block *flow.Block, txID flow.Identifier) (*flow.TransactionBody, error) { - ret := _m.Called(ctx, block, txID) +// ScheduledTransactionsByBlockID provides a mock function with given fields: ctx, header +func (_m *TransactionProvider) ScheduledTransactionsByBlockID(ctx context.Context, header *flow.Header) ([]*flow.TransactionBody, error) { + ret := _m.Called(ctx, header) if len(ret) == 0 { - panic("no return value specified for SystemTransaction") + panic("no return value specified for ScheduledTransactionsByBlockID") } - var r0 *flow.TransactionBody - var r1 error - if rf, ok := ret.Get(0).(func(context.Context, *flow.Block, flow.Identifier) (*flow.TransactionBody, error)); ok { - return rf(ctx, block, txID) - } - if rf, ok := ret.Get(0).(func(context.Context, *flow.Block, flow.Identifier) *flow.TransactionBody); ok { - r0 = rf(ctx, block, txID) - } else { - if ret.Get(0) != nil { - r0 = ret.Get(0).(*flow.TransactionBody) - } - } - - if rf, ok := ret.Get(1).(func(context.Context, *flow.Block, flow.Identifier) error); ok { - r1 = rf(ctx, block, txID) - } else { - r1 = ret.Error(1) - } - - return r0, r1 -} - -// SystemTransactionResult provides a mock function with given fields: ctx, block, txID, requiredEventEncodingVersion -func (_m *TransactionProvider) SystemTransactionResult(ctx context.Context, block *flow.Block, txID flow.Identifier, requiredEventEncodingVersion entities.EventEncodingVersion) (*access.TransactionResult, error) { - ret := _m.Called(ctx, block, txID, requiredEventEncodingVersion) - - if len(ret) == 0 { - panic("no return value specified for SystemTransactionResult") - } - - var r0 *access.TransactionResult + var r0 []*flow.TransactionBody var r1 error - if rf, ok := ret.Get(0).(func(context.Context, *flow.Block, flow.Identifier, entities.EventEncodingVersion) (*access.TransactionResult, error)); ok { - return rf(ctx, block, txID, requiredEventEncodingVersion) + if rf, ok := ret.Get(0).(func(context.Context, *flow.Header) ([]*flow.TransactionBody, error)); ok { + return rf(ctx, header) } - if rf, ok := ret.Get(0).(func(context.Context, *flow.Block, flow.Identifier, entities.EventEncodingVersion) *access.TransactionResult); ok { - r0 = rf(ctx, block, txID, requiredEventEncodingVersion) + if rf, ok := ret.Get(0).(func(context.Context, *flow.Header) []*flow.TransactionBody); ok { + r0 = rf(ctx, header) } else { if ret.Get(0) != nil { - r0 = ret.Get(0).(*access.TransactionResult) + r0 = ret.Get(0).([]*flow.TransactionBody) } } - if rf, ok := ret.Get(1).(func(context.Context, *flow.Block, flow.Identifier, entities.EventEncodingVersion) error); ok { - r1 = rf(ctx, block, txID, requiredEventEncodingVersion) + if rf, ok := ret.Get(1).(func(context.Context, *flow.Header) error); ok { + r1 = rf(ctx, header) } else { r1 = ret.Error(1) } @@ -79,9 +49,9 @@ func (_m *TransactionProvider) SystemTransactionResult(ctx context.Context, bloc return r0, r1 } -// TransactionResult provides a mock function with given fields: ctx, header, txID, encodingVersion -func (_m *TransactionProvider) TransactionResult(ctx context.Context, header *flow.Header, txID flow.Identifier, encodingVersion entities.EventEncodingVersion) (*access.TransactionResult, error) { - ret := _m.Called(ctx, header, txID, encodingVersion) +// TransactionResult provides a mock function with given fields: ctx, header, txID, collectionID, encodingVersion +func (_m *TransactionProvider) TransactionResult(ctx context.Context, header *flow.Header, txID flow.Identifier, collectionID flow.Identifier, encodingVersion entities.EventEncodingVersion) (*access.TransactionResult, error) { + ret := _m.Called(ctx, header, txID, collectionID, encodingVersion) if len(ret) == 0 { panic("no return value specified for TransactionResult") @@ -89,19 +59,19 @@ func (_m *TransactionProvider) TransactionResult(ctx context.Context, header *fl var r0 *access.TransactionResult var r1 error - if rf, ok := ret.Get(0).(func(context.Context, *flow.Header, flow.Identifier, entities.EventEncodingVersion) (*access.TransactionResult, error)); ok { - return rf(ctx, header, txID, encodingVersion) + if rf, ok := ret.Get(0).(func(context.Context, *flow.Header, flow.Identifier, flow.Identifier, entities.EventEncodingVersion) (*access.TransactionResult, error)); ok { + return rf(ctx, header, txID, collectionID, encodingVersion) } - if rf, ok := ret.Get(0).(func(context.Context, *flow.Header, flow.Identifier, entities.EventEncodingVersion) *access.TransactionResult); ok { - r0 = rf(ctx, header, txID, encodingVersion) + if rf, ok := ret.Get(0).(func(context.Context, *flow.Header, flow.Identifier, flow.Identifier, entities.EventEncodingVersion) *access.TransactionResult); ok { + r0 = rf(ctx, header, txID, collectionID, encodingVersion) } else { if ret.Get(0) != nil { r0 = ret.Get(0).(*access.TransactionResult) } } - if rf, ok := ret.Get(1).(func(context.Context, *flow.Header, flow.Identifier, entities.EventEncodingVersion) error); ok { - r1 = rf(ctx, header, txID, encodingVersion) + if rf, ok := ret.Get(1).(func(context.Context, *flow.Header, flow.Identifier, flow.Identifier, entities.EventEncodingVersion) error); ok { + r1 = rf(ctx, header, txID, collectionID, encodingVersion) } else { r1 = ret.Error(1) } @@ -109,9 +79,9 @@ func (_m *TransactionProvider) TransactionResult(ctx context.Context, header *fl return r0, r1 } -// TransactionResultByIndex provides a mock function with given fields: ctx, block, index, encodingVersion -func (_m *TransactionProvider) TransactionResultByIndex(ctx context.Context, block *flow.Block, index uint32, encodingVersion entities.EventEncodingVersion) (*access.TransactionResult, error) { - ret := _m.Called(ctx, block, index, encodingVersion) +// TransactionResultByIndex provides a mock function with given fields: ctx, block, index, collectionID, encodingVersion +func (_m *TransactionProvider) TransactionResultByIndex(ctx context.Context, block *flow.Block, index uint32, collectionID flow.Identifier, encodingVersion entities.EventEncodingVersion) (*access.TransactionResult, error) { + ret := _m.Called(ctx, block, index, collectionID, encodingVersion) if len(ret) == 0 { panic("no return value specified for TransactionResultByIndex") @@ -119,19 +89,19 @@ func (_m *TransactionProvider) TransactionResultByIndex(ctx context.Context, blo var r0 *access.TransactionResult var r1 error - if rf, ok := ret.Get(0).(func(context.Context, *flow.Block, uint32, entities.EventEncodingVersion) (*access.TransactionResult, error)); ok { - return rf(ctx, block, index, encodingVersion) + if rf, ok := ret.Get(0).(func(context.Context, *flow.Block, uint32, flow.Identifier, entities.EventEncodingVersion) (*access.TransactionResult, error)); ok { + return rf(ctx, block, index, collectionID, encodingVersion) } - if rf, ok := ret.Get(0).(func(context.Context, *flow.Block, uint32, entities.EventEncodingVersion) *access.TransactionResult); ok { - r0 = rf(ctx, block, index, encodingVersion) + if rf, ok := ret.Get(0).(func(context.Context, *flow.Block, uint32, flow.Identifier, entities.EventEncodingVersion) *access.TransactionResult); ok { + r0 = rf(ctx, block, index, collectionID, encodingVersion) } else { if ret.Get(0) != nil { r0 = ret.Get(0).(*access.TransactionResult) } } - if rf, ok := ret.Get(1).(func(context.Context, *flow.Block, uint32, entities.EventEncodingVersion) error); ok { - r1 = rf(ctx, block, index, encodingVersion) + if rf, ok := ret.Get(1).(func(context.Context, *flow.Block, uint32, flow.Identifier, entities.EventEncodingVersion) error); ok { + r1 = rf(ctx, block, index, collectionID, encodingVersion) } else { r1 = ret.Error(1) } diff --git a/engine/access/rpc/backend/transactions/provider/provider.go b/engine/access/rpc/backend/transactions/provider/provider.go index 6daa7d6c4bf..61ac32a4388 100644 --- a/engine/access/rpc/backend/transactions/provider/provider.go +++ b/engine/access/rpc/backend/transactions/provider/provider.go @@ -16,6 +16,7 @@ type TransactionProvider interface { ctx context.Context, header *flow.Header, txID flow.Identifier, + collectionID flow.Identifier, encodingVersion entities.EventEncodingVersion, ) (*accessmodel.TransactionResult, error) @@ -23,6 +24,7 @@ type TransactionProvider interface { ctx context.Context, block *flow.Block, index uint32, + collectionID flow.Identifier, encodingVersion entities.EventEncodingVersion, ) (*accessmodel.TransactionResult, error) @@ -37,16 +39,8 @@ type TransactionProvider interface { block *flow.Block, ) ([]*flow.TransactionBody, error) - SystemTransaction( + ScheduledTransactionsByBlockID( ctx context.Context, - block *flow.Block, - txID flow.Identifier, - ) (*flow.TransactionBody, error) - - SystemTransactionResult( - ctx context.Context, - block *flow.Block, - txID flow.Identifier, - requiredEventEncodingVersion entities.EventEncodingVersion, - ) (*accessmodel.TransactionResult, error) + header *flow.Header, + ) ([]*flow.TransactionBody, error) } diff --git a/engine/access/rpc/backend/transactions/retrier/mock/retrier.go b/engine/access/rpc/backend/transactions/retrier/mock/retrier.go index 9dc257e6640..1f1f21ae8e2 100644 --- a/engine/access/rpc/backend/transactions/retrier/mock/retrier.go +++ b/engine/access/rpc/backend/transactions/retrier/mock/retrier.go @@ -12,9 +12,9 @@ type Retrier struct { mock.Mock } -// RegisterTransaction provides a mock function with given fields: height, tx -func (_m *Retrier) RegisterTransaction(height uint64, tx *flow.TransactionBody) { - _m.Called(height, tx) +// RegisterTransaction provides a mock function with given fields: tx +func (_m *Retrier) RegisterTransaction(tx *flow.TransactionBody) { + _m.Called(tx) } // Retry provides a mock function with given fields: height diff --git a/engine/access/rpc/backend/transactions/retrier/noop.go b/engine/access/rpc/backend/transactions/retrier/noop.go index 5f228e078c7..fea47f175d0 100644 --- a/engine/access/rpc/backend/transactions/retrier/noop.go +++ b/engine/access/rpc/backend/transactions/retrier/noop.go @@ -16,4 +16,4 @@ func (n *NoopRetrier) Retry(_ uint64) error { return nil } -func (n *NoopRetrier) RegisterTransaction(_ uint64, _ *flow.TransactionBody) {} +func (n *NoopRetrier) RegisterTransaction(_ *flow.TransactionBody) {} diff --git a/engine/access/rpc/backend/transactions/retrier/retrier.go b/engine/access/rpc/backend/transactions/retrier/retrier.go index 8f829827a14..461ede7e075 100644 --- a/engine/access/rpc/backend/transactions/retrier/retrier.go +++ b/engine/access/rpc/backend/transactions/retrier/retrier.go @@ -11,6 +11,7 @@ import ( "github.com/onflow/flow-go/engine/access/rpc/backend/transactions/status" "github.com/onflow/flow-go/model/flow" "github.com/onflow/flow-go/state" + "github.com/onflow/flow-go/state/protocol" "github.com/onflow/flow-go/storage" ) @@ -26,7 +27,7 @@ type TransactionSender interface { type Retrier interface { Retry(height uint64) error - RegisterTransaction(height uint64, tx *flow.TransactionBody) + RegisterTransaction(tx *flow.TransactionBody) } // RetrierImpl implements a simple retry mechanism for transaction submission. @@ -36,6 +37,7 @@ type RetrierImpl struct { mu sync.RWMutex pendingTransactions BlockHeightToTransactions + state protocol.State blocks storage.Blocks collections storage.Collections @@ -45,6 +47,7 @@ type RetrierImpl struct { func NewRetrier( log zerolog.Logger, + state protocol.State, blocks storage.Blocks, collections storage.Collections, txSender TransactionSender, @@ -53,6 +56,7 @@ func NewRetrier( return &RetrierImpl{ log: log, pendingTransactions: BlockHeightToTransactions{}, + state: state, blocks: blocks, collections: collections, txSender: txSender, @@ -90,7 +94,13 @@ func (r *RetrierImpl) Retry(height uint64) error { } // RegisterTransaction adds a transaction that could possibly be retried -func (r *RetrierImpl) RegisterTransaction(height uint64, tx *flow.TransactionBody) { +func (r *RetrierImpl) RegisterTransaction(tx *flow.TransactionBody) { + referenceBlock, err := r.state.AtBlockID(tx.ReferenceBlockID).Head() + if err != nil { + return + } + height := referenceBlock.Height + r.mu.Lock() defer r.mu.Unlock() if r.pendingTransactions[height] == nil { diff --git a/engine/access/rpc/backend/transactions/stream/stream_backend_test.go b/engine/access/rpc/backend/transactions/stream/stream_backend_test.go index 630a2671480..89013624d2c 100644 --- a/engine/access/rpc/backend/transactions/stream/stream_backend_test.go +++ b/engine/access/rpc/backend/transactions/stream/stream_backend_test.go @@ -8,14 +8,13 @@ import ( "time" lru "github.com/hashicorp/golang-lru/v2" - "google.golang.org/grpc/codes" - "google.golang.org/grpc/status" - "github.com/rs/zerolog" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/mock" "github.com/stretchr/testify/require" "github.com/stretchr/testify/suite" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/status" accessproto "github.com/onflow/flow/protobuf/go/flow/access" "github.com/onflow/flow/protobuf/go/flow/entities" @@ -30,12 +29,12 @@ import ( "github.com/onflow/flow-go/engine/access/rpc/backend/transactions/error_messages" "github.com/onflow/flow-go/engine/access/rpc/backend/transactions/provider" txstatus "github.com/onflow/flow-go/engine/access/rpc/backend/transactions/status" + "github.com/onflow/flow-go/engine/access/rpc/backend/transactions/system" connectionmock "github.com/onflow/flow-go/engine/access/rpc/connection/mock" "github.com/onflow/flow-go/engine/access/subscription" trackermock "github.com/onflow/flow-go/engine/access/subscription/tracker/mock" commonrpc "github.com/onflow/flow-go/engine/common/rpc" "github.com/onflow/flow-go/engine/common/rpc/convert" - "github.com/onflow/flow-go/fvm/blueprints" accessmodel "github.com/onflow/flow-go/model/access" "github.com/onflow/flow-go/model/flow" "github.com/onflow/flow-go/module" @@ -102,8 +101,8 @@ type TransactionStreamSuite struct { db storage.DB dbDir string lastFullBlockHeight *counters.PersistentStrictMonotonicCounter - - systemTx *flow.TransactionBody + systemCollection *system.SystemCollection + scheduledTxEnabled bool fixedExecutionNodeIDs flow.IdentifierList preferredExecutionNodeIDs flow.IdentifierList @@ -148,7 +147,7 @@ func (s *TransactionStreamSuite) SetupTest() { s.eventIndex = index.NewEventsIndex(s.indexReporter, s.events) s.txResultIndex = index.NewTransactionResultsIndex(s.indexReporter, s.transactionResults) - s.systemTx, err = blueprints.SystemChunkTransaction(s.chainID.Chain()) + s.systemCollection, err = system.DefaultSystemCollection(s.chainID, true) s.Require().NoError(err) s.fixedExecutionNodeIDs = nil @@ -235,6 +234,7 @@ func (s *TransactionStreamSuite) initializeBackend() { execNodeProvider, ) + s.scheduledTxEnabled = true localTxProvider := provider.NewLocalTransactionProvider( s.state, s.collections, @@ -242,10 +242,10 @@ func (s *TransactionStreamSuite) initializeBackend() { s.eventIndex, s.txResultIndex, errorMessageProvider, - s.systemTx.ID(), + s.systemCollection, txStatusDeriver, s.chainID, - true, // scheduledCallbacksEnabled + s.scheduledTxEnabled, ) execNodeTxProvider := provider.NewENTransactionProvider( @@ -256,9 +256,9 @@ func (s *TransactionStreamSuite) initializeBackend() { nodeCommunicator, execNodeProvider, txStatusDeriver, - s.systemTx.ID(), + s.systemCollection, s.chainID, - true, // scheduledCallbacksEnabled + s.scheduledTxEnabled, ) txProvider := provider.NewFailoverTransactionProvider(localTxProvider, execNodeTxProvider) @@ -308,7 +308,7 @@ func (s *TransactionStreamSuite) initializeBackend() { Metrics: metrics.NewNoopCollector(), State: s.state, ChainID: s.chainID, - SystemTxID: s.systemTx.ID(), + SystemCollection: s.systemCollection, StaticCollectionRPCClient: client, HistoricalAccessNodeClients: nil, NodeCommunicator: nodeCommunicator, diff --git a/engine/access/rpc/backend/transactions/stream/transaction_metadata.go b/engine/access/rpc/backend/transactions/stream/transaction_metadata.go index 33f7df25686..4bf81731123 100644 --- a/engine/access/rpc/backend/transactions/stream/transaction_metadata.go +++ b/engine/access/rpc/backend/transactions/stream/transaction_metadata.go @@ -233,6 +233,7 @@ func (t *TransactionMetadata) refreshTransactionResult(ctx context.Context) erro ctx, t.blockWithTx, t.txResult.TransactionID, + t.txResult.CollectionID, t.eventEncodingVersion, ) if err != nil { @@ -248,8 +249,6 @@ func (t *TransactionMetadata) refreshTransactionResult(ctx context.Context) erro // If transaction result was found, fully replace it in metadata. New transaction status already included in result. if txResult != nil { - // Preserve the CollectionID to ensure it is not lost during the transaction result assignment. - txResult.CollectionID = t.txResult.CollectionID t.txResult = txResult } diff --git a/engine/access/rpc/backend/transactions/system/system_collection.go b/engine/access/rpc/backend/transactions/system/system_collection.go new file mode 100644 index 00000000000..184d3d916a9 --- /dev/null +++ b/engine/access/rpc/backend/transactions/system/system_collection.go @@ -0,0 +1,99 @@ +package system + +import ( + "fmt" + + "github.com/onflow/flow-go/fvm/blueprints" + "github.com/onflow/flow-go/model/flow" +) + +const ( + // MinSystemTxCount is the minimum number of transactions in a system collection with scheduled + // transactions enabled. This includes the system chunk and the process callbacks transactions. + MinSystemTxCount = 2 +) + +// SystemCollection represents a system collection and exposes the transaction bodies of each transaction +// within the collection. +type SystemCollection struct { + collection *flow.Collection + lookup map[flow.Identifier]*flow.TransactionBody + systemTxID flow.Identifier +} + +// DefaultSystemCollection returns the default system collection for the given chain ID. +// This is the system collection that contains only static system transactions, and no scheduled transactions. +// If scheduled transactions are disabled, the system collection will contain only the system chunk transaction. +// +// No error returns are expected during normal operation. +func DefaultSystemCollection(chainID flow.ChainID, scheduledTransactionsEnabled bool) (*SystemCollection, error) { + if scheduledTransactionsEnabled { + return NewSystemCollection(chainID, nil) + } + + systemTx, err := blueprints.SystemChunkTransaction(chainID.Chain()) + if err != nil { + return nil, fmt.Errorf("failed to construct system chunk transaction: %w", err) + } + systemTxID := systemTx.ID() + + collection, err := flow.NewCollection(flow.UntrustedCollection{ + Transactions: []*flow.TransactionBody{systemTx}, + }) + if err != nil { + return nil, fmt.Errorf("failed to construct system collection: %w", err) + } + + return &SystemCollection{ + collection: collection, + lookup: map[flow.Identifier]*flow.TransactionBody{ + systemTxID: systemTx, + }, + systemTxID: systemTxID, + }, nil +} + +// NewSystemCollection returns a new system collection for the given chain ID including scheduled +// transactions for each PendingExecution event contained in the events list. +// +// No error returns are expected during normal operation. +func NewSystemCollection(chainID flow.ChainID, events flow.EventsList) (*SystemCollection, error) { + systemCollection, err := blueprints.SystemCollection(chainID.Chain(), events) + if err != nil { + return nil, fmt.Errorf("failed to construct system chunk transaction: %w", err) + } + + if len(systemCollection.Transactions) < MinSystemTxCount { + return nil, fmt.Errorf("expected %d transactions in system collection, got %d", MinSystemTxCount, len(systemCollection.Transactions)) + } + + lookup := make(map[flow.Identifier]*flow.TransactionBody, len(systemCollection.Transactions)) + for _, tx := range systemCollection.Transactions { + lookup[tx.ID()] = tx + } + + systemTxIndex := len(systemCollection.Transactions) - 1 + return &SystemCollection{ + collection: systemCollection, + lookup: lookup, + systemTxID: systemCollection.Transactions[systemTxIndex].ID(), + }, nil +} + +// ByID returns the system transaction body by ID. +// Returns true if the transaction was found in the collection, false otherwise. +func (s *SystemCollection) ByID(id flow.Identifier) (*flow.TransactionBody, bool) { + tx, ok := s.lookup[id] + return tx, ok +} + +// Transactions returns the transactions in the system collection. +func (s *SystemCollection) Transactions() []*flow.TransactionBody { + return s.collection.Transactions +} + +// SystemTxID returns the ID of the system transaction. +// This is the last transaction in the system collection, which is responsible for protocol management. +func (s *SystemCollection) SystemTxID() flow.Identifier { + return s.systemTxID +} diff --git a/engine/access/rpc/backend/transactions/system/system_collection_test.go b/engine/access/rpc/backend/transactions/system/system_collection_test.go new file mode 100644 index 00000000000..0f8df1bac41 --- /dev/null +++ b/engine/access/rpc/backend/transactions/system/system_collection_test.go @@ -0,0 +1,92 @@ +package system_test + +import ( + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/onflow/flow-go/engine/access/rpc/backend/transactions/system" + "github.com/onflow/flow-go/fvm/blueprints" + "github.com/onflow/flow-go/model/flow" + "github.com/onflow/flow-go/utils/unittest/fixtures" +) + +// TestDefaultSystemCollection tests getting the default system collection returns the correct +// collection and system transaction bodies. +func TestDefaultSystemCollection(t *testing.T) { + t.Run("scheduled transactions enabled", func(t *testing.T) { + defaultSystemCollection, err := system.DefaultSystemCollection(flow.Mainnet, true) + require.NoError(t, err) + + expectedSystemCollection, err := blueprints.SystemCollection(flow.Mainnet.Chain(), nil) + require.NoError(t, err) + + expectedSystemTx, err := blueprints.SystemChunkTransaction(flow.Mainnet.Chain()) + require.NoError(t, err) + + systemTxID := defaultSystemCollection.SystemTxID() + systemTx, ok := defaultSystemCollection.ByID(systemTxID) + require.True(t, ok) + + assert.Equal(t, expectedSystemCollection.Transactions, defaultSystemCollection.Transactions()) + assert.Equal(t, expectedSystemTx, systemTx) + assert.Equal(t, expectedSystemTx.ID(), systemTxID) + + for _, expectedTx := range expectedSystemCollection.Transactions { + tx, ok := defaultSystemCollection.ByID(expectedTx.ID()) + assert.True(t, ok) + assert.Equal(t, expectedTx, tx) + } + }) + + t.Run("scheduled transactions disabled", func(t *testing.T) { + defaultSystemCollection, err := system.DefaultSystemCollection(flow.Mainnet, false) + require.NoError(t, err) + + expectedSystemTx, err := blueprints.SystemChunkTransaction(flow.Mainnet.Chain()) + require.NoError(t, err) + + systemTxID := defaultSystemCollection.SystemTxID() + systemTx, ok := defaultSystemCollection.ByID(systemTxID) + require.True(t, ok) + + assert.Equal(t, []*flow.TransactionBody{expectedSystemTx}, defaultSystemCollection.Transactions()) + assert.Equal(t, expectedSystemTx, systemTx) + assert.Equal(t, expectedSystemTx.ID(), systemTxID) + }) +} + +// TestNewSystemCollection tests getting the system collection with scheduled transactions returns +// the correct collection and transaction bodies, including the scheduled transactions. +func TestNewSystemCollection(t *testing.T) { + g := fixtures.NewGeneratorSuite() + events := g.PendingExecutionEvents().List(4) + chainID := g.ChainID() + + systemCollection, err := system.NewSystemCollection(chainID, events) + require.NoError(t, err) + + expectedSystemCollection, err := blueprints.SystemCollection(chainID.Chain(), events) + require.NoError(t, err) + + expectedSystemTx, err := blueprints.SystemChunkTransaction(chainID.Chain()) + require.NoError(t, err) + + // scheduled tx + process tx + system chunk tx + assert.Len(t, systemCollection.Transactions(), 2+len(events)) + + systemTxID := systemCollection.SystemTxID() + systemTx, ok := systemCollection.ByID(systemTxID) + require.True(t, ok) + + assert.Equal(t, expectedSystemCollection.Transactions, systemCollection.Transactions()) + assert.Equal(t, expectedSystemTx, systemTx) + assert.Equal(t, expectedSystemTx.ID(), systemTxID) + + for _, expectedTx := range expectedSystemCollection.Transactions { + tx, ok := systemCollection.ByID(expectedTx.ID()) + assert.True(t, ok) + assert.Equal(t, expectedTx, tx) + } +} diff --git a/engine/access/rpc/backend/transactions/transactions.go b/engine/access/rpc/backend/transactions/transactions.go index e04831f7e03..a5fcea9eaf3 100644 --- a/engine/access/rpc/backend/transactions/transactions.go +++ b/engine/access/rpc/backend/transactions/transactions.go @@ -6,7 +6,6 @@ import ( "fmt" "time" - lru "github.com/hashicorp/golang-lru/v2" accessproto "github.com/onflow/flow/protobuf/go/flow/access" "github.com/onflow/flow/protobuf/go/flow/entities" "github.com/rs/zerolog" @@ -21,6 +20,7 @@ import ( "github.com/onflow/flow-go/engine/access/rpc/backend/transactions/provider" "github.com/onflow/flow-go/engine/access/rpc/backend/transactions/retrier" txstatus "github.com/onflow/flow-go/engine/access/rpc/backend/transactions/status" + "github.com/onflow/flow-go/engine/access/rpc/backend/transactions/system" "github.com/onflow/flow-go/engine/access/rpc/connection" "github.com/onflow/flow-go/engine/common/rpc" "github.com/onflow/flow-go/engine/common/rpc/convert" @@ -28,7 +28,7 @@ import ( "github.com/onflow/flow-go/model/flow" "github.com/onflow/flow-go/module" "github.com/onflow/flow-go/module/irrecoverable" - "github.com/onflow/flow-go/state" + "github.com/onflow/flow-go/module/state_synchronization/indexer" "github.com/onflow/flow-go/state/protocol" "github.com/onflow/flow-go/storage" ) @@ -43,8 +43,6 @@ type Transactions struct { state protocol.State chainID flow.ChainID - systemTxID flow.Identifier - // RPC Clients & Network collectionRPCClient accessproto.AccessAPIClient // RPC client tied to a fixed collection node historicalAccessNodeClients []accessproto.AccessAPIClient @@ -53,73 +51,74 @@ type Transactions struct { retrier retrier.Retrier // Storages - blocks storage.Blocks - collections storage.Collections - transactions storage.Transactions - events storage.Events - - txResultCache *lru.Cache[flow.Identifier, *accessmodel.TransactionResult] - - txValidator *validator.TransactionValidator - txProvider provider.TransactionProvider - txStatusDeriver *txstatus.TxStatusDeriver - - scheduledCallbacksEnabled bool + blocks storage.Blocks + collections storage.Collections + transactions storage.Transactions + scheduledTransactions storage.ScheduledTransactionsReader + + txValidator *validator.TransactionValidator + txProvider provider.TransactionProvider + txStatusDeriver *txstatus.TxStatusDeriver + systemCollection *system.SystemCollection + txResultCache TxResultCache + + scheduledTransactionsEnabled bool } var _ access.TransactionsAPI = (*Transactions)(nil) type Params struct { - Log zerolog.Logger - Metrics module.TransactionMetrics - State protocol.State - ChainID flow.ChainID - SystemTxID flow.Identifier - StaticCollectionRPCClient accessproto.AccessAPIClient - HistoricalAccessNodeClients []accessproto.AccessAPIClient - NodeCommunicator node_communicator.Communicator - ConnFactory connection.ConnectionFactory - EnableRetries bool - NodeProvider *rpc.ExecutionNodeIdentitiesProvider - Blocks storage.Blocks - Collections storage.Collections - Transactions storage.Transactions - Events storage.Events - TxErrorMessageProvider error_messages.Provider - TxResultCache *lru.Cache[flow.Identifier, *accessmodel.TransactionResult] - TxProvider provider.TransactionProvider - TxValidator *validator.TransactionValidator - TxStatusDeriver *txstatus.TxStatusDeriver - EventsIndex *index.EventsIndex - TxResultsIndex *index.TransactionResultsIndex - ScheduledCallbacksEnabled bool + Log zerolog.Logger + Metrics module.TransactionMetrics + State protocol.State + ChainID flow.ChainID + SystemCollection *system.SystemCollection + StaticCollectionRPCClient accessproto.AccessAPIClient + HistoricalAccessNodeClients []accessproto.AccessAPIClient + NodeCommunicator node_communicator.Communicator + ConnFactory connection.ConnectionFactory + EnableRetries bool + NodeProvider *rpc.ExecutionNodeIdentitiesProvider + Blocks storage.Blocks + Collections storage.Collections + Transactions storage.Transactions + ScheduledTransactions storage.ScheduledTransactionsReader + TxErrorMessageProvider error_messages.Provider + TxResultCache TxResultCache + TxProvider provider.TransactionProvider + TxValidator *validator.TransactionValidator + TxStatusDeriver *txstatus.TxStatusDeriver + EventsIndex *index.EventsIndex + TxResultsIndex *index.TransactionResultsIndex + ScheduledTransactionsEnabled bool } func NewTransactionsBackend(params Params) (*Transactions, error) { txs := &Transactions{ - log: params.Log, - metrics: params.Metrics, - state: params.State, - chainID: params.ChainID, - systemTxID: params.SystemTxID, - collectionRPCClient: params.StaticCollectionRPCClient, - historicalAccessNodeClients: params.HistoricalAccessNodeClients, - nodeCommunicator: params.NodeCommunicator, - connectionFactory: params.ConnFactory, - blocks: params.Blocks, - collections: params.Collections, - transactions: params.Transactions, - events: params.Events, - txResultCache: params.TxResultCache, - txValidator: params.TxValidator, - txProvider: params.TxProvider, - txStatusDeriver: params.TxStatusDeriver, - scheduledCallbacksEnabled: params.ScheduledCallbacksEnabled, + log: params.Log, + metrics: params.Metrics, + state: params.State, + chainID: params.ChainID, + systemCollection: params.SystemCollection, + collectionRPCClient: params.StaticCollectionRPCClient, + historicalAccessNodeClients: params.HistoricalAccessNodeClients, + nodeCommunicator: params.NodeCommunicator, + connectionFactory: params.ConnFactory, + blocks: params.Blocks, + collections: params.Collections, + transactions: params.Transactions, + scheduledTransactions: params.ScheduledTransactions, + txResultCache: params.TxResultCache, + txValidator: params.TxValidator, + txProvider: params.TxProvider, + txStatusDeriver: params.TxStatusDeriver, + scheduledTransactionsEnabled: params.ScheduledTransactionsEnabled, } if params.EnableRetries { txs.retrier = retrier.NewRetrier( params.Log, + params.State, params.Blocks, params.Collections, txs, @@ -134,7 +133,7 @@ func NewTransactionsBackend(params Params) (*Transactions, error) { // SendTransaction forwards the transaction to the collection node func (t *Transactions) SendTransaction(ctx context.Context, tx *flow.TransactionBody) error { - now := time.Now().UTC() + start := time.Now().UTC() err := t.txValidator.Validate(ctx, tx) if err != nil { @@ -148,7 +147,7 @@ func (t *Transactions) SendTransaction(ctx context.Context, tx *flow.Transaction return rpc.ConvertError(err, "failed to send transaction to a collection node", codes.Internal) } - t.metrics.TransactionReceived(tx.ID(), now) + t.metrics.TransactionReceived(tx.ID(), start) // store the transaction locally err = t.transactions.Store(tx) @@ -174,28 +173,20 @@ func (t *Transactions) trySendTransaction(ctx context.Context, tx *flow.Transact return fmt.Errorf("failed to determine collection node for tx %x: %w", tx, err) } - var sendError error - logAnyError := func() { - if sendError != nil { - t.log.Info().Err(err).Msg("failed to send transactions to collector nodes") - } - } - defer logAnyError() - // try sending the transaction to one of the chosen collection nodes - sendError = t.nodeCommunicator.CallAvailableNode( + err = t.nodeCommunicator.CallAvailableNode( collNodes, func(node *flow.IdentitySkeleton) error { - err = t.sendTransactionToCollector(ctx, tx, node.Address) - if err != nil { - return err - } - return nil + return t.sendTransactionToCollector(ctx, tx, node.Address) }, nil, ) - return sendError + if err != nil { + t.log.Info().Err(err).Msg("failed to send transactions to collector nodes") + } + + return err } // chooseCollectionNodes finds a random subset of size sampleSize of collection node addresses from the @@ -266,19 +257,77 @@ func (t *Transactions) SendRawTransaction( } func (t *Transactions) GetTransaction(ctx context.Context, txID flow.Identifier) (*flow.TransactionBody, error) { - // look up transaction from storage tx, err := t.transactions.ByID(txID) - txErr := rpc.ConvertStorageError(err) + if err == nil { + return tx, nil + } + + if !errors.Is(err, storage.ErrNotFound) { + return nil, status.Errorf(codes.Internal, "failed to lookup transaction: %v", err) + } - if txErr != nil { - if status.Code(txErr) == codes.NotFound { - return t.getHistoricalTransaction(ctx, txID) + // check if it's one of the static system txs + if tx, ok := t.systemCollection.ByID(txID); ok { + return tx, nil + } + + // check if it's a scheduled transaction + if t.scheduledTransactions != nil { + tx, isScheduledTx, err := t.lookupScheduledTransaction(ctx, txID) + if err != nil { + return nil, err + } + if isScheduledTx { + return tx, nil } - // Other Error trying to retrieve the transaction, return with err - return nil, txErr + // else, this is not a system collection tx. continue with the normal lookup } - return tx, nil + // otherwise, check if it's a historic transaction + return t.getHistoricalTransaction(ctx, txID) +} + +// lookupScheduledTransaction looks up the transaction body for a scheduled transaction. +// Returns false and no error if the transaction is not a known scheduled tx. +// +// Expected error returns during normal operation: +// - [codes.NotFound]: if the transaction is not a scheduled transaction was not found in the block +// - [codes.Internal]: if there was an error looking up the events +func (t *Transactions) lookupScheduledTransaction(ctx context.Context, txID flow.Identifier) (*flow.TransactionBody, bool, error) { + blockID, err := t.scheduledTransactions.BlockIDByTransactionID(txID) + if err != nil { + if errors.Is(err, storage.ErrNotFound) { + return nil, false, nil + } + return nil, false, status.Errorf(codes.Internal, "failed to get scheduled transaction block ID: %v", err) + } + + header, err := t.state.AtBlockID(blockID).Head() + if err != nil { + // since the scheduled transaction is indexed at this block, it must exist in storage, otherwise + // the node is in an inconsistent state + err = fmt.Errorf("failed to get block header: %w", err) + irrecoverable.Throw(ctx, err) + return nil, false, err + } + + scheduledTxs, err := t.txProvider.ScheduledTransactionsByBlockID(ctx, header) + if err != nil { + return nil, false, rpc.ConvertError(err, "failed to get scheduled transactions", codes.Internal) + } + + for _, tx := range scheduledTxs { + if tx.ID() == txID { + return tx, true, nil + } + } + + // since the scheduled transaction is indexed in this block, it exist in the data generated using + // events from the block, otherwise the node is in an inconsistent state. + // TODO: not throwing an irrecoverable here since it's possible that we queried an Execution node + // for the events, and the EN provided incorrect data. This should be refactored so we handle the + // condition more precisely. + return nil, false, status.Errorf(codes.Internal, "scheduled transaction not found, but was indexed in block") } func (t *Transactions) GetTransactionsByBlockID( @@ -299,174 +348,287 @@ func (t *Transactions) GetTransactionResult( txID flow.Identifier, blockID flow.Identifier, collectionID flow.Identifier, - requiredEventEncodingVersion entities.EventEncodingVersion, -) (*accessmodel.TransactionResult, error) { - // look up transaction from storage + encodingVersion entities.EventEncodingVersion, +) (txResult *accessmodel.TransactionResult, err error) { + var scriptSize int start := time.Now() + defer func() { + if err == nil { + // scriptSize will be 0 for system and scheduled txs. this is OK since the metrics uses + // size buckets and 0 equates to 1kb, which is about right. + t.metrics.TransactionResultFetched(time.Since(start), scriptSize) + } + }() - tx, err := t.transactions.ByID(txID) + txResult, isSystemTx, err := t.lookupSystemTransactionResult(ctx, txID, blockID, encodingVersion) if err != nil { - txErr := rpc.ConvertStorageError(err) - if status.Code(txErr) != codes.NotFound { - return nil, txErr - } + return nil, err + } + if isSystemTx { + return txResult, nil + } - // Tx not found. If we have historical Sporks setup, lets look through those as well - if t.txResultCache != nil { - val, ok := t.txResultCache.Get(txID) - if ok { - return val, nil - } - } - historicalTxResult, err := t.getHistoricalTransactionResult(ctx, txID) + // if the node is not indexing scheduled transactions, then fallback to the normal lookup. if the + // request was for a scheduled transaction, it will fail with a not found error. + if t.scheduledTransactions != nil { + txResult, isScheduledTx, err := t.lookupScheduledTransactionResult(ctx, txID, blockID, encodingVersion) if err != nil { - // if tx not found in old access nodes either, then assume that the tx was submitted to a different AN - // and return status as unknown - txStatus := flow.TransactionStatusUnknown - result := &accessmodel.TransactionResult{ - Status: txStatus, - StatusCode: uint(txStatus), - } - if t.txResultCache != nil { - t.txResultCache.Add(txID, result) - } - return result, nil + return nil, err } - - if t.txResultCache != nil { - t.txResultCache.Add(txID, historicalTxResult) + if isScheduledTx { + return txResult, nil } - return historicalTxResult, nil } - block, err := t.retrieveBlock(blockID, collectionID, txID) - // an error occurred looking up the block or the requested block or collection was not found. - // If looking up the block based solely on the txID returns not found, then no error is - // returned since the block may not be finalized yet. + txResult, tx, err := t.lookupSubmittedTransactionResult(ctx, txID, blockID, collectionID, encodingVersion) if err != nil { - return nil, rpc.ConvertStorageError(err) + return nil, err + } + if tx != nil { + scriptSize = len(tx.Script) } - var blockHeight uint64 - var txResult *accessmodel.TransactionResult - // access node may not have the block if it hasn't yet been finalized, hence block can be nil at this point - if block != nil { - txResult, err = t.lookupTransactionResult(ctx, txID, block.ToHeader(), requiredEventEncodingVersion) - if err != nil { - return nil, rpc.ConvertError(err, "failed to retrieve result", codes.Internal) - } + return txResult, nil +} - // an additional check to ensure the correctness of the collection ID. - expectedCollectionID, err := t.lookupCollectionIDInBlock(block, txID) - if err != nil { - // if the collection has not been indexed yet, the lookup will return a not found error. - // if the request included a blockID or collectionID in its the search criteria, not found - // should result in an error because it's not possible to guarantee that the result found - // is the correct one. - if blockID != flow.ZeroID || collectionID != flow.ZeroID { - return nil, rpc.ConvertStorageError(err) - } +// lookupSubmittedTransactionResult looks up the transaction result for a user transaction. +// This function assumes that the queried transaction is not a system transaction or scheduled transaction. +// +// Expected error returns during normal operation: +// - [codes.NotFound]: if the transaction is not found or not in the provided block or collection +// - [codes.Internal]: if there was an error looking up the block or collection +func (t *Transactions) lookupSubmittedTransactionResult( + ctx context.Context, + txID flow.Identifier, + blockID flow.Identifier, + collectionID flow.Identifier, + encodingVersion entities.EventEncodingVersion, +) (*accessmodel.TransactionResult, *flow.TransactionBody, error) { + // 1. lookup the the collection that contains the transaction. if it is not found, then the + // collection is not yet indexed and the transaction is either unknown or pending. + // + // BFT corner case: Only the first finalized collection to contain the transaction is indexed. + // If the transaction is included in multiple collections in the same or different blocks, the + // first collection to be _indexed_ by the node is returned. This is not guaranteed to be the + // first collection in execution order! + lightCollection, err := t.collections.LightByTransactionID(txID) + if err != nil { + if !errors.Is(err, storage.ErrNotFound) { + return nil, nil, status.Errorf(codes.Internal, "failed to find collection for transaction: %v", err) } + // we have already checked if this is a system or scheduled tx. at this point, the tx is either + // pending, unknown, or from a past spork. + result, err := t.getUnknownUserTransactionResult(ctx, txID, blockID, collectionID) + return result, nil, err + } + actualCollectionID := lightCollection.ID() + if collectionID == flow.ZeroID { + collectionID = actualCollectionID + } else if collectionID != actualCollectionID { + return nil, nil, status.Errorf(codes.NotFound, "transaction found in collection %s, but %s was provided", actualCollectionID, collectionID) + } - if collectionID == flow.ZeroID { - collectionID = expectedCollectionID - } else if collectionID != expectedCollectionID { - return nil, status.Error(codes.InvalidArgument, "transaction not found in provided collection") - } + // 2. lookup the block containing the collection. + block, err := t.blocks.ByCollectionID(collectionID) + if err != nil { + // this is an exception. the block/collection index must exist if the collection/tx is indexed, + // otherwise the stored state is inconsistent. + err = fmt.Errorf("failed to find block for collection %v: %w", collectionID, err) + irrecoverable.Throw(ctx, err) + return nil, nil, err + } + actualBlockID := block.ID() + if blockID == flow.ZeroID { + blockID = actualBlockID + } else if blockID != actualBlockID { + return nil, nil, status.Errorf(codes.NotFound, "transaction found in block %s, but %s was provided", actualBlockID, blockID) + } - blockID = block.ID() - blockHeight = block.Height + // 3. lookup the transaction and its result + tx, err := t.transactions.ByID(txID) + if err != nil { + // if we've gotten this far, the transaction must exist in storage otherwise the node is in + // an inconsistent state + err = fmt.Errorf("failed to get transaction from storage: %w", err) + irrecoverable.Throw(ctx, err) + return nil, nil, err } - // If there is still no transaction result, provide one based on available information. - if txResult == nil { - var txStatus flow.TransactionStatus - // Derive the status of the transaction. - if block == nil { - txStatus, err = t.txStatusDeriver.DeriveUnknownTransactionStatus(tx.ReferenceBlockID) - } else { - txStatus, err = t.txStatusDeriver.DeriveTransactionStatus(blockHeight, false) + txResult, err := t.txProvider.TransactionResult(ctx, block.ToHeader(), txID, collectionID, encodingVersion) + if err != nil { + switch { + case errors.Is(err, storage.ErrNotFound): + case errors.Is(err, indexer.ErrIndexNotInitialized): + case errors.Is(err, storage.ErrHeightNotIndexed): + case status.Code(err) == codes.NotFound: + default: + return nil, nil, rpc.ConvertError(err, "failed to retrieve result", codes.Internal) } + // all expected errors fall through to be processed as a known unexecuted transaction. + // The transaction is not executed yet + txStatus, err := t.txStatusDeriver.DeriveTransactionStatus(block.Height, false) if err != nil { - if !errors.Is(err, state.ErrUnknownSnapshotReference) { - irrecoverable.Throw(ctx, err) - } - return nil, rpc.ConvertStorageError(err) + irrecoverable.Throw(ctx, fmt.Errorf("failed to derive transaction status: %w", err)) + return nil, nil, err } - txResult = &accessmodel.TransactionResult{ + return &accessmodel.TransactionResult{ BlockID: blockID, - BlockHeight: blockHeight, + BlockHeight: block.Height, TransactionID: txID, Status: txStatus, CollectionID: collectionID, - } - } else { - txResult.CollectionID = collectionID + }, tx, nil } - t.metrics.TransactionResultFetched(time.Since(start), len(tx.Script)) + return txResult, tx, nil +} - return txResult, nil +// lookupSystemTransactionResult looks up the transaction result for a system transaction. +// Returns false and no error if the transaction is not a system tx. +// +// Expected error returns during normal operation: +// - [codes.InvalidArgument]: if the block ID is not provided +// - [codes.Internal]: if there was an error looking up the block +func (t *Transactions) lookupSystemTransactionResult( + ctx context.Context, + txID flow.Identifier, + blockID flow.Identifier, + encodingVersion entities.EventEncodingVersion, +) (*accessmodel.TransactionResult, bool, error) { + // TODO: system transactions can change over time. Use the blockID to get the correct system tx + // for the provided block. + if _, ok := t.systemCollection.ByID(txID); !ok { + return nil, false, nil // tx is not a system tx + } + + // block must be provided to get the correct system tx result + if blockID == flow.ZeroID { + return nil, false, status.Errorf(codes.InvalidArgument, "block ID is required for system transactions") + } + + header, err := t.state.AtBlockID(blockID).Head() + if err != nil { + return nil, false, status.Errorf(codes.NotFound, "could not find block: %v", err) + } + + result, err := t.txProvider.TransactionResult(ctx, header, txID, flow.ZeroID, encodingVersion) + return result, true, err } -// lookupCollectionIDInBlock returns the collection ID based on the transaction ID. The lookup is performed in block -// collections. -func (t *Transactions) lookupCollectionIDInBlock( - block *flow.Block, +// lookupScheduledTransactionResult looks up the transaction result for a scheduled transaction. +// Returns false and no error if the transaction is not a known scheduled tx. +// +// Expected error returns during normal operation: +// - [codes.NotFound]: if the transaction was found in a different block than the provided block ID +// - [codes.Internal]: if there was an error looking up the block +func (t *Transactions) lookupScheduledTransactionResult( + ctx context.Context, txID flow.Identifier, -) (flow.Identifier, error) { - for _, guarantee := range block.Payload.Guarantees { - collectionID := guarantee.CollectionID - collection, err := t.collections.LightByID(collectionID) - if err != nil { - return flow.ZeroID, fmt.Errorf("failed to get collection %s in indexed block: %w", collectionID, err) - } - for _, collectionTxID := range collection.Transactions { - if collectionTxID == txID { - return collectionID, nil - } + blockID flow.Identifier, + encodingVersion entities.EventEncodingVersion, +) (*accessmodel.TransactionResult, bool, error) { + scheduledTxBlockID, err := t.scheduledTransactions.BlockIDByTransactionID(txID) + if err != nil { + if errors.Is(err, storage.ErrNotFound) { + return nil, false, nil // tx is not a scheduled tx } + return nil, false, status.Errorf(codes.Internal, "failed to get scheduled transaction block ID: %v", err) } - return flow.ZeroID, ErrTransactionNotInBlock + + if blockID != flow.ZeroID && scheduledTxBlockID != blockID { + return nil, false, status.Errorf(codes.NotFound, "scheduled transaction found in block %s, but %s was provided", scheduledTxBlockID, blockID) + } + + header, err := t.state.AtBlockID(scheduledTxBlockID).Head() + if err != nil { + // the scheduled transaction is indexed at this block, so this block must exist in storage. + // otherwise the node is in an inconsistent state + err = fmt.Errorf("failed to get scheduled transaction's block from storage: %w", err) + irrecoverable.Throw(ctx, err) + return nil, false, err + } + + result, err := t.txProvider.TransactionResult(ctx, header, txID, flow.ZeroID, encodingVersion) + return result, true, err } -// retrieveBlock function returns a block based on the input arguments. -// The block ID lookup has the highest priority, followed by the collection ID lookup. -// If both are missing, the default lookup by transaction ID is performed. -// -// If looking up the block based solely on the txID returns not found, then no error is returned. +// getUnknownUserTransactionResult returns the transaction result for a transaction that is not yet +// indexed in a finalized block. // -// Expected errors: -// - storage.ErrNotFound if the requested block or collection was not found. -func (t *Transactions) retrieveBlock( +// Expected error returns during normal operation: +// - [codes.NotFound]: if the transaction is not found or is in the provided block or collection +// - [codes.Internal]: if there was an error looking up the transaction, block, or collection. +func (t *Transactions) getUnknownUserTransactionResult( + ctx context.Context, + txID flow.Identifier, blockID flow.Identifier, collectionID flow.Identifier, - txID flow.Identifier, -) (*flow.Block, error) { - if blockID != flow.ZeroID { - return t.blocks.ByID(blockID) +) (*accessmodel.TransactionResult, error) { + tx, err := t.transactions.ByID(txID) + if err == nil { + // since the tx was not indexed, if it exists in storage that means it was submitted through + // this node. + txStatus, err := t.txStatusDeriver.DeriveUnknownTransactionStatus(tx.ReferenceBlockID) + if err != nil { + if errors.Is(err, storage.ErrNotFound) { + return nil, status.Errorf(codes.NotFound, "transaction's reference block not found") + } + err = fmt.Errorf("failed to derive transaction status: %w", err) + irrecoverable.Throw(ctx, err) + return nil, err + } + + return &accessmodel.TransactionResult{ + TransactionID: txID, + Status: txStatus, + }, nil } - if collectionID != flow.ZeroID { - return t.blocks.ByCollectionID(collectionID) + if !errors.Is(err, storage.ErrNotFound) { + return nil, status.Errorf(codes.Internal, "failed to lookup unknown transaction: %v", err) } - // find the block for the transaction - block, err := t.lookupBlock(txID) + // The transaction does not exist locally, so check if the block or collection help identify its status. + // If we know the queried block or collection exist locally, then we can avoid querying historical Access Node. + if blockID != flow.ZeroID { + _, err := t.blocks.ByID(blockID) + if err == nil { + // the user's specified block exists locally, so assume the tx is not yet indexed + // but will be eventually + return &accessmodel.TransactionResult{ + TransactionID: txID, + Status: flow.TransactionStatusUnknown, + }, nil + } + if !errors.Is(err, storage.ErrNotFound) { + return nil, status.Errorf(codes.Internal, "failed to get block from storage: %v", err) + } + // search historical access nodes + } - if err != nil && !errors.Is(err, storage.ErrNotFound) { - return nil, err + if collectionID != flow.ZeroID { + _, err := t.collections.LightByID(collectionID) + if err == nil { + // the user's specified collection exists locally. since the tx is not indexed, this + // means the provided collection does not contain the tx + return nil, status.Errorf(codes.NotFound, "transaction not found in collection %s", collectionID) + } + if !errors.Is(err, storage.ErrNotFound) { + return nil, status.Errorf(codes.Internal, "failed to get collection from storage: %v", err) + } + // search historical access nodes } - return block, nil + historicalTxResult := t.searchHistoricalAccessNodes(ctx, txID) + return historicalTxResult, nil } func (t *Transactions) GetTransactionResultsByBlockID( ctx context.Context, blockID flow.Identifier, - requiredEventEncodingVersion entities.EventEncodingVersion, + encodingVersion entities.EventEncodingVersion, ) ([]*accessmodel.TransactionResult, error) { // TODO: consider using storage.Index.ByBlockID, the index contains collection id and seals ID block, err := t.blocks.ByID(blockID) @@ -474,7 +636,7 @@ func (t *Transactions) GetTransactionResultsByBlockID( return nil, rpc.ConvertStorageError(err) } - return t.txProvider.TransactionResultsByBlockID(ctx, block, requiredEventEncodingVersion) + return t.txProvider.TransactionResultsByBlockID(ctx, block, encodingVersion) } // GetTransactionResultByIndex returns transactions Results for an index in a block that is executed, @@ -483,91 +645,142 @@ func (t *Transactions) GetTransactionResultByIndex( ctx context.Context, blockID flow.Identifier, index uint32, - requiredEventEncodingVersion entities.EventEncodingVersion, + encodingVersion entities.EventEncodingVersion, ) (*accessmodel.TransactionResult, error) { block, err := t.blocks.ByID(blockID) if err != nil { return nil, rpc.ConvertStorageError(err) } - return t.txProvider.TransactionResultByIndex(ctx, block, index, requiredEventEncodingVersion) + collectionID, err := t.lookupCollectionIDByBlockAndTxIndex(block, index) + if err != nil { + if errors.Is(err, storage.ErrNotFound) { + return nil, status.Errorf(codes.NotFound, "could not find collection for transaction result: %v", err) + } + return nil, status.Errorf(codes.Internal, "failed to lookup collection ID in block by index: %v", err) + } + + return t.txProvider.TransactionResultByIndex(ctx, block, index, collectionID, encodingVersion) } -// GetSystemTransaction returns system transaction +// GetSystemTransaction returns a system transaction by ID. +// If no transaction ID is provided, the last system transaction is queried. +// Note: this function only returns privileged system transactions. It does NOT return user scheduled +// transactions, which are also contained within the system collection. func (t *Transactions) GetSystemTransaction( ctx context.Context, txID flow.Identifier, blockID flow.Identifier, ) (*flow.TransactionBody, error) { - block, err := t.blocks.ByID(blockID) - if err != nil { - return nil, rpc.ConvertStorageError(err) + if txID == flow.ZeroID { + txID = t.systemCollection.SystemTxID() } - if txID == flow.ZeroID { - txID = t.systemTxID + tx, ok := t.systemCollection.ByID(txID) + if !ok { + return nil, status.Errorf(codes.NotFound, "no system transaction with the provided ID found") } - return t.txProvider.SystemTransaction(ctx, block, txID) + // TODO: system tx can change. we should lookup the correct system tx for the block instead of + // always returning the current system tx. + _, err := t.state.AtBlockID(blockID).Head() + if err != nil { + return nil, rpc.ConvertStorageError(err) + } + + return tx, nil } -// GetSystemTransactionResult returns system transaction result +// GetSystemTransactionResult returns a system transaction result by ID. +// If no transaction ID is provided, the last system transaction is queried. +// Note: this function only returns privileged system transactions. It does NOT return user scheduled +// transactions, which are also contained within the system collection. func (t *Transactions) GetSystemTransactionResult( ctx context.Context, txID flow.Identifier, blockID flow.Identifier, - requiredEventEncodingVersion entities.EventEncodingVersion, + encodingVersion entities.EventEncodingVersion, ) (*accessmodel.TransactionResult, error) { - block, err := t.blocks.ByID(blockID) - if err != nil { - return nil, rpc.ConvertStorageError(err) - } - if txID == flow.ZeroID { - txID = t.systemTxID + txID = t.systemCollection.SystemTxID() } - return t.txProvider.SystemTransactionResult(ctx, block, txID, requiredEventEncodingVersion) + txResult, isSystemTx, err := t.lookupSystemTransactionResult(ctx, txID, blockID, encodingVersion) + if err != nil { + return nil, err + } + if !isSystemTx { + return nil, status.Errorf(codes.NotFound, "no system transaction with the provided ID found") + } + return txResult, nil } -// Error returns: -// - `storage.ErrNotFound` - collection referenced by transaction or block by a collection has not been found. -// - all other errors are unexpected and potentially symptoms of internal implementation bugs or state corruption (fatal). -func (t *Transactions) lookupBlock(txID flow.Identifier) (*flow.Block, error) { - collection, err := t.collections.LightByTransactionID(txID) +// GetScheduledTransaction returns the transaction body of the scheduled transaction by ID. +// +// Expected error returns during normal operation: +// - [codes.NotFound]: if the scheduled transaction is not found +func (t *Transactions) GetScheduledTransaction(ctx context.Context, scheduledTxID uint64) (*flow.TransactionBody, error) { + // The scheduled transactions index is only written if execution state indexing is enabled. + // Note: it's possible indexing is enabled and requests are still served from execution nodes. + if t.scheduledTransactions == nil { + return nil, status.Errorf(codes.Unimplemented, "scheduled transactions endpoints require execution state indexing.") + } + + txID, err := t.scheduledTransactions.TransactionIDByID(scheduledTxID) if err != nil { - return nil, err + return nil, rpc.ConvertStorageError(err) } - block, err := t.blocks.ByCollectionID(collection.ID()) + tx, isScheduledTx, err := t.lookupScheduledTransaction(ctx, txID) if err != nil { return nil, err } - - return block, nil + if !isScheduledTx { + // since the scheduled transaction is indexed at this block, it must exist in storage, otherwise + // the node is in an inconsistent state + // TODO: not throwing an irrecoverable here since it's possible that we queried an Execution node + // for the events, and the EN provided incorrect data. This should be refactored so we handle the + // condition more precisely. + return nil, status.Errorf(codes.Internal, "scheduled transaction not found, but was indexed in block") + } + return tx, nil } -func (t *Transactions) lookupTransactionResult( - ctx context.Context, - txID flow.Identifier, - header *flow.Header, - requiredEventEncodingVersion entities.EventEncodingVersion, -) (*accessmodel.TransactionResult, error) { - txResult, err := t.txProvider.TransactionResult(ctx, header, txID, requiredEventEncodingVersion) +// GetScheduledTransactionResult returns the transaction result of the scheduled transaction by ID. +// +// Expected error returns during normal operation: +// - [codes.NotFound]: if the scheduled transaction is not found +func (t *Transactions) GetScheduledTransactionResult(ctx context.Context, scheduledTxID uint64, encodingVersion entities.EventEncodingVersion) (*accessmodel.TransactionResult, error) { + // The scheduled transactions index is only written if execution state indexing is enabled. + // Note: it's possible indexing is enabled and requests are still served from execution nodes. + if t.scheduledTransactions == nil { + return nil, status.Errorf(codes.Unimplemented, "scheduled transactions endpoints require execution state indexing.") + } + + txID, err := t.scheduledTransactions.TransactionIDByID(scheduledTxID) if err != nil { - // if either the storage or execution node reported no results or there were not enough execution results - if status.Code(err) == codes.NotFound { - // No result yet, indicate that it has not been executed - return nil, nil - } - // Other Error trying to retrieve the result, return with err - return nil, err + return nil, rpc.ConvertStorageError(err) } - // considered executed as long as some result is returned, even if it's an error message + txResult, isScheduledTx, err := t.lookupScheduledTransactionResult(ctx, txID, flow.ZeroID, encodingVersion) + if err != nil { + return nil, err + } + if !isScheduledTx { + // since the scheduled transaction is indexed at this block, it must exist in storage, otherwise + // the node is in an inconsistent state + // TODO: not throwing an irrecoverable here since it's possible that we queried an Execution node + // for the events, and the EN provided incorrect data. This should be refactored so we handle the + // condition more precisely. + return nil, status.Errorf(codes.Internal, "scheduled transaction not found, but was indexed in block") + } return txResult, nil } +// getHistoricalTransaction searches the historical access nodes for the transaction body. +// +// All errors are benign and side-effect free for the node. They indicate an issue communicating with +// external nodes. func (t *Transactions) getHistoricalTransaction( ctx context.Context, txID flow.Identifier, @@ -592,6 +805,39 @@ func (t *Transactions) getHistoricalTransaction( return nil, status.Errorf(codes.NotFound, "no known transaction with ID %s", txID) } +// searchHistoricalAccessNodes searches the historical access nodes for the transaction result +// and caches the result if enabled. +func (t *Transactions) searchHistoricalAccessNodes( + ctx context.Context, + txID flow.Identifier, +) *accessmodel.TransactionResult { + // if the tx is not known locally, search the historical access nodes + if result, ok := t.txResultCache.Get(txID); ok { + return result + } + + historicalTxResult, err := t.getHistoricalTransactionResult(ctx, txID) + if err != nil { + // if tx not found on historic access nodes either, then assume that the tx was + // submitted to a different AN and return status as unknown + historicalTxResult = &accessmodel.TransactionResult{ + TransactionID: txID, + Status: flow.TransactionStatusUnknown, + } + } + + // always cache the result even if it's an error to avoid unnecessary load on the nodes. + // the cache is limited so retries will happen eventually. users can also query the nodes + // directly for more precise results. + t.txResultCache.Add(txID, historicalTxResult) + + return historicalTxResult +} + +// getHistoricalTransactionResult searches the historical access nodes for the transaction result. +// +// All errors are benign and side-effect free for the node. They indicate an issue communicating with +// external nodes. func (t *Transactions) getHistoricalTransactionResult( ctx context.Context, txID flow.Identifier, @@ -628,13 +874,37 @@ func (t *Transactions) getHistoricalTransactionResult( return nil, status.Errorf(codes.NotFound, "no known transaction with ID %s", txID) } -func (t *Transactions) registerTransactionForRetry(tx *flow.TransactionBody) { - referenceBlock, err := t.state.AtBlockID(tx.ReferenceBlockID).Head() - if err != nil { - return +// lookupCollectionIDByBlockAndTxIndex returns the collection ID that contains the transasction with +// the provided transaction index. +// +// If the index is larger that the number of user transactions, flow.ZeroID is returned, indicating +// that the transaction is a system transaction. The caller should verify that the index does in fact +// correspond to a system transaction. +// +// Expected errors during normal operations: +// - [storage.ErrNotFound] - if any of the collections in the block cannot be found. +func (t *Transactions) lookupCollectionIDByBlockAndTxIndex(block *flow.Block, index uint32) (flow.Identifier, error) { + txIndex := uint32(0) + for _, guarantee := range block.Payload.Guarantees { + collection, err := t.collections.LightByID(guarantee.CollectionID) + if err != nil { + return flow.ZeroID, fmt.Errorf("could not find collection %s: %w", guarantee.CollectionID, err) + } + + for range collection.Transactions { + if txIndex == index { + return guarantee.CollectionID, nil + } + txIndex++ + } } - t.retrier.RegisterTransaction(referenceBlock.Height, tx) + // otherwise, assume it's a system transaction and return the ZeroID + return flow.ZeroID, nil +} + +func (t *Transactions) registerTransactionForRetry(tx *flow.TransactionBody) { + t.retrier.RegisterTransaction(tx) } // ATTENTION: might be a source of problems in future. We run this code on finalization gorotuine, diff --git a/engine/access/rpc/backend/transactions/transactions_functional_test.go b/engine/access/rpc/backend/transactions/transactions_functional_test.go new file mode 100644 index 00000000000..e12a58c7446 --- /dev/null +++ b/engine/access/rpc/backend/transactions/transactions_functional_test.go @@ -0,0 +1,757 @@ +package transactions + +import ( + "context" + "os" + "testing" + + "github.com/jordanschalm/lockctx" + "github.com/rs/zerolog" + "github.com/stretchr/testify/mock" + "github.com/stretchr/testify/suite" + + "github.com/onflow/flow/protobuf/go/flow/entities" + "github.com/onflow/flow/protobuf/go/flow/execution" + execproto "github.com/onflow/flow/protobuf/go/flow/execution" + + "github.com/onflow/flow-go/access/validator" + "github.com/onflow/flow-go/engine/access/index" + accessmock "github.com/onflow/flow-go/engine/access/mock" + "github.com/onflow/flow-go/engine/access/rpc/backend/node_communicator" + "github.com/onflow/flow-go/engine/access/rpc/backend/transactions/error_messages" + "github.com/onflow/flow-go/engine/access/rpc/backend/transactions/provider" + txstatus "github.com/onflow/flow-go/engine/access/rpc/backend/transactions/status" + "github.com/onflow/flow-go/engine/access/rpc/backend/transactions/system" + connectionmock "github.com/onflow/flow-go/engine/access/rpc/connection/mock" + commonrpc "github.com/onflow/flow-go/engine/common/rpc" + "github.com/onflow/flow-go/engine/common/rpc/convert" + "github.com/onflow/flow-go/fvm/blueprints" + "github.com/onflow/flow-go/fvm/systemcontracts" + accessmodel "github.com/onflow/flow-go/model/access" + "github.com/onflow/flow-go/model/flow" + "github.com/onflow/flow-go/module" + "github.com/onflow/flow-go/module/counters" + execmock "github.com/onflow/flow-go/module/execution/mock" + testutil "github.com/onflow/flow-go/module/executiondatasync/testutil" + "github.com/onflow/flow-go/module/metrics" + syncmock "github.com/onflow/flow-go/module/state_synchronization/mock" + protocol "github.com/onflow/flow-go/state/protocol/badger" + "github.com/onflow/flow-go/state/protocol/inmem" + protocolmock "github.com/onflow/flow-go/state/protocol/mock" + "github.com/onflow/flow-go/storage" + bstorage "github.com/onflow/flow-go/storage/badger" + "github.com/onflow/flow-go/storage/operation/pebbleimpl" + "github.com/onflow/flow-go/storage/store" + "github.com/onflow/flow-go/utils/unittest" + "github.com/onflow/flow-go/utils/unittest/fixtures" + "github.com/onflow/flow-go/utils/unittest/mocks" +) + +func TestTransactionsFunctionalSuite(t *testing.T) { + suite.Run(t, new(TransactionsFunctionalSuite)) +} + +// TransactionsFunctionalSuite tests implements functional happy path tests of the transaction backend +// for local and execution node providers. The tests use a full database with real storages. +// +// Mocking is only used for the following components: +// - Execution node backends +// - Index reporter - to avoid needing to run a registers db +// - State for the execution node provider to avoid issues having to needing to deal with identities +// when selecting execution nodes for each request. +// +// Not all methods are tested, just methods required to exercise each of the provider methods. Detailed +// per-method testing is implemented in transactions_test.go. +type TransactionsFunctionalSuite struct { + suite.Suite + + log zerolog.Logger + g *fixtures.GeneratorSuite + db storage.DB + lockManager lockctx.Manager + + blocks storage.Blocks + collections storage.Collections + transactions storage.Transactions + events storage.Events + results storage.LightTransactionResults + receipts storage.ExecutionReceipts + txErrorMessages storage.TransactionResultErrorMessages + scheduledTransactions storage.ScheduledTransactions + + eventsIndex *index.EventsIndex + txResultsIndex *index.TransactionResultsIndex + validatorBlocks *validator.ProtocolStateBlocks + txErrorMessageProvider error_messages.Provider + + state *protocol.State + rootSnapshot *inmem.Snapshot + participants flow.IdentityList + lastFullBlockHeight *counters.PersistentStrictMonotonicCounter + txStatusDeriver *txstatus.TxStatusDeriver + nodeProvider *commonrpc.ExecutionNodeIdentitiesProvider + reporter *syncmock.IndexReporter + + rootBlock *flow.Block + tf *testutil.TestFixture + systemCollection *system.SystemCollection + + mockState *protocolmock.State + execClient *accessmock.ExecutionAPIClient +} + +func (s *TransactionsFunctionalSuite) SetupTest() { + s.g = fixtures.NewGeneratorSuite() + + s.log = unittest.Logger() + metrics := metrics.NewNoopCollector() + + // Setup database + s.lockManager = storage.NewTestingLockManager() + + dbDir := unittest.TempDir(s.T()) + s.T().Cleanup(func() { s.Require().NoError(os.RemoveAll(dbDir)) }) + + pdb := unittest.PebbleDB(s.T(), dbDir) + s.T().Cleanup(func() { s.Require().NoError(pdb.Close()) }) + + s.db = pebbleimpl.ToDB(pdb) + + // Instantiate storages + all := store.InitAll(metrics, s.db) + + s.blocks = all.Blocks + s.collections = all.Collections + s.transactions = all.Transactions + s.receipts = all.Receipts + s.events = store.NewEvents(metrics, s.db) + s.results = store.NewLightTransactionResults(metrics, s.db, bstorage.DefaultCacheSize) + s.txErrorMessages = store.NewTransactionResultErrorMessages(metrics, s.db, bstorage.DefaultCacheSize) + s.scheduledTransactions = store.NewScheduledTransactions(metrics, s.db, bstorage.DefaultCacheSize) + + s.reporter = syncmock.NewIndexReporter(s.T()) + + reporter := index.NewReporter() + err := reporter.Initialize(s.reporter) + s.Require().NoError(err) + + s.eventsIndex = index.NewEventsIndex(reporter, s.events) + s.txResultsIndex = index.NewTransactionResultsIndex(reporter, s.results) + s.validatorBlocks = validator.NewProtocolStateBlocks(s.state, reporter) + + s.txErrorMessageProvider = error_messages.NewTxErrorMessageProvider(s.log, s.txErrorMessages, nil, nil, nil, nil) + + s.participants = s.g.Identities().List(5, fixtures.Identity.WithAllRoles()) + s.rootSnapshot = unittest.RootSnapshotFixtureWithChainID(s.participants, s.g.ChainID()) + + s.state, err = protocol.Bootstrap( + metrics, + s.db, + s.lockManager, + all.Headers, + all.Seals, + all.Results, + all.Blocks, + all.QuorumCertificates, + all.EpochSetups, + all.EpochCommits, + all.EpochProtocolStateEntries, + all.ProtocolKVStore, + all.VersionBeacons, + s.rootSnapshot, + ) + s.Require().NoError(err) + + // Generate fixture data + s.systemCollection, err = system.DefaultSystemCollection(s.g.ChainID(), true) + s.Require().NoError(err) + + s.rootBlock = s.state.Params().SporkRootBlock() + + s.tf = testutil.CompleteFixture(s.T(), s.g, s.rootBlock) + + block := s.tf.Block + blockID := s.tf.Block.ID() + + // Populate the database + err = unittest.WithLock(s.T(), s.lockManager, storage.LockInsertBlock, func(lctx lockctx.Context) error { + return s.db.WithReaderBatchWriter(func(rw storage.ReaderBatchWriter) error { + if err := all.EpochProtocolStateEntries.BatchIndex(lctx, rw, blockID, block.Payload.ProtocolStateID); err != nil { + return err + } + + return s.blocks.BatchStore(lctx, rw, unittest.ProposalFromBlock(block)) + }) + }) + s.Require().NoError(err) + + err = unittest.WithLock(s.T(), s.lockManager, storage.LockInsertCollection, func(lctx lockctx.Context) error { + return s.db.WithReaderBatchWriter(func(rw storage.ReaderBatchWriter) error { + for _, collection := range s.tf.ExpectedCollections { + if _, err := s.collections.BatchStoreAndIndexByTransaction(lctx, collection, rw); err != nil { + return err + } + } + return nil + }) + }) + s.Require().NoError(err) + + err = s.blocks.IndexBlockContainingCollectionGuarantees(blockID, flow.GetIDs(block.Payload.Guarantees)) + s.Require().NoError(err) + + err = unittest.WithLocks(s.T(), s.lockManager, []string{ + storage.LockInsertLightTransactionResult, + storage.LockIndexScheduledTransaction, + }, func(lctx lockctx.Context) error { + return s.db.WithReaderBatchWriter(func(rw storage.ReaderBatchWriter) error { + if err := s.results.BatchStore(lctx, rw, blockID, s.tf.ExpectedResults); err != nil { + return err + } + + if err := s.events.BatchStore(blockID, []flow.EventsList{s.tf.ExpectedEvents}, rw); err != nil { + return err + } + + for txID, scheduledTxID := range s.tf.ExpectedScheduledTransactions { + if err := s.scheduledTransactions.BatchIndex(lctx, blockID, txID, scheduledTxID, rw); err != nil { + return err + } + } + + return nil + }) + }) + s.Require().NoError(err) + + err = unittest.WithLock(s.T(), s.lockManager, storage.LockInsertTransactionResultErrMessage, func(lctx lockctx.Context) error { + return s.db.WithReaderBatchWriter(func(rw storage.ReaderBatchWriter) error { + return s.txErrorMessages.BatchStore(lctx, rw, blockID, s.tf.TxErrorMessages) + }) + }) + s.Require().NoError(err) + + lastFullBlockHeightProgress, err := store.NewConsumerProgress(s.db, module.ConsumeProgressLastFullBlockHeight).Initialize(s.rootBlock.Height) + s.Require().NoError(err) + + s.lastFullBlockHeight, err = counters.NewPersistentStrictMonotonicCounter(lastFullBlockHeightProgress) + s.Require().NoError(err) + + // Instantiate intermediate components + s.txStatusDeriver = txstatus.NewTxStatusDeriver(s.state, s.lastFullBlockHeight) + + s.mockState = protocolmock.NewState(s.T()) + s.nodeProvider = commonrpc.NewExecutionNodeIdentitiesProvider(s.log, s.mockState, s.receipts, nil, nil) + + s.execClient = accessmock.NewExecutionAPIClient(s.T()) +} + +func (s *TransactionsFunctionalSuite) defaultTransactionsParams() Params { + txValidator, err := validator.NewTransactionValidator( + s.validatorBlocks, + s.g.ChainID().Chain(), + metrics.NewNoopCollector(), + validator.TransactionValidationOptions{}, + execmock.NewScriptExecutor(s.T()), + ) + s.Require().NoError(err) + + return Params{ + Log: s.log, + Metrics: metrics.NewNoopCollector(), + ChainID: s.g.ChainID(), + State: s.state, + SystemCollection: s.systemCollection, + NodeProvider: s.nodeProvider, + Blocks: s.blocks, + Collections: s.collections, + Transactions: s.transactions, + ScheduledTransactions: s.scheduledTransactions, + TxErrorMessageProvider: s.txErrorMessageProvider, + TxValidator: txValidator, + TxStatusDeriver: s.txStatusDeriver, + EventsIndex: s.eventsIndex, + TxResultsIndex: s.txResultsIndex, + ScheduledTransactionsEnabled: true, + } +} + +func (s *TransactionsFunctionalSuite) defaultExecutionNodeParams() Params { + blockID := s.tf.Block.ID() + + connectionFactory := connectionmock.NewConnectionFactory(s.T()) + connectionFactory.On("GetExecutionAPIClient", mock.Anything).Return(s.execClient, &mocks.MockCloser{}, nil) + nodeCommunicator := node_communicator.NewNodeCommunicator(false) + + stateParams := protocolmock.NewParams(s.T()) + stateParams.On("FinalizedRoot").Return(s.rootBlock.ToHeader()) + s.mockState.On("Params").Return(stateParams) + + params := s.defaultTransactionsParams() + params.TxProvider = provider.NewENTransactionProvider( + s.log, + s.state, + s.collections, + connectionFactory, + nodeCommunicator, + s.nodeProvider, + s.txStatusDeriver, + s.systemCollection, + s.g.ChainID(), + true, + ) + + snapshot := protocolmock.NewSnapshot(s.T()) + snapshot.On("Identities", mock.Anything).Return(func(filter flow.IdentityFilter[flow.Identity]) (flow.IdentityList, error) { + return s.participants.Filter(filter), nil + }) + s.mockState.On("AtBlockID", blockID).Return(snapshot) + + finalizedSnapshot := protocolmock.NewSnapshot(s.T()) + finalizedSnapshot.On("Identities", mock.Anything).Return(func(filter flow.IdentityFilter[flow.Identity]) (flow.IdentityList, error) { + return s.participants.Filter(filter), nil + }) + s.mockState.On("Final").Return(finalizedSnapshot) + + return params +} + +func eventsForTransaction(events flow.EventsList, txID flow.Identifier) flow.EventsList { + filtered := make(flow.EventsList, 0) + for _, event := range events { + if event.TransactionID == txID { + filtered = append(filtered, event) + } + } + return filtered +} + +func (s *TransactionsFunctionalSuite) expectedResultForIndex(index int, encodingVersion entities.EventEncodingVersion) *accessmodel.TransactionResult { + block := s.tf.Block + blockID := s.tf.Block.ID() + + txResult := s.tf.ExpectedResults[index] + txID := txResult.TransactionID + + txCount := 0 + collectionID := flow.ZeroID + for _, collection := range s.tf.ExpectedCollections { + if index < txCount+len(collection.Transactions) { + collectionID = collection.ID() + break + } + txCount += len(collection.Transactions) + } + // if the tx is a system tx, its index is greater than the total number of transactions, so the + // collection ID will default to flow.ZeroID. + + events := eventsForTransaction(s.tf.ExpectedEvents, txID) + if encodingVersion == entities.EventEncodingVersion_JSON_CDC_V0 { + convertedEvents, err := convert.CcfEventsToJsonEvents(events) + s.Require().NoError(err) + + events = convertedEvents + } + + errorMessage := "" + statusCode := uint(0) + if txResult.Failed { + statusCode = uint(1) + for _, txErrorMessage := range s.tf.TxErrorMessages { + if txErrorMessage.TransactionID == txID { + errorMessage = txErrorMessage.ErrorMessage + break + } + } + s.Require().NotEmpty(errorMessage) + } + + return &accessmodel.TransactionResult{ + TransactionID: txID, + Status: flow.TransactionStatusExecuted, + StatusCode: statusCode, + Events: events, + ErrorMessage: errorMessage, + BlockID: blockID, + BlockHeight: block.Height, + CollectionID: collectionID, + } +} + +func (s *TransactionsFunctionalSuite) TestTransactionResult_Local() { + block := s.tf.Block + blockID := s.tf.Block.ID() + + collection := s.tf.ExpectedCollections[0] + collectionID := collection.ID() + + txID := s.tf.ExpectedResults[1].TransactionID + + expectedResult := s.expectedResultForIndex(1, entities.EventEncodingVersion_JSON_CDC_V0) + s.reporter.On("HighestIndexedHeight").Return(block.Height, nil) + s.reporter.On("LowestIndexedHeight").Return(s.rootBlock.Height, nil) + + params := s.defaultTransactionsParams() + params.TxProvider = provider.NewLocalTransactionProvider( + s.state, + s.collections, + s.blocks, + s.eventsIndex, + s.txResultsIndex, + s.txErrorMessageProvider, + s.systemCollection, + s.txStatusDeriver, + s.g.ChainID(), + true, + ) + + txBackend, err := NewTransactionsBackend(params) + s.Require().NoError(err) + + result, err := txBackend.GetTransactionResult(context.Background(), txID, blockID, collectionID, entities.EventEncodingVersion_JSON_CDC_V0) + s.Require().NoError(err) + s.Require().Equal(expectedResult, result) +} + +func (s *TransactionsFunctionalSuite) TestTransactionResultByIndex_Local() { + block := s.tf.Block + blockID := s.tf.Block.ID() + + expectedResult := s.expectedResultForIndex(1, entities.EventEncodingVersion_JSON_CDC_V0) + s.reporter.On("HighestIndexedHeight").Return(block.Height, nil) + s.reporter.On("LowestIndexedHeight").Return(s.rootBlock.Height, nil) + + params := s.defaultTransactionsParams() + params.TxProvider = provider.NewLocalTransactionProvider( + s.state, + s.collections, + s.blocks, + s.eventsIndex, + s.txResultsIndex, + s.txErrorMessageProvider, + s.systemCollection, + s.txStatusDeriver, + s.g.ChainID(), + true, + ) + + txBackend, err := NewTransactionsBackend(params) + s.Require().NoError(err) + + result, err := txBackend.GetTransactionResultByIndex(context.Background(), blockID, 1, entities.EventEncodingVersion_JSON_CDC_V0) + s.Require().NoError(err) + s.Require().Equal(expectedResult, result) +} + +func (s *TransactionsFunctionalSuite) TestTransactionResultsByBlockID_Local() { + block := s.tf.Block + blockID := s.tf.Block.ID() + + expectedResults := make([]*accessmodel.TransactionResult, len(s.tf.ExpectedResults)) + for i := range s.tf.ExpectedResults { + expectedResults[i] = s.expectedResultForIndex(i, entities.EventEncodingVersion_JSON_CDC_V0) + } + + s.reporter.On("HighestIndexedHeight").Return(block.Height, nil) + s.reporter.On("LowestIndexedHeight").Return(s.rootBlock.Height, nil) + + params := s.defaultTransactionsParams() + params.TxProvider = provider.NewLocalTransactionProvider( + s.state, + s.collections, + s.blocks, + s.eventsIndex, + s.txResultsIndex, + s.txErrorMessageProvider, + s.systemCollection, + s.txStatusDeriver, + s.g.ChainID(), + true, + ) + + txBackend, err := NewTransactionsBackend(params) + s.Require().NoError(err) + + results, err := txBackend.GetTransactionResultsByBlockID(context.Background(), blockID, entities.EventEncodingVersion_JSON_CDC_V0) + s.Require().NoError(err) + s.Require().Equal(expectedResults, results) +} + +func (s *TransactionsFunctionalSuite) TestTransactionsByBlockID_Local() { + block := s.tf.Block + blockID := block.ID() + + expectedTransactions := make([]*flow.TransactionBody, 0, len(s.tf.ExpectedResults)) + for _, collection := range s.tf.ExpectedCollections { + expectedTransactions = append(expectedTransactions, collection.Transactions...) + } + + systemCollection, err := blueprints.SystemCollection(s.g.ChainID().Chain(), s.tf.ExpectedEvents) + s.Require().NoError(err) + expectedTransactions = append(expectedTransactions, systemCollection.Transactions...) + + s.reporter.On("HighestIndexedHeight").Return(block.Height, nil) + s.reporter.On("LowestIndexedHeight").Return(s.rootBlock.Height, nil) + + params := s.defaultTransactionsParams() + params.TxProvider = provider.NewLocalTransactionProvider( + s.state, + s.collections, + s.blocks, + s.eventsIndex, + s.txResultsIndex, + s.txErrorMessageProvider, + s.systemCollection, + s.txStatusDeriver, + s.g.ChainID(), + true, + ) + + txBackend, err := NewTransactionsBackend(params) + s.Require().NoError(err) + + results, err := txBackend.GetTransactionsByBlockID(context.Background(), blockID) + s.Require().NoError(err) + s.Require().Equal(expectedTransactions, results) +} + +func (s *TransactionsFunctionalSuite) TestScheduledTransactionsByBlockID_Local() { + block := s.tf.Block + + systemCollection, err := system.NewSystemCollection(s.g.ChainID(), s.tf.ExpectedEvents) + s.Require().NoError(err) + + s.reporter.On("HighestIndexedHeight").Return(block.Height, nil) + s.reporter.On("LowestIndexedHeight").Return(s.rootBlock.Height, nil) + + params := s.defaultTransactionsParams() + params.TxProvider = provider.NewLocalTransactionProvider( + s.state, + s.collections, + s.blocks, + s.eventsIndex, + s.txResultsIndex, + s.txErrorMessageProvider, + s.systemCollection, + s.txStatusDeriver, + s.g.ChainID(), + true, + ) + + txBackend, err := NewTransactionsBackend(params) + s.Require().NoError(err) + + for txID, scheduledTxID := range s.tf.ExpectedScheduledTransactions { + expectedTransaction, ok := systemCollection.ByID(txID) + s.Require().True(ok) + + results, err := txBackend.GetScheduledTransaction(context.Background(), scheduledTxID) + s.Require().NoError(err) + s.Require().Equal(expectedTransaction, results) + + break // call for the first scheduled transaction iterated + } +} + +func (s *TransactionsFunctionalSuite) TestTransactionResult_ExecutionNode() { + blockID := s.tf.Block.ID() + + collection := s.tf.ExpectedCollections[0] + collectionID := collection.ID() + + txID := s.tf.ExpectedResults[1].TransactionID + + accessResponse := convert.TransactionResultToMessage(s.expectedResultForIndex(1, entities.EventEncodingVersion_CCF_V0)) + nodeResponse := &execution.GetTransactionResultResponse{ + StatusCode: accessResponse.StatusCode, + ErrorMessage: accessResponse.ErrorMessage, + Events: accessResponse.Events, + EventEncodingVersion: entities.EventEncodingVersion_CCF_V0, + } + expectedResult := s.expectedResultForIndex(1, entities.EventEncodingVersion_JSON_CDC_V0) + + expectedRequest := &execproto.GetTransactionResultRequest{ + BlockId: blockID[:], + TransactionId: txID[:], + } + + s.execClient. + On("GetTransactionResult", mock.Anything, expectedRequest). + Return(nodeResponse, nil) + + params := s.defaultExecutionNodeParams() + txBackend, err := NewTransactionsBackend(params) + s.Require().NoError(err) + + result, err := txBackend.GetTransactionResult(context.Background(), txID, blockID, collectionID, entities.EventEncodingVersion_JSON_CDC_V0) + s.Require().NoError(err) + s.Require().Equal(expectedResult, result) +} + +func (s *TransactionsFunctionalSuite) TestTransactionResultByIndex_ExecutionNode() { + blockID := s.tf.Block.ID() + + accessResponse := convert.TransactionResultToMessage(s.expectedResultForIndex(1, entities.EventEncodingVersion_CCF_V0)) + nodeResponse := &execution.GetTransactionResultResponse{ + StatusCode: accessResponse.StatusCode, + ErrorMessage: accessResponse.ErrorMessage, + Events: accessResponse.Events, + EventEncodingVersion: entities.EventEncodingVersion_CCF_V0, + } + expectedResult := s.expectedResultForIndex(1, entities.EventEncodingVersion_JSON_CDC_V0) + + expectedRequest := &execproto.GetTransactionByIndexRequest{ + BlockId: blockID[:], + Index: 1, + } + + s.execClient. + On("GetTransactionResultByIndex", mock.Anything, expectedRequest). + Return(nodeResponse, nil) + + params := s.defaultExecutionNodeParams() + txBackend, err := NewTransactionsBackend(params) + s.Require().NoError(err) + + result, err := txBackend.GetTransactionResultByIndex(context.Background(), blockID, 1, entities.EventEncodingVersion_JSON_CDC_V0) + s.Require().NoError(err) + s.Require().Equal(expectedResult, result) +} + +func (s *TransactionsFunctionalSuite) TestTransactionResultsByBlockID_ExecutionNode() { + blockID := s.tf.Block.ID() + + expectedResults := make([]*accessmodel.TransactionResult, len(s.tf.ExpectedResults)) + nodeResults := make([]*execution.GetTransactionResultResponse, len(s.tf.ExpectedResults)) + for i := range s.tf.ExpectedResults { + accessResponse := convert.TransactionResultToMessage(s.expectedResultForIndex(i, entities.EventEncodingVersion_CCF_V0)) + nodeResults[i] = &execution.GetTransactionResultResponse{ + StatusCode: accessResponse.StatusCode, + ErrorMessage: accessResponse.ErrorMessage, + Events: accessResponse.Events, + } + expectedResults[i] = s.expectedResultForIndex(i, entities.EventEncodingVersion_JSON_CDC_V0) + } + + nodeResponse := &execution.GetTransactionResultsResponse{ + TransactionResults: nodeResults, + EventEncodingVersion: entities.EventEncodingVersion_CCF_V0, + } + + expectedRequest := &execproto.GetTransactionsByBlockIDRequest{ + BlockId: blockID[:], + } + + s.execClient. + On("GetTransactionResultsByBlockID", mock.Anything, expectedRequest). + Return(nodeResponse, nil) + + params := s.defaultExecutionNodeParams() + txBackend, err := NewTransactionsBackend(params) + s.Require().NoError(err) + + result, err := txBackend.GetTransactionResultsByBlockID(context.Background(), blockID, entities.EventEncodingVersion_JSON_CDC_V0) + s.Require().NoError(err) + s.Require().Equal(expectedResults, result) +} + +func (s *TransactionsFunctionalSuite) TestTransactionsByBlockID_ExecutionNode() { + block := s.tf.Block + blockID := block.ID() + + expectedTransactions := make([]*flow.TransactionBody, 0, len(s.tf.ExpectedResults)) + for _, collection := range s.tf.ExpectedCollections { + expectedTransactions = append(expectedTransactions, collection.Transactions...) + } + + systemCollection, err := blueprints.SystemCollection(s.g.ChainID().Chain(), s.tf.ExpectedEvents) + s.Require().NoError(err) + expectedTransactions = append(expectedTransactions, systemCollection.Transactions...) + + env := systemcontracts.SystemContractsForChain(s.g.ChainID()).AsTemplateEnv() + pendingExecuteEventType := blueprints.PendingExecutionEventType(env) + + expectedRequest := &execproto.GetEventsForBlockIDsRequest{ + Type: string(pendingExecuteEventType), + BlockIds: [][]byte{blockID[:]}, + } + + events := make([]*entities.Event, 0) + for _, event := range s.tf.ExpectedEvents { + if blueprints.IsPendingExecutionEvent(env, event) { + events = append(events, convert.EventToMessage(event)) + } + } + + nodeResponse := &execution.GetEventsForBlockIDsResponse{ + Results: []*execution.GetEventsForBlockIDsResponse_Result{{ + BlockId: blockID[:], + BlockHeight: block.Height, + Events: events, + }}, + EventEncodingVersion: entities.EventEncodingVersion_CCF_V0, + } + + s.execClient. + On("GetEventsForBlockIDs", mock.Anything, expectedRequest). + Return(nodeResponse, nil) + + params := s.defaultExecutionNodeParams() + txBackend, err := NewTransactionsBackend(params) + s.Require().NoError(err) + + results, err := txBackend.GetTransactionsByBlockID(context.Background(), blockID) + s.Require().NoError(err) + s.Require().Equal(expectedTransactions, results) +} + +func (s *TransactionsFunctionalSuite) TestScheduledTransactionsByBlockID_ExecutionNode() { + block := s.tf.Block + blockID := block.ID() + + systemCollection, err := system.NewSystemCollection(s.g.ChainID(), s.tf.ExpectedEvents) + s.Require().NoError(err) + + env := systemcontracts.SystemContractsForChain(s.g.ChainID()).AsTemplateEnv() + pendingExecuteEventType := blueprints.PendingExecutionEventType(env) + + expectedRequest := &execproto.GetEventsForBlockIDsRequest{ + Type: string(pendingExecuteEventType), + BlockIds: [][]byte{blockID[:]}, + } + + events := make([]*entities.Event, 0) + for _, event := range s.tf.ExpectedEvents { + if blueprints.IsPendingExecutionEvent(env, event) { + events = append(events, convert.EventToMessage(event)) + } + } + + nodeResponse := &execution.GetEventsForBlockIDsResponse{ + Results: []*execution.GetEventsForBlockIDsResponse_Result{{ + BlockId: blockID[:], + BlockHeight: block.Height, + Events: events, + }}, + EventEncodingVersion: entities.EventEncodingVersion_CCF_V0, + } + + s.execClient. + On("GetEventsForBlockIDs", mock.Anything, expectedRequest). + Return(nodeResponse, nil) + + params := s.defaultExecutionNodeParams() + txBackend, err := NewTransactionsBackend(params) + s.Require().NoError(err) + + for txID, scheduledTxID := range s.tf.ExpectedScheduledTransactions { + expectedTransaction, ok := systemCollection.ByID(txID) + s.Require().True(ok) + + results, err := txBackend.GetScheduledTransaction(context.Background(), scheduledTxID) + s.Require().NoError(err) + s.Require().Equal(expectedTransaction, results) + + break // call for the first scheduled transaction iterated + } +} diff --git a/engine/access/rpc/backend/transactions/transactions_test.go b/engine/access/rpc/backend/transactions/transactions_test.go index 7531cf6d29d..6ebb861a551 100644 --- a/engine/access/rpc/backend/transactions/transactions_test.go +++ b/engine/access/rpc/backend/transactions/transactions_test.go @@ -1,68 +1,55 @@ package transactions import ( - "bytes" "context" "fmt" - "math/rand" - "os" "testing" - "github.com/cockroachdb/pebble/v2" lru "github.com/hashicorp/golang-lru/v2" - "github.com/onflow/cadence" - cadenceCommon "github.com/onflow/cadence/common" - "github.com/onflow/cadence/encoding/ccf" - jsoncdc "github.com/onflow/cadence/encoding/json" - "github.com/onflow/flow/protobuf/go/flow/access" - "github.com/onflow/flow/protobuf/go/flow/entities" - execproto "github.com/onflow/flow/protobuf/go/flow/execution" "github.com/rs/zerolog" "github.com/stretchr/testify/mock" - "github.com/stretchr/testify/require" "github.com/stretchr/testify/suite" "google.golang.org/grpc/codes" "google.golang.org/grpc/status" + "github.com/onflow/flow/protobuf/go/flow/access" + "github.com/onflow/flow/protobuf/go/flow/entities" + "github.com/onflow/flow-go/access/validator" - validatormock "github.com/onflow/flow-go/access/validator/mock" "github.com/onflow/flow-go/engine/access/index" accessmock "github.com/onflow/flow-go/engine/access/mock" "github.com/onflow/flow-go/engine/access/rpc/backend/node_communicator" "github.com/onflow/flow-go/engine/access/rpc/backend/transactions/error_messages" - "github.com/onflow/flow-go/engine/access/rpc/backend/transactions/provider" - "github.com/onflow/flow-go/engine/access/rpc/backend/transactions/retrier" + providermock "github.com/onflow/flow-go/engine/access/rpc/backend/transactions/provider/mock" txstatus "github.com/onflow/flow-go/engine/access/rpc/backend/transactions/status" + "github.com/onflow/flow-go/engine/access/rpc/backend/transactions/system" connectionmock "github.com/onflow/flow-go/engine/access/rpc/connection/mock" commonrpc "github.com/onflow/flow-go/engine/common/rpc" "github.com/onflow/flow-go/engine/common/rpc/convert" "github.com/onflow/flow-go/fvm/blueprints" - "github.com/onflow/flow-go/fvm/systemcontracts" accessmodel "github.com/onflow/flow-go/model/access" "github.com/onflow/flow-go/model/flow" - "github.com/onflow/flow-go/model/flow/filter" - "github.com/onflow/flow-go/module" "github.com/onflow/flow-go/module/counters" execmock "github.com/onflow/flow-go/module/execution/mock" + "github.com/onflow/flow-go/module/irrecoverable" "github.com/onflow/flow-go/module/metrics" syncmock "github.com/onflow/flow-go/module/state_synchronization/mock" - "github.com/onflow/flow-go/state/protocol" - bprotocol "github.com/onflow/flow-go/state/protocol/badger" protocolmock "github.com/onflow/flow-go/state/protocol/mock" - "github.com/onflow/flow-go/state/protocol/util" "github.com/onflow/flow-go/storage" storagemock "github.com/onflow/flow-go/storage/mock" - "github.com/onflow/flow-go/storage/operation/pebbleimpl" - "github.com/onflow/flow-go/storage/store" "github.com/onflow/flow-go/utils/unittest" + "github.com/onflow/flow-go/utils/unittest/fixtures" "github.com/onflow/flow-go/utils/unittest/mocks" ) -const expectedErrorMsg = "expected test error" +func TestTransactionsBackend(t *testing.T) { + suite.Run(t, new(Suite)) +} type Suite struct { suite.Suite + g *fixtures.GeneratorSuite log zerolog.Logger state *protocolmock.State snapshot *protocolmock.Snapshot @@ -77,11 +64,9 @@ type Suite struct { lightTxResults *storagemock.LightTransactionResults events *storagemock.Events txResultErrorMessages *storagemock.TransactionResultErrorMessages + scheduledTransactions *storagemock.ScheduledTransactions txResultCache *lru.Cache[flow.Identifier, *accessmodel.TransactionResult] - - db *pebble.DB - dbDir string - lastFullBlockHeight *counters.PersistentStrictMonotonicCounter + lastFullBlockHeight *counters.PersistentStrictMonotonicCounter executionAPIClient *accessmock.ExecutionAPIClient historicalAccessAPIClient *accessmock.AccessAPIClient @@ -95,26 +80,24 @@ type Suite struct { errorMessageProvider error_messages.Provider - chainID flow.ChainID - systemTx *flow.TransactionBody - systemCollection *flow.Collection - pendingExecutionEvents []flow.Event - processScheduledCallbackEventType flow.EventType - scheduledCallbacksEnabled bool + chainID flow.ChainID + defaultSystemCollection *system.SystemCollection + systemCollection *flow.Collection + pendingExecutionEvents []flow.Event + processScheduledTransactionEventType flow.EventType + scheduledTransactionsEnabled bool fixedExecutionNodeIDs flow.IdentifierList preferredExecutionNodeIDs flow.IdentifierList } -func TestTransactionsBackend(t *testing.T) { - suite.Run(t, new(Suite)) -} - func (suite *Suite) SetupTest() { suite.log = unittest.Logger() - suite.snapshot = protocolmock.NewSnapshot(suite.T()) + suite.chainID = flow.Testnet + suite.g = fixtures.NewGeneratorSuite(fixtures.WithChainID(suite.chainID)) header := unittest.BlockHeaderFixture() + suite.snapshot = protocolmock.NewSnapshot(suite.T()) suite.params = protocolmock.NewParams(suite.T()) suite.params.On("FinalizedRoot").Return(header, nil).Maybe() suite.params.On("SporkID").Return(unittest.IdentifierFixture(), nil).Maybe() @@ -134,7 +117,7 @@ func (suite *Suite) SetupTest() { suite.executionAPIClient = accessmock.NewExecutionAPIClient(suite.T()) suite.lightTxResults = storagemock.NewLightTransactionResults(suite.T()) suite.events = storagemock.NewEvents(suite.T()) - suite.chainID = flow.Testnet + suite.scheduledTransactions = storagemock.NewScheduledTransactions(suite.T()) suite.historicalAccessAPIClient = accessmock.NewAccessAPIClient(suite.T()) suite.connectionFactory = connectionmock.NewConnectionFactory(suite.T()) @@ -149,19 +132,18 @@ func (suite *Suite) SetupTest() { suite.eventsIndex = index.NewEventsIndex(suite.indexReporter, suite.events) suite.txResultsIndex = index.NewTransactionResultsIndex(suite.indexReporter, suite.lightTxResults) - suite.systemTx, err = blueprints.SystemChunkTransaction(flow.Testnet.Chain()) + // this is the system collection with no scheduled transactions used within the backend + suite.defaultSystemCollection, err = system.DefaultSystemCollection(suite.chainID, true) suite.Require().NoError(err) - suite.scheduledCallbacksEnabled = true + suite.scheduledTransactionsEnabled = true - suite.pendingExecutionEvents = suite.createPendingExecutionEvents(2) // 2 callbacks + // this is the system collection with scheduled transactions used as block data + suite.pendingExecutionEvents = suite.g.PendingExecutionEvents().List(2) suite.systemCollection, err = blueprints.SystemCollection(suite.chainID.Chain(), suite.pendingExecutionEvents) suite.Require().NoError(err) - suite.processScheduledCallbackEventType = suite.pendingExecutionEvents[0].Type + suite.processScheduledTransactionEventType = suite.pendingExecutionEvents[0].Type - suite.db, suite.dbDir = unittest.TempPebbleDB(suite.T()) - progress, err := store.NewConsumerProgress(pebbleimpl.ToDB(suite.db), module.ConsumeProgressLastFullBlockHeight).Initialize(0) - require.NoError(suite.T(), err) - suite.lastFullBlockHeight, err = counters.NewPersistentStrictMonotonicCounter(progress) + suite.lastFullBlockHeight, err = counters.NewPersistentStrictMonotonicCounter(newMockConsumerProgress()) suite.Require().NoError(err) suite.fixedExecutionNodeIDs = nil @@ -169,9 +151,29 @@ func (suite *Suite) SetupTest() { suite.errorMessageProvider = nil } -func (suite *Suite) TearDownTest() { - err := os.RemoveAll(suite.dbDir) - suite.Require().NoError(err) +var _ storage.ConsumerProgress = (*mockConsumerProgress)(nil) + +type mockConsumerProgress struct { + counter counters.StrictMonotonicCounter +} + +func newMockConsumerProgress() *mockConsumerProgress { + return &mockConsumerProgress{counter: counters.NewMonotonicCounter(0)} +} + +func (m *mockConsumerProgress) ProcessedIndex() (uint64, error) { + return m.counter.Value(), nil +} + +func (m *mockConsumerProgress) SetProcessedIndex(processed uint64) error { + if !m.counter.Set(processed) { + return fmt.Errorf("value must not decrease: %d", processed) + } + return nil +} + +func (m *mockConsumerProgress) BatchSetProcessedIndex(processed uint64, batch storage.ReaderBatchWriter) error { + return m.SetProcessedIndex(processed) } func (suite *Suite) defaultTransactionsParams() Params { @@ -188,1721 +190,2116 @@ func (suite *Suite) defaultTransactionsParams() Params { suite.lastFullBlockHeight, ) + validatorBlocks := validator.NewProtocolStateBlocks(suite.state, suite.indexReporter) txValidator, err := validator.NewTransactionValidator( - validatormock.NewBlocks(suite.T()), + validatorBlocks, suite.chainID.Chain(), metrics.NewNoopCollector(), - validator.TransactionValidationOptions{}, + validator.TransactionValidationOptions{ + Expiry: flow.DefaultTransactionExpiry, + ExpiryBuffer: flow.DefaultTransactionExpiryBuffer, + AllowEmptyReferenceBlockID: false, + AllowUnknownReferenceBlockID: false, + CheckScriptsParse: false, + MaxGasLimit: flow.DefaultMaxTransactionGasLimit, + MaxTransactionByteSize: flow.DefaultMaxTransactionByteSize, + MaxCollectionByteSize: flow.DefaultMaxCollectionByteSize, + CheckPayerBalanceMode: validator.Disabled, + }, execmock.NewScriptExecutor(suite.T()), ) suite.Require().NoError(err) - nodeCommunicator := node_communicator.NewNodeCommunicator(false) - - txProvider := provider.NewENTransactionProvider( - suite.log, - suite.state, - suite.collections, - suite.connectionFactory, - nodeCommunicator, - nodeProvider, - txStatusDeriver, - suite.systemTx.ID(), - suite.chainID, - suite.scheduledCallbacksEnabled, - ) - return Params{ - Log: suite.log, - Metrics: metrics.NewNoopCollector(), - State: suite.state, - ChainID: flow.Testnet, - SystemTxID: suite.systemTx.ID(), - StaticCollectionRPCClient: suite.historicalAccessAPIClient, - HistoricalAccessNodeClients: nil, - NodeCommunicator: nodeCommunicator, - ConnFactory: suite.connectionFactory, - EnableRetries: true, - NodeProvider: nodeProvider, - Blocks: suite.blocks, - Collections: suite.collections, - Transactions: suite.transactions, - Events: suite.events, - TxErrorMessageProvider: suite.errorMessageProvider, - TxResultCache: suite.txResultCache, - TxProvider: txProvider, - TxValidator: txValidator, - TxStatusDeriver: txStatusDeriver, - EventsIndex: suite.eventsIndex, - TxResultsIndex: suite.txResultsIndex, - ScheduledCallbacksEnabled: suite.scheduledCallbacksEnabled, + Log: suite.log, + Metrics: metrics.NewNoopCollector(), + State: suite.state, + ChainID: flow.Testnet, + SystemCollection: suite.defaultSystemCollection, + NodeCommunicator: node_communicator.NewNodeCommunicator(false), + ConnFactory: suite.connectionFactory, + NodeProvider: nodeProvider, + Blocks: suite.blocks, + Collections: suite.collections, + Transactions: suite.transactions, + TxErrorMessageProvider: suite.errorMessageProvider, + ScheduledTransactions: suite.scheduledTransactions, + TxResultCache: suite.txResultCache, + TxValidator: txValidator, + TxStatusDeriver: txStatusDeriver, + EventsIndex: suite.eventsIndex, + TxResultsIndex: suite.txResultsIndex, + ScheduledTransactionsEnabled: suite.scheduledTransactionsEnabled, } } -// TestGetTransactionResult_UnknownTx returns unknown result when tx not found -func (suite *Suite) TestGetTransactionResult_UnknownTx() { - block := unittest.BlockFixture() - tx := unittest.TransactionBodyFixture() - coll := unittest.CollectionFromTransactions(&tx) - - suite.transactions. - On("ByID", tx.ID()). - Return(nil, storage.ErrNotFound) - +// TestGetTransaction_SubmittedTx tests getting a user submitted transaction by ID returns the +// correct transaction. +func (suite *Suite) TestGetTransaction_SubmittedTx() { params := suite.defaultTransactionsParams() txBackend, err := NewTransactionsBackend(params) - require.NoError(suite.T(), err) - res, err := txBackend.GetTransactionResult( - context.Background(), - tx.ID(), - block.ID(), - coll.ID(), - entities.EventEncodingVersion_JSON_CDC_V0, - ) suite.Require().NoError(err) - suite.Require().Equal(res.Status, flow.TransactionStatusUnknown) - suite.Require().Empty(res.BlockID) - suite.Require().Empty(res.BlockHeight) - suite.Require().Empty(res.TransactionID) - suite.Require().Empty(res.CollectionID) - suite.Require().Empty(res.ErrorMessage) -} -// TestGetTransactionResult_TxLookupFailure returns error from transaction storage -func (suite *Suite) TestGetTransactionResult_TxLookupFailure() { - block := unittest.BlockFixture() - tx := unittest.TransactionBodyFixture() - coll := unittest.CollectionFromTransactions(&tx) + suite.Run("submitted transaction found", func() { + tx := suite.g.Transactions().Fixture() + txID := tx.ID() - expectedErr := fmt.Errorf("some other error") - suite.transactions. - On("ByID", tx.ID()). - Return(nil, expectedErr) - - params := suite.defaultTransactionsParams() - txBackend, err := NewTransactionsBackend(params) - require.NoError(suite.T(), err) - - _, err = txBackend.GetTransactionResult( - context.Background(), - tx.ID(), - block.ID(), - coll.ID(), - entities.EventEncodingVersion_JSON_CDC_V0, - ) - suite.Require().Equal(err, status.Errorf(codes.Internal, "failed to find: %v", expectedErr)) -} + suite.transactions. + On("ByID", txID). + Return(tx, nil). + Once() -// TestGetTransactionResult_HistoricNodes_Success tests lookup in historic nodes -func (suite *Suite) TestGetTransactionResult_HistoricNodes_Success() { - block := unittest.BlockFixture() - tx := unittest.TransactionBodyFixture() - coll := unittest.CollectionFromTransactions(&tx) + actual, err := txBackend.GetTransaction(context.Background(), txID) + suite.Require().NoError(err) + suite.Require().Equal(tx, actual) + }) - suite.transactions. - On("ByID", tx.ID()). - Return(nil, storage.ErrNotFound) + suite.Run("submitted transaction - unexpected error", func() { + tx := suite.g.Transactions().Fixture() + txID := tx.ID() - transactionResultResponse := access.TransactionResultResponse{ - Status: entities.TransactionStatus_EXECUTED, - StatusCode: uint32(entities.TransactionStatus_EXECUTED), - } + expectedErr := fmt.Errorf("some other error") + suite.transactions. + On("ByID", txID). + Return(nil, expectedErr). + Once() - suite.historicalAccessAPIClient. - On("GetTransactionResult", mock.Anything, mock.MatchedBy(func(req *access.GetTransactionRequest) bool { - txID := tx.ID() - return bytes.Equal(txID[:], req.Id) - })). - Return(&transactionResultResponse, nil). - Once() + actual, err := txBackend.GetTransaction(context.Background(), txID) + suite.Require().Error(err) + suite.Require().Equal(codes.Internal, status.Code(err)) + suite.Require().Nil(actual) + }) +} +// TestGetTransaction_SystemTx tests getting a system transaction by ID returns the correct transaction. +func (suite *Suite) TestGetTransaction_SystemTx() { params := suite.defaultTransactionsParams() - params.HistoricalAccessNodeClients = []access.AccessAPIClient{suite.historicalAccessAPIClient} txBackend, err := NewTransactionsBackend(params) - require.NoError(suite.T(), err) - - resp, err := txBackend.GetTransactionResult( - context.Background(), - tx.ID(), - block.ID(), - coll.ID(), - entities.EventEncodingVersion_JSON_CDC_V0, - ) suite.Require().NoError(err) - suite.Require().Equal(flow.TransactionStatusExecuted, resp.Status) - suite.Require().Equal(uint(flow.TransactionStatusExecuted), resp.StatusCode) -} -// TestGetTransactionResult_HistoricNodes_FromCache get historic transaction result from cache -func (suite *Suite) TestGetTransactionResult_HistoricNodes_FromCache() { - block := unittest.BlockFixture() - tx := unittest.TransactionBodyFixture() + tx := suite.systemCollection.Transactions[0] + txID := tx.ID() suite.transactions. - On("ByID", tx.ID()). - Return(nil, storage.ErrNotFound) - - transactionResultResponse := access.TransactionResultResponse{ - Status: entities.TransactionStatus_EXECUTED, - StatusCode: uint32(entities.TransactionStatus_EXECUTED), - } - - suite.historicalAccessAPIClient. - On("GetTransactionResult", mock.Anything, mock.MatchedBy(func(req *access.GetTransactionRequest) bool { - txID := tx.ID() - return bytes.Equal(txID[:], req.Id) - })). - Return(&transactionResultResponse, nil). + On("ByID", txID). + Return(nil, storage.ErrNotFound). Once() - params := suite.defaultTransactionsParams() - params.HistoricalAccessNodeClients = []access.AccessAPIClient{suite.historicalAccessAPIClient} - txBackend, err := NewTransactionsBackend(params) - require.NoError(suite.T(), err) - - coll := unittest.CollectionFromTransactions(&tx) - resp, err := txBackend.GetTransactionResult( - context.Background(), - tx.ID(), - block.ID(), - coll.ID(), - entities.EventEncodingVersion_JSON_CDC_V0, - ) - suite.Require().NoError(err) - suite.Require().Equal(flow.TransactionStatusExecuted, resp.Status) - suite.Require().Equal(uint(flow.TransactionStatusExecuted), resp.StatusCode) - - resp2, err := txBackend.GetTransactionResult( - context.Background(), - tx.ID(), - block.ID(), - coll.ID(), - entities.EventEncodingVersion_JSON_CDC_V0, - ) + actual, err := txBackend.GetTransaction(context.Background(), txID) suite.Require().NoError(err) - suite.Require().Equal(flow.TransactionStatusExecuted, resp2.Status) - suite.Require().Equal(uint(flow.TransactionStatusExecuted), resp2.StatusCode) + suite.Require().Equal(tx, actual) } -// TestGetTransactionResultUnknownFromCache retrieve unknown result from cache. -func (suite *Suite) TestGetTransactionResultUnknownFromCache() { - block := unittest.BlockFixture() - tx := unittest.TransactionBodyFixture() +// TestGetTransaction_ScheduledTx tests getting a scheduled transaction by ID returns the correct transaction. +func (suite *Suite) TestGetTransaction_ScheduledTx() { + block := suite.g.Blocks().Fixture() + blockID := block.ID() - suite.transactions. - On("ByID", tx.ID()). - Return(nil, storage.ErrNotFound) + suite.Run("happy path", func() { + tx := suite.systemCollection.Transactions[1] + txID := tx.ID() - suite.historicalAccessAPIClient. - On("GetTransactionResult", mock.Anything, mock.MatchedBy(func(req *access.GetTransactionRequest) bool { - txID := tx.ID() - return bytes.Equal(txID[:], req.Id) - })). - Return(nil, status.Errorf(codes.NotFound, "no known transaction with ID %s", tx.ID())). - Once() + scheduledTxs := []*flow.TransactionBody{ + suite.g.Transactions().Fixture(), + tx, + suite.g.Transactions().Fixture(), + } - params := suite.defaultTransactionsParams() - params.HistoricalAccessNodeClients = []access.AccessAPIClient{suite.historicalAccessAPIClient} - txBackend, err := NewTransactionsBackend(params) - require.NoError(suite.T(), err) - - coll := unittest.CollectionFromTransactions(&tx) - resp, err := txBackend.GetTransactionResult( - context.Background(), - tx.ID(), - block.ID(), - coll.ID(), - entities.EventEncodingVersion_JSON_CDC_V0, - ) - suite.Require().NoError(err) - suite.Require().Equal(flow.TransactionStatusUnknown, resp.Status) - suite.Require().Equal(uint(flow.TransactionStatusUnknown), resp.StatusCode) - - // ensure the unknown transaction is cached when not found anywhere - txStatus := flow.TransactionStatusUnknown - res, ok := txBackend.txResultCache.Get(tx.ID()) - suite.Require().True(ok) - suite.Require().Equal(res, &accessmodel.TransactionResult{ - Status: txStatus, - StatusCode: uint(txStatus), - }) - - // ensure underlying GetTransactionResult() won't be called the second time - resp2, err := txBackend.GetTransactionResult( - context.Background(), - tx.ID(), - block.ID(), - coll.ID(), - entities.EventEncodingVersion_JSON_CDC_V0, - ) - suite.Require().NoError(err) - suite.Require().Equal(flow.TransactionStatusUnknown, resp2.Status) - suite.Require().Equal(uint(flow.TransactionStatusUnknown), resp2.StatusCode) -} + suite.transactions. + On("ByID", txID). + Return(nil, storage.ErrNotFound). + Once() -// TestGetSystemTransaction_HappyPath tests that GetSystemTransaction call returns system chunk transaction. -func (suite *Suite) TestGetSystemTransaction_ExecutionNode_HappyPath() { - block := unittest.BlockFixture() - blockID := block.ID() + suite.scheduledTransactions. + On("BlockIDByTransactionID", txID). + Return(blockID, nil). + Once() - params := suite.defaultTransactionsParams() - enabledProvider := provider.NewENTransactionProvider( - suite.log, - suite.state, - suite.collections, - suite.connectionFactory, - params.NodeCommunicator, - params.NodeProvider, - params.TxStatusDeriver, - suite.systemTx.ID(), - suite.chainID, - true, - ) - disabledProvider := provider.NewENTransactionProvider( - suite.log, - suite.state, - suite.collections, - suite.connectionFactory, - params.NodeCommunicator, - params.NodeProvider, - params.TxStatusDeriver, - suite.systemTx.ID(), - suite.chainID, - false, - ) + snapshot := protocolmock.NewSnapshot(suite.T()) + snapshot.On("Head").Return(block.ToHeader(), nil) - suite.params.On("FinalizedRoot").Unset() + suite.state. + On("AtBlockID", blockID). + Return(snapshot, nil). + Once() - suite.Run("scheduled callbacks DISABLED - ZeroID", func() { - suite.blocks.On("ByID", blockID).Return(block, nil).Once() + provider := providermock.NewTransactionProvider(suite.T()) + provider. + On("ScheduledTransactionsByBlockID", mock.Anything, block.ToHeader()). + Return(scheduledTxs, nil) - params.TxProvider = disabledProvider + params := suite.defaultTransactionsParams() + params.TxProvider = provider txBackend, err := NewTransactionsBackend(params) suite.Require().NoError(err) - res, err := txBackend.GetSystemTransaction(context.Background(), flow.ZeroID, blockID) + actual, err := txBackend.GetTransaction(context.Background(), txID) suite.Require().NoError(err) - - suite.Require().Equal(suite.systemTx, res) + suite.Require().Equal(tx, actual) }) - suite.Run("scheduled callbacks DISABLED - system txID", func() { - suite.blocks.On("ByID", blockID).Return(block, nil).Once() + // Tests the case where the scheduled transaction exists in the indices, but not in the generated + // system collection (produced from events). This indicates inconsistent state, however it is handled + // by returning an internal error instead of an irrecoverable since the events may be queried from + // an Execution node, which could have returned incorrect data. + suite.Run("not in generated collection", func() { + tx := suite.systemCollection.Transactions[1] + txID := tx.ID() + + scheduledTxs := []*flow.TransactionBody{ + suite.g.Transactions().Fixture(), + suite.g.Transactions().Fixture(), + } - params.TxProvider = disabledProvider + suite.transactions. + On("ByID", txID). + Return(nil, storage.ErrNotFound). + Once() - txBackend, err := NewTransactionsBackend(params) - suite.Require().NoError(err) + suite.scheduledTransactions. + On("BlockIDByTransactionID", txID). + Return(blockID, nil). + Once() - res, err := txBackend.GetSystemTransaction(context.Background(), suite.systemTx.ID(), blockID) - suite.Require().NoError(err) + snapshot := protocolmock.NewSnapshot(suite.T()) + snapshot.On("Head").Return(block.ToHeader(), nil) - suite.Require().Equal(suite.systemTx, res) - }) + suite.state. + On("AtBlockID", blockID). + Return(snapshot, nil). + Once() - suite.Run("scheduled callbacks DISABLED - non-system txID fails", func() { - suite.blocks.On("ByID", blockID).Return(block, nil).Once() + provider := providermock.NewTransactionProvider(suite.T()) + provider. + On("ScheduledTransactionsByBlockID", mock.Anything, block.ToHeader()). + Return(scheduledTxs, nil) - params.TxProvider = disabledProvider + params := suite.defaultTransactionsParams() + params.TxProvider = provider txBackend, err := NewTransactionsBackend(params) suite.Require().NoError(err) - res, err := txBackend.GetSystemTransaction(context.Background(), unittest.IdentifierFixture(), blockID) + actual, err := txBackend.GetTransaction(context.Background(), txID) suite.Require().Error(err) - suite.Require().Nil(res) + suite.Require().Equal(codes.Internal, status.Code(err)) + suite.Require().Nil(actual) }) - suite.Run("scheduled callbacks ENABLED - ZeroID", func() { - suite.blocks.On("ByID", blockID).Return(block, nil).Once() + suite.Run("provider error", func() { + tx := suite.systemCollection.Transactions[1] + txID := tx.ID() - params.TxProvider = enabledProvider + expectedErr := status.Errorf(codes.Internal, "some other error") - txBackend, err := NewTransactionsBackend(params) - suite.Require().NoError(err) + suite.transactions. + On("ByID", txID). + Return(nil, storage.ErrNotFound). + Once() - res, err := txBackend.GetSystemTransaction(context.Background(), flow.ZeroID, blockID) + suite.scheduledTransactions. + On("BlockIDByTransactionID", txID). + Return(blockID, nil). + Once() + + snapshot := protocolmock.NewSnapshot(suite.T()) + snapshot.On("Head").Return(block.ToHeader(), nil) + + suite.state. + On("AtBlockID", blockID). + Return(snapshot, nil). + Once() + + provider := providermock.NewTransactionProvider(suite.T()) + provider. + On("ScheduledTransactionsByBlockID", mock.Anything, block.ToHeader()). + Return(nil, expectedErr) + + params := suite.defaultTransactionsParams() + params.TxProvider = provider + + txBackend, err := NewTransactionsBackend(params) suite.Require().NoError(err) - suite.Require().Equal(suite.systemTx, res) + actual, err := txBackend.GetTransaction(context.Background(), txID) + suite.Require().ErrorIs(err, expectedErr) + suite.Require().Nil(actual) }) - suite.Run("scheduled callbacks ENABLED - system txID", func() { - suite.blocks.On("ByID", blockID).Return(block, nil).Once() + suite.Run("block not found exception", func() { + tx := suite.systemCollection.Transactions[1] + txID := tx.ID() + + suite.transactions. + On("ByID", txID). + Return(nil, storage.ErrNotFound). + Once() + + suite.scheduledTransactions. + On("BlockIDByTransactionID", txID). + Return(blockID, nil). + Once() + + snapshot := protocolmock.NewSnapshot(suite.T()) + snapshot.On("Head").Return(nil, storage.ErrNotFound) + + suite.state. + On("AtBlockID", blockID). + Return(snapshot, nil). + Once() - params.TxProvider = enabledProvider + params := suite.defaultTransactionsParams() + params.TxProvider = providermock.NewTransactionProvider(suite.T()) txBackend, err := NewTransactionsBackend(params) suite.Require().NoError(err) - res, err := txBackend.GetSystemTransaction(context.Background(), suite.systemTx.ID(), blockID) - suite.Require().NoError(err) + ctx := irrecoverable.NewMockSignalerContextExpectError(suite.T(), context.Background(), + fmt.Errorf("failed to get block header: %w", storage.ErrNotFound)) + signalerCtx := irrecoverable.WithSignalerContext(context.Background(), ctx) - suite.Require().Equal(suite.systemTx, res) + actual, err := txBackend.GetTransaction(signalerCtx, txID) + suite.Require().Error(err) // specific error doen't matter since it's thrown + suite.Require().Nil(actual) }) +} - suite.Run("scheduled callbacks ENABLED - system collection TX", func() { - suite.blocks.On("ByID", blockID).Return(block, nil).Once() +// TestGetTransaction_HistoricalTx tests getting a historical transaction by ID from the historical +// access nodes. +func (suite *Suite) TestGetTransaction_HistoricalTx() { + suite.Run("historical tx", func() { + tx := suite.g.Transactions().Fixture() + txID := tx.ID() + + suite.transactions. + On("ByID", txID). + Return(nil, storage.ErrNotFound). + Once() + + suite.scheduledTransactions. + On("BlockIDByTransactionID", txID). + Return(flow.ZeroID, storage.ErrNotFound). + Once() - // get execution node identities - suite.params.On("FinalizedRoot").Return(block.ToHeader(), nil) - suite.state.On("Final").Return(suite.snapshot, nil).Twice() - suite.snapshot.On("Identities", mock.Anything).Return(unittest.IdentityListFixture(1), nil).Twice() + expectedRequest := &access.GetTransactionRequest{ + Id: txID[:], + } + response := &access.TransactionResponse{ + Transaction: convert.TransactionToMessage(*tx), + } - suite.setupExecutionGetEventsRequest(blockID, block.Height, suite.pendingExecutionEvents) + suite.historicalAccessAPIClient. + On("GetTransaction", mock.Anything, expectedRequest). + Return(response, nil) - params.TxProvider = enabledProvider + params := suite.defaultTransactionsParams() + params.HistoricalAccessNodeClients = []access.AccessAPIClient{suite.historicalAccessAPIClient} txBackend, err := NewTransactionsBackend(params) suite.Require().NoError(err) - systemTx := suite.systemCollection.Transactions[2] - res, err := txBackend.GetSystemTransaction(context.Background(), systemTx.ID(), blockID) + actual, err := txBackend.GetTransaction(context.Background(), txID) suite.Require().NoError(err) - - suite.Require().Equal(systemTx, res) + suite.Require().Equal(tx, actual) }) - suite.Run("scheduled callbacks ENABLED - non-system txID fails", func() { - suite.blocks.On("ByID", blockID).Return(block, nil).Once() + suite.Run("historical tx returns unexpected error", func() { + tx := suite.g.Transactions().Fixture() + txID := tx.ID() + + suite.transactions. + On("ByID", txID). + Return(nil, storage.ErrNotFound). + Once() + + suite.scheduledTransactions. + On("BlockIDByTransactionID", txID). + Return(flow.ZeroID, storage.ErrNotFound). + Once() - params.TxProvider = enabledProvider + expectedRequest := &access.GetTransactionRequest{ + Id: txID[:], + } - suite.params.On("FinalizedRoot").Return(block.ToHeader(), nil) - suite.state.On("Final").Return(suite.snapshot, nil).Twice() - suite.snapshot.On("Identities", mock.Anything).Return(unittest.IdentityListFixture(1), nil).Twice() + suite.historicalAccessAPIClient. + On("GetTransaction", mock.Anything, expectedRequest). + Return(nil, status.Errorf(codes.Internal, "some other error")) - suite.setupExecutionGetEventsRequest(blockID, block.Height, suite.pendingExecutionEvents) + params := suite.defaultTransactionsParams() + params.HistoricalAccessNodeClients = []access.AccessAPIClient{suite.historicalAccessAPIClient} txBackend, err := NewTransactionsBackend(params) suite.Require().NoError(err) - res, err := txBackend.GetSystemTransaction(context.Background(), unittest.IdentifierFixture(), blockID) + actual, err := txBackend.GetTransaction(context.Background(), txID) suite.Require().Error(err) - suite.Require().Nil(res) + suite.Require().Equal(codes.NotFound, status.Code(err)) + suite.Require().Nil(actual) }) } -// TestGetSystemTransaction_HappyPath tests that GetSystemTransaction call returns system chunk transaction. -func (suite *Suite) TestGetSystemTransaction_Local_HappyPath() { - block := unittest.BlockFixture() +func (suite *Suite) TestGetTransactionResult_SystemTx() { + tx := suite.systemCollection.Transactions[0] + txID := tx.ID() + + block := suite.g.Blocks().Fixture() blockID := block.ID() - params := suite.defaultTransactionsParams() - enabledProvider := provider.NewLocalTransactionProvider( - suite.state, - suite.collections, - suite.blocks, - params.EventsIndex, - params.TxResultsIndex, - params.TxErrorMessageProvider, - suite.systemTx.ID(), - params.TxStatusDeriver, - suite.chainID, - true, - ) - disabledProvider := provider.NewLocalTransactionProvider( - suite.state, - suite.collections, - suite.blocks, - params.EventsIndex, - params.TxResultsIndex, - params.TxErrorMessageProvider, - suite.systemTx.ID(), - params.TxStatusDeriver, - suite.chainID, - false, - ) + expectedResult := &accessmodel.TransactionResult{ + BlockID: blockID, + TransactionID: txID, + CollectionID: flow.ZeroID, + BlockHeight: block.Height, + Status: flow.TransactionStatusExecuted, + } + + encodingVersion := entities.EventEncodingVersion_JSON_CDC_V0 + + suite.Run("happy path", func() { + snapshot := protocolmock.NewSnapshot(suite.T()) + snapshot.On("Head").Return(block.ToHeader(), nil) - suite.params.On("FinalizedRoot").Unset() + suite.state. + On("AtBlockID", blockID). + Return(snapshot, nil). + Once() - suite.Run("scheduled callbacks DISABLED - ZeroID", func() { - suite.blocks.On("ByID", blockID).Return(block, nil).Once() + provider := providermock.NewTransactionProvider(suite.T()) + provider. + On("TransactionResult", mock.Anything, block.ToHeader(), txID, flow.ZeroID, encodingVersion). + Return(expectedResult, nil) - params.TxProvider = disabledProvider + params := suite.defaultTransactionsParams() + params.TxProvider = provider txBackend, err := NewTransactionsBackend(params) suite.Require().NoError(err) - res, err := txBackend.GetSystemTransaction(context.Background(), flow.ZeroID, blockID) + res, err := txBackend.GetTransactionResult(context.Background(), txID, blockID, flow.ZeroID, encodingVersion) suite.Require().NoError(err) - - suite.Require().Equal(suite.systemTx, res) + suite.Require().Equal(expectedResult, res) }) - suite.Run("scheduled callbacks DISABLED - system txID", func() { - suite.blocks.On("ByID", blockID).Return(block, nil).Once() + suite.Run("provider error", func() { + expectedErr := status.Errorf(codes.Internal, "some other error") - params.TxProvider = disabledProvider + snapshot := protocolmock.NewSnapshot(suite.T()) + snapshot.On("Head").Return(block.ToHeader(), nil) - txBackend, err := NewTransactionsBackend(params) - suite.Require().NoError(err) + suite.state. + On("AtBlockID", blockID). + Return(snapshot, nil). + Once() + + provider := providermock.NewTransactionProvider(suite.T()) + provider. + On("TransactionResult", mock.Anything, block.ToHeader(), txID, flow.ZeroID, encodingVersion). + Return(nil, expectedErr) + + params := suite.defaultTransactionsParams() + params.TxProvider = provider - res, err := txBackend.GetSystemTransaction(context.Background(), suite.systemTx.ID(), blockID) + txBackend, err := NewTransactionsBackend(params) suite.Require().NoError(err) - suite.Require().Equal(suite.systemTx, res) + res, err := txBackend.GetTransactionResult(context.Background(), txID, blockID, flow.ZeroID, encodingVersion) + suite.Require().ErrorIs(err, expectedErr) + suite.Require().Nil(res) + suite.Require().Equal(expectedErr, err) }) - suite.Run("scheduled callbacks DISABLED - non-system txID fails", func() { - suite.blocks.On("ByID", blockID).Return(block, nil).Once() + suite.Run("block not found exception", func() { + snapshot := protocolmock.NewSnapshot(suite.T()) + snapshot.On("Head").Return(nil, storage.ErrNotFound) + + suite.state. + On("AtBlockID", blockID). + Return(snapshot, nil). + Once() - params.TxProvider = disabledProvider + params := suite.defaultTransactionsParams() + params.TxProvider = providermock.NewTransactionProvider(suite.T()) txBackend, err := NewTransactionsBackend(params) suite.Require().NoError(err) - res, err := txBackend.GetSystemTransaction(context.Background(), unittest.IdentifierFixture(), blockID) + res, err := txBackend.GetTransactionResult(context.Background(), txID, blockID, flow.ZeroID, encodingVersion) suite.Require().Error(err) + suite.Require().Equal(codes.NotFound, status.Code(err)) suite.Require().Nil(res) }) - suite.Run("scheduled callbacks ENABLED - ZeroID", func() { - suite.blocks.On("ByID", blockID).Return(block, nil).Once() - - params.TxProvider = enabledProvider + suite.Run("block not provided", func() { + params := suite.defaultTransactionsParams() + params.TxProvider = providermock.NewTransactionProvider(suite.T()) txBackend, err := NewTransactionsBackend(params) suite.Require().NoError(err) - res, err := txBackend.GetSystemTransaction(context.Background(), flow.ZeroID, blockID) - suite.Require().NoError(err) - - suite.Require().Equal(suite.systemTx, res) + res, err := txBackend.GetTransactionResult(context.Background(), txID, flow.ZeroID, flow.ZeroID, encodingVersion) + suite.Require().Error(err) + suite.Require().Equal(codes.InvalidArgument, status.Code(err)) + suite.Require().Nil(res) }) +} - suite.Run("scheduled callbacks ENABLED - system txID", func() { - suite.blocks.On("ByID", blockID).Return(block, nil).Once() +func (suite *Suite) TestGetTransactionResult_ScheduledTx() { + tx := suite.systemCollection.Transactions[1] + txID := tx.ID() - params.TxProvider = enabledProvider + block := suite.g.Blocks().Fixture() + blockID := block.ID() - txBackend, err := NewTransactionsBackend(params) - suite.Require().NoError(err) + encodingVersion := entities.EventEncodingVersion_JSON_CDC_V0 - res, err := txBackend.GetSystemTransaction(context.Background(), suite.systemTx.ID(), blockID) - suite.Require().NoError(err) + expectedResult := &accessmodel.TransactionResult{ + BlockID: blockID, + TransactionID: txID, + CollectionID: flow.ZeroID, + BlockHeight: block.Height, + Status: flow.TransactionStatusExecuted, + } - suite.Require().Equal(suite.systemTx, res) - }) + suite.Run("happy path", func() { + suite.scheduledTransactions. + On("BlockIDByTransactionID", txID). + Return(blockID, nil). + Once() + + snapshot := protocolmock.NewSnapshot(suite.T()) + snapshot.On("Head").Return(block.ToHeader(), nil) - suite.Run("scheduled callbacks ENABLED - system collection TX", func() { - suite.blocks.On("ByID", blockID).Return(block, nil).Once() + suite.state. + On("AtBlockID", blockID). + Return(snapshot, nil). + Once() - suite.reporter.On("LowestIndexedHeight").Return(block.Height, nil).Once() - suite.reporter.On("HighestIndexedHeight").Return(block.Height+10, nil).Once() - suite.events.On("ByBlockID", blockID).Return(suite.pendingExecutionEvents, nil).Once() + provider := providermock.NewTransactionProvider(suite.T()) + provider. + On("TransactionResult", mock.Anything, block.ToHeader(), txID, flow.ZeroID, encodingVersion). + Return(expectedResult, nil) - params.TxProvider = enabledProvider + params := suite.defaultTransactionsParams() + params.TxProvider = provider txBackend, err := NewTransactionsBackend(params) suite.Require().NoError(err) - systemTx := suite.systemCollection.Transactions[2] - res, err := txBackend.GetSystemTransaction(context.Background(), systemTx.ID(), blockID) + res, err := txBackend.GetTransactionResult(context.Background(), txID, blockID, flow.ZeroID, encodingVersion) suite.Require().NoError(err) - - suite.Require().Equal(systemTx, res) + suite.Require().Equal(expectedResult, res) }) - suite.Run("scheduled callbacks ENABLED - non-system txID fails", func() { - suite.blocks.On("ByID", blockID).Return(block, nil).Once() + suite.Run("scheduled tx lookup failure", func() { + expectedErr := fmt.Errorf("some other error") - params.TxProvider = enabledProvider + suite.scheduledTransactions. + On("BlockIDByTransactionID", txID). + Return(flow.ZeroID, expectedErr). + Once() - suite.reporter.On("LowestIndexedHeight").Return(block.Height, nil).Once() - suite.reporter.On("HighestIndexedHeight").Return(block.Height+10, nil).Once() - suite.events.On("ByBlockID", blockID).Return(suite.pendingExecutionEvents, nil).Once() + params := suite.defaultTransactionsParams() + params.TxProvider = providermock.NewTransactionProvider(suite.T()) txBackend, err := NewTransactionsBackend(params) suite.Require().NoError(err) - res, err := txBackend.GetSystemTransaction(context.Background(), unittest.IdentifierFixture(), blockID) + res, err := txBackend.GetTransactionResult(context.Background(), txID, blockID, flow.ZeroID, encodingVersion) suite.Require().Error(err) + suite.Require().Equal(codes.Internal, status.Code(err)) suite.Require().Nil(res) }) -} -func (suite *Suite) TestGetSystemTransactionResult_ExecutionNode_HappyPath() { - test := func(snapshot protocol.Snapshot) { - suite.state. - On("Sealed"). - Return(snapshot, nil). + suite.Run("scheduled tx block mismatch", func() { + suite.scheduledTransactions. + On("BlockIDByTransactionID", txID). + Return(blockID, nil). Once() - lastBlock, err := snapshot.Head() - suite.Require().NoError(err) + params := suite.defaultTransactionsParams() + params.TxProvider = providermock.NewTransactionProvider(suite.T()) - identities, err := snapshot.Identities(filter.Any) + txBackend, err := NewTransactionsBackend(params) suite.Require().NoError(err) - block := unittest.BlockWithParentFixture(lastBlock) - blockID := block.ID() + incorrectBlockID := suite.g.Identifiers().Fixture() + res, err := txBackend.GetTransactionResult(context.Background(), txID, incorrectBlockID, flow.ZeroID, encodingVersion) + suite.Require().Error(err) + suite.Require().Equal(codes.NotFound, status.Code(err)) + suite.Require().Nil(res) + }) + + suite.Run("scheduled tx block not found exception", func() { + suite.scheduledTransactions. + On("BlockIDByTransactionID", txID). + Return(blockID, nil). + Once() + + snapshot := protocolmock.NewSnapshot(suite.T()) + snapshot.On("Head").Return(nil, storage.ErrNotFound) + suite.state. On("AtBlockID", blockID). - Return(unittest.StateSnapshotForKnownBlock(block.ToHeader(), identities.Lookup()), nil). + Return(snapshot, nil). Once() - // block storage returns the corresponding block - suite.blocks. - On("ByID", blockID). - Return(block, nil). - Once() + params := suite.defaultTransactionsParams() + params.TxProvider = providermock.NewTransactionProvider(suite.T()) - receipt1 := unittest.ReceiptForBlockFixture(block) - suite.receipts. - On("ByBlockID", block.ID()). - Return(flow.ExecutionReceiptList{receipt1}, nil) + txBackend, err := NewTransactionsBackend(params) + suite.Require().NoError(err) - // Generating events with event generator - exeNodeEventEncodingVersion := entities.EventEncodingVersion_CCF_V0 - events := unittest.EventGenerator.GetEventsWithEncoding(1, exeNodeEventEncodingVersion) - eventMessages := convert.EventsToMessages(events) + ctx := irrecoverable.NewMockSignalerContextExpectError(suite.T(), context.Background(), + fmt.Errorf("failed to get scheduled transaction's block from storage: %w", storage.ErrNotFound)) + signalerCtx := irrecoverable.WithSignalerContext(context.Background(), ctx) - systemTxID := suite.systemTx.ID() - expectedRequest := &execproto.GetTransactionResultRequest{ - BlockId: blockID[:], - TransactionId: systemTxID[:], - } - exeEventResp := &execproto.GetTransactionResultResponse{ - Events: eventMessages, - EventEncodingVersion: exeNodeEventEncodingVersion, - } + res, err := txBackend.GetTransactionResult(signalerCtx, txID, flow.ZeroID, flow.ZeroID, encodingVersion) + suite.Require().Error(err) // specific error doen't matter since it's thrown + suite.Require().Nil(res) + }) - suite.executionAPIClient. - On("GetTransactionResult", mock.Anything, expectedRequest). - Return(exeEventResp, nil). - Once() + suite.Run("provider error", func() { + expectedErr := status.Errorf(codes.Internal, "some other error") - suite.connectionFactory. - On("GetExecutionAPIClient", mock.Anything). - Return(suite.executionAPIClient, &mocks.MockCloser{}, nil). + suite.scheduledTransactions. + On("BlockIDByTransactionID", txID). + Return(blockID, nil). Once() - // the connection factory should be used to get the execution node client - params := suite.defaultTransactionsParams() - backend, err := NewTransactionsBackend(params) - suite.Require().NoError(err) + snapshot := protocolmock.NewSnapshot(suite.T()) + snapshot.On("Head").Return(block.ToHeader(), nil) - res, err := backend.GetSystemTransactionResult( - context.Background(), - flow.ZeroID, - block.ID(), - entities.EventEncodingVersion_JSON_CDC_V0, - ) - suite.Require().NoError(err) + suite.state. + On("AtBlockID", blockID). + Return(snapshot, nil). + Once() - // Expected system chunk transaction - suite.Require().Equal(flow.TransactionStatusExecuted, res.Status) - suite.Require().Equal(suite.systemTx.ID(), res.TransactionID) + provider := providermock.NewTransactionProvider(suite.T()) + provider. + On("TransactionResult", mock.Anything, block.ToHeader(), txID, flow.ZeroID, encodingVersion). + Return(nil, expectedErr) - // Check for successful decoding of event - _, err = jsoncdc.Decode(nil, res.Events[0].Payload) - suite.Require().NoError(err) + params := suite.defaultTransactionsParams() + params.TxProvider = provider - events, err = convert.MessagesToEventsWithEncodingConversion( - eventMessages, - exeNodeEventEncodingVersion, - entities.EventEncodingVersion_JSON_CDC_V0, - ) + txBackend, err := NewTransactionsBackend(params) suite.Require().NoError(err) - suite.Require().Equal(events, res.Events) - } - identities := unittest.CompleteIdentitySet() - rootSnapshot := unittest.RootSnapshotFixture(identities) - util.RunWithFullProtocolStateAndMutator( - suite.T(), - rootSnapshot, - func(db storage.DB, state *bprotocol.ParticipantState, mutableState protocol.MutableProtocolState) { - epochBuilder := unittest.NewEpochBuilder(suite.T(), mutableState, state) - - epochBuilder. - BuildEpoch(). - CompleteEpoch() - - // get heights of each phase in built epochs - epoch1, ok := epochBuilder.EpochHeights(1) - require.True(suite.T(), ok) - - snapshot := state.AtHeight(epoch1.FinalHeight()) - suite.state.On("Final").Return(snapshot) - test(snapshot) - }, - ) + res, err := txBackend.GetTransactionResult(context.Background(), txID, blockID, flow.ZeroID, encodingVersion) + suite.Require().ErrorIs(err, expectedErr) + suite.Require().Nil(res) + }) } -func (suite *Suite) TestGetSystemTransactionResult_Local_HappyPath() { - block := unittest.BlockFixture() - sysTx, err := blueprints.SystemChunkTransaction(suite.chainID.Chain()) - suite.Require().NoError(err) - suite.Require().NotNil(sysTx) - txId := suite.systemTx.ID() - blockId := block.ID() +func (suite *Suite) TestGetTransactionResult_SubmittedTx() { + block := suite.g.Blocks().Fixture() + blockID := block.ID() - suite.blocks. - On("ByID", blockId). - Return(block, nil). - Once() + collection := suite.g.Collections().Fixture() + collectionID := collection.ID() + lightCollection := collection.Light() + + tx := collection.Transactions[0] + txID := tx.ID() + + encodingVersion := entities.EventEncodingVersion_JSON_CDC_V0 + + expectedResult := &accessmodel.TransactionResult{ + BlockID: blockID, + BlockHeight: block.Height, + TransactionID: txID, + CollectionID: collectionID, + Status: flow.TransactionStatusExecuted, + } + + ctx := irrecoverable.NewMockSignalerContext(suite.T(), context.Background()) + ctxNoErr := irrecoverable.WithSignalerContext(context.Background(), ctx) + + suite.scheduledTransactions. + On("BlockIDByTransactionID", txID). + Return(flow.ZeroID, storage.ErrNotFound) + + suite.Run("happy path - only txID provided", func() { + suite.collections. + On("LightByTransactionID", txID). + Return(lightCollection, nil). + Once() + + suite.blocks. + On("ByCollectionID", collectionID). + Return(block, nil). + Once() + + suite.transactions. + On("ByID", txID). + Return(tx, nil). + Once() + + provider := providermock.NewTransactionProvider(suite.T()) + provider. + On("TransactionResult", mock.Anything, block.ToHeader(), txID, collectionID, encodingVersion). + Return(expectedResult, nil) + + params := suite.defaultTransactionsParams() + params.TxProvider = provider + + txBackend, err := NewTransactionsBackend(params) + suite.Require().NoError(err) + + res, err := txBackend.GetTransactionResult(ctxNoErr, txID, flow.ZeroID, flow.ZeroID, encodingVersion) + suite.Require().NoError(err) + suite.Require().Equal(expectedResult, res) + }) + + suite.Run("happy path - all args provided", func() { + suite.collections. + On("LightByTransactionID", txID). + Return(lightCollection, nil). + Once() + + suite.blocks. + On("ByCollectionID", collectionID). + Return(block, nil). + Once() + + suite.transactions. + On("ByID", txID). + Return(tx, nil). + Once() + + provider := providermock.NewTransactionProvider(suite.T()) + provider. + On("TransactionResult", mock.Anything, block.ToHeader(), txID, collectionID, encodingVersion). + Return(expectedResult, nil) + + params := suite.defaultTransactionsParams() + params.TxProvider = provider + + txBackend, err := NewTransactionsBackend(params) + suite.Require().NoError(err) + + res, err := txBackend.GetTransactionResult(ctxNoErr, txID, blockID, collectionID, encodingVersion) + suite.Require().NoError(err) + suite.Require().Equal(expectedResult, res) + }) + + suite.Run("happy path - not executed", func() { + suite.collections. + On("LightByTransactionID", txID). + Return(lightCollection, nil). + Once() + + suite.blocks. + On("ByCollectionID", collectionID). + Return(block, nil). + Once() + + suite.transactions. + On("ByID", txID). + Return(tx, nil). + Once() + + provider := providermock.NewTransactionProvider(suite.T()) + provider. + On("TransactionResult", mock.Anything, block.ToHeader(), txID, collectionID, encodingVersion). + Return(nil, storage.ErrNotFound) + + params := suite.defaultTransactionsParams() + params.TxProvider = provider + + txBackend, err := NewTransactionsBackend(params) + suite.Require().NoError(err) + + expected := *expectedResult + expected.Status = flow.TransactionStatusFinalized + + res, err := txBackend.GetTransactionResult(ctxNoErr, txID, blockID, collectionID, encodingVersion) + suite.Require().NoError(err) + suite.Require().Equal(expected, *res) + }) + + suite.Run("collection ID mismatch", func() { + suite.collections. + On("LightByTransactionID", txID). + Return(lightCollection, nil). + Once() + + params := suite.defaultTransactionsParams() + params.TxProvider = providermock.NewTransactionProvider(suite.T()) + + txBackend, err := NewTransactionsBackend(params) + suite.Require().NoError(err) + + incorrectCollectionID := suite.g.Identifiers().Fixture() + res, err := txBackend.GetTransactionResult(ctxNoErr, txID, blockID, incorrectCollectionID, encodingVersion) + suite.Require().Error(err) + suite.Require().Equal(codes.NotFound, status.Code(err)) + suite.Require().Nil(res) + }) + + suite.Run("block ID mismatch", func() { + suite.collections. + On("LightByTransactionID", txID). + Return(lightCollection, nil). + Once() + + suite.blocks. + On("ByCollectionID", collectionID). + Return(block, nil). + Once() + + params := suite.defaultTransactionsParams() + params.TxProvider = providermock.NewTransactionProvider(suite.T()) + + txBackend, err := NewTransactionsBackend(params) + suite.Require().NoError(err) + + incorrectBlockID := suite.g.Identifiers().Fixture() + res, err := txBackend.GetTransactionResult(ctxNoErr, txID, incorrectBlockID, collectionID, encodingVersion) + suite.Require().Error(err) + suite.Require().Equal(codes.NotFound, status.Code(err)) + suite.Require().Nil(res) + }) + + suite.Run("collection lookup error", func() { + expectedErr := fmt.Errorf("some other error") + suite.collections. + On("LightByTransactionID", txID). + Return(nil, expectedErr). + Once() + + params := suite.defaultTransactionsParams() + params.TxProvider = providermock.NewTransactionProvider(suite.T()) + + txBackend, err := NewTransactionsBackend(params) + suite.Require().NoError(err) + + res, err := txBackend.GetTransactionResult(ctxNoErr, txID, blockID, collectionID, encodingVersion) + suite.Require().Error(err) + suite.Require().Equal(codes.Internal, status.Code(err)) + suite.Require().Nil(res) + }) + + suite.Run("block lookup failure throws exception", func() { + suite.collections. + On("LightByTransactionID", txID). + Return(lightCollection, nil). + Once() + + suite.blocks. + On("ByCollectionID", collectionID). + Return(nil, storage.ErrNotFound). + Once() + + params := suite.defaultTransactionsParams() + params.TxProvider = providermock.NewTransactionProvider(suite.T()) + + txBackend, err := NewTransactionsBackend(params) + suite.Require().NoError(err) + + expectedErr := fmt.Errorf("failed to find block for collection %v: %w", collectionID, storage.ErrNotFound) + ctx := irrecoverable.NewMockSignalerContextExpectError(suite.T(), context.Background(), expectedErr) + signalerCtx := irrecoverable.WithSignalerContext(context.Background(), ctx) + + res, err := txBackend.GetTransactionResult(signalerCtx, txID, blockID, collectionID, encodingVersion) + suite.Require().Error(err) // specific error doen't matter since it's thrown + suite.Require().Nil(res) + }) + + suite.Run("tx body lookup failure throws exception", func() { + suite.collections. + On("LightByTransactionID", txID). + Return(lightCollection, nil). + Once() + + suite.blocks. + On("ByCollectionID", collectionID). + Return(block, nil). + Once() + + suite.transactions. + On("ByID", txID). + Return(nil, storage.ErrNotFound). + Once() + + params := suite.defaultTransactionsParams() + txBackend, err := NewTransactionsBackend(params) + suite.Require().NoError(err) + + expectedErr := fmt.Errorf("failed to get transaction from storage: %w", storage.ErrNotFound) + ctx := irrecoverable.NewMockSignalerContextExpectError(suite.T(), context.Background(), expectedErr) + signalerCtx := irrecoverable.WithSignalerContext(context.Background(), ctx) + + res, err := txBackend.GetTransactionResult(signalerCtx, txID, blockID, collectionID, encodingVersion) + suite.Require().Error(err) + suite.Require().Nil(res) + }) + + suite.Run("provider error", func() { + expectedErr := status.Errorf(codes.Internal, "some other error") + + suite.collections. + On("LightByTransactionID", txID). + Return(lightCollection, nil). + Once() + + suite.blocks. + On("ByCollectionID", collectionID). + Return(block, nil). + Once() + + suite.transactions. + On("ByID", txID). + Return(tx, nil). + Once() + + provider := providermock.NewTransactionProvider(suite.T()) + provider. + On("TransactionResult", mock.Anything, block.ToHeader(), txID, collectionID, encodingVersion). + Return(nil, expectedErr) + + params := suite.defaultTransactionsParams() + params.TxProvider = provider + + txBackend, err := NewTransactionsBackend(params) + suite.Require().NoError(err) + + res, err := txBackend.GetTransactionResult(ctxNoErr, txID, blockID, collectionID, encodingVersion) + suite.Require().ErrorIs(err, expectedErr) + suite.Require().Nil(res) + }) +} + +func (suite *Suite) TestGetTransactionResult_SubmittedTx_Unknown() { + blocks := suite.g.Blocks().List(3) + + finalizedBlock := blocks[0] + + refBlock := blocks[1] + refBlockID := refBlock.ID() + + block := blocks[2] + blockID := block.ID() + + tx := suite.g.Transactions().Fixture() + tx.ReferenceBlockID = refBlockID + txID := tx.ID() + + collection := suite.g.Collections().Fixture() + lightCollection := collection.Light() + collectionID := collection.ID() + + encodingVersion := entities.EventEncodingVersion_JSON_CDC_V0 + + ctx := irrecoverable.NewMockSignalerContext(suite.T(), context.Background()) + ctxNoErr := irrecoverable.WithSignalerContext(context.Background(), ctx) + + suite.scheduledTransactions. + On("BlockIDByTransactionID", txID). + Return(flow.ZeroID, storage.ErrNotFound) + + suite.collections. + On("LightByTransactionID", txID). + Return(nil, storage.ErrNotFound) + + suite.Run("pending tx", func() { + suite.transactions. + On("ByID", txID). + Return(tx, nil). + Once() + + snapshot := protocolmock.NewSnapshot(suite.T()) + snapshot.On("Head").Return(refBlock.ToHeader(), nil) + suite.state.On("AtBlockID", refBlockID).Return(snapshot, nil).Once() + + finalSnapshot := protocolmock.NewSnapshot(suite.T()) + finalSnapshot.On("Head").Return(finalizedBlock.ToHeader(), nil) + suite.state.On("Final").Return(finalSnapshot, nil).Once() + + expectedResult := &accessmodel.TransactionResult{ + TransactionID: txID, + Status: flow.TransactionStatusPending, + } + + params := suite.defaultTransactionsParams() + params.TxProvider = providermock.NewTransactionProvider(suite.T()) + + txBackend, err := NewTransactionsBackend(params) + suite.Require().NoError(err) + + res, err := txBackend.GetTransactionResult(ctxNoErr, txID, flow.ZeroID, flow.ZeroID, encodingVersion) + suite.Require().NoError(err) + suite.Require().Equal(expectedResult, res) + }) + + suite.Run("transaction lookup error", func() { + expectedErr := fmt.Errorf("some other error") + suite.transactions. + On("ByID", txID). + Return(nil, expectedErr). + Once() + + params := suite.defaultTransactionsParams() + params.TxProvider = providermock.NewTransactionProvider(suite.T()) + + txBackend, err := NewTransactionsBackend(params) + suite.Require().NoError(err) + + res, err := txBackend.GetTransactionResult(ctxNoErr, txID, flow.ZeroID, flow.ZeroID, encodingVersion) + suite.Require().Error(err) + suite.Require().Equal(codes.Internal, status.Code(err)) + suite.Require().Nil(res) + }) + + suite.Run("pending block with derive status exception", func() { + suite.transactions. + On("ByID", txID). + Return(tx, nil). + Once() + + snapshot := protocolmock.NewSnapshot(suite.T()) + snapshot.On("Head").Return(refBlock.ToHeader(), nil) + suite.state.On("AtBlockID", refBlockID).Return(snapshot, nil).Once() + + finalSnapshot := protocolmock.NewSnapshot(suite.T()) + finalSnapshot.On("Head").Return(nil, storage.ErrNotFound) + suite.state.On("Final").Return(finalSnapshot, nil).Once() + + params := suite.defaultTransactionsParams() + params.TxProvider = providermock.NewTransactionProvider(suite.T()) + + txBackend, err := NewTransactionsBackend(params) + suite.Require().NoError(err) + + ctx := irrecoverable.NewMockSignalerContextExpectError(suite.T(), context.Background(), + fmt.Errorf("failed to derive transaction status: %w", irrecoverable.NewExceptionf("failed to lookup final header: %w", storage.ErrNotFound))) + signalerCtx := irrecoverable.WithSignalerContext(context.Background(), ctx) + + res, err := txBackend.GetTransactionResult(signalerCtx, txID, flow.ZeroID, flow.ZeroID, encodingVersion) + suite.Require().Error(err) // specific error doen't matter since it's thrown + suite.Require().Nil(res) + }) + + suite.Run("unknown tx in known block", func() { + suite.transactions. + On("ByID", txID). + Return(nil, storage.ErrNotFound). + Once() + + suite.blocks. + On("ByID", blockID). + Return(block, nil). + Once() + + params := suite.defaultTransactionsParams() + params.TxProvider = providermock.NewTransactionProvider(suite.T()) + + txBackend, err := NewTransactionsBackend(params) + suite.Require().NoError(err) + + expectedResult := &accessmodel.TransactionResult{ + TransactionID: txID, + Status: flow.TransactionStatusUnknown, + } + + res, err := txBackend.GetTransactionResult(ctxNoErr, txID, blockID, flow.ZeroID, encodingVersion) + suite.Require().NoError(err) + suite.Require().Equal(expectedResult, res) + }) + + suite.Run("block lookup error", func() { + expectedErr := fmt.Errorf("some other error") + + suite.transactions. + On("ByID", txID). + Return(nil, storage.ErrNotFound). + Once() + + suite.blocks. + On("ByID", blockID). + Return(nil, expectedErr). + Once() + + params := suite.defaultTransactionsParams() + params.TxProvider = providermock.NewTransactionProvider(suite.T()) + + txBackend, err := NewTransactionsBackend(params) + suite.Require().NoError(err) + + res, err := txBackend.GetTransactionResult(ctxNoErr, txID, blockID, flow.ZeroID, encodingVersion) + suite.Require().Error(err) + suite.Require().Equal(codes.Internal, status.Code(err)) + suite.Require().Nil(res) + }) + + suite.Run("unknown tx in known collection", func() { + suite.transactions. + On("ByID", txID). + Return(nil, storage.ErrNotFound). + Once() + + suite.collections. + On("LightByID", collectionID). + Return(lightCollection, nil). + Once() + + params := suite.defaultTransactionsParams() + params.TxProvider = providermock.NewTransactionProvider(suite.T()) + + txBackend, err := NewTransactionsBackend(params) + suite.Require().NoError(err) + + res, err := txBackend.GetTransactionResult(ctxNoErr, txID, flow.ZeroID, collectionID, encodingVersion) + suite.Require().Error(err) + suite.Require().Equal(codes.NotFound, status.Code(err)) + suite.Require().Nil(res) + }) + + suite.Run("collection lookup error", func() { + expectedErr := fmt.Errorf("some other error") + + suite.transactions. + On("ByID", txID). + Return(nil, storage.ErrNotFound). + Once() + + suite.collections. + On("LightByID", collectionID). + Return(nil, expectedErr). + Once() + + params := suite.defaultTransactionsParams() + params.TxProvider = providermock.NewTransactionProvider(suite.T()) + + txBackend, err := NewTransactionsBackend(params) + suite.Require().NoError(err) + + res, err := txBackend.GetTransactionResult(ctxNoErr, txID, flow.ZeroID, collectionID, encodingVersion) + suite.Require().Error(err) + suite.Require().Equal(codes.Internal, status.Code(err)) + suite.Require().Nil(res) + }) +} + +func (suite *Suite) TestGetTransactionResult_SubmittedTx_HistoricalNodes() { + tx := suite.g.Transactions().Fixture() + txID := tx.ID() - lightTxShouldFail := false - suite.lightTxResults. - On("ByBlockIDTransactionID", blockId, txId). - Return(&flow.LightTransactionResult{ - TransactionID: txId, - Failed: lightTxShouldFail, - ComputationUsed: 0, - }, nil). - Once() + block := suite.g.Blocks().Fixture() + blockID := block.ID() - // Set up the events storage mock - var eventsForTx []flow.Event - // expect a call to lookup events by block ID and transaction ID - suite.events.On("ByBlockIDTransactionID", blockId, txId).Return(eventsForTx, nil) + expectedFoundResponse := &accessmodel.TransactionResult{ + TransactionID: txID, + BlockID: blockID, + BlockHeight: block.Height, + Status: flow.TransactionStatusSealed, + Events: []flow.Event{}, // needed because converter uses an empty slice for nil events + } - // Set up the state and snapshot mocks - suite.state.On("Sealed").Return(suite.snapshot, nil) - suite.snapshot.On("Head").Return(block.ToHeader(), nil) + expectedNotFoundResponse := &accessmodel.TransactionResult{ + TransactionID: txID, + Status: flow.TransactionStatusUnknown, + } - // create a mock index reporter - reporter := syncmock.NewIndexReporter(suite.T()) - reporter.On("LowestIndexedHeight").Return(block.Height, nil) - reporter.On("HighestIndexedHeight").Return(block.Height+10, nil) + expectedRequest := &access.GetTransactionRequest{ + Id: txID[:], + } - indexReporter := index.NewReporter() - err = indexReporter.Initialize(reporter) - suite.Require().NoError(err) + foundResponse := convert.TransactionResultToMessage(expectedFoundResponse) - // Set up the backend parameters and the backend instance - params := suite.defaultTransactionsParams() - params.EventsIndex = index.NewEventsIndex(indexReporter, suite.events) - params.TxResultsIndex = index.NewTransactionResultsIndex(indexReporter, suite.lightTxResults) - params.TxProvider = provider.NewLocalTransactionProvider( - params.State, - params.Collections, - params.Blocks, - params.EventsIndex, - params.TxResultsIndex, - params.TxErrorMessageProvider, - params.SystemTxID, - params.TxStatusDeriver, - params.ChainID, - params.ScheduledCallbacksEnabled, - ) + encodingVersion := entities.EventEncodingVersion_JSON_CDC_V0 - txBackend, err := NewTransactionsBackend(params) - suite.Require().NoError(err) - response, err := txBackend.GetSystemTransactionResult(context.Background(), flow.ZeroID, blockId, entities.EventEncodingVersion_JSON_CDC_V0) - suite.assertTransactionResultResponse(err, response, *block, txId, lightTxShouldFail, eventsForTx) -} + ctx := irrecoverable.NewMockSignalerContext(suite.T(), context.Background()) + ctxNoErr := irrecoverable.WithSignalerContext(context.Background(), ctx) -// TestGetSystemTransactionResult_BlockNotFound tests GetSystemTransactionResult function when block was not found. -func (suite *Suite) TestGetSystemTransactionResult_BlockNotFound() { - block := unittest.BlockFixture() - suite.blocks. - On("ByID", block.ID()). - Return(nil, storage.ErrNotFound). - Once() + suite.scheduledTransactions. + On("BlockIDByTransactionID", txID). + Return(flow.ZeroID, storage.ErrNotFound) - params := suite.defaultTransactionsParams() - txBackend, err := NewTransactionsBackend(params) - suite.Require().NoError(err) - res, err := txBackend.GetSystemTransactionResult( - context.Background(), - flow.ZeroID, - block.ID(), - entities.EventEncodingVersion_JSON_CDC_V0, - ) + suite.collections. + On("LightByTransactionID", txID). + Return(nil, storage.ErrNotFound) - suite.Require().Nil(res) - suite.Require().Error(err) - suite.Require().Equal(err, status.Errorf(codes.NotFound, "not found: %v", fmt.Errorf("key not found"))) -} + suite.transactions. + On("ByID", txID). + Return(nil, storage.ErrNotFound) -// TestGetSystemTransactionResult_FailedEncodingConversion tests the GetSystemTransactionResult function with different -// event encoding versions. -func (suite *Suite) TestGetSystemTransactionResult_FailedEncodingConversion() { - block := unittest.BlockFixture() - blockID := block.ID() + suite.Run("result found", func() { + suite.historicalAccessAPIClient. + On("GetTransactionResult", mock.Anything, expectedRequest). + Return(foundResponse, nil). + Once() - _, fixedENIDs := suite.setupReceipts(block) - suite.fixedExecutionNodeIDs = fixedENIDs.NodeIDs() + params := suite.defaultTransactionsParams() + params.TxResultCache = new(NoopTxResultCache) + params.HistoricalAccessNodeClients = []access.AccessAPIClient{suite.historicalAccessAPIClient} + txBackend, err := NewTransactionsBackend(params) + suite.Require().NoError(err) - suite.snapshot.On("Head").Return(block.ToHeader(), nil) - suite.snapshot.On("Identities", mock.Anything).Return(fixedENIDs, nil) - suite.state.On("Sealed").Return(suite.snapshot, nil) - suite.state.On("Final").Return(suite.snapshot, nil) + resp, err := txBackend.GetTransactionResult(ctxNoErr, txID, flow.ZeroID, flow.ZeroID, encodingVersion) + suite.Require().NoError(err) + suite.Require().Equal(expectedFoundResponse, resp) + }) - // block storage returns the corresponding block - suite.blocks. - On("ByID", blockID). - Return(block, nil). - Once() + suite.Run("result not found", func() { + suite.historicalAccessAPIClient. + On("GetTransactionResult", mock.Anything, expectedRequest). + Return(nil, status.Error(codes.Internal, "some other error")). + Once() - // create empty events - eventsPerBlock := 10 - eventMessages := make([]*entities.Event, eventsPerBlock) + params := suite.defaultTransactionsParams() + params.TxResultCache = new(NoopTxResultCache) + params.HistoricalAccessNodeClients = []access.AccessAPIClient{suite.historicalAccessAPIClient} + txBackend, err := NewTransactionsBackend(params) + suite.Require().NoError(err) - systemTxID := suite.systemTx.ID() - expectedRequest := &execproto.GetTransactionResultRequest{ - BlockId: blockID[:], - TransactionId: systemTxID[:], - } - exeEventResp := &execproto.GetTransactionResultResponse{ - Events: eventMessages, - EventEncodingVersion: entities.EventEncodingVersion_JSON_CDC_V0, - } + resp, err := txBackend.GetTransactionResult(ctxNoErr, txID, flow.ZeroID, flow.ZeroID, encodingVersion) + suite.Require().NoError(err) + suite.Require().Equal(expectedNotFoundResponse, resp) + }) - suite.executionAPIClient. - On("GetTransactionResult", mock.Anything, expectedRequest). - Return(exeEventResp, nil). - Once() + suite.Run("success result is cached", func() { + // make sure the cache is starting empty + suite.txResultCache.Purge() - suite.connectionFactory. - On("GetExecutionAPIClient", mock.Anything). - Return(suite.executionAPIClient, &mocks.MockCloser{}, nil). - Once() + // this should only be called once + // unset to make sure we're definitely testing what we expect + suite.historicalAccessAPIClient. + On("GetTransactionResult", mock.Anything, expectedRequest). + Unset() + suite.historicalAccessAPIClient. + On("GetTransactionResult", mock.Anything, expectedRequest). + Return(foundResponse, nil). + Once() - params := suite.defaultTransactionsParams() - txBackend, err := NewTransactionsBackend(params) - suite.Require().NoError(err) + params := suite.defaultTransactionsParams() + // use real cache (used by default) + params.HistoricalAccessNodeClients = []access.AccessAPIClient{suite.historicalAccessAPIClient} + txBackend, err := NewTransactionsBackend(params) + suite.Require().NoError(err) - res, err := txBackend.GetSystemTransactionResult( - context.Background(), - flow.ZeroID, - block.ID(), - entities.EventEncodingVersion_CCF_V0, - ) + // first call should populate the cache + resp, err := txBackend.GetTransactionResult(ctxNoErr, txID, flow.ZeroID, flow.ZeroID, encodingVersion) + suite.Require().NoError(err) + suite.Require().Equal(expectedFoundResponse, resp) + + // second call should return the cached result + resp, err = txBackend.GetTransactionResult(ctxNoErr, txID, flow.ZeroID, flow.ZeroID, encodingVersion) + suite.Require().NoError(err) + suite.Require().Equal(expectedFoundResponse, resp) + }) + + suite.Run("not found result is cached", func() { + // make sure the cache is starting empty + suite.txResultCache.Purge() + + // this should only be called once + // unset to make sure we're definitely testing what we expect + suite.historicalAccessAPIClient. + On("GetTransactionResult", mock.Anything, expectedRequest). + Unset() + suite.historicalAccessAPIClient. + On("GetTransactionResult", mock.Anything, expectedRequest). + Return(nil, status.Error(codes.Internal, "some other error")). + Once() + + params := suite.defaultTransactionsParams() + // use real cache (used by default) + params.HistoricalAccessNodeClients = []access.AccessAPIClient{suite.historicalAccessAPIClient} + txBackend, err := NewTransactionsBackend(params) + suite.Require().NoError(err) + + // first call should populate the cache + resp, err := txBackend.GetTransactionResult(ctxNoErr, txID, flow.ZeroID, flow.ZeroID, encodingVersion) + suite.Require().NoError(err) + suite.Require().Equal(expectedNotFoundResponse, resp) - suite.Require().Nil(res) - suite.Require().Error(err) - suite.Require().Equal(err, status.Errorf(codes.Internal, "failed to convert events to message: %v", - fmt.Errorf("conversion from format JSON_CDC_V0 to CCF_V0 is not supported"))) + // second call should return the cached result + resp, err = txBackend.GetTransactionResult(ctxNoErr, txID, flow.ZeroID, flow.ZeroID, encodingVersion) + suite.Require().NoError(err) + suite.Require().Equal(expectedNotFoundResponse, resp) + }) } -// TestGetTransactionResult_FromStorage tests the retrieval of a transaction result (flow.TransactionResult) from storage -// instead of requesting it from the Execution Node. -func (suite *Suite) TestGetTransactionResult_FromStorage() { - // Create fixtures for block, transaction, and collection - transaction := unittest.TransactionBodyFixture() - col := unittest.CollectionFromTransactions(&transaction) - guarantee := &flow.CollectionGuarantee{CollectionID: col.ID()} - block := unittest.BlockFixture( - unittest.Block.WithPayload(unittest.PayloadFixture(unittest.WithGuarantees(guarantee))), - ) - txId := transaction.ID() - blockId := block.ID() +func (suite *Suite) TestGetHistoricalTransactionResult() { + tx := suite.g.Transactions().Fixture() + txID := tx.ID() - suite.blocks. - On("ByID", blockId). - Return(block, nil) + block := suite.g.Blocks().Fixture() + blockID := block.ID() - suite.lightTxResults.On("ByBlockIDTransactionID", blockId, txId). - Return(&flow.LightTransactionResult{ - TransactionID: txId, - Failed: true, - ComputationUsed: 0, - }, nil) + expectedFoundResponse := &accessmodel.TransactionResult{ + TransactionID: txID, + BlockID: blockID, + BlockHeight: block.Height, + Status: flow.TransactionStatusSealed, + Events: []flow.Event{}, // needed because converter uses an empty slice for nil events + } + response := convert.TransactionResultToMessage(expectedFoundResponse) - suite.transactions. - On("ByID", txId). - Return(&transaction, nil) - - // Set up the light collection and mock the behavior of the collections object - lightCol := col.Light() - suite.collections.On("LightByID", col.ID()).Return(lightCol, nil) - - // Set up the events storage mock - totalEvents := 5 - eventsForTx := unittest.EventsFixture(totalEvents) - eventMessages := make([]*entities.Event, totalEvents) - for j, event := range eventsForTx { - eventMessages[j] = convert.EventToMessage(event) + expectedRequest := &access.GetTransactionRequest{ + Id: txID[:], } - // expect a call to lookup events by block ID and transaction ID - suite.events.On("ByBlockIDTransactionID", blockId, txId).Return(eventsForTx, nil) - // Set up the state and snapshot mocks - _, fixedENIDs := suite.setupReceipts(block) - suite.fixedExecutionNodeIDs = fixedENIDs.NodeIDs() + an1 := accessmock.NewAccessAPIClient(suite.T()) + an2 := accessmock.NewAccessAPIClient(suite.T()) + an3 := accessmock.NewAccessAPIClient(suite.T()) - suite.state.On("Final").Return(suite.snapshot, nil) - suite.state.On("Sealed").Return(suite.snapshot, nil) - suite.snapshot.On("Identities", mock.Anything).Return(fixedENIDs, nil) - suite.snapshot.On("Head").Return(block.ToHeader(), nil) + suite.Run("iterates through multiple nodes", func() { + an1.On("GetTransactionResult", mock.Anything, expectedRequest). + Return(nil, status.Error(codes.NotFound, "not found")). + Once() - suite.reporter.On("LowestIndexedHeight").Return(block.Height, nil) - suite.reporter.On("HighestIndexedHeight").Return(block.Height+10, nil) + an2.On("GetTransactionResult", mock.Anything, expectedRequest). + Return(nil, status.Error(codes.Unavailable, "unavailable")). + Once() - suite.connectionFactory. - On("GetExecutionAPIClient", mock.Anything). - Return(suite.executionAPIClient, &mocks.MockCloser{}, nil). - Once() + an3.On("GetTransactionResult", mock.Anything, expectedRequest). + Return(response, nil). + Once() - // Set up the expected error message for the execution node response - exeEventReq := &execproto.GetTransactionErrorMessageRequest{ - BlockId: blockId[:], - TransactionId: txId[:], - } - exeEventResp := &execproto.GetTransactionErrorMessageResponse{ - TransactionId: txId[:], - ErrorMessage: expectedErrorMsg, - } - suite.executionAPIClient. - On("GetTransactionErrorMessage", mock.Anything, exeEventReq). - Return(exeEventResp, nil). - Once() + params := suite.defaultTransactionsParams() + params.HistoricalAccessNodeClients = []access.AccessAPIClient{an1, an2, an3} + txBackend, err := NewTransactionsBackend(params) + suite.Require().NoError(err) - params := suite.defaultTransactionsParams() - params.TxErrorMessageProvider = error_messages.NewTxErrorMessageProvider( - params.Log, - nil, - params.TxResultsIndex, - params.ConnFactory, - params.NodeCommunicator, - params.NodeProvider, - ) - params.TxProvider = provider.NewLocalTransactionProvider( - params.State, - params.Collections, - params.Blocks, - params.EventsIndex, - params.TxResultsIndex, - params.TxErrorMessageProvider, - params.SystemTxID, - params.TxStatusDeriver, - params.ChainID, - params.ScheduledCallbacksEnabled, - ) + resp, err := txBackend.getHistoricalTransactionResult(context.Background(), txID) + suite.Require().NoError(err) + suite.Require().Equal(expectedFoundResponse, resp) + }) + + suite.Run("returns not found when all nodes return not found", func() { + an1.On("GetTransactionResult", mock.Anything, expectedRequest). + Return(nil, status.Error(codes.OutOfRange, "out of range")). + Once() + + an2.On("GetTransactionResult", mock.Anything, expectedRequest). + Return(nil, status.Error(codes.Unavailable, "unavailable")). + Once() + + an3.On("GetTransactionResult", mock.Anything, expectedRequest). + Return(nil, status.Error(codes.Internal, "internal error")). + Once() + + params := suite.defaultTransactionsParams() + params.HistoricalAccessNodeClients = []access.AccessAPIClient{an1, an2, an3} + txBackend, err := NewTransactionsBackend(params) + suite.Require().NoError(err) + + resp, err := txBackend.getHistoricalTransactionResult(context.Background(), txID) + suite.Require().Error(err) + suite.Require().Equal(codes.NotFound, status.Code(err)) + suite.Require().Nil(resp) + }) + + suite.Run("returns unknown when result status is unknown", func() { + expectedResponse := &accessmodel.TransactionResult{ + TransactionID: txID, + Status: flow.TransactionStatusUnknown, + } + response := convert.TransactionResultToMessage(expectedResponse) + + an1.On("GetTransactionResult", mock.Anything, expectedRequest). + Return(response, nil). + Once() + + params := suite.defaultTransactionsParams() + params.HistoricalAccessNodeClients = []access.AccessAPIClient{an1} + txBackend, err := NewTransactionsBackend(params) + suite.Require().NoError(err) + + resp, err := txBackend.getHistoricalTransactionResult(context.Background(), txID) + suite.Require().Error(err) + suite.Require().Equal(codes.NotFound, status.Code(err)) + suite.Require().Nil(resp) + }) + + suite.Run("returns expired when result status is pending", func() { + nodeResponse := &accessmodel.TransactionResult{ + TransactionID: txID, + BlockID: blockID, + BlockHeight: block.Height, + Status: flow.TransactionStatusPending, + } + response := convert.TransactionResultToMessage(nodeResponse) + + // result status should be updated to expired + expectedResponse := &accessmodel.TransactionResult{ + TransactionID: txID, + BlockID: blockID, + BlockHeight: block.Height, + Status: flow.TransactionStatusExpired, + Events: []flow.Event{}, // needed because converter uses an empty slice for nil events + } + + an1.On("GetTransactionResult", mock.Anything, expectedRequest). + Return(response, nil). + Once() + + params := suite.defaultTransactionsParams() + params.HistoricalAccessNodeClients = []access.AccessAPIClient{an1} + txBackend, err := NewTransactionsBackend(params) + suite.Require().NoError(err) + + resp, err := txBackend.getHistoricalTransactionResult(context.Background(), txID) + suite.Require().NoError(err) + suite.Require().Equal(expectedResponse, resp) + }) + + suite.Run("returns error when convert fails", func() { + nodeResponse := &accessmodel.TransactionResult{ + TransactionID: txID, + BlockID: blockID, + BlockHeight: block.Height, + Status: flow.TransactionStatusSealed, + Events: []flow.Event{ + suite.g.Events().Fixture( + // this is invalid and will cause the converter to fail + suite.g.Events().WithTransactionID(flow.ZeroID), + ), + }, + } + response := convert.TransactionResultToMessage(nodeResponse) + + an1.On("GetTransactionResult", mock.Anything, expectedRequest). + Return(response, nil). + Once() + + params := suite.defaultTransactionsParams() + params.HistoricalAccessNodeClients = []access.AccessAPIClient{an1} + txBackend, err := NewTransactionsBackend(params) + suite.Require().NoError(err) + + resp, err := txBackend.getHistoricalTransactionResult(context.Background(), txID) + suite.Require().Error(err) + suite.Require().Equal(codes.Internal, status.Code(err)) + suite.Require().Nil(resp) + }) +} + +func (suite *Suite) TestGetSystemTransaction() { + block := suite.g.Blocks().Fixture() + blockID := block.ID() + params := suite.defaultTransactionsParams() txBackend, err := NewTransactionsBackend(params) suite.Require().NoError(err) - response, err := txBackend.GetTransactionResult(context.Background(), txId, blockId, flow.ZeroID, entities.EventEncodingVersion_JSON_CDC_V0) - suite.assertTransactionResultResponse(err, response, *block, txId, true, eventsForTx) + suite.Run("returns process transactions transaction", func() { + tx := suite.systemCollection.Transactions[0] + txID := tx.ID() + + snapshot := protocolmock.NewSnapshot(suite.T()) + snapshot.On("Head").Return(block.ToHeader(), nil) + suite.state. + On("AtBlockID", blockID). + Return(snapshot, nil). + Once() + + res, err := txBackend.GetSystemTransaction(context.Background(), txID, blockID) + suite.Require().NoError(err) + suite.Require().Equal(tx, res) + }) + + suite.Run("returns system chunk transaction", func() { + tx := suite.systemCollection.Transactions[len(suite.systemCollection.Transactions)-1] + txID := tx.ID() + + snapshot := protocolmock.NewSnapshot(suite.T()) + snapshot.On("Head").Return(block.ToHeader(), nil) + suite.state. + On("AtBlockID", blockID). + Return(snapshot, nil). + Once() + + res, err := txBackend.GetSystemTransaction(context.Background(), txID, blockID) + suite.Require().NoError(err) + suite.Require().Equal(tx, res) + }) + + suite.Run("returns system chunk transaction when tx is not provided", func() { + tx := suite.systemCollection.Transactions[len(suite.systemCollection.Transactions)-1] - suite.reporter.AssertExpectations(suite.T()) - suite.connectionFactory.AssertExpectations(suite.T()) - suite.executionAPIClient.AssertExpectations(suite.T()) - suite.blocks.AssertExpectations(suite.T()) - suite.events.AssertExpectations(suite.T()) - suite.state.AssertExpectations(suite.T()) + snapshot := protocolmock.NewSnapshot(suite.T()) + snapshot.On("Head").Return(block.ToHeader(), nil) + suite.state. + On("AtBlockID", blockID). + Return(snapshot, nil). + Once() + + res, err := txBackend.GetSystemTransaction(context.Background(), flow.ZeroID, blockID) + suite.Require().NoError(err) + suite.Require().Equal(tx, res) + }) + + suite.Run("returns error when block not found", func() { + tx := suite.systemCollection.Transactions[0] + txID := tx.ID() + + snapshot := protocolmock.NewSnapshot(suite.T()) + snapshot.On("Head").Return(nil, storage.ErrNotFound) + suite.state. + On("AtBlockID", blockID). + Return(snapshot, nil). + Once() + + res, err := txBackend.GetSystemTransaction(context.Background(), txID, blockID) + suite.Require().Error(err) + suite.Require().Equal(codes.NotFound, status.Code(err)) + suite.Require().Nil(res) + }) + + suite.Run("returns not found for non-system transaction", func() { + tx := suite.g.Transactions().Fixture() + txID := tx.ID() + + res, err := txBackend.GetSystemTransaction(context.Background(), txID, blockID) + suite.Require().Error(err) + suite.Require().Equal(codes.NotFound, status.Code(err)) + suite.Require().Nil(res) + }) } -// TestTransactionByIndexFromStorage tests the retrieval of a transaction result (flow.TransactionResult) by index -// and returns it from storage instead of requesting from the Execution Node. -func (suite *Suite) TestTransactionByIndexFromStorage() { - // Create fixtures for block, transaction, and collection - transaction := unittest.TransactionBodyFixture() - col := unittest.CollectionFromTransactions(&transaction) - guarantee := &flow.CollectionGuarantee{CollectionID: col.ID()} - block := unittest.BlockFixture( - unittest.Block.WithPayload(unittest.PayloadFixture(unittest.WithGuarantees(guarantee))), - ) - blockId := block.ID() - txId := transaction.ID() - txIndex := rand.Uint32() - - // Set up the light collection and mock the behavior of the collections object - lightCol := col.Light() - suite.collections.On("LightByID", col.ID()).Return(lightCol, nil) - - // Mock the behavior of the blocks and lightTxResults objects - suite.blocks. - On("ByID", blockId). - Return(block, nil) - - suite.lightTxResults.On("ByBlockIDTransactionIndex", blockId, txIndex). - Return(&flow.LightTransactionResult{ - TransactionID: txId, - Failed: true, - ComputationUsed: 0, - }, nil) - - // Set up the events storage mock - totalEvents := 5 - eventsForTx := unittest.EventsFixture(totalEvents) - eventMessages := make([]*entities.Event, totalEvents) - for j, event := range eventsForTx { - eventMessages[j] = convert.EventToMessage(event) +func (suite *Suite) TestGetSystemTransactionResult() { + tx := suite.systemCollection.Transactions[0] + txID := tx.ID() + + block := suite.g.Blocks().Fixture() + blockID := block.ID() + + expectedResult := &accessmodel.TransactionResult{ + BlockID: blockID, + TransactionID: txID, + CollectionID: flow.ZeroID, + BlockHeight: block.Height, + Status: flow.TransactionStatusExecuted, } - // expect a call to lookup events by block ID and transaction ID - suite.events.On("ByBlockIDTransactionIndex", blockId, txIndex).Return(eventsForTx, nil) + encodingVersion := entities.EventEncodingVersion_JSON_CDC_V0 - // Set up the state and snapshot mocks - _, fixedENIDs := suite.setupReceipts(block) - suite.fixedExecutionNodeIDs = fixedENIDs.NodeIDs() - suite.state.On("Final").Return(suite.snapshot, nil) - suite.state.On("Sealed").Return(suite.snapshot, nil) - suite.snapshot.On("Identities", mock.Anything).Return(fixedENIDs, nil) - suite.snapshot.On("Head").Return(block.ToHeader(), nil) + suite.Run("happy path", func() { + snapshot := protocolmock.NewSnapshot(suite.T()) + snapshot.On("Head").Return(block.ToHeader(), nil) - suite.reporter.On("LowestIndexedHeight").Return(block.Height, nil) - suite.reporter.On("HighestIndexedHeight").Return(block.Height+10, nil) + suite.state. + On("AtBlockID", blockID). + Return(snapshot, nil). + Once() - suite.connectionFactory. - On("GetExecutionAPIClient", mock.Anything). - Return(suite.executionAPIClient, &mocks.MockCloser{}, nil). - Once() + provider := providermock.NewTransactionProvider(suite.T()) + provider. + On("TransactionResult", mock.Anything, block.ToHeader(), txID, flow.ZeroID, encodingVersion). + Return(expectedResult, nil) - params := suite.defaultTransactionsParams() - params.TxErrorMessageProvider = error_messages.NewTxErrorMessageProvider( - params.Log, - nil, - params.TxResultsIndex, - params.ConnFactory, - params.NodeCommunicator, - params.NodeProvider, - ) - params.TxProvider = provider.NewLocalTransactionProvider( - params.State, - params.Collections, - params.Blocks, - params.EventsIndex, - params.TxResultsIndex, - params.TxErrorMessageProvider, - params.SystemTxID, - params.TxStatusDeriver, - params.ChainID, - params.ScheduledCallbacksEnabled, - ) + params := suite.defaultTransactionsParams() + params.TxProvider = provider - txBackend, err := NewTransactionsBackend(params) - suite.Require().NoError(err) + txBackend, err := NewTransactionsBackend(params) + suite.Require().NoError(err) + + res, err := txBackend.GetSystemTransactionResult(context.Background(), txID, blockID, encodingVersion) + suite.Require().NoError(err) + suite.Require().Equal(expectedResult, res) + }) + + suite.Run("uses system chunk tx when txID is not provided", func() { + systemTx := suite.systemCollection.Transactions[len(suite.systemCollection.Transactions)-1] + systemTxID := systemTx.ID() + + snapshot := protocolmock.NewSnapshot(suite.T()) + snapshot.On("Head").Return(block.ToHeader(), nil) + + suite.state. + On("AtBlockID", blockID). + Return(snapshot, nil). + Once() + + provider := providermock.NewTransactionProvider(suite.T()) + provider. + On("TransactionResult", mock.Anything, block.ToHeader(), systemTxID, flow.ZeroID, encodingVersion). + Return(expectedResult, nil) + + params := suite.defaultTransactionsParams() + params.TxProvider = provider + + txBackend, err := NewTransactionsBackend(params) + suite.Require().NoError(err) + + res, err := txBackend.GetSystemTransactionResult(context.Background(), flow.ZeroID, blockID, encodingVersion) + suite.Require().NoError(err) + suite.Require().Equal(expectedResult, res) + }) + + suite.Run("returns not found when tx is not a system tx", func() { + tx := suite.g.Transactions().Fixture() + txID := tx.ID() + + params := suite.defaultTransactionsParams() + params.TxProvider = providermock.NewTransactionProvider(suite.T()) + + txBackend, err := NewTransactionsBackend(params) + suite.Require().NoError(err) + + res, err := txBackend.GetSystemTransactionResult(context.Background(), txID, blockID, encodingVersion) + suite.Require().Error(err) + suite.Require().Equal(codes.NotFound, status.Code(err)) + suite.Require().Nil(res) + }) + + suite.Run("provider error", func() { + expectedErr := status.Errorf(codes.Internal, "some other error") + + snapshot := protocolmock.NewSnapshot(suite.T()) + snapshot.On("Head").Return(block.ToHeader(), nil) + + suite.state. + On("AtBlockID", blockID). + Return(snapshot, nil). + Once() + + provider := providermock.NewTransactionProvider(suite.T()) + provider. + On("TransactionResult", mock.Anything, block.ToHeader(), txID, flow.ZeroID, encodingVersion). + Return(nil, expectedErr) + + params := suite.defaultTransactionsParams() + params.TxProvider = provider + + txBackend, err := NewTransactionsBackend(params) + suite.Require().NoError(err) + + res, err := txBackend.GetSystemTransactionResult(context.Background(), txID, blockID, encodingVersion) + suite.Require().ErrorIs(err, expectedErr) + suite.Require().Nil(res) + suite.Require().Equal(expectedErr, err) + }) + + suite.Run("block not found exception", func() { + snapshot := protocolmock.NewSnapshot(suite.T()) + snapshot.On("Head").Return(nil, storage.ErrNotFound) + + suite.state. + On("AtBlockID", blockID). + Return(snapshot, nil). + Once() + + params := suite.defaultTransactionsParams() + params.TxProvider = providermock.NewTransactionProvider(suite.T()) + + txBackend, err := NewTransactionsBackend(params) + suite.Require().NoError(err) - // Set up the expected error message for the execution node response - exeEventReq := &execproto.GetTransactionErrorMessageByIndexRequest{ - BlockId: blockId[:], - Index: txIndex, - } + res, err := txBackend.GetSystemTransactionResult(context.Background(), txID, blockID, encodingVersion) + suite.Require().Error(err) + suite.Require().Equal(codes.NotFound, status.Code(err)) + suite.Require().Nil(res) + }) - exeEventResp := &execproto.GetTransactionErrorMessageResponse{ - TransactionId: txId[:], - ErrorMessage: expectedErrorMsg, - } + suite.Run("block not provided", func() { + params := suite.defaultTransactionsParams() + params.TxProvider = providermock.NewTransactionProvider(suite.T()) - suite.executionAPIClient. - On("GetTransactionErrorMessageByIndex", mock.Anything, exeEventReq). - Return(exeEventResp, nil). - Once() + txBackend, err := NewTransactionsBackend(params) + suite.Require().NoError(err) - response, err := txBackend.GetTransactionResultByIndex(context.Background(), blockId, txIndex, entities.EventEncodingVersion_JSON_CDC_V0) - suite.assertTransactionResultResponse(err, response, *block, txId, true, eventsForTx) + res, err := txBackend.GetSystemTransactionResult(context.Background(), txID, flow.ZeroID, encodingVersion) + suite.Require().Error(err) + suite.Require().Equal(codes.InvalidArgument, status.Code(err)) + suite.Require().Nil(res) + }) } -// TestTransactionResultsByBlockIDFromStorage tests the retrieval of transaction results ([]flow.TransactionResult) -// by block ID from storage instead of requesting from the Execution Node. -func (suite *Suite) TestTransactionResultsByBlockIDFromStorage() { - // Create fixtures for the block and collection - col := unittest.CollectionFixture(2) - guarantee := &flow.CollectionGuarantee{CollectionID: col.ID()} - block := unittest.BlockFixture( - unittest.Block.WithPayload(unittest.PayloadFixture(unittest.WithGuarantees(guarantee))), - ) - blockId := block.ID() +func (suite *Suite) TestGetScheduledTransaction() { + scheduledTxID := suite.g.Random().Uint64() + tx := suite.systemCollection.Transactions[1] + txID := tx.ID() - // Mock the behavior of the blocks, collections and light transaction results objects - suite.blocks. - On("ByID", blockId). - Return(block, nil) - lightCol := col.Light() - suite.collections. - On("LightByID", mock.Anything). - Return(lightCol, nil). - Once() + block := suite.g.Blocks().Fixture() + blockID := block.ID() - lightTxResults := make([]flow.LightTransactionResult, len(lightCol.Transactions)) - for i, txID := range lightCol.Transactions { - lightTxResults[i] = flow.LightTransactionResult{ - TransactionID: txID, - Failed: false, - ComputationUsed: 0, + suite.Run("happy path", func() { + scheduledTxs := []*flow.TransactionBody{ + suite.g.Transactions().Fixture(), + tx, + suite.g.Transactions().Fixture(), } - } - // simulate the system tx - lightTxResults = append(lightTxResults, flow.LightTransactionResult{ - TransactionID: suite.systemTx.ID(), - Failed: false, - ComputationUsed: 10, - }) - - // Mark the first transaction as failed - lightTxResults[0].Failed = true - suite.lightTxResults. - On("ByBlockID", blockId). - Return(lightTxResults, nil). - Once() - // Set up the events storage mock - totalEvents := 5 - eventsForTx := unittest.EventsFixture(totalEvents) - eventMessages := make([]*entities.Event, totalEvents) - for j, event := range eventsForTx { - eventMessages[j] = convert.EventToMessage(event) - } + suite.scheduledTransactions. + On("TransactionIDByID", scheduledTxID). + Return(txID, nil). + Once() - // expect a call to lookup events by block ID and transaction ID - suite.events. - On("ByBlockIDTransactionID", blockId, mock.Anything). - Return(eventsForTx, nil) + suite.scheduledTransactions. + On("BlockIDByTransactionID", txID). + Return(blockID, nil). + Once() - // Set up the state and snapshot mocks - _, fixedENIDs := suite.setupReceipts(block) - suite.fixedExecutionNodeIDs = fixedENIDs.NodeIDs() - suite.state.On("Final").Return(suite.snapshot, nil) - suite.state.On("Sealed").Return(suite.snapshot, nil) - suite.snapshot.On("Identities", mock.Anything).Return(fixedENIDs, nil) - suite.snapshot.On("Head").Return(block.ToHeader(), nil) + snapshot := protocolmock.NewSnapshot(suite.T()) + snapshot.On("Head").Return(block.ToHeader(), nil) - suite.reporter.On("LowestIndexedHeight").Return(block.Height, nil) - suite.reporter.On("HighestIndexedHeight").Return(block.Height+10, nil) + suite.state. + On("AtBlockID", blockID). + Return(snapshot, nil). + Once() - suite.connectionFactory. - On("GetExecutionAPIClient", mock.Anything). - Return(suite.executionAPIClient, &mocks.MockCloser{}, nil). - Once() + provider := providermock.NewTransactionProvider(suite.T()) + provider. + On("ScheduledTransactionsByBlockID", mock.Anything, block.ToHeader()). + Return(scheduledTxs, nil) - params := suite.defaultTransactionsParams() - params.TxErrorMessageProvider = error_messages.NewTxErrorMessageProvider( - params.Log, - nil, - params.TxResultsIndex, - params.ConnFactory, - params.NodeCommunicator, - params.NodeProvider, - ) - params.TxProvider = provider.NewLocalTransactionProvider( - params.State, - params.Collections, - params.Blocks, - params.EventsIndex, - params.TxResultsIndex, - params.TxErrorMessageProvider, - params.SystemTxID, - params.TxStatusDeriver, - params.ChainID, - params.ScheduledCallbacksEnabled, - ) - txBackend, err := NewTransactionsBackend(params) - suite.Require().NoError(err) + params := suite.defaultTransactionsParams() + params.TxProvider = provider - // Set up the expected error message for the execution node response - exeEventReq := &execproto.GetTransactionErrorMessagesByBlockIDRequest{ - BlockId: blockId[:], - } - res := &execproto.GetTransactionErrorMessagesResponse_Result{ - TransactionId: lightTxResults[0].TransactionID[:], - ErrorMessage: expectedErrorMsg, - Index: 1, - } - exeEventResp := &execproto.GetTransactionErrorMessagesResponse{ - Results: []*execproto.GetTransactionErrorMessagesResponse_Result{ - res, - }, - } - suite.executionAPIClient. - On("GetTransactionErrorMessagesByBlockID", mock.Anything, exeEventReq). - Return(exeEventResp, nil). - Once() + txBackend, err := NewTransactionsBackend(params) + suite.Require().NoError(err) - response, err := txBackend.GetTransactionResultsByBlockID(context.Background(), blockId, entities.EventEncodingVersion_JSON_CDC_V0) - suite.Require().NoError(err) - suite.Assert().Equal(len(lightTxResults), len(response)) + actual, err := txBackend.GetScheduledTransaction(context.Background(), scheduledTxID) + suite.Require().NoError(err) + suite.Require().Equal(tx, actual) + }) - // Assertions for each transaction result in the response - for i, responseResult := range response { - lightTx := lightTxResults[i] - suite.assertTransactionResultResponse(err, responseResult, *block, lightTx.TransactionID, lightTx.Failed, eventsForTx) - } -} + suite.Run("not a scheduled tx", func() { + suite.scheduledTransactions. + On("TransactionIDByID", scheduledTxID). + Return(nil, storage.ErrNotFound). + Once() -func (suite *Suite) TestGetTransactionsByBlockID() { - // Create fixtures - col := unittest.CollectionFixture(3) - guarantee := &flow.CollectionGuarantee{CollectionID: col.ID()} - block := unittest.BlockFixture( - unittest.Block.WithPayload(unittest.PayloadFixture(unittest.WithGuarantees(guarantee))), - ) - blockID := block.ID() + params := suite.defaultTransactionsParams() + params.TxProvider = providermock.NewTransactionProvider(suite.T()) - // Create PendingExecution events for scheduled callbacks - pendingExecutionEvents := suite.createPendingExecutionEvents(2) // 2 callbacks + txBackend, err := NewTransactionsBackend(params) + suite.Require().NoError(err) - // Reconstruct expected system collection to get the actual transaction IDs - expectedSystemCollection, err := blueprints.SystemCollection(suite.chainID.Chain(), pendingExecutionEvents) - suite.Require().NoError(err) + actual, err := txBackend.GetScheduledTransaction(context.Background(), scheduledTxID) + suite.Require().Error(err) + suite.Require().Equal(codes.NotFound, status.Code(err)) + suite.Require().Nil(actual) + }) - // Expected transaction counts - expectedUserTxCount := len(col.Transactions) - expectedSystemTxCount := len(expectedSystemCollection.Transactions) - expectedTotalCount := expectedUserTxCount + expectedSystemTxCount + // Tests the case where the scheduled transaction exists in the indices, but not in the generated + // system collection (produced from events). This indicates inconsistent state, however it is handled + // by returning an internal error instead of an irrecoverable since the events may be queried from + // an Execution node, which could have returned incorrect data. + suite.Run("not in generated collection", func() { + scheduledTxs := []*flow.TransactionBody{ + suite.g.Transactions().Fixture(), + suite.g.Transactions().Fixture(), + } - // Test with Local Provider - suite.Run("LocalProvider", func() { - // Mock the blocks storage - suite.blocks. - On("ByID", blockID). - Return(block, nil). + suite.scheduledTransactions. + On("TransactionIDByID", scheduledTxID). + Return(txID, nil). Once() - // Mock the collections storage - suite.collections. - On("ByID", col.ID()). - Return(&col, nil). + suite.scheduledTransactions. + On("BlockIDByTransactionID", txID). + Return(blockID, nil). Once() - // Mock the events storage to return PendingExecution events - suite.events. - On("ByBlockID", blockID). - Return(pendingExecutionEvents, nil). + snapshot := protocolmock.NewSnapshot(suite.T()) + snapshot.On("Head").Return(block.ToHeader(), nil) + + suite.state. + On("AtBlockID", blockID). + Return(snapshot, nil). Once() - suite.reporter.On("LowestIndexedHeight").Return(block.Height, nil) - suite.reporter.On("HighestIndexedHeight").Return(block.Height+10, nil) + provider := providermock.NewTransactionProvider(suite.T()) + provider. + On("ScheduledTransactionsByBlockID", mock.Anything, block.ToHeader()). + Return(scheduledTxs, nil) - // Set up the backend parameters with local transaction provider params := suite.defaultTransactionsParams() - - params.TxProvider = provider.NewLocalTransactionProvider( - params.State, - params.Collections, - params.Blocks, - params.EventsIndex, - params.TxResultsIndex, - params.TxErrorMessageProvider, - params.SystemTxID, - params.TxStatusDeriver, - params.ChainID, - params.ScheduledCallbacksEnabled, - ) + params.TxProvider = provider txBackend, err := NewTransactionsBackend(params) suite.Require().NoError(err) - // Call GetTransactionsByBlockID - transactions, err := txBackend.GetTransactionsByBlockID(context.Background(), blockID) - suite.Require().NoError(err) + actual, err := txBackend.GetScheduledTransaction(context.Background(), scheduledTxID) + suite.Require().Error(err) + suite.Require().Equal(codes.Internal, status.Code(err)) + suite.Require().Nil(actual) + }) - // Verify transaction count - suite.Require().Equal(expectedTotalCount, len(transactions), "expected %d transactions but got %d", expectedTotalCount, len(transactions)) + suite.Run("provider error", func() { + expectedErr := status.Errorf(codes.Internal, "some other error") - // Verify user transactions - for i, tx := range col.Transactions { - suite.Assert().Equal(tx.ID(), transactions[i].ID(), "user transaction %d mismatch", i) - } + suite.scheduledTransactions. + On("TransactionIDByID", scheduledTxID). + Return(txID, nil). + Once() - // Verify system transactions - for i, expectedTx := range expectedSystemCollection.Transactions { - actualTx := transactions[expectedUserTxCount+i] - suite.Assert().Equal(expectedTx.ID(), actualTx.ID(), "system transaction %d mismatch", i) - } - }) + suite.scheduledTransactions. + On("BlockIDByTransactionID", txID). + Return(blockID, nil). + Once() - // Test with Execution Node Provider - suite.Run("ExecutionNodeProvider", func() { - // Set up the state and snapshot mocks first - _, fixedENIDs := suite.setupReceipts(block) - suite.fixedExecutionNodeIDs = fixedENIDs.NodeIDs() - suite.state.On("Final").Return(suite.snapshot, nil).Maybe() - suite.state.On("Sealed").Return(suite.snapshot, nil).Maybe() - suite.snapshot.On("Identities", mock.Anything).Return(fixedENIDs, nil).Maybe() - suite.snapshot.On("Head").Return(block.ToHeader(), nil).Maybe() + snapshot := protocolmock.NewSnapshot(suite.T()) + snapshot.On("Head").Return(block.ToHeader(), nil) - // Mock the blocks storage - suite.blocks. - On("ByID", blockID). - Return(block, nil). + suite.state. + On("AtBlockID", blockID). + Return(snapshot, nil). Once() - // Mock the collections storage - suite.collections. - On("ByID", col.ID()). - Return(&col, nil). + provider := providermock.NewTransactionProvider(suite.T()) + provider. + On("ScheduledTransactionsByBlockID", mock.Anything, block.ToHeader()). + Return(nil, expectedErr) + + params := suite.defaultTransactionsParams() + params.TxProvider = provider + + txBackend, err := NewTransactionsBackend(params) + suite.Require().NoError(err) + + actual, err := txBackend.GetScheduledTransaction(context.Background(), scheduledTxID) + suite.Require().ErrorIs(err, expectedErr) + suite.Require().Nil(actual) + }) + + suite.Run("block not found exception", func() { + suite.scheduledTransactions. + On("TransactionIDByID", scheduledTxID). + Return(txID, nil). Once() - suite.setupExecutionGetEventsRequest(blockID, block.Height, pendingExecutionEvents) + suite.scheduledTransactions. + On("BlockIDByTransactionID", txID). + Return(blockID, nil). + Once() + + snapshot := protocolmock.NewSnapshot(suite.T()) + snapshot.On("Head").Return(nil, storage.ErrNotFound) + + suite.state. + On("AtBlockID", blockID). + Return(snapshot, nil). + Once() - // Set up the backend parameters with EN transaction provider params := suite.defaultTransactionsParams() - params.TxProvider = provider.NewENTransactionProvider( - params.Log, - params.State, - params.Collections, - params.ConnFactory, - params.NodeCommunicator, - params.NodeProvider, - params.TxStatusDeriver, - params.SystemTxID, - params.ChainID, - params.ScheduledCallbacksEnabled, - ) + params.TxProvider = providermock.NewTransactionProvider(suite.T()) txBackend, err := NewTransactionsBackend(params) suite.Require().NoError(err) - // Call GetTransactionsByBlockID - transactions, err := txBackend.GetTransactionsByBlockID(context.Background(), blockID) - suite.Require().NoError(err) + ctx := irrecoverable.NewMockSignalerContextExpectError(suite.T(), context.Background(), + fmt.Errorf("failed to get block header: %w", storage.ErrNotFound)) + signalerCtx := irrecoverable.WithSignalerContext(context.Background(), ctx) - // For empty events, we expect: user transactions + process tx + system chunk tx (no execute callback txs) - expectedSystemCollectionEmpty, err := blueprints.SystemCollection(suite.chainID.Chain(), pendingExecutionEvents) - suite.Require().NoError(err) - expectedTotalCountEmpty := len(col.Transactions) + len(expectedSystemCollectionEmpty.Transactions) + actual, err := txBackend.GetScheduledTransaction(signalerCtx, scheduledTxID) + suite.Require().Error(err) // specific error doen't matter since it's thrown + suite.Require().Nil(actual) + }) - // Verify transaction count - suite.Assert().Equal(expectedTotalCountEmpty, len(transactions)) + suite.Run("returns unimplemented error when scheduled transactions are not enabled", func() { + params := suite.defaultTransactionsParams() + params.ScheduledTransactions = nil - // Verify user transactions - for i, tx := range col.Transactions { - suite.Assert().Equal(tx.ID(), transactions[i].ID()) - } + txBackend, err := NewTransactionsBackend(params) + suite.Require().NoError(err) - // Verify system transactions (process + system chunk only, no execute callbacks) - for i, expectedTx := range expectedSystemCollectionEmpty.Transactions { - actualTx := transactions[len(col.Transactions)+i] - suite.Assert().Equal(expectedTx.ID(), actualTx.ID()) - } + res, err := txBackend.GetScheduledTransaction(context.Background(), 0) + suite.Require().Error(err) + suite.Require().Equal(codes.Unimplemented, status.Code(err)) + suite.Require().Nil(res) }) } -func (suite *Suite) TestTransactionResultsByBlockIDFromExecutionNode() { - // Create fixtures for the block and collection - col := unittest.CollectionFixture(2) - guarantee := &flow.CollectionGuarantee{CollectionID: col.ID()} - block := unittest.BlockFixture( - unittest.Block.WithPayload(unittest.PayloadFixture(unittest.WithGuarantees(guarantee))), - ) - blockId := block.ID() +func (suite *Suite) TestGetScheduledTransactionResult() { + scheduledTxID := suite.g.Random().Uint64() + tx := suite.systemCollection.Transactions[1] + txID := tx.ID() - // Mock the behavior of the blocks, collections and light transaction results objects - suite.blocks. - On("ByID", blockId). - Return(block, nil) - lightCol := col.Light() - suite.collections. - On("LightByID", mock.Anything). - Return(lightCol, nil). - Once() + block := suite.g.Blocks().Fixture() + blockID := block.ID() - // Execute callback transactions will be reconstructed from PendingExecution events - // We don't create them manually - they'll be generated by blueprints.SystemCollection - // System collection will be reconstructed from events, so we don't need to pre-populate lightTxResults + encodingVersion := entities.EventEncodingVersion_JSON_CDC_V0 - lightTxResults := make([]flow.LightTransactionResult, len(lightCol.Transactions)) - for i, txID := range lightCol.Transactions { - lightTxResults[i] = flow.LightTransactionResult{ - TransactionID: txID, - Failed: false, - ComputationUsed: 0, - } + expectedResult := &accessmodel.TransactionResult{ + BlockID: blockID, + TransactionID: txID, + CollectionID: flow.ZeroID, + BlockHeight: block.Height, + Status: flow.TransactionStatusExecuted, } - // Create PendingExecution events that would be emitted by the process callback transaction - pendingExecutionEvents := suite.createPendingExecutionEvents(2) // 2 callbacks + suite.Run("happy path", func() { + suite.scheduledTransactions. + On("TransactionIDByID", scheduledTxID). + Return(txID, nil). + Once() - // Convert PendingExecution events to protobuf messages for execution node response - pendingEventMessages := make([]*entities.Event, len(pendingExecutionEvents)) - for i, event := range pendingExecutionEvents { - pendingEventMessages[i] = convert.EventToMessage(event) - } + suite.scheduledTransactions. + On("BlockIDByTransactionID", txID). + Return(blockID, nil). + Once() - // Reconstruct the expected system collection to get the actual transaction IDs - expectedSystemCollection, err := blueprints.SystemCollection(suite.chainID.Chain(), pendingExecutionEvents) - suite.Require().NoError(err) + snapshot := protocolmock.NewSnapshot(suite.T()) + snapshot.On("Head").Return(block.ToHeader(), nil) - // Extract the expected transaction IDs in order: process, execute callbacks, system chunk - expectedSystemTxIDs := make([]flow.Identifier, len(expectedSystemCollection.Transactions)) - for i, tx := range expectedSystemCollection.Transactions { - expectedSystemTxIDs[i] = tx.ID() - } + suite.state. + On("AtBlockID", blockID). + Return(snapshot, nil). + Once() - suite.Require().Equal(4, len(expectedSystemTxIDs), "should have 4 system transactions: process + 2 execute callbacks + system chunk") + provider := providermock.NewTransactionProvider(suite.T()) + provider. + On("TransactionResult", mock.Anything, block.ToHeader(), txID, flow.ZeroID, encodingVersion). + Return(expectedResult, nil) - // Build the execution response with all transaction results including proper events - userTxResults := make([]*execproto.GetTransactionResultResponse, len(lightCol.Transactions)) - for i := 0; i < len(lightCol.Transactions); i++ { - userTxResults[i] = &execproto.GetTransactionResultResponse{ - Events: []*entities.Event{}, - } - } + params := suite.defaultTransactionsParams() + params.TxProvider = provider - // System transaction results: process (with events), execute callback txs, system chunk - systemTxResults := []*execproto.GetTransactionResultResponse{ - // Process callback transaction with PendingExecution events - {Events: pendingEventMessages}, - // Execute callback transaction 1 - {Events: []*entities.Event{}}, - // Execute callback transaction 2 - {Events: []*entities.Event{}}, - // System chunk transaction - {Events: []*entities.Event{}}, - } + txBackend, err := NewTransactionsBackend(params) + suite.Require().NoError(err) - allTxResults := append(userTxResults, systemTxResults...) + res, err := txBackend.GetScheduledTransactionResult(context.Background(), scheduledTxID, encodingVersion) + suite.Require().NoError(err) + suite.Require().Equal(expectedResult, res) + }) - // Set up execution node response with system transactions - // The execution node response should include: user txs + process tx (with PendingExecution events) + execute txs + system chunk tx - exeGetTxReq := &execproto.GetTransactionsByBlockIDRequest{ - BlockId: blockId[:], - } - exeGetTxResp := &execproto.GetTransactionResultsResponse{ - TransactionResults: allTxResults, - EventEncodingVersion: entities.EventEncodingVersion_CCF_V0, - } - suite.executionAPIClient. - On("GetTransactionResultsByBlockID", mock.Anything, exeGetTxReq). - Return(exeGetTxResp, nil). - Once() + suite.Run("not a scheduled tx", func() { + suite.scheduledTransactions. + On("TransactionIDByID", scheduledTxID). + Return(nil, storage.ErrNotFound). + Once() - // Set up the state and snapshot mocks - _, fixedENIDs := suite.setupReceipts(block) - suite.fixedExecutionNodeIDs = fixedENIDs.NodeIDs() - suite.state.On("Final").Return(suite.snapshot, nil) - suite.state.On("Sealed").Return(suite.snapshot, nil) - suite.snapshot.On("Identities", mock.Anything).Return(fixedENIDs, nil) - suite.snapshot.On("Head").Return(block.ToHeader(), nil) + params := suite.defaultTransactionsParams() + params.TxProvider = providermock.NewTransactionProvider(suite.T()) - suite.connectionFactory. - On("GetExecutionAPIClient", mock.Anything). - Return(suite.executionAPIClient, &mocks.MockCloser{}, nil). - Once() + txBackend, err := NewTransactionsBackend(params) + suite.Require().NoError(err) - params := suite.defaultTransactionsParams() - params.TxErrorMessageProvider = error_messages.NewTxErrorMessageProvider( - params.Log, - nil, - params.TxResultsIndex, - params.ConnFactory, - params.NodeCommunicator, - params.NodeProvider, - ) + res, err := txBackend.GetScheduledTransactionResult(context.Background(), scheduledTxID, encodingVersion) + suite.Require().Error(err) + suite.Require().Equal(codes.NotFound, status.Code(err)) + suite.Require().Nil(res) + }) - params.TxProvider = provider.NewENTransactionProvider( - params.Log, - params.State, - params.Collections, - params.ConnFactory, - params.NodeCommunicator, - params.NodeProvider, - params.TxStatusDeriver, - params.SystemTxID, - params.ChainID, - params.ScheduledCallbacksEnabled, - ) - txBackend, err := NewTransactionsBackend(params) - suite.Require().NoError(err) + suite.Run("scheduled tx by block ID lookup failure", func() { + expectedErr := fmt.Errorf("some other error") - response, err := txBackend.GetTransactionResultsByBlockID(context.Background(), blockId, entities.EventEncodingVersion_CCF_V0) - suite.Require().NoError(err) + suite.scheduledTransactions. + On("TransactionIDByID", scheduledTxID). + Return(txID, nil). + Once() - // Expected total: user transactions + system transactions (process + 2 execute callbacks + system chunk) - expectedTotal := len(lightTxResults) + len(expectedSystemTxIDs) - suite.Assert().Equal(expectedTotal, len(response), "should have user txs + system txs") - - // Verify user transactions - userTxCount := len(lightCol.Transactions) - for i := 0; i < userTxCount; i++ { - suite.Assert().Equal(lightTxResults[i].TransactionID, response[i].TransactionID) - suite.Assert().Equal(block.Payload.Guarantees[0].CollectionID, response[i].CollectionID) - suite.Assert().Equal(block.ID(), response[i].BlockID) - suite.Assert().Equal(block.Height, response[i].BlockHeight) - suite.Assert().Equal(flow.TransactionStatusSealed, response[i].Status) - } + suite.scheduledTransactions. + On("BlockIDByTransactionID", txID). + Return(flow.ZeroID, expectedErr). + Once() - // Verify system collection transactions (all should have ZeroID as collectionID) - systemTxCount := len(response) - userTxCount - suite.Assert().Equal(len(expectedSystemTxIDs), systemTxCount, "should have 4 system transactions: process + 2 execute callbacks + system chunk") - - for i := 0; i < systemTxCount; i++ { - systemTxIndex := userTxCount + i - suite.Assert().Equal(flow.ZeroID, response[systemTxIndex].CollectionID) - suite.Assert().Equal(block.ID(), response[systemTxIndex].BlockID) - suite.Assert().Equal(block.Height, response[systemTxIndex].BlockHeight) - suite.Assert().Equal(flow.TransactionStatusSealed, response[systemTxIndex].Status) - suite.Assert().Equal(expectedSystemTxIDs[i], response[userTxCount+i].TransactionID, "system transaction %d should match reconstructed ID", i) - } -} + params := suite.defaultTransactionsParams() + params.TxProvider = providermock.NewTransactionProvider(suite.T()) -// TestTransactionRetry tests that the retry mechanism will send retries at specific times -func (suite *Suite) TestTransactionRetry() { - block := unittest.BlockFixture( - // Height needs to be at least DefaultTransactionExpiry before we start doing retries - unittest.Block.WithHeight(flow.DefaultTransactionExpiry + 1), - ) - transactionBody := unittest.TransactionBodyFixture(unittest.WithReferenceBlock(block.ID())) - headBlock := unittest.BlockFixture() - headBlock.Height = block.Height - 1 // head is behind the current block - suite.state.On("Final").Return(suite.snapshot, nil) + txBackend, err := NewTransactionsBackend(params) + suite.Require().NoError(err) - suite.snapshot.On("Head").Return(headBlock.ToHeader(), nil) - snapshotAtBlock := protocolmock.NewSnapshot(suite.T()) - snapshotAtBlock.On("Head").Return(block.ToHeader(), nil) - suite.state.On("AtBlockID", block.ID()).Return(snapshotAtBlock, nil) + res, err := txBackend.GetScheduledTransactionResult(context.Background(), scheduledTxID, encodingVersion) + suite.Require().Error(err) + suite.Require().Equal(codes.Internal, status.Code(err)) + suite.Require().Nil(res) + }) - // collection storage returns a not found error - suite.collections. - On("LightByTransactionID", transactionBody.ID()). - Return(nil, storage.ErrNotFound) + suite.Run("block not found exception", func() { + suite.scheduledTransactions. + On("TransactionIDByID", scheduledTxID). + Return(txID, nil). + Once() - client := accessmock.NewAccessAPIClient(suite.T()) - params := suite.defaultTransactionsParams() - params.StaticCollectionRPCClient = client - txBackend, err := NewTransactionsBackend(params) - suite.Require().NoError(err) + suite.scheduledTransactions. + On("BlockIDByTransactionID", txID). + Return(blockID, nil). + Once() - retry := retrier.NewRetrier( - suite.log, - suite.blocks, - suite.collections, - txBackend, - txBackend.txStatusDeriver, - ) - retry.RegisterTransaction(block.Height, &transactionBody) + snapshot := protocolmock.NewSnapshot(suite.T()) + snapshot.On("Head").Return(nil, storage.ErrNotFound) - client.On("SendTransaction", mock.Anything, mock.Anything).Return(&access.SendTransactionResponse{}, nil) + suite.state. + On("AtBlockID", blockID). + Return(snapshot, nil). + Once() - // Don't retry on every height - err = retry.Retry(block.Height + 1) - suite.Require().NoError(err) + params := suite.defaultTransactionsParams() + params.TxProvider = providermock.NewTransactionProvider(suite.T()) - client.AssertNotCalled(suite.T(), "SendTransaction", mock.Anything, mock.Anything) + txBackend, err := NewTransactionsBackend(params) + suite.Require().NoError(err) - // Retry every `retryFrequency` - err = retry.Retry(block.Height + retrier.RetryFrequency) - suite.Require().NoError(err) + ctx := irrecoverable.NewMockSignalerContextExpectError(suite.T(), context.Background(), + fmt.Errorf("failed to get scheduled transaction's block from storage: %w", storage.ErrNotFound)) + signalerCtx := irrecoverable.WithSignalerContext(context.Background(), ctx) - client.AssertNumberOfCalls(suite.T(), "SendTransaction", 1) + res, err := txBackend.GetScheduledTransactionResult(signalerCtx, scheduledTxID, encodingVersion) + suite.Require().Error(err) // specific error doen't matter since it's thrown + suite.Require().Nil(res) + }) - // do not retry if expired - err = retry.Retry(block.Height + retrier.RetryFrequency + flow.DefaultTransactionExpiry) - suite.Require().NoError(err) + suite.Run("provider error", func() { + expectedErr := status.Errorf(codes.Internal, "some other error") - // Should've still only been called once - client.AssertNumberOfCalls(suite.T(), "SendTransaction", 1) -} + suite.scheduledTransactions. + On("TransactionIDByID", scheduledTxID). + Return(txID, nil). + Once() -// TestSuccessfulTransactionsDontRetry tests that the retry mechanism will send retries at specific times -func (suite *Suite) TestSuccessfulTransactionsDontRetry() { - collection := unittest.CollectionFixture(1) - light := collection.Light() - transactionBody := collection.Transactions[0] - txID := transactionBody.ID() + suite.scheduledTransactions. + On("BlockIDByTransactionID", txID). + Return(blockID, nil). + Once() - block := unittest.BlockFixture() - blockID := block.ID() + snapshot := protocolmock.NewSnapshot(suite.T()) + snapshot.On("Head").Return(block.ToHeader(), nil) - // setup chain state - _, fixedENIDs := suite.setupReceipts(block) - suite.fixedExecutionNodeIDs = fixedENIDs.NodeIDs() + suite.state. + On("AtBlockID", blockID). + Return(snapshot, nil). + Once() - suite.state.On("Final").Return(suite.snapshot, nil) - suite.transactions.On("ByID", transactionBody.ID()).Return(transactionBody, nil) - suite.collections.On("LightByTransactionID", transactionBody.ID()).Return(light, nil) - suite.blocks.On("ByCollectionID", collection.ID()).Return(block, nil) - suite.snapshot.On("Identities", mock.Anything).Return(fixedENIDs, nil) + provider := providermock.NewTransactionProvider(suite.T()) + provider. + On("TransactionResult", mock.Anything, block.ToHeader(), txID, flow.ZeroID, encodingVersion). + Return(nil, expectedErr) - exeEventReq := execproto.GetTransactionResultRequest{ - BlockId: blockID[:], - TransactionId: txID[:], - } - exeEventResp := execproto.GetTransactionResultResponse{ - Events: nil, - } - suite.executionAPIClient. - On("GetTransactionResult", context.Background(), &exeEventReq). - Return(&exeEventResp, status.Errorf(codes.NotFound, "not found")). - Times(len(fixedENIDs)) // should call each EN once + params := suite.defaultTransactionsParams() + params.TxProvider = provider - suite.connectionFactory. - On("GetExecutionAPIClient", mock.Anything). - Return(suite.executionAPIClient, &mocks.MockCloser{}, nil). - Times(len(fixedENIDs)) + txBackend, err := NewTransactionsBackend(params) + suite.Require().NoError(err) - params := suite.defaultTransactionsParams() - client := accessmock.NewAccessAPIClient(suite.T()) - params.StaticCollectionRPCClient = client - txBackend, err := NewTransactionsBackend(params) - suite.Require().NoError(err) + res, err := txBackend.GetScheduledTransactionResult(context.Background(), scheduledTxID, encodingVersion) + suite.Require().ErrorIs(err, expectedErr) + suite.Require().Nil(res) + }) - retry := retrier.NewRetrier( - suite.log, - suite.blocks, - suite.collections, - txBackend, - txBackend.txStatusDeriver, - ) - retry.RegisterTransaction(block.Height, transactionBody) - - // first call - when block under test is greater height than the sealed head, but execution node does not know about Tx - result, err := txBackend.GetTransactionResult( - context.Background(), - txID, - flow.ZeroID, - flow.ZeroID, - entities.EventEncodingVersion_JSON_CDC_V0, - ) - suite.Require().NoError(err) - suite.Require().NotNil(result) + suite.Run("returns unimplemented error when scheduled transactions are not enabled", func() { + params := suite.defaultTransactionsParams() + params.ScheduledTransactions = nil - // status should be finalized since the sealed Blocks is smaller in height - suite.Assert().Equal(flow.TransactionStatusFinalized, result.Status) + txBackend, err := NewTransactionsBackend(params) + suite.Require().NoError(err) - // Don't retry when block is finalized - err = retry.Retry(block.Height + 1) - suite.Require().NoError(err) + res, err := txBackend.GetScheduledTransactionResult(context.Background(), scheduledTxID, encodingVersion) + suite.Require().Error(err) + suite.Require().Equal(codes.Unimplemented, status.Code(err)) + suite.Require().Nil(res) + }) +} - client.AssertNotCalled(suite.T(), "SendTransaction", mock.Anything, mock.Anything) +func (suite *Suite) TestSendTransaction() { + refBlock := suite.g.Headers().Fixture() + refBlockID := refBlock.ID() + finalBlock := suite.g.Headers().Fixture(fixtures.Header.WithParentHeader(refBlock)) + tx := suite.g.Transactions().Fixture(fixtures.Transaction.WithReferenceBlockID(refBlockID)) - // Don't retry when block is finalized - err = retry.Retry(block.Height + retrier.RetryFrequency) - suite.Require().NoError(err) + expectedRequest := &access.SendTransactionRequest{ + Transaction: convert.TransactionToMessage(*tx), + } - client.AssertNotCalled(suite.T(), "SendTransaction", mock.Anything, mock.Anything) + response := &access.SendTransactionResponse{ + Id: convert.IdentifierToMessage(tx.ID()), + } - // Don't retry when block is finalized - err = retry.Retry(block.Height + retrier.RetryFrequency + flow.DefaultTransactionExpiry) - suite.Require().NoError(err) + nodes := suite.g.Identities().List(2, fixtures.Identity.WithRole(flow.RoleCollection)) - // Should've still should not be called - client.AssertNotCalled(suite.T(), "SendTransaction", mock.Anything, mock.Anything) -} + suite.Run("happy path", func() { + client := suite.addCollectionClient(nodes[0].Address) + client.On("SendTransaction", mock.Anything, expectedRequest).Return(response, nil) -func (suite *Suite) setupReceipts(block *flow.Block) ([]*flow.ExecutionReceipt, flow.IdentityList) { - ids := unittest.IdentityListFixture(2, unittest.WithRole(flow.RoleExecution)) - receipt1 := unittest.ReceiptForBlockFixture(block) - receipt1.ExecutorID = ids[0].NodeID - receipt2 := unittest.ReceiptForBlockFixture(block) - receipt2.ExecutorID = ids[1].NodeID - receipt1.ExecutionResult = receipt2.ExecutionResult + suite.transactions.On("Store", tx).Return(nil).Once() - receipts := flow.ExecutionReceiptList{receipt1, receipt2} - suite.receipts. - On("ByBlockID", block.ID()). - Return(receipts, nil) + finalSnapshot := protocolmock.NewSnapshot(suite.T()) + finalSnapshot.On("Head").Return(finalBlock, nil).Once() + suite.state.On("Final").Return(finalSnapshot, nil).Twice() // once to get clustering, once to validate the tx - return receipts, ids -} + refBlockSnapshot := protocolmock.NewSnapshot(suite.T()) + refBlockSnapshot.On("Head").Return(refBlock, nil).Once() + suite.state.On("AtBlockID", refBlockID).Return(refBlockSnapshot, nil).Once() -func (suite *Suite) assertTransactionResultResponse( - err error, - response *accessmodel.TransactionResult, - block flow.Block, - txId flow.Identifier, - txFailed bool, - eventsForTx []flow.Event, -) { - suite.Require().NoError(err) - suite.Assert().Equal(block.ID(), response.BlockID) - suite.Assert().Equal(block.Height, response.BlockHeight) - suite.Assert().Equal(txId, response.TransactionID) - if txId == suite.systemTx.ID() { - suite.Assert().Equal(flow.ZeroID, response.CollectionID) - } else { - suite.Assert().Equal(block.Payload.Guarantees[0].CollectionID, response.CollectionID) - } - suite.Assert().Equal(len(eventsForTx), len(response.Events)) - // When there are error messages occurred in the transaction, the status should be 1 - if txFailed { - suite.Assert().Equal(uint(1), response.StatusCode) - suite.Assert().Equal(expectedErrorMsg, response.ErrorMessage) - } else { - suite.Assert().Equal(uint(0), response.StatusCode) - suite.Assert().Equal("", response.ErrorMessage) - } - suite.Assert().Equal(flow.TransactionStatusSealed, response.Status) -} + configureClustering(suite.T(), flow.IdentityList{nodes[0]}, finalSnapshot) -// createPendingExecutionEvents creates properly formatted PendingExecution events -// that blueprints.SystemCollection expects for reconstructing the system collection. -func (suite *Suite) createPendingExecutionEvents(numCallbacks int) []flow.Event { - events := make([]flow.Event, numCallbacks) - - // Get system contracts for the test chain - env := systemcontracts.SystemContractsForChain(suite.chainID).AsTemplateEnv() - - for i := 0; i < numCallbacks; i++ { - // Create the PendingExecution event as it would be emitted by the process callback transaction - const processedEventTypeTemplate = "A.%v.FlowTransactionScheduler.PendingExecution" - eventTypeString := fmt.Sprintf(processedEventTypeTemplate, env.FlowTransactionSchedulerAddress) - - // Create Cadence event type - loc, err := cadenceCommon.HexToAddress(env.FlowTransactionSchedulerAddress) - suite.Require().NoError(err) - location := cadenceCommon.NewAddressLocation(nil, loc, "PendingExecution") - - eventType := cadence.NewEventType( - location, - "PendingExecution", - []cadence.Field{ - {Identifier: "id", Type: cadence.UInt64Type}, - {Identifier: "priority", Type: cadence.UInt8Type}, - {Identifier: "executionEffort", Type: cadence.UInt64Type}, - {Identifier: "fees", Type: cadence.UFix64Type}, - {Identifier: "callbackOwner", Type: cadence.AddressType}, - }, - nil, - ) + params := suite.defaultTransactionsParams() + txBackend, err := NewTransactionsBackend(params) + suite.Require().NoError(err) - fees, err := cadence.NewUFix64("0.0") + err = txBackend.SendTransaction(context.Background(), tx) suite.Require().NoError(err) + }) - // Create the Cadence event with proper values - event := cadence.NewEvent( - []cadence.Value{ - cadence.NewUInt64(uint64(i + 1)), // id: unique callback ID - cadence.NewUInt8(1), // priority - cadence.NewUInt64(uint64((i+1)*100 + 100)), // executionEffort (200, 300, etc.) - fees, // fees: 0.0 - cadence.NewAddress([8]byte{}), // callbackOwner - }, - ).WithType(eventType) + // test that the correct node is used when a static collection node is provided and the + // clustering lookup is never performed + suite.Run("static collection node", func() { + client := accessmock.NewAccessAPIClient(suite.T()) + client.On("SendTransaction", mock.Anything, expectedRequest).Return(response, nil) + + finalSnapshot := protocolmock.NewSnapshot(suite.T()) + finalSnapshot.On("Head").Return(finalBlock, nil).Once() + suite.state.On("Final").Return(finalSnapshot, nil).Once() + + refBlockSnapshot := protocolmock.NewSnapshot(suite.T()) + refBlockSnapshot.On("Head").Return(refBlock, nil).Once() + suite.state.On("AtBlockID", refBlockID).Return(refBlockSnapshot, nil).Once() + + suite.transactions.On("Store", tx).Return(nil).Once() + + params := suite.defaultTransactionsParams() + params.StaticCollectionRPCClient = client - // Encode the event using CCF - payload, err := ccf.Encode(event) + txBackend, err := NewTransactionsBackend(params) + suite.Require().NoError(err) + + err = txBackend.SendTransaction(context.Background(), tx) suite.Require().NoError(err) + }) - // Create the Flow event - events[i] = flow.Event{ - Type: flow.EventType(eventTypeString), - TransactionID: unittest.IdentifierFixture(), // Process callback transaction ID - TransactionIndex: 0, - EventIndex: uint32(i), - Payload: payload, + // test that each collection node is tried when requests fail + suite.Run("multiple collection nodes tried when first fails", func() { + expectedErr := status.Errorf(codes.Internal, "test failed to send transaction") + + for _, node := range nodes { + client := suite.addCollectionClient(node.Address) + client.On("SendTransaction", mock.Anything, expectedRequest).Return(nil, expectedErr) } - } - return events + finalSnapshot := protocolmock.NewSnapshot(suite.T()) + finalSnapshot.On("Head").Return(finalBlock, nil).Once() + suite.state.On("Final").Return(finalSnapshot, nil).Twice() // once to get clustering, once to validate the tx + + refBlockSnapshot := protocolmock.NewSnapshot(suite.T()) + refBlockSnapshot.On("Head").Return(refBlock, nil).Once() + suite.state.On("AtBlockID", refBlockID).Return(refBlockSnapshot, nil).Once() + + configureClustering(suite.T(), nodes, finalSnapshot) + + params := suite.defaultTransactionsParams() + txBackend, err := NewTransactionsBackend(params) + suite.Require().NoError(err) + + err = txBackend.SendTransaction(context.Background(), tx) + suite.Require().Error(err) + suite.Require().Equal(codes.Internal, status.Code(err)) + suite.Require().Contains(err.Error(), expectedErr.Error()) + }) } -func (suite *Suite) setupExecutionGetEventsRequest(blockID flow.Identifier, blockHeight uint64, events []flow.Event) { - eventMessages := make([]*entities.Event, len(events)) - for i, event := range events { - eventMessages[i] = convert.EventToMessage(event) - } +func configureClustering(t *testing.T, identities flow.IdentityList, finalSnapshot *protocolmock.Snapshot) { + epoch := protocolmock.NewCommittedEpoch(t) + epoch.On("Clustering").Return(flow.ClusterList{identities.ToSkeleton()}, nil).Once() - request := &execproto.GetEventsForBlockIDsRequest{ - Type: string(suite.processScheduledCallbackEventType), - BlockIds: [][]byte{blockID[:]}, - } - expectedResponse := &execproto.GetEventsForBlockIDsResponse{ - Results: []*execproto.GetEventsForBlockIDsResponse_Result{ - { - BlockId: blockID[:], - BlockHeight: blockHeight, - Events: eventMessages, - }, - }, - EventEncodingVersion: entities.EventEncodingVersion_CCF_V0, - } + epochQuery := protocolmock.NewEpochQuery(t) + epochQuery.On("Current").Return(epoch, nil).Once() - suite.executionAPIClient. - On("GetEventsForBlockIDs", mock.Anything, request). - Return(expectedResponse, nil). - Once() + finalSnapshot.On("Epochs").Return(epochQuery, nil).Once() +} +func (suite *Suite) addCollectionClient(address string) *accessmock.AccessAPIClient { + client := accessmock.NewAccessAPIClient(suite.T()) suite.connectionFactory. - On("GetExecutionAPIClient", mock.Anything). - Return(suite.executionAPIClient, &mocks.MockCloser{}, nil). + On("GetCollectionAPIClient", address, mock.Anything). + Return(client, &mocks.MockCloser{}, nil). Once() + + return client } diff --git a/engine/access/rpc/handler.go b/engine/access/rpc/handler.go index e6a79af259c..485813cbb33 100644 --- a/engine/access/rpc/handler.go +++ b/engine/access/rpc/handler.go @@ -458,6 +458,46 @@ func (h *Handler) GetSystemTransactionResult( return message, nil } +func (h *Handler) GetScheduledTransaction( + ctx context.Context, + req *accessproto.GetScheduledTransactionRequest, +) (*accessproto.TransactionResponse, error) { + metadata, err := h.buildMetadataResponse() + if err != nil { + return nil, err + } + + tx, err := h.api.GetScheduledTransaction(ctx, req.GetId()) + if err != nil { + return nil, err + } + + return &accessproto.TransactionResponse{ + Transaction: convert.TransactionToMessage(*tx), + Metadata: metadata, + }, nil +} + +func (h *Handler) GetScheduledTransactionResult( + ctx context.Context, + req *accessproto.GetScheduledTransactionResultRequest, +) (*accessproto.TransactionResultResponse, error) { + metadata, err := h.buildMetadataResponse() + if err != nil { + return nil, err + } + + result, err := h.api.GetScheduledTransactionResult(ctx, req.GetId(), req.GetEventEncodingVersion()) + if err != nil { + return nil, err + } + + message := convert.TransactionResultToMessage(result) + message.Metadata = metadata + + return message, nil +} + func (h *Handler) GetTransactionsByBlockID( ctx context.Context, req *accessproto.GetTransactionsByBlockIDRequest, diff --git a/engine/common/rpc/convert/execution_data_test.go b/engine/common/rpc/convert/execution_data_test.go index a046b3579d2..2d8ec9d5eb1 100644 --- a/engine/common/rpc/convert/execution_data_test.go +++ b/engine/common/rpc/convert/execution_data_test.go @@ -13,7 +13,9 @@ import ( "github.com/onflow/flow-go/ledger/common/testutils" "github.com/onflow/flow-go/model/flow" "github.com/onflow/flow-go/module/executiondatasync/execution_data" + "github.com/onflow/flow-go/module/executiondatasync/testutil" "github.com/onflow/flow-go/utils/unittest" + "github.com/onflow/flow-go/utils/unittest/fixtures" ) func TestConvertBlockExecutionDataEventPayloads(t *testing.T) { @@ -72,39 +74,14 @@ func TestConvertBlockExecutionDataEventPayloads(t *testing.T) { func TestConvertBlockExecutionData(t *testing.T) { t.Parallel() - chain := flow.Testnet.Chain() // this is used by the AddressFixture - events := unittest.EventsFixture(5) - - chunks := 5 - chunkData := make([]*execution_data.ChunkExecutionData, 0, chunks) - for i := 0; i < chunks-1; i++ { - ced := unittest.ChunkExecutionDataFixture(t, - 0, // updates set explicitly to target 160-320KB per chunk - unittest.WithChunkEvents(events), - unittest.WithTrieUpdate(testutils.TrieUpdateFixture(5, 32*1024, 64*1024)), - ) - - chunkData = append(chunkData, ced) - } - makeServiceTx := func(ced *execution_data.ChunkExecutionData) { - // proposal key and payer are empty addresses for service tx - collection := unittest.CollectionFixture(1) - collection.Transactions[0].ProposalKey.Address = flow.EmptyAddress - collection.Transactions[0].Payer = flow.EmptyAddress - ced.Collection = &collection - - // the service chunk sometimes does not have any trie updates - ced.TrieUpdate = nil - } - chunk := unittest.ChunkExecutionDataFixture(t, execution_data.DefaultMaxBlobSize/5, unittest.WithChunkEvents(events), makeServiceTx) - chunkData = append(chunkData, chunk) - - blockData := unittest.BlockExecutionDataFixture(unittest.WithChunkExecutionDatas(chunkData...)) + g := fixtures.NewGeneratorSuite() + tf := testutil.CompleteFixture(t, g, g.Blocks().Fixture()) + blockData := tf.ExecutionData msg, err := convert.BlockExecutionDataToMessage(blockData) require.NoError(t, err) - converted, err := convert.MessageToBlockExecutionData(msg, chain) + converted, err := convert.MessageToBlockExecutionData(msg, g.ChainID().Chain()) require.NoError(t, err) require.Equal(t, blockData, converted) diff --git a/engine/common/rpc/convert/transactions.go b/engine/common/rpc/convert/transactions.go index 69a25e83839..63b337d8116 100644 --- a/engine/common/rpc/convert/transactions.go +++ b/engine/common/rpc/convert/transactions.go @@ -10,10 +10,21 @@ import ( // TransactionToMessage converts a flow.TransactionBody to a protobuf message func TransactionToMessage(tb flow.TransactionBody) *entities.Transaction { - proposalKeyMessage := &entities.Transaction_ProposalKey{ - Address: tb.ProposalKey.Address.Bytes(), - KeyId: uint32(tb.ProposalKey.KeyIndex), - SequenceNumber: tb.ProposalKey.SequenceNumber, + // Note: system and scheduled transactions have nil/empty values for some fields. This method + // intentionally uses unset values for these fields to ensure that encoding and decoding a system + // or scheduled transaction results in the same transaction body. + var proposalKeyMessage *entities.Transaction_ProposalKey + if tb.ProposalKey.Address != flow.EmptyAddress { + proposalKeyMessage = &entities.Transaction_ProposalKey{ + Address: tb.ProposalKey.Address.Bytes(), + KeyId: uint32(tb.ProposalKey.KeyIndex), + SequenceNumber: tb.ProposalKey.SequenceNumber, + } + } + + var payer []byte + if tb.Payer != flow.EmptyAddress { + payer = tb.Payer.Bytes() } authMessages := make([][]byte, len(tb.Authorizers)) @@ -22,7 +33,6 @@ func TransactionToMessage(tb flow.TransactionBody) *entities.Transaction { } payloadSigMessages := make([]*entities.Transaction_Signature, len(tb.PayloadSignatures)) - for i, sig := range tb.PayloadSignatures { payloadSigMessages[i] = &entities.Transaction_Signature{ Address: sig.Address.Bytes(), @@ -33,7 +43,6 @@ func TransactionToMessage(tb flow.TransactionBody) *entities.Transaction { } envelopeSigMessages := make([]*entities.Transaction_Signature, len(tb.EnvelopeSignatures)) - for i, sig := range tb.EnvelopeSignatures { envelopeSigMessages[i] = &entities.Transaction_Signature{ Address: sig.Address.Bytes(), @@ -49,7 +58,7 @@ func TransactionToMessage(tb flow.TransactionBody) *entities.Transaction { ReferenceBlockId: tb.ReferenceBlockID[:], GasLimit: tb.GasLimit, ProposalKey: proposalKeyMessage, - Payer: tb.Payer.Bytes(), + Payer: payer, Authorizers: authMessages, PayloadSignatures: payloadSigMessages, EnvelopeSignatures: envelopeSigMessages, @@ -68,43 +77,43 @@ func MessageToTransaction( tb := flow.NewTransactionBodyBuilder() proposalKey := m.GetProposalKey() - if proposalKey != nil { + if proposalKey != nil && IsNonEmptyAddress(proposalKey.GetAddress()) { proposalAddress, err := Address(proposalKey.GetAddress(), chain) if err != nil { - return t, err + return t, fmt.Errorf("could not convert proposer address: %w", err) } tb.SetProposalKey(proposalAddress, proposalKey.GetKeyId(), proposalKey.GetSequenceNumber()) } payer := m.GetPayer() - if payer != nil { + if payer != nil && IsNonEmptyAddress(payer) { payerAddress, err := Address(payer, chain) if err != nil { - return t, err + return t, fmt.Errorf("could not convert payer address: %w", err) } tb.SetPayer(payerAddress) } - for _, authorizer := range m.GetAuthorizers() { + for i, authorizer := range m.GetAuthorizers() { authorizerAddress, err := Address(authorizer, chain) if err != nil { - return t, err + return t, fmt.Errorf("could not convert authorizer %d address: %w", i, err) } tb.AddAuthorizer(authorizerAddress) } - for _, sig := range m.GetPayloadSignatures() { + for i, sig := range m.GetPayloadSignatures() { addr, err := Address(sig.GetAddress(), chain) if err != nil { - return t, err + return t, fmt.Errorf("could not convert payload signature %d address: %w", i, err) } tb.AddPayloadSignatureWithExtensionData(addr, sig.GetKeyId(), sig.GetSignature(), sig.GetExtensionData()) } - for _, sig := range m.GetEnvelopeSignatures() { + for i, sig := range m.GetEnvelopeSignatures() { addr, err := Address(sig.GetAddress(), chain) if err != nil { - return t, err + return t, fmt.Errorf("could not convert envelope signature %d address: %w", i, err) } tb.AddEnvelopeSignatureWithExtensionData(addr, sig.GetKeyId(), sig.GetSignature(), sig.GetExtensionData()) } diff --git a/engine/common/rpc/convert/transactions_test.go b/engine/common/rpc/convert/transactions_test.go index c9c5141f9a8..dcd91f1e34c 100644 --- a/engine/common/rpc/convert/transactions_test.go +++ b/engine/common/rpc/convert/transactions_test.go @@ -10,25 +10,51 @@ import ( jsoncdc "github.com/onflow/cadence/encoding/json" "github.com/onflow/flow-go/engine/common/rpc/convert" - "github.com/onflow/flow-go/model/flow" - "github.com/onflow/flow-go/utils/unittest" + "github.com/onflow/flow-go/fvm/blueprints" + "github.com/onflow/flow-go/utils/unittest/fixtures" ) +// TestConvertTransaction tests that converting a transaction to a protobuf message and back results in the +// same transaction body. func TestConvertTransaction(t *testing.T) { t.Parallel() - tx := unittest.TransactionBodyFixture() - arg, err := jsoncdc.Encode(cadence.NewAddress(unittest.AddressFixture())) - require.NoError(t, err) + g := fixtures.NewGeneratorSuite() + tx := g.Transactions().Fixture() // add fields not included in the fixture + arg, err := jsoncdc.Encode(cadence.NewAddress(g.Addresses().Fixture())) + require.NoError(t, err) tx.Arguments = append(tx.Arguments, arg) - tx.EnvelopeSignatures = append(tx.EnvelopeSignatures, unittest.TransactionSignatureFixture()) - msg := convert.TransactionToMessage(tx) - converted, err := convert.MessageToTransaction(msg, flow.Testnet.Chain()) + msg := convert.TransactionToMessage(*tx) + converted, err := convert.MessageToTransaction(msg, g.ChainID().Chain()) require.NoError(t, err) - assert.Equal(t, tx, converted) + assert.Equal(t, tx, &converted) assert.Equal(t, tx.ID(), converted.ID()) } + +// TestConvertSystemTransaction tests that converting a system transaction to a protobuf message and +// back results in the same transaction body. +// +// System and scheduled transactions have nil/empty values for some fields. This test ensures that +// these fields are properly handled when converting to and from protobuf messages and the resulting +// transaction body identical. +func TestConvertSystemTransaction(t *testing.T) { + t.Parallel() + + g := fixtures.NewGeneratorSuite() + events := g.PendingExecutionEvents().List(3) + + systemCollection, err := blueprints.SystemCollection(g.ChainID().Chain(), events) + require.NoError(t, err) + + for _, tx := range systemCollection.Transactions { + msg := convert.TransactionToMessage(*tx) + converted, err := convert.MessageToTransaction(msg, g.ChainID().Chain()) + require.NoError(t, err) + assert.Equal(t, tx, &converted) + assert.Equal(t, tx.ID(), converted.ID()) + } +} diff --git a/engine/common/rpc/convert/validate.go b/engine/common/rpc/convert/validate.go index 35b93851198..d27cadcca1e 100644 --- a/engine/common/rpc/convert/validate.go +++ b/engine/common/rpc/convert/validate.go @@ -1,6 +1,7 @@ package convert import ( + "bytes" "strings" "google.golang.org/grpc/codes" @@ -26,6 +27,12 @@ func Address(rawAddress []byte, chain flow.Chain) (flow.Address, error) { return address, nil } +// IsNonEmptyAddress returns true if the address is the correct length and not the empty address. +// Does not convert or check the validity of the address. +func IsNonEmptyAddress(address []byte) bool { + return len(address) == flow.AddressLength && !bytes.Equal(address, flow.EmptyAddress.Bytes()) +} + func HexToAddress(hexAddress string, chain flow.Chain) (flow.Address, error) { if len(hexAddress) == 0 { return flow.EmptyAddress, status.Error(codes.InvalidArgument, "address cannot be empty") diff --git a/engine/common/rpc/execution_node_identities_provider.go b/engine/common/rpc/execution_node_identities_provider.go index 7374fcc102c..8bd9ec26d3e 100644 --- a/engine/common/rpc/execution_node_identities_provider.go +++ b/engine/common/rpc/execution_node_identities_provider.go @@ -89,7 +89,7 @@ func (e *ExecutionNodeIdentitiesProvider) ExecutionNodesForBlockID( if rootBlock.ID() == blockID { executorIdentities, err := e.state.Final().Identities(filter.HasRole[flow.Identity](flow.RoleExecution)) if err != nil { - return nil, fmt.Errorf("failed to retreive execution IDs for block ID %v: %w", blockID, err) + return nil, fmt.Errorf("failed to retreive execution IDs for root block %v: %w", blockID, err) } executorIDs = executorIdentities.NodeIDs() } else { @@ -135,7 +135,7 @@ func (e *ExecutionNodeIdentitiesProvider) ExecutionNodesForBlockID( // choose from the preferred or fixed execution nodes subsetENs, err := e.chooseExecutionNodes(executorIDs) if err != nil { - return nil, fmt.Errorf("failed to retreive execution IDs for block ID %v: %w", blockID, err) + return nil, fmt.Errorf("failed to select execution IDs for block ID %v: %w", blockID, err) } if len(subsetENs) == 0 { @@ -162,7 +162,7 @@ func (e *ExecutionNodeIdentitiesProvider) ExecutionNodesForResultID( if rootBlock.ID() == blockID { executorIdentities, err := e.state.Final().Identities(filter.HasRole[flow.Identity](flow.RoleExecution)) if err != nil { - return nil, fmt.Errorf("failed to retreive execution IDs for block ID %v: %w", blockID, err) + return nil, fmt.Errorf("failed to retreive execution IDs for root block ID %v: %w", blockID, err) } executorIDs = append(executorIDs, executorIdentities.NodeIDs()...) diff --git a/go.mod b/go.mod index d3d5cc6b081..27644db0447 100644 --- a/go.mod +++ b/go.mod @@ -53,7 +53,7 @@ require ( github.com/onflow/flow-core-contracts/lib/go/contracts v1.9.1 github.com/onflow/flow-core-contracts/lib/go/templates v1.9.1 github.com/onflow/flow-go-sdk v1.9.2 - github.com/onflow/flow/protobuf/go/flow v0.4.16 + github.com/onflow/flow/protobuf/go/flow v0.4.18 github.com/pbnjay/memory v0.0.0-20210728143218-7b4eea64cf58 github.com/pkg/errors v0.9.1 github.com/pkg/profile v1.7.0 diff --git a/go.sum b/go.sum index c63cd4cd68a..b2b90ca7235 100644 --- a/go.sum +++ b/go.sum @@ -962,8 +962,8 @@ github.com/onflow/flow-nft/lib/go/contracts v1.3.0 h1:DmNop+O0EMyicZvhgdWboFG57x github.com/onflow/flow-nft/lib/go/contracts v1.3.0/go.mod h1:eZ9VMMNfCq0ho6kV25xJn1kXeCfxnkhj3MwF3ed08gY= github.com/onflow/flow-nft/lib/go/templates v1.3.0 h1:uGIBy4GEY6Z9hKP7sm5nA5kwvbvLWW4nWx5NN9Wg0II= github.com/onflow/flow-nft/lib/go/templates v1.3.0/go.mod h1:gVbb5fElaOwKhV5UEUjM+JQTjlsguHg2jwRupfM/nng= -github.com/onflow/flow/protobuf/go/flow v0.4.16 h1:UADQeq/mpuqFk+EkwqDNoF70743raWQKmB/Dm/eKt2Q= -github.com/onflow/flow/protobuf/go/flow v0.4.16/go.mod h1:NA2pX2nw8zuaxfKphhKsk00kWLwfd+tv8mS23YXO4Sk= +github.com/onflow/flow/protobuf/go/flow v0.4.18 h1:KOujA6lg9kTXCV6oK0eErD1rwRnM9taKZss3Szi+T3Q= +github.com/onflow/flow/protobuf/go/flow v0.4.18/go.mod h1:NA2pX2nw8zuaxfKphhKsk00kWLwfd+tv8mS23YXO4Sk= github.com/onflow/go-ds-pebble v0.0.0-20251003225212-131edca3a897 h1:ZtFYJ3OSR00aiKMMxgm3fRYWqYzjvDXeoBGQm6yC8DE= github.com/onflow/go-ds-pebble v0.0.0-20251003225212-131edca3a897/go.mod h1:aiCRVcj3K60sxc6k5C+HO9C6rouqiSkjR/WKnbTcMfQ= github.com/onflow/go-ethereum v1.13.4 h1:iNO86fm8RbBbhZ87ZulblInqCdHnAQVY8okBrNsTevc= diff --git a/insecure/go.mod b/insecure/go.mod index 8aacbeff696..ac433b11980 100644 --- a/insecure/go.mod +++ b/insecure/go.mod @@ -224,7 +224,7 @@ require ( github.com/onflow/flow-go-sdk v1.9.2 // indirect github.com/onflow/flow-nft/lib/go/contracts v1.3.0 // indirect github.com/onflow/flow-nft/lib/go/templates v1.3.0 // indirect - github.com/onflow/flow/protobuf/go/flow v0.4.16 // indirect + github.com/onflow/flow/protobuf/go/flow v0.4.18 // indirect github.com/onflow/go-ethereum v1.16.2 // indirect github.com/onflow/nft-storefront/lib/go/contracts v1.0.0 // indirect github.com/onflow/sdks v0.6.0-preview.1 // indirect diff --git a/insecure/go.sum b/insecure/go.sum index e22d47f99ba..33e736e4929 100644 --- a/insecure/go.sum +++ b/insecure/go.sum @@ -910,8 +910,8 @@ github.com/onflow/flow-nft/lib/go/contracts v1.3.0 h1:DmNop+O0EMyicZvhgdWboFG57x github.com/onflow/flow-nft/lib/go/contracts v1.3.0/go.mod h1:eZ9VMMNfCq0ho6kV25xJn1kXeCfxnkhj3MwF3ed08gY= github.com/onflow/flow-nft/lib/go/templates v1.3.0 h1:uGIBy4GEY6Z9hKP7sm5nA5kwvbvLWW4nWx5NN9Wg0II= github.com/onflow/flow-nft/lib/go/templates v1.3.0/go.mod h1:gVbb5fElaOwKhV5UEUjM+JQTjlsguHg2jwRupfM/nng= -github.com/onflow/flow/protobuf/go/flow v0.4.16 h1:UADQeq/mpuqFk+EkwqDNoF70743raWQKmB/Dm/eKt2Q= -github.com/onflow/flow/protobuf/go/flow v0.4.16/go.mod h1:NA2pX2nw8zuaxfKphhKsk00kWLwfd+tv8mS23YXO4Sk= +github.com/onflow/flow/protobuf/go/flow v0.4.18 h1:KOujA6lg9kTXCV6oK0eErD1rwRnM9taKZss3Szi+T3Q= +github.com/onflow/flow/protobuf/go/flow v0.4.18/go.mod h1:NA2pX2nw8zuaxfKphhKsk00kWLwfd+tv8mS23YXO4Sk= github.com/onflow/go-ds-pebble v0.0.0-20251003225212-131edca3a897 h1:ZtFYJ3OSR00aiKMMxgm3fRYWqYzjvDXeoBGQm6yC8DE= github.com/onflow/go-ds-pebble v0.0.0-20251003225212-131edca3a897/go.mod h1:aiCRVcj3K60sxc6k5C+HO9C6rouqiSkjR/WKnbTcMfQ= github.com/onflow/go-ethereum v1.16.2 h1:yhC3DA5PTNmUmu7ziq8GmWyQ23KNjle4jCabxpKYyNk= diff --git a/integration/go.mod b/integration/go.mod index 6c2fbfb576c..a8da8c78686 100644 --- a/integration/go.mod +++ b/integration/go.mod @@ -27,7 +27,7 @@ require ( github.com/onflow/flow-go v0.38.0-preview.0.0.20241021221952-af9cd6e99de1 github.com/onflow/flow-go-sdk v1.9.2 github.com/onflow/flow-go/insecure v0.0.0-00010101000000-000000000000 - github.com/onflow/flow/protobuf/go/flow v0.4.16 + github.com/onflow/flow/protobuf/go/flow v0.4.18 github.com/prometheus/client_golang v1.20.5 github.com/prometheus/client_model v0.6.1 github.com/prometheus/common v0.61.0 diff --git a/integration/go.sum b/integration/go.sum index 51bb45cf622..c82f1c357ae 100644 --- a/integration/go.sum +++ b/integration/go.sum @@ -782,8 +782,8 @@ github.com/onflow/flow-nft/lib/go/contracts v1.3.0 h1:DmNop+O0EMyicZvhgdWboFG57x github.com/onflow/flow-nft/lib/go/contracts v1.3.0/go.mod h1:eZ9VMMNfCq0ho6kV25xJn1kXeCfxnkhj3MwF3ed08gY= github.com/onflow/flow-nft/lib/go/templates v1.3.0 h1:uGIBy4GEY6Z9hKP7sm5nA5kwvbvLWW4nWx5NN9Wg0II= github.com/onflow/flow-nft/lib/go/templates v1.3.0/go.mod h1:gVbb5fElaOwKhV5UEUjM+JQTjlsguHg2jwRupfM/nng= -github.com/onflow/flow/protobuf/go/flow v0.4.16 h1:UADQeq/mpuqFk+EkwqDNoF70743raWQKmB/Dm/eKt2Q= -github.com/onflow/flow/protobuf/go/flow v0.4.16/go.mod h1:NA2pX2nw8zuaxfKphhKsk00kWLwfd+tv8mS23YXO4Sk= +github.com/onflow/flow/protobuf/go/flow v0.4.18 h1:KOujA6lg9kTXCV6oK0eErD1rwRnM9taKZss3Szi+T3Q= +github.com/onflow/flow/protobuf/go/flow v0.4.18/go.mod h1:NA2pX2nw8zuaxfKphhKsk00kWLwfd+tv8mS23YXO4Sk= github.com/onflow/go-ds-pebble v0.0.0-20251003225212-131edca3a897 h1:ZtFYJ3OSR00aiKMMxgm3fRYWqYzjvDXeoBGQm6yC8DE= github.com/onflow/go-ds-pebble v0.0.0-20251003225212-131edca3a897/go.mod h1:aiCRVcj3K60sxc6k5C+HO9C6rouqiSkjR/WKnbTcMfQ= github.com/onflow/go-ethereum v1.16.2 h1:yhC3DA5PTNmUmu7ziq8GmWyQ23KNjle4jCabxpKYyNk= diff --git a/integration/tests/access/cohort1/access_api_test.go b/integration/tests/access/cohort1/access_api_test.go index 933c4bb4dd4..9938a958357 100644 --- a/integration/tests/access/cohort1/access_api_test.go +++ b/integration/tests/access/cohort1/access_api_test.go @@ -7,33 +7,35 @@ import ( "testing" "time" - "github.com/onflow/flow-go-sdk/templates" - "github.com/onflow/flow-go-sdk/test" - - accessproto "github.com/onflow/flow/protobuf/go/flow/access" - "github.com/onflow/flow/protobuf/go/flow/entities" - "google.golang.org/grpc" - "google.golang.org/grpc/credentials/insecure" - - "github.com/onflow/flow-go/engine/access/rpc/backend/query_mode" - "github.com/onflow/flow-go/integration/tests/mvp" - "github.com/onflow/flow-go/utils/dsl" - "github.com/rs/zerolog" + "github.com/stretchr/testify/require" "github.com/stretchr/testify/suite" + "google.golang.org/grpc" "google.golang.org/grpc/codes" + "google.golang.org/grpc/credentials/insecure" "google.golang.org/grpc/status" "github.com/onflow/cadence" - sdk "github.com/onflow/flow-go-sdk" client "github.com/onflow/flow-go-sdk/access/grpc" + "github.com/onflow/flow-go-sdk/templates" + "github.com/onflow/flow-go-sdk/test" + accessproto "github.com/onflow/flow/protobuf/go/flow/access" + "github.com/onflow/flow/protobuf/go/flow/entities" + "github.com/onflow/flow-go/engine/access/rpc/backend/query_mode" + "github.com/onflow/flow-go/engine/common/rpc/convert" + "github.com/onflow/flow-go/fvm/blueprints" + "github.com/onflow/flow-go/fvm/systemcontracts" "github.com/onflow/flow-go/integration/testnet" "github.com/onflow/flow-go/integration/tests/lib" + "github.com/onflow/flow-go/integration/tests/mvp" "github.com/onflow/flow-go/integration/utils" + accessmodel "github.com/onflow/flow-go/model/access" "github.com/onflow/flow-go/model/flow" + "github.com/onflow/flow-go/utils/dsl" "github.com/onflow/flow-go/utils/unittest" + "github.com/onflow/flow-go/utils/unittest/fixtures" ) // This is a collection of tests that validate various Access API endpoints work as expected. @@ -1157,3 +1159,176 @@ func (s *AccessAPISuite) TestRejectedInvalidSignatureFormat() { }) } } + +// TestScheduledTransactions tests that scheduled transactions are properly indexed and can be +// retrieved through the Access API. +func (s *AccessAPISuite) TestScheduledTransactions() { + sc := systemcontracts.SystemContractsForChain(s.net.Root().HeaderBody.ChainID) + + accessClient, err := s.net.ContainerByName("access_2").TestnetClient() + s.Require().NoError(err) + rpcClient := s.an2Client.RPCClient() + + // Deploy the test contract first + txID, err := lib.DeployScheduledCallbackTestContract(accessClient, sc) + require.NoError(s.T(), err, "could not deploy test contract") + + // wait for the tx to be sealed before attempting to schedule the callback. this helps make sure + // the proposer's sequence number is updated. + _, err = accessClient.WaitForSealed(s.ctx, txID) + s.Require().NoError(err) + + // Schedule a callback for 10 seconds in the future. Use a larger wait time to ensure that there + // is enough time to submit the tx even on slower CI machines. + futureTimestamp := time.Now().Unix() + int64(10) + + s.T().Logf("scheduling callback at timestamp: %v, current timestamp: %v", futureTimestamp, time.Now().Unix()) + callbackID, err := lib.ScheduleCallbackAtTimestamp(futureTimestamp, accessClient, sc) + require.NoError(s.T(), err, "could not schedule callback transaction") + s.T().Logf("scheduled callback with ID: %d", callbackID) + + // construct the pending execution event using the parameters used by ScheduleCallbackAtTimestamp + g := fixtures.NewGeneratorSuite() + expectedPendingExecutionEvent := g.PendingExecutionEvents().Fixture( + fixtures.PendingExecutionEvent.WithID(callbackID), + fixtures.PendingExecutionEvent.WithPriority(0), // high priority + fixtures.PendingExecutionEvent.WithExecutionEffort(1000), + ) + + // construct the expected scheduled transaction body to compare to the API responses + scheduledTxs, err := blueprints.ExecuteCallbacksTransactions(s.net.Root().ChainID.Chain(), []flow.Event{expectedPendingExecutionEvent}) + require.NoError(s.T(), err, "could not execute callback transaction") + expectedTxID := scheduledTxs[0].ID() + + // Block until the API returns the scheduled transaction. + require.Eventually(s.T(), func() bool { + _, err := rpcClient.GetScheduledTransaction(s.ctx, &accessproto.GetScheduledTransactionRequest{Id: callbackID}) + return err == nil + }, 30*time.Second, 500*time.Millisecond) + + // Verify the results of the scheduled transaction and its result. + s.Run("GetScheduledTransaction", func() { + scheduledTxResponse, err := rpcClient.GetScheduledTransaction(s.ctx, &accessproto.GetScheduledTransactionRequest{Id: callbackID}) + s.Require().NoError(err) + + actual, err := convert.MessageToTransaction(scheduledTxResponse.GetTransaction(), s.net.Root().ChainID.Chain()) + s.Require().NoError(err) + s.Require().Equal(expectedTxID, actual.ID()) + }) + + var scheduledTxResult *accessmodel.TransactionResult + s.Run("GetScheduledTransactionResult", func() { + scheduledTxResultResponse, err := rpcClient.GetScheduledTransactionResult(s.ctx, &accessproto.GetScheduledTransactionResultRequest{Id: callbackID}) + s.Require().NoError(err) + + actual, err := convert.MessageToTransactionResult(scheduledTxResultResponse) + s.Require().NoError(err) + + s.Greater(actual.BlockHeight, uint64(0)) // make block height is set + s.NotEqual(flow.ZeroID, flow.Identifier(actual.BlockID)) // make sure block id is set + s.Equal(expectedTxID, actual.TransactionID) + s.Equal(flow.TransactionStatusSealed, actual.Status) + s.Equal(uint(0), actual.StatusCode) + s.Empty(actual.ErrorMessage) + + scheduledTxResult = actual + }) + + s.Run("GetTransaction", func() { + txReponse, err := rpcClient.GetTransaction(s.ctx, &accessproto.GetTransactionRequest{Id: expectedTxID[:]}) + s.Require().NoError(err) + + actualTx, err := convert.MessageToTransaction(txReponse.GetTransaction(), s.net.Root().ChainID.Chain()) + s.Require().NoError(err) + s.Equal(expectedTxID, actualTx.ID()) + }) + + blockID := scheduledTxResult.BlockID + s.Run("GetTransactionResult", func() { + txResultResponse, err := rpcClient.GetTransactionResult(s.ctx, &accessproto.GetTransactionRequest{Id: expectedTxID[:], BlockId: blockID[:]}) + s.Require().NoError(err) + + actualTxResult, err := convert.MessageToTransactionResult(txResultResponse) + s.Require().NoError(err) + s.Equal(scheduledTxResult, actualTxResult) + }) +} + +// TestSystemTransactions tests getting a system transaction using each of the supported endpoints. +func (s *AccessAPISuite) TestSystemTransactions() { + rpcClient := s.an2Client.RPCClient() + + // block until a few blocks have executed to ensure there are blocks with system transactions + var blockID flow.Identifier + require.Eventually(s.T(), func() bool { + header, err := rpcClient.GetBlockHeaderByHeight(s.ctx, &accessproto.GetBlockHeaderByHeightRequest{Height: 5}) + if err == nil { + blockID = convert.MessageToIdentifier(header.GetBlock().GetId()) + return true + } + return false + }, 30*time.Second, 500*time.Millisecond) + + // construct the expected system collection + systemCollection, err := blueprints.SystemCollection(s.net.Root().ChainID.Chain(), nil) + s.Require().NoError(err) + s.Require().Len(systemCollection.Transactions, 2) + + systemTxs := make([]flow.Identifier, len(systemCollection.Transactions)) + for i, tx := range systemCollection.Transactions { + systemTxs[i] = tx.ID() + } + + // query the system transactions using each of the supported endpoints and verify the results are correct + s.Run("GetTransaction", func() { + for _, txID := range systemTxs { + txReponse, err := rpcClient.GetTransaction(s.ctx, &accessproto.GetTransactionRequest{Id: txID[:]}) + s.Require().NoError(err) + + actualTx, err := convert.MessageToTransaction(txReponse.GetTransaction(), s.net.Root().ChainID.Chain()) + s.Require().NoError(err) + s.Equal(txID, actualTx.ID()) + } + }) + + s.Run("GetSystemTransaction", func() { + for _, txID := range systemTxs { + systemTxResponse, err := rpcClient.GetSystemTransaction(s.ctx, &accessproto.GetSystemTransactionRequest{Id: txID[:], BlockId: blockID[:]}) + s.Require().NoError(err) + + actualSystemTx, err := convert.MessageToTransaction(systemTxResponse.GetTransaction(), s.net.Root().ChainID.Chain()) + s.Require().NoError(err) + s.Equal(txID, actualSystemTx.ID()) + } + }) + + s.Run("GetTransactionResult", func() { + for _, txID := range systemTxs { + txResultResponse, err := rpcClient.GetTransactionResult(s.ctx, &accessproto.GetTransactionRequest{Id: txID[:], BlockId: blockID[:]}) + s.Require().NoError(err) + + actualTxResult, err := convert.MessageToTransactionResult(txResultResponse) + s.Require().NoError(err) + + s.Equal(txID, actualTxResult.TransactionID) + s.Equal(blockID, actualTxResult.BlockID) + s.Equal(uint(0), actualTxResult.StatusCode) + s.Empty(actualTxResult.ErrorMessage) + } + }) + + s.Run("GetSystemTransactionResult", func() { + for _, txID := range systemTxs { + systemTxResultResponse, err := rpcClient.GetSystemTransactionResult(s.ctx, &accessproto.GetSystemTransactionResultRequest{Id: txID[:], BlockId: blockID[:]}) + s.Require().NoError(err) + + actualSystemTxResult, err := convert.MessageToTransactionResult(systemTxResultResponse) + s.Require().NoError(err) + + s.Equal(txID, actualSystemTxResult.TransactionID) + s.Equal(blockID, actualSystemTxResult.BlockID) + s.Equal(uint(0), actualSystemTxResult.StatusCode) + s.Empty(actualSystemTxResult.ErrorMessage) + } + }) +} diff --git a/integration/tests/execution/scheduled_callbacks_test.go b/integration/tests/execution/scheduled_callbacks_test.go index adddf92040d..efc47107027 100644 --- a/integration/tests/execution/scheduled_callbacks_test.go +++ b/integration/tests/execution/scheduled_callbacks_test.go @@ -7,7 +7,6 @@ import ( "time" "github.com/onflow/cadence" - sdk "github.com/onflow/flow-go-sdk" "github.com/stretchr/testify/require" "github.com/stretchr/testify/suite" @@ -51,13 +50,7 @@ func (s *ScheduledCallbacksSuite) TestScheduleCallback_ScheduledAndExecuted() { s.T().Logf("got blockA height %v ID %v", blockA.HeaderBody.Height, blockA.ID()) // Deploy the test contract first - err := lib.DeployScheduledCallbackTestContract( - s.AccessClient(), - sdk.Address(sc.FlowCallbackScheduler.Address), - sdk.Address(sc.FlowToken.Address), - sdk.Address(sc.FungibleToken.Address), - sdk.Identifier(s.net.Root().ID()), - ) + _, err := lib.DeployScheduledCallbackTestContract(s.AccessClient(), sc) require.NoError(s.T(), err, "could not deploy test contract") // Wait for next height finalized before scheduling callback @@ -68,13 +61,7 @@ func (s *ScheduledCallbacksSuite) TestScheduleCallback_ScheduledAndExecuted() { futureTimestamp := time.Now().Unix() + scheduleDelta s.T().Logf("scheduling callback at timestamp: %v, current timestamp: %v", futureTimestamp, time.Now().Unix()) - callbackID, err := lib.ScheduleCallbackAtTimestamp( - futureTimestamp, - s.AccessClient(), - sdk.Address(sc.FlowCallbackScheduler.Address), - sdk.Address(sc.FlowToken.Address), - sdk.Address(sc.FungibleToken.Address), - ) + callbackID, err := lib.ScheduleCallbackAtTimestamp(futureTimestamp, s.AccessClient(), sc) require.NoError(s.T(), err, "could not schedule callback transaction") s.T().Logf("scheduled callback with ID: %d", callbackID) @@ -121,13 +108,7 @@ func (s *ScheduledCallbacksSuite) TestScheduleCallback_ScheduleAndCancelCallback s.T().Logf("got blockA height %v ID %v", blockA.HeaderBody.Height, blockA.ID()) // Deploy the test contract first - err := lib.DeployScheduledCallbackTestContract( - s.AccessClient(), - sdk.Address(sc.FlowCallbackScheduler.Address), - sdk.Address(sc.FlowToken.Address), - sdk.Address(sc.FungibleToken.Address), - sdk.Identifier(s.net.Root().ID()), - ) + _, err := lib.DeployScheduledCallbackTestContract(s.AccessClient(), sc) require.NoError(s.T(), err, "could not deploy test contract") // Wait for next height finalized before scheduling callback @@ -138,13 +119,7 @@ func (s *ScheduledCallbacksSuite) TestScheduleCallback_ScheduleAndCancelCallback futureTimestamp := time.Now().Unix() + scheduleDelta s.T().Logf("scheduling callback at timestamp: %v, current timestamp: %v", futureTimestamp, time.Now().Unix()) - callbackID, err := lib.ScheduleCallbackAtTimestamp( - futureTimestamp, - s.AccessClient(), - sdk.Address(sc.FlowCallbackScheduler.Address), - sdk.Address(sc.FlowToken.Address), - sdk.Address(sc.FungibleToken.Address), - ) + callbackID, err := lib.ScheduleCallbackAtTimestamp(futureTimestamp, s.AccessClient(), sc) require.NoError(s.T(), err, "could not schedule callback transaction") s.T().Logf("scheduled callback with ID: %d", callbackID) @@ -166,13 +141,7 @@ func (s *ScheduledCallbacksSuite) TestScheduleCallback_ScheduleAndCancelCallback require.NotContains(s.T(), executedCallbacks, callbackID, "callback should not be executed immediately") // Cancel the callback - canceledID, err := lib.CancelCallbackByID( - callbackID, - s.AccessClient(), - sdk.Address(sc.FlowCallbackScheduler.Address), - sdk.Address(sc.FlowToken.Address), - sdk.Address(sc.FungibleToken.Address), - ) + canceledID, err := lib.CancelCallbackByID(callbackID, s.AccessClient(), sc) require.NoError(s.T(), err, "could not cancel callback transaction") require.Equal(s.T(), callbackID, canceledID, "canceled callback ID should be the same as scheduled") diff --git a/integration/tests/lib/util.go b/integration/tests/lib/util.go index 1e8c521771d..c9f050fcd8c 100644 --- a/integration/tests/lib/util.go +++ b/integration/tests/lib/util.go @@ -15,6 +15,7 @@ import ( sdk "github.com/onflow/flow-go-sdk" sdkcrypto "github.com/onflow/flow-go-sdk/crypto" + "github.com/onflow/flow-go/fvm/systemcontracts" "github.com/onflow/flow-go/model/flow" "github.com/onflow/flow-go/utils/dsl" "github.com/onflow/flow-go/utils/unittest" @@ -348,20 +349,18 @@ func LogStatusPeriodically(t *testing.T, parent context.Context, log zerolog.Log func ScheduleCallbackAtTimestamp( timestamp int64, client *testnet.Client, - flowCallbackScheduler sdk.Address, - flowToken sdk.Address, - fungibleToken sdk.Address, + sc *systemcontracts.SystemContracts, ) (uint64, error) { - header, err := client.GetLatestFinalizedBlockHeader(context.Background()) + referenceBlock, err := client.GetLatestFinalizedBlockHeader(context.Background()) if err != nil { return 0, fmt.Errorf("could not get latest block ID: %w", err) } - serviceAccount, err := client.GetAccountAtBlockHeight(context.Background(), client.SDKServiceAddress(), header.Height) - if err != nil { - return 0, fmt.Errorf("could not get account: %w", err) - } + flowCallbackScheduler := sdk.Address(sc.FlowCallbackScheduler.Address) + flowToken := sdk.Address(sc.FlowToken.Address) + fungibleToken := sdk.Address(sc.FungibleToken.Address) + serviceAccountAddress := client.SDKServiceAddress() script := []byte(fmt.Sprintf(` import FlowTransactionScheduler from 0x%s import TestFlowCallbackHandler from 0x%s @@ -404,7 +403,7 @@ func ScheduleCallbackAtTimestamp( TestFlowCallbackHandler.addScheduledCallback(callback: <-scheduledCallback) } } - `, serviceAccount.Address.Hex(), flowCallbackScheduler.Hex(), flowToken.Hex(), fungibleToken.Hex())) + `, serviceAccountAddress.Hex(), flowCallbackScheduler.Hex(), flowToken.Hex(), fungibleToken.Hex())) timeArg, err := cadence.NewUFix64(fmt.Sprintf("%d.0", timestamp)) if err != nil { @@ -413,10 +412,10 @@ func ScheduleCallbackAtTimestamp( tx := sdk.NewTransaction(). SetScript(script). - SetReferenceBlockID(header.ID). - SetProposalKey(serviceAccount.Address, serviceAccount.Keys[0].Index, serviceAccount.Keys[0].SequenceNumber). - SetPayer(serviceAccount.Address). - AddAuthorizer(serviceAccount.Address) + SetReferenceBlockID(referenceBlock.ID). + SetProposalKey(serviceAccountAddress, 0, client.GetAndIncrementSeqNumber()). + SetPayer(serviceAccountAddress). + AddAuthorizer(serviceAccountAddress) err = tx.AddArgument(timeArg) if err != nil { @@ -430,21 +429,18 @@ func ScheduleCallbackAtTimestamp( func CancelCallbackByID( callbackID uint64, client *testnet.Client, - flowCallbackScheduler sdk.Address, - flowToken sdk.Address, - fungibleToken sdk.Address, + sc *systemcontracts.SystemContracts, ) (uint64, error) { - - header, err := client.GetLatestFinalizedBlockHeader(context.Background()) + referenceBlock, err := client.GetLatestFinalizedBlockHeader(context.Background()) if err != nil { return 0, fmt.Errorf("could not get latest block ID: %w", err) } - serviceAccount, err := client.GetAccountAtBlockHeight(context.Background(), client.SDKServiceAddress(), header.Height) - if err != nil { - return 0, fmt.Errorf("could not get account: %w", err) - } + flowCallbackScheduler := sdk.Address(sc.FlowCallbackScheduler.Address) + flowToken := sdk.Address(sc.FlowToken.Address) + fungibleToken := sdk.Address(sc.FungibleToken.Address) + serviceAccountAddress := client.SDKServiceAddress() cancelTx := fmt.Sprintf(` import FlowTransactionScheduler from 0x%s import TestFlowCallbackHandler from 0x%s @@ -461,14 +457,14 @@ func CancelCallbackByID( vault.deposit(from: <-TestFlowCallbackHandler.cancelCallback(id: id)) } } - `, serviceAccount.Address.Hex(), flowCallbackScheduler.Hex(), flowToken.Hex(), fungibleToken.Hex()) + `, serviceAccountAddress.Hex(), flowCallbackScheduler.Hex(), flowToken.Hex(), fungibleToken.Hex()) tx := sdk.NewTransaction(). SetScript([]byte(cancelTx)). - SetReferenceBlockID(header.ID). - SetProposalKey(serviceAccount.Address, serviceAccount.Keys[0].Index, serviceAccount.Keys[0].SequenceNumber). - SetPayer(serviceAccount.Address). - AddAuthorizer(serviceAccount.Address) + SetReferenceBlockID(referenceBlock.ID). + SetProposalKey(serviceAccountAddress, 0, client.GetAndIncrementSeqNumber()). + SetPayer(serviceAccountAddress). + AddAuthorizer(serviceAccountAddress) err = tx.AddArgument(cadence.UInt64(callbackID)) if err != nil { @@ -498,27 +494,33 @@ func ExtractCallbackIDFromEvents(result *sdk.TransactionResult) uint64 { // DeployScheduledCallbackTestContract deploys the test contract for scheduled callbacks. func DeployScheduledCallbackTestContract( client *testnet.Client, - callbackScheduler sdk.Address, - flowToken sdk.Address, - fungibleToken sdk.Address, - refID sdk.Identifier, -) error { - testContract := TestFlowCallbackHandlerContract(callbackScheduler, flowToken, fungibleToken) - tx, err := client.DeployContract(context.Background(), refID, testContract) + sc *systemcontracts.SystemContracts, +) (sdk.Identifier, error) { + referenceBlock, err := client.GetLatestFinalizedBlockHeader(context.Background()) + if err != nil { + return sdk.Identifier{}, fmt.Errorf("could not get latest block ID: %w", err) + } + + flowCallbackScheduler := sdk.Address(sc.FlowCallbackScheduler.Address) + flowToken := sdk.Address(sc.FlowToken.Address) + fungibleToken := sdk.Address(sc.FungibleToken.Address) + + testContract := TestFlowCallbackHandlerContract(flowCallbackScheduler, flowToken, fungibleToken) + tx, err := client.DeployContract(context.Background(), referenceBlock.ID, testContract) if err != nil { - return fmt.Errorf("could not deploy test contract: %w", err) + return sdk.Identifier{}, fmt.Errorf("could not deploy test contract: %w", err) } res, err := client.WaitForExecuted(context.Background(), tx.ID()) if err != nil { - return fmt.Errorf("could not wait for deploy transaction to be sealed: %w", err) + return sdk.Identifier{}, fmt.Errorf("could not wait for deploy transaction to be sealed: %w", err) } if res.Error != nil { - return fmt.Errorf("deploy transaction should not have error: %w", res.Error) + return sdk.Identifier{}, fmt.Errorf("deploy transaction should not have error: %w", res.Error) } - return nil + return tx.ID(), nil } func sendCallbackTx(client *testnet.Client, tx *sdk.Transaction) (uint64, error) { diff --git a/integration/tests/verification/suite.go b/integration/tests/verification/suite.go index f4a8b7d101c..e92272b7f21 100644 --- a/integration/tests/verification/suite.go +++ b/integration/tests/verification/suite.go @@ -23,13 +23,14 @@ type Suite struct { lib.TestnetStateTracker // used to track messages over testnet cancel context.CancelFunc // used to tear down the testnet net *testnet.FlowNetwork // used to keep an instance of testnet - nodeConfigs []testnet.NodeConfig // used to keep configuration of nodes in testnet nodeIDs []flow.Identifier // used to keep identifier of nodes in testnet ghostID flow.Identifier // represents id of ghost node exe1ID flow.Identifier exe2ID flow.Identifier verID flow.Identifier // represents id of verification node PreferredUnicasts string // preferred unicast protocols between execution and verification nodes. + + accessClient *testnet.Client } // Ghost returns a client to interact with the Ghost node on testnet. @@ -41,9 +42,12 @@ func (s *Suite) Ghost() *client.GhostClient { // AccessClient returns a client to interact with the access node api on testnet. func (s *Suite) AccessClient() *testnet.Client { - client, err := s.net.ContainerByName(testnet.PrimaryAN).TestnetClient() - require.NoError(s.T(), err, "could not get access client") - return client + if s.accessClient == nil { // cache access client + client, err := s.net.ContainerByName(testnet.PrimaryAN).TestnetClient() + require.NoError(s.T(), err, "could not get access client") + s.accessClient = client + } + return s.accessClient } // AccessPort returns the port number of access node api on testnet. @@ -65,7 +69,8 @@ func (s *Suite) SetupSuite() { s.log = unittest.LoggerForTest(s.Suite.T(), zerolog.InfoLevel) s.log.Info().Msg("================> SetupTest") - s.nodeConfigs = append(s.nodeConfigs, testnet.NewNodeConfig(flow.RoleAccess, testnet.WithLogLevel(zerolog.FatalLevel))) + var nodeConfigs []testnet.NodeConfig + nodeConfigs = append(nodeConfigs, testnet.NewNodeConfig(flow.RoleAccess, testnet.WithLogLevel(zerolog.FatalLevel))) // generate the four consensus identities s.nodeIDs = unittest.IdentifierListFixture(4) @@ -77,7 +82,7 @@ func (s *Suite) SetupSuite() { testnet.WithAdditionalFlag("--required-construction-seal-approvals=1"), testnet.WithAdditionalFlag("cruise-ctl-fallback-proposal-duration=1ms"), ) - s.nodeConfigs = append(s.nodeConfigs, nodeConfig) + nodeConfigs = append(nodeConfigs, nodeConfig) } // generates one verification node @@ -89,7 +94,7 @@ func (s *Suite) SetupSuite() { testnet.WithAdditionalFlag(fmt.Sprintf("--preferred-unicast-protocols=%s", s.PreferredUnicasts)), testnet.WithAdditionalFlag("--scheduled-callbacks-enabled=true"), ) - s.nodeConfigs = append(s.nodeConfigs, verConfig) + nodeConfigs = append(nodeConfigs, verConfig) // generates two execution nodes s.exe1ID = unittest.IdentifierFixture() @@ -100,7 +105,7 @@ func (s *Suite) SetupSuite() { testnet.WithAdditionalFlag(fmt.Sprintf("--preferred-unicast-protocols=%s", s.PreferredUnicasts)), testnet.WithAdditionalFlag("--scheduled-callbacks-enabled=true"), ) - s.nodeConfigs = append(s.nodeConfigs, exe1Config) + nodeConfigs = append(nodeConfigs, exe1Config) s.exe2ID = unittest.IdentifierFixture() exe2Config := testnet.NewNodeConfig(flow.RoleExecution, @@ -110,7 +115,7 @@ func (s *Suite) SetupSuite() { testnet.WithAdditionalFlag(fmt.Sprintf("--preferred-unicast-protocols=%s", s.PreferredUnicasts)), testnet.WithAdditionalFlag("--scheduled-callbacks-enabled=true"), ) - s.nodeConfigs = append(s.nodeConfigs, exe2Config) + nodeConfigs = append(nodeConfigs, exe2Config) // generates two collection node coll1Config := testnet.NewNodeConfig(flow.RoleCollection, @@ -121,7 +126,7 @@ func (s *Suite) SetupSuite() { testnet.WithLogLevel(zerolog.FatalLevel), testnet.WithAdditionalFlag("--hotstuff-proposal-duration=1ms"), ) - s.nodeConfigs = append(s.nodeConfigs, coll1Config, coll2Config) + nodeConfigs = append(nodeConfigs, coll1Config, coll2Config) // Ghost Node // the ghost node's objective is to observe the messages exchanged on the @@ -133,12 +138,12 @@ func (s *Suite) SetupSuite() { testnet.AsGhost(), testnet.WithLogLevel(zerolog.FatalLevel), ) - s.nodeConfigs = append(s.nodeConfigs, ghostConfig) + nodeConfigs = append(nodeConfigs, ghostConfig) // generates, initializes, and starts the Flow network netConfig := testnet.NewNetworkConfig( "verification_tests", - s.nodeConfigs, + nodeConfigs, // set long staking phase to avoid QC/DKG transactions during test run testnet.WithViewsInStakingAuction(10_000), testnet.WithViewsInEpoch(100_000), diff --git a/integration/tests/verification/verify_scheduled_callback_test.go b/integration/tests/verification/verify_scheduled_callback_test.go index 3030948d894..23c7abde025 100644 --- a/integration/tests/verification/verify_scheduled_callback_test.go +++ b/integration/tests/verification/verify_scheduled_callback_test.go @@ -9,8 +9,6 @@ import ( "github.com/stretchr/testify/require" "github.com/stretchr/testify/suite" - sdk "github.com/onflow/flow-go-sdk" - "github.com/onflow/flow-go/fvm/systemcontracts" "github.com/onflow/flow-go/integration/tests/lib" "github.com/onflow/flow-go/model/flow" @@ -33,13 +31,7 @@ func (s *VerifyScheduledCallbackSuite) TestVerifyScheduledCallback() { s.T().Logf("got blockA height %v ID %v", blockA.HeaderBody.Height, blockA.ID()) // Deploy the test contract first - err := lib.DeployScheduledCallbackTestContract( - s.AccessClient(), - sdk.Address(sc.FlowCallbackScheduler.Address), - sdk.Address(sc.FlowToken.Address), - sdk.Address(sc.FungibleToken.Address), - sdk.Identifier(s.net.Root().ID()), - ) + _, err := lib.DeployScheduledCallbackTestContract(s.AccessClient(), sc) require.NoError(s.T(), err, "could not deploy test contract") // Wait for next height finalized before scheduling callback @@ -50,13 +42,7 @@ func (s *VerifyScheduledCallbackSuite) TestVerifyScheduledCallback() { futureTimestamp := time.Now().Unix() + scheduleDelta s.T().Logf("scheduling callback at timestamp: %v, current timestamp: %v", futureTimestamp, time.Now().Unix()) - callbackID, err := lib.ScheduleCallbackAtTimestamp( - futureTimestamp, - s.AccessClient(), - sdk.Address(sc.FlowCallbackScheduler.Address), - sdk.Address(sc.FlowToken.Address), - sdk.Address(sc.FungibleToken.Address), - ) + callbackID, err := lib.ScheduleCallbackAtTimestamp(futureTimestamp, s.AccessClient(), sc) require.NoError(s.T(), err, "could not schedule callback transaction") s.T().Logf("scheduled callback with ID: %d", callbackID) diff --git a/module/executiondatasync/testutil/fixtures.go b/module/executiondatasync/testutil/fixtures.go index 37a22e1b7a4..b6792566854 100644 --- a/module/executiondatasync/testutil/fixtures.go +++ b/module/executiondatasync/testutil/fixtures.go @@ -108,7 +108,7 @@ func (tf *TestFixture) accumulateRegisterEntries( // - Every 3rd transaction is failed // - There are tx error messages for all failed transactions // - There is an execution result for the block, whose ExecutionDataID matches the BlockExecutionData -func CompleteFixture(t *testing.T, g *fixtures.GeneratorSuite, parentHeader *flow.Header) *TestFixture { +func CompleteFixture(t *testing.T, g *fixtures.GeneratorSuite, parentBlock *flow.Block) *TestFixture { collectionCount := 4 chunkExecutionDatas := make([]*execution_data.ChunkExecutionData, 0, collectionCount+1) @@ -122,6 +122,7 @@ func CompleteFixture(t *testing.T, g *fixtures.GeneratorSuite, parentHeader *flo for i, collection := range collections { chunkData := g.ChunkExecutionDatas().Fixture( fixtures.ChunkExecutionData.WithCollection(collection), + fixtures.ChunkExecutionData.WithStartTxIndex(uint32(txCount)), ) // use the same path for the first ledger payload in each chunk. the indexer should chose the // last value in the register entry. @@ -165,9 +166,15 @@ func CompleteFixture(t *testing.T, g *fixtures.GeneratorSuite, parentHeader *flo chunkExecutionDatas = append(chunkExecutionDatas, systemChunk) // generate the block containing guarantees for the user collections - payload := g.Payloads().Fixture(fixtures.Payload.WithGuarantees(guarantees...)) + payload := g.Payloads().Fixture( + fixtures.Payload.WithProtocolStateID(parentBlock.Payload.ProtocolStateID), + fixtures.Payload.WithGuarantees(guarantees...), + fixtures.Payload.WithReceiptStubs(), + fixtures.Payload.WithResults(), + fixtures.Payload.WithSeals(), + ) block := g.Blocks().Fixture( - fixtures.Block.WithParentHeader(parentHeader), + fixtures.Block.WithParentHeader(parentBlock.ToHeader()), fixtures.Block.WithPayload(payload), ) diff --git a/module/state_synchronization/indexer/indexer_core_test.go b/module/state_synchronization/indexer/indexer_core_test.go index 24ae7424c48..b302d86ac9e 100644 --- a/module/state_synchronization/indexer/indexer_core_test.go +++ b/module/state_synchronization/indexer/indexer_core_test.go @@ -249,7 +249,7 @@ func TestExecutionState_IndexBlockData(t *testing.T) { g := fixtures.NewGeneratorSuite() blocks := g.Blocks().List(4) - tf := testutil.CompleteFixture(t, g, blocks[len(blocks)-1].ToHeader()) + tf := testutil.CompleteFixture(t, g, blocks[len(blocks)-1]) blockID := tf.Block.ID() blocks = append(blocks, tf.Block) @@ -538,7 +538,7 @@ func TestIndexerIntegration_StoreAndGet(t *testing.T) { func TestCollectScheduledTransactions(t *testing.T) { g := fixtures.NewGeneratorSuite() blocks := g.Blocks().List(5) - tf := testutil.CompleteFixture(t, g, blocks[len(blocks)-1].ToHeader()) + tf := testutil.CompleteFixture(t, g, blocks[len(blocks)-1]) chainID := g.ChainID() fvmEnv := systemcontracts.SystemContractsForChain(chainID).AsTemplateEnv() diff --git a/storage/operation/execution_fork_evidence_test.go b/storage/operation/execution_fork_evidence_test.go index 5d0b91d8e14..716428f905d 100644 --- a/storage/operation/execution_fork_evidence_test.go +++ b/storage/operation/execution_fork_evidence_test.go @@ -4,14 +4,13 @@ import ( "testing" "github.com/jordanschalm/lockctx" + "github.com/stretchr/testify/require" "github.com/onflow/flow-go/model/flow" "github.com/onflow/flow-go/storage" "github.com/onflow/flow-go/storage/operation" "github.com/onflow/flow-go/storage/operation/dbtest" "github.com/onflow/flow-go/utils/unittest" - - "github.com/stretchr/testify/require" ) func Test_ExecutionForkEvidenceOperations(t *testing.T) { diff --git a/utils/unittest/fixtures/chunk_execution_data.go b/utils/unittest/fixtures/chunk_execution_data.go index e628a17a482..ad7a6f27a72 100644 --- a/utils/unittest/fixtures/chunk_execution_data.go +++ b/utils/unittest/fixtures/chunk_execution_data.go @@ -13,45 +13,58 @@ var ChunkExecutionData chunkExecutionDataFactory type chunkExecutionDataFactory struct{} -type ChunkExecutionDataOption func(*ChunkExecutionDataGenerator, *execution_data.ChunkExecutionData) +type ChunkExecutionDataOption func(*ChunkExecutionDataGenerator, *chunkExecutionDataConfig) + +type chunkExecutionDataConfig struct { + startTxIndex uint32 + ced *execution_data.ChunkExecutionData +} // WithCollection is an option that sets the collection for the chunk execution data. func (f chunkExecutionDataFactory) WithCollection(collection *flow.Collection) ChunkExecutionDataOption { - return func(g *ChunkExecutionDataGenerator, ced *execution_data.ChunkExecutionData) { - ced.Collection = collection + return func(g *ChunkExecutionDataGenerator, config *chunkExecutionDataConfig) { + config.ced.Collection = collection } } // WithEvents is an option that sets the events for the chunk execution data. func (f chunkExecutionDataFactory) WithEvents(events flow.EventsList) ChunkExecutionDataOption { - return func(g *ChunkExecutionDataGenerator, ced *execution_data.ChunkExecutionData) { - ced.Events = events + return func(g *ChunkExecutionDataGenerator, config *chunkExecutionDataConfig) { + config.ced.Events = events } } // WithTrieUpdate is an option that sets the trie update for the chunk execution data. func (f chunkExecutionDataFactory) WithTrieUpdate(trieUpdate *ledger.TrieUpdate) ChunkExecutionDataOption { - return func(g *ChunkExecutionDataGenerator, ced *execution_data.ChunkExecutionData) { - ced.TrieUpdate = trieUpdate + return func(g *ChunkExecutionDataGenerator, config *chunkExecutionDataConfig) { + config.ced.TrieUpdate = trieUpdate } } // WithTransactionResults is an option that sets the transaction results for the chunk execution data. func (f chunkExecutionDataFactory) WithTransactionResults(results ...flow.LightTransactionResult) ChunkExecutionDataOption { - return func(g *ChunkExecutionDataGenerator, ced *execution_data.ChunkExecutionData) { - ced.TransactionResults = results + return func(g *ChunkExecutionDataGenerator, config *chunkExecutionDataConfig) { + config.ced.TransactionResults = results } } // WithMinSize is an option that sets the minimum size for the chunk execution data. func (f chunkExecutionDataFactory) WithMinSize(minSize int) ChunkExecutionDataOption { - return func(g *ChunkExecutionDataGenerator, ced *execution_data.ChunkExecutionData) { - if minSize > 0 && ced.TrieUpdate != nil { - g.ensureMinSize(ced, minSize) + return func(g *ChunkExecutionDataGenerator, config *chunkExecutionDataConfig) { + if minSize > 0 && config.ced.TrieUpdate != nil { + g.ensureMinSize(config.ced, minSize) } } } +// WithStartTxIndex is an option that sets the start transaction index for the chunk execution data. +// Use this to ensure the transaction index within events is consistent across chunks. +func (f chunkExecutionDataFactory) WithStartTxIndex(startTxIndex uint32) ChunkExecutionDataOption { + return func(g *ChunkExecutionDataGenerator, config *chunkExecutionDataConfig) { + config.startTxIndex = startTxIndex + } +} + // ChunkExecutionDataGenerator generates chunk execution data with consistent randomness. type ChunkExecutionDataGenerator struct { chunkExecutionDataFactory @@ -81,34 +94,34 @@ func NewChunkExecutionDataGenerator( // Fixture generates a [execution_data.ChunkExecutionData] with random data based on the provided options. func (g *ChunkExecutionDataGenerator) Fixture(opts ...ChunkExecutionDataOption) *execution_data.ChunkExecutionData { - ced := &execution_data.ChunkExecutionData{ - Collection: g.collections.Fixture(Collection.WithTxCount(5)), - TrieUpdate: g.trieUpdates.Fixture(), + config := &chunkExecutionDataConfig{ + ced: &execution_data.ChunkExecutionData{ + Collection: g.collections.Fixture(Collection.WithTxCount(5)), + TrieUpdate: g.trieUpdates.Fixture(), + }, } for _, opt := range opts { - opt(g, ced) + opt(g, config) } - if len(ced.TransactionResults) == 0 { - ced.TransactionResults = make([]flow.LightTransactionResult, len(ced.Collection.Transactions)) - for i, tx := range ced.Collection.Transactions { - ced.TransactionResults[i] = g.lightTxResults.Fixture(LightTransactionResult.WithTransactionID(tx.ID())) + if len(config.ced.TransactionResults) == 0 { + config.ced.TransactionResults = make([]flow.LightTransactionResult, len(config.ced.Collection.Transactions)) + for i, tx := range config.ced.Collection.Transactions { + config.ced.TransactionResults[i] = g.lightTxResults.Fixture(LightTransactionResult.WithTransactionID(tx.ID())) } } - if len(ced.Events) == 0 { - for txIndex, result := range ced.TransactionResults { - events := g.events.List(5, - Event.WithTransactionID(result.TransactionID), - Event.WithTransactionIndex(uint32(txIndex)), - ) - ced.Events = append(ced.Events, events...) + if len(config.ced.Events) == 0 { + for i, result := range config.ced.TransactionResults { + txIndex := config.startTxIndex + uint32(i) + events := g.events.ForTransaction(result.TransactionID, txIndex, 5) + config.ced.Events = append(config.ced.Events, events...) } - ced.Events = AdjustEventsMetadata(ced.Events) + config.ced.Events = AdjustEventsMetadata(config.ced.Events) } - return ced + return config.ced } // List generates a list of [execution_data.ChunkExecutionData]. diff --git a/utils/unittest/fixtures/event.go b/utils/unittest/fixtures/event.go index 37071abf85c..5c64b9e449f 100644 --- a/utils/unittest/fixtures/event.go +++ b/utils/unittest/fixtures/event.go @@ -304,7 +304,7 @@ func AdjustEventsMetadata(events []flow.Event) []flow.Event { } lastTxID := events[0].TransactionID - txIndex := uint32(0) + txIndex := events[0].TransactionIndex eventIndex := uint32(0) output := make([]flow.Event, len(events)) diff --git a/utils/unittest/fixtures/event_test.go b/utils/unittest/fixtures/event_test.go index 05e6810b912..454c575b6b3 100644 --- a/utils/unittest/fixtures/event_test.go +++ b/utils/unittest/fixtures/event_test.go @@ -33,7 +33,7 @@ func TestAdjustEventsMetadata(t *testing.T) { require.Len(t, result, 1) assert.Equal(t, uint32(0), result[0].EventIndex) - assert.Equal(t, uint32(0), result[0].TransactionIndex) + assert.Equal(t, uint32(999), result[0].TransactionIndex) // unchanged assert.Equal(t, txID, result[0].TransactionID) @@ -70,7 +70,8 @@ func TestAdjustEventsMetadata(t *testing.T) { for i, event := range result { assert.Equal(t, txID, event.TransactionID) - assert.Equal(t, uint32(0), event.TransactionIndex) + // all tx have the same txID, so they should all have the same txIndex + assert.Equal(t, uint32(999), event.TransactionIndex) assert.Equal(t, uint32(i), event.EventIndex) } }) @@ -133,6 +134,9 @@ func TestAdjustEventsMetadata(t *testing.T) { ) } + // will adjust according to the first tx's index + events[0].TransactionIndex = 0 + result := AdjustEventsMetadata(events) require.Len(t, result, len(expected)) diff --git a/utils/unittest/fixtures/payload.go b/utils/unittest/fixtures/payload.go index 3fa7ca617ce..a8934feedc5 100644 --- a/utils/unittest/fixtures/payload.go +++ b/utils/unittest/fixtures/payload.go @@ -51,7 +51,7 @@ func (f payloadFactory) WithReceiptStubs(receipts ...*flow.ExecutionReceiptStub) } // WithResults is an option that sets the `Results` of the payload. -func (f payloadFactory) WithResults(results flow.ExecutionResultList) PayloadOption { +func (f payloadFactory) WithResults(results ...*flow.ExecutionResult) PayloadOption { return func(g *PayloadGenerator, payload *flow.Payload) { payload.Results = results }