From cd15647baffe9bf54a3f90b21878d210b0b08332 Mon Sep 17 00:00:00 2001 From: Patrick O'Grady Date: Fri, 18 Sep 2020 18:04:37 -0700 Subject: [PATCH 1/3] Add support for mempool --- bitcoin/client.go | 20 ++++++++++++++++++++ bitcoin/types.go | 19 +++++++++++++++++++ mocks/services/client.go | 23 +++++++++++++++++++++++ services/mempool_service.go | 24 ++++++++++++++++++++---- services/mempool_service_test.go | 25 +++++++++++++++++++++---- services/router.go | 2 +- services/types.go | 1 + 7 files changed, 105 insertions(+), 9 deletions(-) diff --git a/bitcoin/client.go b/bitcoin/client.go index 6ac38cc..bdb1efd 100644 --- a/bitcoin/client.go +++ b/bitcoin/client.go @@ -78,6 +78,9 @@ const ( // https://developer.bitcoin.org/reference/rpc/estimatesmartfee.html requestMethodEstimateSmartFee requestMethod = "estimatesmartfee" + // https://developer.bitcoin.org/reference/rpc/getrawmempool.html + requestMethodRawMempool requestMethod = "getrawmempool" + // blockNotFoundErrCode is the RPC error code when a block cannot be found blockNotFoundErrCode = -5 ) @@ -316,6 +319,23 @@ func (b *Client) PruneBlockchain( return response.Result, nil } +// RawMempool returns an array of all transaction +// hashes currently in the mempool. +func (b *Client) RawMempool( + ctx context.Context, +) ([]string, error) { + // Parameters: + // 1. verbose + params := []interface{}{false} + + response := &rawMempoolResponse{} + if err := b.post(ctx, requestMethodRawMempool, params, response); err != nil { + return nil, fmt.Errorf("%w: error getting raw mempool", err) + } + + return response.Result, nil +} + // getPeerInfo performs the `getpeerinfo` JSON-RPC request func (b *Client) getPeerInfo( ctx context.Context, diff --git a/bitcoin/types.go b/bitcoin/types.go index 9891157..8942577 100644 --- a/bitcoin/types.go +++ b/bitcoin/types.go @@ -478,6 +478,25 @@ func (s suggestedFeeRateResponse) Err() error { ) } +// rawMempoolResponse is the response body for `getrawmempool` requests. +type rawMempoolResponse struct { + Result []string `json:"result"` + Error *responseError `json:"error"` +} + +func (r rawMempoolResponse) Err() error { + if r.Error == nil { + return nil + } + + return fmt.Errorf( + "%w: error JSON RPC response, code: %d, message: %s", + ErrJSONRPCError, + r.Error.Code, + r.Error.Message, + ) +} + // CoinIdentifier converts a tx hash and vout into // the canonical CoinIdentifier.Identifier used in // rosetta-bitcoin. diff --git a/mocks/services/client.go b/mocks/services/client.go index d6aa25d..838124f 100644 --- a/mocks/services/client.go +++ b/mocks/services/client.go @@ -38,6 +38,29 @@ func (_m *Client) NetworkStatus(_a0 context.Context) (*types.NetworkStatusRespon return r0, r1 } +// RawMempool provides a mock function with given fields: _a0 +func (_m *Client) RawMempool(_a0 context.Context) ([]string, error) { + ret := _m.Called(_a0) + + var r0 []string + if rf, ok := ret.Get(0).(func(context.Context) []string); ok { + r0 = rf(_a0) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).([]string) + } + } + + var r1 error + if rf, ok := ret.Get(1).(func(context.Context) error); ok { + r1 = rf(_a0) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + // SendRawTransaction provides a mock function with given fields: _a0, _a1 func (_m *Client) SendRawTransaction(_a0 context.Context, _a1 string) (string, error) { ret := _m.Called(_a0, _a1) diff --git a/services/mempool_service.go b/services/mempool_service.go index f5d232b..3e4498a 100644 --- a/services/mempool_service.go +++ b/services/mempool_service.go @@ -22,11 +22,15 @@ import ( ) // MempoolAPIService implements the server.MempoolAPIServicer interface. -type MempoolAPIService struct{} +type MempoolAPIService struct { + client Client +} // NewMempoolAPIService creates a new instance of a MempoolAPIService. -func NewMempoolAPIService() server.MempoolAPIServicer { - return &MempoolAPIService{} +func NewMempoolAPIService(client Client) server.MempoolAPIServicer { + return &MempoolAPIService{ + client: client, + } } // Mempool implements the /mempool endpoint. @@ -34,7 +38,19 @@ func (s *MempoolAPIService) Mempool( ctx context.Context, request *types.NetworkRequest, ) (*types.MempoolResponse, *types.Error) { - return nil, wrapErr(ErrUnimplemented, nil) + mempoolTransactions, err := s.client.RawMempool(ctx) + if err != nil { + return nil, wrapErr(ErrBitcoind, err) + } + + transactionIdentifiers := make([]*types.TransactionIdentifier, len(mempoolTransactions)) + for i, mempoolTransaction := range mempoolTransactions { + transactionIdentifiers[i] = &types.TransactionIdentifier{Hash: mempoolTransaction} + } + + return &types.MempoolResponse{ + TransactionIdentifiers: transactionIdentifiers, + }, nil } // MempoolTransaction implements the /mempool/transaction endpoint. diff --git a/services/mempool_service_test.go b/services/mempool_service_test.go index d1e6a4b..0484120 100644 --- a/services/mempool_service_test.go +++ b/services/mempool_service_test.go @@ -18,20 +18,37 @@ import ( "context" "testing" + mocks "github.com/coinbase/rosetta-bitcoin/mocks/services" + + "github.com/coinbase/rosetta-sdk-go/types" "github.com/stretchr/testify/assert" ) func TestMempoolEndpoints(t *testing.T) { - servicer := NewMempoolAPIService() + mockClient := &mocks.Client{} + servicer := NewMempoolAPIService(mockClient) ctx := context.Background() + mockClient.On("RawMempool", ctx).Return([]string{ + "tx1", + "tx2", + }, nil) mem, err := servicer.Mempool(ctx, nil) - assert.Nil(t, mem) - assert.Equal(t, ErrUnimplemented.Code, err.Code) - assert.Equal(t, ErrUnimplemented.Message, err.Message) + assert.Nil(t, err) + assert.Equal(t, &types.MempoolResponse{ + TransactionIdentifiers: []*types.TransactionIdentifier{ + { + Hash: "tx1", + }, + { + Hash: "tx2", + }, + }, + }, mem) memTransaction, err := servicer.MempoolTransaction(ctx, nil) assert.Nil(t, memTransaction) assert.Equal(t, ErrUnimplemented.Code, err.Code) assert.Equal(t, ErrUnimplemented.Message, err.Message) + mockClient.AssertExpectations(t) } diff --git a/services/router.go b/services/router.go index bd0e6cd..423389d 100644 --- a/services/router.go +++ b/services/router.go @@ -55,7 +55,7 @@ func NewBlockchainRouter( asserter, ) - mempoolAPIService := NewMempoolAPIService() + mempoolAPIService := NewMempoolAPIService(client) mempoolAPIController := server.NewMempoolAPIController( mempoolAPIService, asserter, diff --git a/services/types.go b/services/types.go index 7d8b8af..dad143d 100644 --- a/services/types.go +++ b/services/types.go @@ -28,6 +28,7 @@ type Client interface { NetworkStatus(context.Context) (*types.NetworkStatusResponse, error) SendRawTransaction(context.Context, string) (string, error) SuggestedFeeRate(context.Context, int64) (float64, error) + RawMempool(context.Context) ([]string, error) } // Indexer is used by the servicers to get block and account data. From 4818a544f49312245b68c7eae21d16f1b1f543d2 Mon Sep 17 00:00:00 2001 From: Patrick O'Grady Date: Fri, 18 Sep 2020 18:10:56 -0700 Subject: [PATCH 2/3] Add test for bitcoin mempool implementation --- bitcoin/client_fixtures/raw_mempool.json | 9 ++++ bitcoin/client_test.go | 66 ++++++++++++++++++++++++ 2 files changed, 75 insertions(+) create mode 100644 bitcoin/client_fixtures/raw_mempool.json diff --git a/bitcoin/client_fixtures/raw_mempool.json b/bitcoin/client_fixtures/raw_mempool.json new file mode 100644 index 0000000..115fa59 --- /dev/null +++ b/bitcoin/client_fixtures/raw_mempool.json @@ -0,0 +1,9 @@ +{ + "result": [ + "9cec12d170e97e21a876fa2789e6bfc25aa22b8a5e05f3f276650844da0c33ab", + "37b4fcc8e0b229412faeab8baad45d3eb8e4eec41840d6ac2103987163459e75", + "7bbb29ae32117597fcdf21b464441abd571dad52d053b9c2f7204f8ea8c4762e" + ], + "error": null, + "id": "curltest" +} diff --git a/bitcoin/client_test.go b/bitcoin/client_test.go index b2f8278..554e3dc 100644 --- a/bitcoin/client_test.go +++ b/bitcoin/client_test.go @@ -1243,6 +1243,72 @@ func TestSuggestedFeeRate(t *testing.T) { } } +func TestRawMempool(t *testing.T) { + tests := map[string]struct { + responses []responseFixture + + expectedTransactions []string + expectedError error + }{ + "successful": { + responses: []responseFixture{ + { + status: http.StatusOK, + body: loadFixture("raw_mempool.json"), + url: url, + }, + }, + expectedTransactions: []string{ + "9cec12d170e97e21a876fa2789e6bfc25aa22b8a5e05f3f276650844da0c33ab", + "37b4fcc8e0b229412faeab8baad45d3eb8e4eec41840d6ac2103987163459e75", + "7bbb29ae32117597fcdf21b464441abd571dad52d053b9c2f7204f8ea8c4762e", + }, + }, + "500 error": { + responses: []responseFixture{ + { + status: http.StatusInternalServerError, + body: "{}", + url: url, + }, + }, + expectedError: errors.New("invalid response: 500 Internal Server Error"), + }, + } + + for name, test := range tests { + t.Run(name, func(t *testing.T) { + var ( + assert = assert.New(t) + ) + + responses := make(chan responseFixture, len(test.responses)) + for _, response := range test.responses { + responses <- response + } + + ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + response := <-responses + assert.Equal("application/json", r.Header.Get("Content-Type")) + assert.Equal("POST", r.Method) + assert.Equal(response.url, r.URL.RequestURI()) + + w.WriteHeader(response.status) + fmt.Fprintln(w, response.body) + })) + + client := NewClient(ts.URL, MainnetGenesisBlockIdentifier, MainnetCurrency) + txs, err := client.RawMempool(context.Background()) + if test.expectedError != nil { + assert.Contains(err.Error(), test.expectedError.Error()) + } else { + assert.NoError(err) + assert.Equal(test.expectedTransactions, txs) + } + }) + } +} + // loadFixture takes a file name and returns the response fixture. func loadFixture(fileName string) string { content, err := ioutil.ReadFile(fmt.Sprintf("client_fixtures/%s", fileName)) From 32d101ddc5c5fd4a7a68f7fb050e863f6f0514c1 Mon Sep 17 00:00:00 2001 From: Patrick O'Grady Date: Fri, 18 Sep 2020 18:12:07 -0700 Subject: [PATCH 3/3] Update README --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 0cd0361..3c0762b 100644 --- a/README.md +++ b/README.md @@ -168,7 +168,7 @@ and run one of the following commands: ## Future Work * Publish benchamrks for sync speed, storage usage, and load testing -* Rosetta API `/mempool/*` implementation +* [Rosetta API `/mempool/transaction`](https://www.rosetta-api.org/docs/MempoolApi.html#mempooltransaction) implementation * Add CI test using `rosetta-cli` to run on each PR (likely on a regtest network) * Add performance mode to use unlimited RAM (implementation currently optimized to use <= 16 GB of RAM) * Support Multi-Sig Sends