diff --git a/.github/label-actions.yml b/.github/label-actions.yml new file mode 100644 index 000000000..b4dc1814d --- /dev/null +++ b/.github/label-actions.yml @@ -0,0 +1,35 @@ +# When `devnet-e2e-test` is added, also assign `devnet` to the PR. +devnet-e2e-test: + prs: + comment: The CI will now also run the e2e tests on devnet, which increases the time it takes to complete all CI checks. + label: + - devnet + +# When `devnet-e2e-test` is removed, also delete `devnet` from the PR. +-devnet-e2e-test: + prs: + unlabel: + - devnet + +# When `devnet` is added, also assign `push-image` to the PR. +devnet: + prs: + label: + - push-image + +# When `devnet` is removed, also delete `devnet-e2e-test` from the PR. +-devnet: + prs: + unlabel: + - devnet-e2e-test + +# Let the developer know that they need to push another commit after attaching the label to PR. +push-image: + prs: + comment: The image is going to be pushed after the next commit. If you want to run an e2e test, it is necessary to push another commit. You can use `make trigger_ci` to push an empty commit. + +# When `push-image` is removed, also delete `devnet` from the PR. +-push-image: + prs: + unlabel: + - devnet diff --git a/.github/workflows/go.yml b/.github/workflows/go.yml index 1b60ddc4c..6f76334c1 100644 --- a/.github/workflows/go.yml +++ b/.github/workflows/go.yml @@ -8,11 +8,16 @@ on: branches: ["main"] pull_request: +concurrency: + group: ${{ github.head_ref || github.ref_name }} + cancel-in-progress: true + jobs: build: runs-on: ubuntu-latest steps: - name: install ignite + # If this step fails due to ignite.com failing, see #116 for a temporary workaround run: | curl https://get.ignite.com/cli! | bash ignite version @@ -39,7 +44,54 @@ jobs: run: make go_lint - name: Build - run: ignite chain build --debug --skip-proto + run: ignite chain build -v --debug --skip-proto - name: Test run: make go_test + + - name: Set up Docker Buildx + if: (github.ref == 'refs/heads/main') || (contains(github.event.pull_request.labels.*.name, 'push-image')) + uses: docker/setup-buildx-action@v3 + + - name: Docker Metadata action + if: (github.ref == 'refs/heads/main') || (contains(github.event.pull_request.labels.*.name, 'push-image')) + id: meta + uses: docker/metadata-action@v5 + env: + DOCKER_METADATA_PR_HEAD_SHA: "true" + with: + images: | + ghcr.io/pokt-network/pocketd + tags: | + type=ref,event=branch + type=ref,event=pr + type=sha + type=sha,format=long + + - name: Login to GitHub Container Registry + if: (github.ref == 'refs/heads/main') || (contains(github.event.pull_request.labels.*.name, 'push-image')) + uses: docker/login-action@v3 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Copy binary to inside of the Docker context + if: (github.ref == 'refs/heads/main') || (contains(github.event.pull_request.labels.*.name, 'push-image')) + run: | + mkdir -p ./bin # Make sure the bin directory exists + cp $(go env GOPATH)/bin/poktrolld ./bin # Copy the binary to the repo's bin directory + + - name: Build and push Docker image + if: (github.ref == 'refs/heads/main') || (contains(github.event.pull_request.labels.*.name, 'push-image')) + uses: docker/build-push-action@v5 + with: + push: true + tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} + # NB: Uncomment below if arm64 build is needed; arm64 builds are off by default because build times are significant. + platforms: linux/amd64 #,linux/arm64 + file: Dockerfile.dev + cache-from: type=gha + cache-to: type=gha,mode=max + context: . diff --git a/.github/workflows/label-actions.yml b/.github/workflows/label-actions.yml new file mode 100644 index 000000000..caf1a31cc --- /dev/null +++ b/.github/workflows/label-actions.yml @@ -0,0 +1,21 @@ +name: 'Label Actions' + +on: + issues: + types: [labeled, unlabeled] + pull_request_target: + types: [labeled, unlabeled] + discussion: + types: [labeled, unlabeled] + +permissions: + contents: read + issues: write + pull-requests: write + discussions: write + +jobs: + action: + runs-on: ubuntu-latest + steps: + - uses: dessant/label-actions@v3 diff --git a/.gitignore b/.gitignore index aa7066b08..5dc239e58 100644 --- a/.gitignore +++ b/.gitignore @@ -22,7 +22,6 @@ go.work # Don't commit binaries bin -!bin/.keep # Before we provision the localnet, `ignite` creates the accounts, genesis, etc. for us # As many of the files are dynamic, we only preserve the config files in git history. @@ -57,4 +56,7 @@ ts-client/ **/*_mock.go # Localnet config -localnet_config.yaml \ No newline at end of file +localnet_config.yaml + +# Relase artifacts produced by `ignite chain build --release` +release diff --git a/Dockerfile.dev b/Dockerfile.dev new file mode 100644 index 000000000..2d10955e0 --- /dev/null +++ b/Dockerfile.dev @@ -0,0 +1,23 @@ +# This Dockerfile is used to build container image for development purposes. +# It intentionally contains no security features, ships with code and troubleshooting tools. + +FROM golang:1.20 as base + +RUN apt update && \ + apt-get install -y \ + ca-certificates \ + curl jq make + +# enable faster module downloading. +ENV GOPROXY https://proxy.golang.org + +COPY . /poktroll + +WORKDIR /poktroll + +RUN mv /poktroll/bin/poktrolld /usr/bin/poktrolld + +EXPOSE 8545 +EXPOSE 8546 + +ENTRYPOINT ["ignite"] diff --git a/Makefile b/Makefile index d6d3257dc..19c8fe59e 100644 --- a/Makefile +++ b/Makefile @@ -231,11 +231,11 @@ todo_this_commit: ## List all the TODOs needed to be done in this commit .PHONY: gateway_list gateway_list: ## List all the staked gateways - pocketd --home=$(POCKETD_HOME) q gateway list-gateway --node $(POCKET_NODE) + poktrolld --home=$(POCKETD_HOME) q gateway list-gateway --node $(POCKET_NODE) .PHONY: gateway_stake gateway_stake: ## Stake tokens for the gateway specified (must specify the gateway env var) - pocketd --home=$(POCKETD_HOME) tx gateway stake-gateway 1000upokt --keyring-backend test --from $(GATEWAY) --node $(POCKET_NODE) + poktrolld --home=$(POCKETD_HOME) tx gateway stake-gateway 1000upokt --keyring-backend test --from $(GATEWAY) --node $(POCKET_NODE) .PHONY: gateway1_stake gateway1_stake: ## Stake gateway1 @@ -251,7 +251,7 @@ gateway3_stake: ## Stake gateway3 .PHONY: gateway_unstake gateway_unstake: ## Unstake an gateway (must specify the GATEWAY env var) - pocketd --home=$(POCKETD_HOME) tx gateway unstake-gateway --keyring-backend test --from $(GATEWAY) --node $(POCKET_NODE) + poktrolld --home=$(POCKETD_HOME) tx gateway unstake-gateway --keyring-backend test --from $(GATEWAY) --node $(POCKET_NODE) .PHONY: gateway1_unstake gateway1_unstake: ## Unstake gateway1 @@ -271,11 +271,11 @@ gateway3_unstake: ## Unstake gateway3 .PHONY: app_list app_list: ## List all the staked applications - pocketd --home=$(POCKETD_HOME) q application list-application --node $(POCKET_NODE) + poktrolld --home=$(POCKETD_HOME) q application list-application --node $(POCKET_NODE) .PHONY: app_stake app_stake: ## Stake tokens for the application specified (must specify the APP and SERVICES env vars) - pocketd --home=$(POCKETD_HOME) tx application stake-application 1000upokt $(SERVICES) --keyring-backend test --from $(APP) --node $(POCKET_NODE) + poktrolld --home=$(POCKETD_HOME) tx application stake-application 1000upokt $(SERVICES) --keyring-backend test --from $(APP) --node $(POCKET_NODE) .PHONY: app1_stake app1_stake: ## Stake app1 @@ -291,7 +291,7 @@ app3_stake: ## Stake app3 .PHONY: app_unstake app_unstake: ## Unstake an application (must specify the APP env var) - pocketd --home=$(POCKETD_HOME) tx application unstake-application --keyring-backend test --from $(APP) --node $(POCKET_NODE) + poktrolld --home=$(POCKETD_HOME) tx application unstake-application --keyring-backend test --from $(APP) --node $(POCKET_NODE) .PHONY: app1_unstake app1_unstake: ## Unstake app1 @@ -307,7 +307,7 @@ app3_unstake: ## Unstake app3 .PHONY: app_delegate app_delegate: ## Delegate trust to a gateway (must specify the APP and GATEWAY_ADDR env vars). Requires the app to be staked - pocketd --home=$(POCKETD_HOME) tx application delegate-to-gateway $(GATEWAY_ADDR) --keyring-backend test --from $(APP) --node $(POCKET_NODE) + poktrolld --home=$(POCKETD_HOME) tx application delegate-to-gateway $(GATEWAY_ADDR) --keyring-backend test --from $(APP) --node $(POCKET_NODE) .PHONY: app1_delegate_gateway1 app1_delegate_gateway1: ## Delegate trust to gateway1 @@ -323,7 +323,7 @@ app3_delegate_gateway3: ## Delegate trust to gateway3 .PHONY: app_undelegate app_undelegate: ## Undelegate trust to a gateway (must specify the APP and GATEWAY_ADDR env vars). Requires the app to be staked - pocketd --home=$(POCKETD_HOME) tx application undelegate-from-gateway $(GATEWAY_ADDR) --keyring-backend test --from $(APP) --node $(POCKET_NODE) + poktrolld --home=$(POCKETD_HOME) tx application undelegate-from-gateway $(GATEWAY_ADDR) --keyring-backend test --from $(APP) --node $(POCKET_NODE) .PHONY: app1_undelegate_gateway1 app1_undelegate_gateway1: ## Undelegate trust to gateway1 @@ -343,13 +343,13 @@ app3_undelegate_gateway3: ## Undelegate trust to gateway3 .PHONY: supplier_list supplier_list: ## List all the staked supplier - pocketd --home=$(POCKETD_HOME) q supplier list-supplier --node $(POCKET_NODE) + poktrolld --home=$(POCKETD_HOME) q supplier list-supplier --node $(POCKET_NODE) # TODO(@Olshansk, @okdas): Add more services (in addition to anvil) for apps and suppliers to stake for. # TODO_TECHDEBT: svc1, svc2 and svc3 below are only in place to make GetSession testable .PHONY: supplier_stake supplier_stake: ## Stake tokens for the supplier specified (must specify the APP env var) - pocketd --home=$(POCKETD_HOME) tx supplier stake-supplier 1000upokt "$(SERVICES)" --keyring-backend test --from $(SUPPLIER) --node $(POCKET_NODE) + poktrolld --home=$(POCKETD_HOME) tx supplier stake-supplier 1000upokt "$(SERVICES)" --keyring-backend test --from $(SUPPLIER) --node $(POCKET_NODE) .PHONY: supplier1_stake supplier1_stake: ## Stake supplier1 @@ -365,7 +365,7 @@ supplier3_stake: ## Stake supplier3 .PHONY: supplier_unstake supplier_unstake: ## Unstake an supplier (must specify the SUPPLIER env var) - pocketd --home=$(POCKETD_HOME) tx supplier unstake-supplier --keyring-backend test --from $(SUPPLIER) --node $(POCKET_NODE) + poktrolld --home=$(POCKETD_HOME) tx supplier unstake-supplier --keyring-backend test --from $(SUPPLIER) --node $(POCKET_NODE) .PHONY: supplier1_unstake supplier1_unstake: ## Unstake supplier1 @@ -386,10 +386,10 @@ supplier3_unstake: ## Unstake supplier3 .PHONY: acc_balance_query acc_balance_query: ## Query the balance of the account specified (make acc_balance_query ACC=pokt...) @echo "~~~ Balances ~~~" - pocketd --home=$(POCKETD_HOME) q bank balances $(ACC) --node $(POCKET_NODE) + poktrolld --home=$(POCKETD_HOME) q bank balances $(ACC) --node $(POCKET_NODE) @echo "~~~ Spendable Balances ~~~" @echo "Querying spendable balance for $(ACC)" - pocketd --home=$(POCKETD_HOME) q bank spendable-balances $(ACC) --node $(POCKET_NODE) + poktrolld --home=$(POCKETD_HOME) q bank spendable-balances $(ACC) --node $(POCKET_NODE) .PHONY: acc_balance_query_module_app acc_balance_query_module_app: ## Query the balance of the network level "application" module @@ -405,7 +405,7 @@ acc_balance_query_app1: ## Query the balance of app1 .PHONY: acc_balance_total_supply acc_balance_total_supply: ## Query the total supply of the network - pocketd --home=$(POCKETD_HOME) q bank total --node $(POCKET_NODE) + poktrolld --home=$(POCKETD_HOME) q bank total --node $(POCKET_NODE) ###################### ### Ignite Helpers ### @@ -415,6 +415,15 @@ acc_balance_total_supply: ## Query the total supply of the network ignite_acc_list: ## List all the accounts in LocalNet ignite account list --keyring-dir=$(POCKETD_HOME) --keyring-backend test --address-prefix $(POCKET_ADDR_PREFIX) +################## +### CI Helpers ### +################## + +.PHONY: trigger_ci +trigger_ci: ## Trigger the CI pipeline by submitting an empty commit; See https://github.com/pokt-network/pocket/issues/900 for details + git commit --allow-empty -m "Empty commit" + git push + ##################### ### Documentation ### ##################### diff --git a/Tiltfile b/Tiltfile index 1fd0dd779..17521b14b 100644 --- a/Tiltfile +++ b/Tiltfile @@ -101,12 +101,12 @@ docker_build_with_restart( dockerfile_contents="""FROM golang:1.20.8 RUN apt-get -q update && apt-get install -qyy curl jq RUN go install github.com/go-delve/delve/cmd/dlv@latest -COPY bin/pocketd /usr/local/bin/pocketd +COPY bin/poktrolld /usr/local/bin/pocketd WORKDIR / """, - only=["./bin/pocketd"], + only=["./bin/poktrolld"], entrypoint=["/bin/sh", "/scripts/pocket.sh"], - live_update=[sync("bin/pocketd", "/usr/local/bin/pocketd")], + live_update=[sync("bin/poktrolld", "/usr/local/bin/pocketd")], ) # Run celestia and anvil nodes diff --git a/docs/pkg/client/README.md b/docs/pkg/client/README.md index becaac9b3..6f4032800 100644 --- a/docs/pkg/client/README.md +++ b/docs/pkg/client/README.md @@ -1,18 +1,59 @@ # Package `client` -> Standardized interfaces facilitating interactions with blockchain functionalities. +## Table of Contents + +- [Overview](#overview) +- [Features](#features) +- [Architecture Overview](#architecture-overview) + - [Component Diagram Legend](#component-diagram-legend) + - [Clients Dependency Tree](#clients-dependency-tree) + - [Network Interaction](#network-interaction) +- [Installation](#installation) +- [Usage](#usage) + - [Basic Example](#basic-example) + - [Advanced Usage](#advanced-usage) + - [Configuration](#configuration) +- [API Reference](#api-reference) +- [Best Practices](#best-practices) +- [FAQ](#faq) + ## Overview -The `client` package serves as a foundational layer for applications aiming to integrate with various blockchain platforms. It abstracts the complexities of sending, receiving, and querying blockchain data, ensuring a consistent experience irrespective of the underlying blockchain: +The `client` package exposes go APIs to facilitate interactions with the Pocket network. +It includes lower-level interfaces for working with transactions and subscribing to events generally, as well as higher-level interfaces for tracking blocks and broadcasting protocol-specific transactions. + +## Features -- **Simplifies Blockchain Interactions**: Provides a clear interface for initiating transactions and querying blockchain events without dealing with platform-specific quirks. -- **Modular and Extendable**: Designed with separation of concerns in mind, allowing developers to customize or replace components as necessary. -- **Unified Communication**: Regardless of the blockchain in use, the interfaces offer a standard way to communicate, streamlining the development process. +| Interface | Description | +|-------------------------|----------------------------------------------------------------------------------------------------| +| **`SupplierClient`** | A high-level client for use by the "supplier" actor. | +| **`TxClient`** | A high-level client used to build, sign, and broadcast transaction from cosmos-sdk messages. | +| **`TxContext`** | Abstracts and encapsulates the transaction building, signing, encoding, and broadcasting concerns. | +| **`BlockClient`** | Exposes methods for receiving notifications about newly committed blocks. | +| **`EventsQueryClient`** | Encapsulates blockchain event subscriptions. | +| **`Connection`** | A transport agnostic communication channel for sending and receiving messages. | +| **`Dialer`** | Abstracts the establishment of connections. | -## Architecture Diagrams +## Architecture Overview + +```mermaid +--- +title: Component Diagram Legend +--- +flowchart + +c[Component] +d[Dependency Component] +s[[Subcomponent]] +r[Remote Component] + +c --"direct usage via #DependencyMethod()"--> d +c -."usage via network I/O".-> r +c --> s +``` -Visual representations often make it easier to understand the design and flow of a package. Diagrams specific to this package can be added here. +> **Figure 1**: A legend for the component diagrams in this document. ```mermaid --- @@ -20,53 +61,46 @@ title: Clients Dependency Tree --- flowchart - sup[SupplierClient] - tx[TxClient] - bl[BlockClient] - evt[EventsQueryClient] +sup[SupplierClient] +tx[TxClient] +txctx[[TxContext]] +bl[BlockClient] +evt[EventsQueryClient] +conn[[Connection]] +dial[[Dialer]] + +sup --"#SignAndBroadcast()"--> tx - sup --"#SignAndBroadcast()"--> tx - tx --"#CommittedBlocksSequence()"--> bl +tx --"#BroadcastTx"--> txctx tx --"#EventsBytes()"--> evt bl --"#EventsBytes()"--> evt +evt --> conn +evt --"#DialContext()"--> dial +dial --"(returns)"--> conn ``` -> **Figure 1**: An overview diagram showing the interaction between the various interfaces and the blockchain. +> **Figure 2**: An overview which articulates the dependency relationships between the various client interfaces and their subcompnents. ```mermaid --- -title: TxClient Dependency Graph +title: Network Interaction --- - - flowchart - subgraph tclient[TxClient] - tctx[TxContext] - builder[TxBuilder] - keyring[Keyring] - - bclient[BlockClient] - eclient[EventsQueryClient] +txctx[[TxContext]] +conn[[Connection]] +dial[[Dialer]] - tclient_internal((_)) - end +chain[Blockchain] - chain[Blockchain] - - tctx --"#GetKeyring()"--> keyring -tctx --"#SignTx()"--> builder -tctx --"#EncodeTx()"--> builder -tctx -."#BroadcastTx()".-> chain -tctx -."#QueryTx()".-> chain - -eclient -."websocket connection".-> chain -bclient --"committed block subscription"--> eclient -tclient_internal --"own tx subscription"--> eclient +conn <-."subscribed events".-> chain +dial -."RPC subscribe".-> chain +txctx -."tx broadcast".-> chain +txctx -."tx query".-> chain ``` -> **Figure 2**: A focused look at how `TxClient` functions and interacts with the underlying blockchain. +> **Figure 3**: An overview of how client subcomponents interact with the network. ## Installation @@ -74,26 +108,18 @@ tclient_internal --"own tx subscription"--> eclient go get github.com/pokt-network/poktroll/pkg/client ``` -## Features - -- **TxClient Interface**: A streamlined way to sign and broadcast multiple messages as part of a blockchain transaction. -- **BlockClient Interface**: Notifications about newly committed blocks and access to the latest block. -- **EventsQueryClient**: Enables subscription to chain event messages. -- **Connection Interface**: A transport agnostic communication channel for sending and receiving messages. -- **Dialer**: Simplifies the establishment of connections. - ## Usage ### Basic Example ```go -// Code example showcasing the use of TxClient or any other primary interface. +// TODO: Code example showcasing the use of TxClient or any other primary interface. ``` ### Advanced Usage ```go -// Example illustrating advanced features or edge cases of the package. +// TODO: Example illustrating advanced features or edge cases of the package. ``` ### Configuration diff --git a/e2e/tests/node.go b/e2e/tests/node.go index 4e34fa827..e46ad2889 100644 --- a/e2e/tests/node.go +++ b/e2e/tests/node.go @@ -67,7 +67,7 @@ func (p *pocketdBin) RunCommandOnHost(rpcUrl string, args ...string) (*commandRe func (p *pocketdBin) runCmd(args ...string) (*commandResult, error) { base := []string{"--home", defaultHome} args = append(base, args...) - cmd := exec.Command("pocketd", args...) + cmd := exec.Command("poktrolld", args...) r := &commandResult{} out, err := cmd.Output() if err != nil { diff --git a/internal/testclient/testeventsquery/client.go b/internal/testclient/testeventsquery/client.go index 6d06b62fb..fbf7daeb1 100644 --- a/internal/testclient/testeventsquery/client.go +++ b/internal/testclient/testeventsquery/client.go @@ -4,6 +4,7 @@ import ( "context" "fmt" "testing" + "time" cosmoskeyring "github.com/cosmos/cosmos-sdk/crypto/keyring" "github.com/golang/mock/gomock" @@ -17,7 +18,7 @@ import ( "github.com/pokt-network/poktroll/pkg/observable/channel" ) -// NewLocalnetClient creates and returns a new events query client that configured +// NewLocalnetClient creates and returns a new events query client that's configured // for use with the localnet sequencer. Any options provided are applied to the client. func NewLocalnetClient(t *testing.T, opts ...client.EventsQueryClientOption) client.EventsQueryClient { t.Helper() @@ -80,3 +81,39 @@ func NewOneTimeTxEventsQueryClient( publishCh, ) } + +// NewAnyTimesEventsBytesEventsQueryClient returns a new events query client which +// is configured to return the expected event bytes when queried with the expected +// query, any number of times. The returned client also expects to be closed once. +func NewAnyTimesEventsBytesEventsQueryClient( + ctx context.Context, + t *testing.T, + expectedQuery string, + expectedEventBytes []byte, +) client.EventsQueryClient { + t.Helper() + + ctrl := gomock.NewController(t) + eventsQueryClient := mockclient.NewMockEventsQueryClient(ctrl) + eventsQueryClient.EXPECT().Close().Times(1) + eventsQueryClient.EXPECT(). + EventsBytes(gomock.AssignableToTypeOf(ctx), gomock.Eq(expectedQuery)). + DoAndReturn( + func(ctx context.Context, query string) (client.EventsBytesObservable, error) { + bytesObsvbl, bytesPublishCh := channel.NewReplayObservable[either.Bytes](ctx, 1) + + // Now that the observable is set up, publish the expected event bytes. + // Only need to send once because it's a ReplayObservable. + bytesPublishCh <- either.Success(expectedEventBytes) + + // Wait a tick for the observables to be set up. This isn't strictly + // necessary but is done to mitigate test flakiness. + time.Sleep(10 * time.Millisecond) + + return bytesObsvbl, nil + }, + ). + AnyTimes() + + return eventsQueryClient +} diff --git a/pkg/client/block/client.go b/pkg/client/block/client.go index 54569e60d..18526508d 100644 --- a/pkg/client/block/client.go +++ b/pkg/client/block/client.go @@ -155,7 +155,7 @@ func (bClient *blockClient) retryPublishBlocksFactory(ctx context.Context) func( } // NB: must cast back to generic observable type to use with Map. - // client.BlocksObservable is only used to workaround gomock's lack of + // client.BlocksObservable cannot be an alias due to gomock's lack of // support for generic types. eventsBz := observable.Observable[either.Either[[]byte]](eventsBzObsvbl) blockEventFromEventBz := newEventsBytesToBlockMapFn(errCh) diff --git a/pkg/client/block/client_test.go b/pkg/client/block/client_test.go index b983ff274..b2a5515b3 100644 --- a/pkg/client/block/client_test.go +++ b/pkg/client/block/client_test.go @@ -8,17 +8,20 @@ import ( "cosmossdk.io/depinject" comettypes "github.com/cometbft/cometbft/types" - "github.com/golang/mock/gomock" "github.com/stretchr/testify/require" "github.com/pokt-network/poktroll/internal/testclient" "github.com/pokt-network/poktroll/internal/testclient/testeventsquery" "github.com/pokt-network/poktroll/pkg/client" "github.com/pokt-network/poktroll/pkg/client/block" - eventsquery "github.com/pokt-network/poktroll/pkg/client/events_query" ) -const blockAssertionLoopTimeout = 500 * time.Millisecond +const ( + testTimeoutDuration = 100 * time.Millisecond + + // duplicates pkg/client/block/client.go's committedBlocksQuery for testing purposes + committedBlocksQuery = "tm.event='NewBlock'" +) func TestBlockClient(t *testing.T) { var ( @@ -38,19 +41,15 @@ func TestBlockClient(t *testing.T) { ctx = context.Background() ) - // Set up a mock connection and dialer which are expected to be used once. - connMock, dialerMock := testeventsquery.NewOneTimeMockConnAndDialer(t) - connMock.EXPECT().Send(gomock.Any()).Return(nil).Times(1) - // Mock the Receive method to return the expected block event. - connMock.EXPECT().Receive().DoAndReturn(func() ([]byte, error) { - blockEventJson, err := json.Marshal(expectedBlockEvent) - require.NoError(t, err) - return blockEventJson, nil - }).AnyTimes() - - // Set up events query client dependency. - dialerOpt := eventsquery.WithDialer(dialerMock) - eventsQueryClient := testeventsquery.NewLocalnetClient(t, dialerOpt) + expectedEventBz, err := json.Marshal(expectedBlockEvent) + require.NoError(t, err) + + eventsQueryClient := testeventsquery.NewAnyTimesEventsBytesEventsQueryClient( + ctx, t, + committedBlocksQuery, + expectedEventBz, + ) + deps := depinject.Supply(eventsQueryClient) // Set up block client. @@ -58,60 +57,54 @@ func TestBlockClient(t *testing.T) { require.NoError(t, err) require.NotNil(t, blockClient) - // Run LatestBlock and CommittedBlockSequence concurrently because they can - // block, leading to an unresponsive test. This function sends multiple values - // on the actualBlockCh which are all asserted against in blockAssertionLoop. - // If any of the methods under test hang, the test will time out. - var ( - actualBlockCh = make(chan client.Block, 1) - done = make(chan struct{}, 1) - ) - go func() { - // Test LatestBlock method. - actualBlock := blockClient.LatestBlock(ctx) - require.Equal(t, expectedHeight, actualBlock.Height()) - require.Equal(t, expectedHash, actualBlock.Hash()) - - // Test CommittedBlockSequence method. - blockObservable := blockClient.CommittedBlocksSequence(ctx) - require.NotNil(t, blockObservable) - - // Ensure that the observable is replayable via Last. - actualBlockCh <- blockObservable.Last(ctx, 1)[0] - - // Ensure that the observable is replayable via Subscribe. - blockObserver := blockObservable.Subscribe(ctx) - for block := range blockObserver.Ch() { - actualBlockCh <- block - break - } - - // Signal test completion - done <- struct{}{} - }() - - // blockAssertionLoop ensures that the blocks retrieved from both LatestBlock - // method and CommittedBlocksSequence method match the expected block height - // and hash. This loop waits for blocks to be sent on the actualBlockCh channel - // by the methods being tested. Once the methods are done, they send a signal on - // the "done" channel. If the blockAssertionLoop doesn't receive any block or - // the done signal within a specific timeout, it assumes something has gone wrong - // and fails the test. -blockAssertionLoop: - for { - select { - case actualBlock := <-actualBlockCh: - require.Equal(t, expectedHeight, actualBlock.Height()) - require.Equal(t, expectedHash, actualBlock.Hash()) - case <-done: - break blockAssertionLoop - case <-time.After(blockAssertionLoopTimeout): - t.Fatal("timed out waiting for block event") - } + tests := []struct { + name string + fn func() client.Block + }{ + { + name: "LatestBlock successfully returns latest block", + fn: func() client.Block { + lastBlock := blockClient.LatestBlock(ctx) + return lastBlock + }, + }, + { + name: "CommittedBlocksSequence successfully returns latest block", + fn: func() client.Block { + blockObservable := blockClient.CommittedBlocksSequence(ctx) + require.NotNil(t, blockObservable) + + // Ensure that the observable is replayable via Last. + lastBlock := blockObservable.Last(ctx, 1)[0] + require.Equal(t, expectedHeight, lastBlock.Height()) + require.Equal(t, expectedHash, lastBlock.Hash()) + + return lastBlock + }, + }, } - // Wait a tick for the observables to be set up. - time.Sleep(time.Millisecond) + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + var actualBlockCh = make(chan client.Block, 10) + + // Run test functions asynchronously because they can block, leading + // to an unresponsive test. If any of the methods under test hang, + // the test will time out in the select statement that follows. + go func(fn func() client.Block) { + actualBlockCh <- fn() + close(actualBlockCh) + }(tt.fn) + + select { + case actualBlock := <-actualBlockCh: + require.Equal(t, expectedHeight, actualBlock.Height()) + require.Equal(t, expectedHash, actualBlock.Hash()) + case <-time.After(testTimeoutDuration): + t.Fatal("timed out waiting for block event") + } + }) + } blockClient.Close() } diff --git a/pkg/client/interface.go b/pkg/client/interface.go index c78de5b86..32dab250c 100644 --- a/pkg/client/interface.go +++ b/pkg/client/interface.go @@ -20,7 +20,7 @@ import ( ) // TxClient provides a synchronous interface initiating and waiting for transactions -// in a cosmos-sdk based blockchain network, derived from cosmos-sdk messages. +// derived from cosmos-sdk messages, in a cosmos-sdk based blockchain network. type TxClient interface { SignAndBroadcast( ctx context.Context, @@ -29,8 +29,8 @@ type TxClient interface { } // TxContext provides an interface which consolidates the operational dependencies -// required to facilitate the sender side of the transaction lifecycle: build, sign, encode, -// broadcast, query (optional). +// required to facilitate the sender side of the transaction lifecycle: build, sign, +// encode, broadcast, and query (optional). // // TODO_IMPROVE: Avoid depending on cosmos-sdk structs or interfaces; add Pocket // interface types to substitute: @@ -70,6 +70,7 @@ type TxContext interface { // BlocksObservable is an observable which is notified with an either // value which contains either an error or the event message bytes. +// // TODO_HACK: The purpose of this type is to work around gomock's lack of // support for generic types. For the same reason, this type cannot be an // alias (i.e. EventsBytesObservable = observable.Observable[either.Either[[]byte]]). @@ -94,23 +95,24 @@ type Block interface { Hash() []byte } -// TODO_CONSIDERATION: the cosmos-sdk CLI code seems to use a cometbft RPC client -// which includes a `#Subscribe()` method for a similar purpose. Perhaps we could -// replace this custom websocket client with that. -// (see: https://github.com/cometbft/cometbft/blob/main/rpc/client/http/http.go#L110) -// (see: https://github.com/cosmos/cosmos-sdk/blob/main/client/rpc/tx.go#L114) -// -// NOTE: a branch which attempts this is available at: -// https://github.com/pokt-network/poktroll/pull/74 - // EventsBytesObservable is an observable which is notified with an either // value which contains either an error or the event message bytes. +// // TODO_HACK: The purpose of this type is to work around gomock's lack of // support for generic types. For the same reason, this type cannot be an // alias (i.e. EventsBytesObservable = observable.Observable[either.Bytes]). type EventsBytesObservable observable.Observable[either.Bytes] // EventsQueryClient is used to subscribe to chain event messages matching the given query, +// +// TODO_CONSIDERATION: the cosmos-sdk CLI code seems to use a cometbft RPC client +// which includes a `#Subscribe()` method for a similar purpose. Perhaps we could +// replace our custom implementation with one which wraps that. +// (see: https://github.com/cometbft/cometbft/blob/main/rpc/client/http/http.go#L110) +// (see: https://github.com/cosmos/cosmos-sdk/blob/main/client/rpc/tx.go#L114) +// +// NOTE: a branch which attempts this is available at: +// https://github.com/pokt-network/poktroll/pull/74 type EventsQueryClient interface { // EventsBytes returns an observable which is notified about chain event messages // matching the given query. It receives an either value which contains either an diff --git a/pkg/client/tx/client.go b/pkg/client/tx/client.go index 0b2bdc766..1c083559b 100644 --- a/pkg/client/tx/client.go +++ b/pkg/client/tx/client.go @@ -46,7 +46,7 @@ var _ client.TxClient = (*txClient)(nil) // the last status of the transaction, which is used to derive the asynchronous // error that's populated in the either.AsyncError. type txClient struct { - // INCOMPLETE: this should be configurable & integrated w/ viper, flags, etc. + // TODO_TECHDEBT: this should be configurable & integrated w/ viper, flags, etc. // commitTimeoutHeightOffset is the number of blocks after the latest block // that a transactions should be considered errored if it has not been committed. commitTimeoutHeightOffset int64 diff --git a/x/application/client/cli/tx_delegate_to_gateway.go b/x/application/client/cli/tx_delegate_to_gateway.go index 324f88622..ea251e6cd 100644 --- a/x/application/client/cli/tx_delegate_to_gateway.go +++ b/x/application/client/cli/tx_delegate_to_gateway.go @@ -22,7 +22,7 @@ that delegates authority to the gateway specified to sign relays requests for th act on the behalf of the application during a session. Example: -$ pocketd --home=$(POCKETD_HOME) tx application delegate-to-gateway $(GATEWAY_ADDR) --keyring-backend test --from $(APP) --node $(POCKET_NODE)`, +$ poktrolld --home=$(POCKETD_HOME) tx application delegate-to-gateway $(GATEWAY_ADDR) --keyring-backend test --from $(APP) --node $(POCKET_NODE)`, Args: cobra.ExactArgs(1), RunE: func(cmd *cobra.Command, args []string) (err error) { gatewayAddress := args[0] diff --git a/x/application/client/cli/tx_stake_application.go b/x/application/client/cli/tx_stake_application.go index 4b077e6c2..510cfd648 100644 --- a/x/application/client/cli/tx_stake_application.go +++ b/x/application/client/cli/tx_stake_application.go @@ -27,7 +27,7 @@ func CmdStakeApplication() *cobra.Command { will stake the tokens and serviceIds and associate them with the application specified by the 'from' address. Example: -$ pocketd --home=$(POCKETD_HOME) tx application stake-application 1000upokt svc1,svc2,svc3 --keyring-backend test --from $(APP) --node $(POCKET_NODE)`, +$ poktrolld --home=$(POCKETD_HOME) tx application stake-application 1000upokt svc1,svc2,svc3 --keyring-backend test --from $(APP) --node $(POCKET_NODE)`, Args: cobra.ExactArgs(2), RunE: func(cmd *cobra.Command, args []string) (err error) { stakeString := args[0] diff --git a/x/application/client/cli/tx_undelegate_from_gateway.go b/x/application/client/cli/tx_undelegate_from_gateway.go index 95a770baa..308a5d8a0 100644 --- a/x/application/client/cli/tx_undelegate_from_gateway.go +++ b/x/application/client/cli/tx_undelegate_from_gateway.go @@ -22,7 +22,7 @@ that removes the authority from the gateway specified to sign relays requests fo act on the behalf of the application during a session. Example: -$ pocketd --home=$(POCKETD_HOME) tx application undelegate-from-gateway $(GATEWAY_ADDR) --keyring-backend test --from $(APP) --node $(POCKET_NODE)`, +$ poktrolld --home=$(POCKETD_HOME) tx application undelegate-from-gateway $(GATEWAY_ADDR) --keyring-backend test --from $(APP) --node $(POCKET_NODE)`, Args: cobra.ExactArgs(1), RunE: func(cmd *cobra.Command, args []string) (err error) { gatewayAddress := args[0] diff --git a/x/application/client/cli/tx_unstake_application.go b/x/application/client/cli/tx_unstake_application.go index ebf720a82..bfbf10e32 100644 --- a/x/application/client/cli/tx_unstake_application.go +++ b/x/application/client/cli/tx_unstake_application.go @@ -22,7 +22,7 @@ func CmdUnstakeApplication() *cobra.Command { the application specified by the 'from' address. Example: -$ pocketd --home=$(POCKETD_HOME) tx application unstake-application --keyring-backend test --from $(APP) --node $(POCKET_NODE)`, +$ poktrolld --home=$(POCKETD_HOME) tx application unstake-application --keyring-backend test --from $(APP) --node $(POCKET_NODE)`, Args: cobra.ExactArgs(0), RunE: func(cmd *cobra.Command, args []string) (err error) { diff --git a/x/gateway/client/cli/tx_stake_gateway.go b/x/gateway/client/cli/tx_stake_gateway.go index 2104b2523..2c363b43b 100644 --- a/x/gateway/client/cli/tx_stake_gateway.go +++ b/x/gateway/client/cli/tx_stake_gateway.go @@ -21,7 +21,7 @@ func CmdStakeGateway() *cobra.Command { Long: `Stake a gateway with the provided parameters. This is a broadcast operation that will stake the tokens and associate them with the gateway specified by the 'from' address. Example: -$ pocketd --home=$(POCKETD_HOME) tx gateway stake-gateway 1000upokt --keyring-backend test --from $(GATEWAY) --node $(POCKET_NODE)`, +$ poktrolld --home=$(POCKETD_HOME) tx gateway stake-gateway 1000upokt --keyring-backend test --from $(GATEWAY) --node $(POCKET_NODE)`, Args: cobra.ExactArgs(1), RunE: func(cmd *cobra.Command, args []string) (err error) { clientCtx, err := client.GetClientTxContext(cmd) diff --git a/x/gateway/client/cli/tx_unstake_gateway.go b/x/gateway/client/cli/tx_unstake_gateway.go index b57fd9eb7..e417b7540 100644 --- a/x/gateway/client/cli/tx_unstake_gateway.go +++ b/x/gateway/client/cli/tx_unstake_gateway.go @@ -21,7 +21,7 @@ func CmdUnstakeGateway() *cobra.Command { Long: `Unstake a gateway. This is a broadcast operation that will unstake the gateway specified by the 'from' address. Example: -$ pocketd --home=$(POCKETD_HOME) tx gateway unstake-gateway --keyring-backend test --from $(GATEWAY) --node $(POCKET_NODE)`, +$ poktrolld --home=$(POCKETD_HOME) tx gateway unstake-gateway --keyring-backend test --from $(GATEWAY) --node $(POCKET_NODE)`, Args: cobra.ExactArgs(0), RunE: func(cmd *cobra.Command, _ []string) (err error) { clientCtx, err := client.GetClientTxContext(cmd) diff --git a/x/supplier/client/cli/tx_stake_supplier.go b/x/supplier/client/cli/tx_stake_supplier.go index f223799cf..eac4b4044 100644 --- a/x/supplier/client/cli/tx_stake_supplier.go +++ b/x/supplier/client/cli/tx_stake_supplier.go @@ -32,7 +32,7 @@ of comma separated values of the form 'service;url' where 'service' is the servi For example, an application that stakes for 'anvil' could be matched with a supplier staking for 'anvil;http://anvil:8547'. Example: -$ pocketd --home=$(POCKETD_HOME) tx supplier stake-supplier 1000upokt anvil;http://anvil:8547 --keyring-backend test --from $(APP) --node $(POCKET_NODE)`, +$ poktrolld --home=$(POCKETD_HOME) tx supplier stake-supplier 1000upokt anvil;http://anvil:8547 --keyring-backend test --from $(APP) --node $(POCKET_NODE)`, Args: cobra.ExactArgs(2), RunE: func(cmd *cobra.Command, args []string) (err error) { stakeString := args[0] diff --git a/x/supplier/client/cli/tx_unstake_supplier.go b/x/supplier/client/cli/tx_unstake_supplier.go index 40ac4a83f..2daf7c00a 100644 --- a/x/supplier/client/cli/tx_unstake_supplier.go +++ b/x/supplier/client/cli/tx_unstake_supplier.go @@ -17,7 +17,7 @@ func CmdUnstakeSupplier() *cobra.Command { Long: `Unstake an supplier with the provided parameters. This is a broadcast operation that will unstake the supplier specified by the 'from' address. Example: -$ pocketd --home=$(POCKETD_HOME) tx supplier unstake-supplier --keyring-backend test --from $(SUPPLIER) --node $(POCKET_NODE)`, +$ poktrolld --home=$(POCKETD_HOME) tx supplier unstake-supplier --keyring-backend test --from $(SUPPLIER) --node $(POCKET_NODE)`, Args: cobra.ExactArgs(0), RunE: func(cmd *cobra.Command, args []string) (err error) {