nearapi.go 6.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204
  1. package nearapi
  2. import (
  3. "bytes"
  4. "context"
  5. "errors"
  6. "fmt"
  7. "math/rand"
  8. "net/http"
  9. "time"
  10. "github.com/certusone/wormhole/node/pkg/common"
  11. "github.com/mr-tron/base58"
  12. )
  13. const (
  14. nearRPCTimeout = 20 * time.Second
  15. /*
  16. NEAR JSON RPC node is starting up with 4 workers
  17. (https://github.com/near/nearcore/blob/8dc9a0bab8aa4648fc7af777e9fa7e3e545c95a5/chain/jsonrpc/src/lib.rs#L1372)
  18. and actix_web by default supports 256 concurrent TLS connections per worker and 25k non-TLS
  19. (https://actix.rs/actix-web/actix_web/struct.HttpServer.html#method.workers).
  20. Therefore, the Guardian NEAR RPC node should allow at least 500 concurrent connections.
  21. According to https://explorer.near.org/stats, NEAR blockchain has bursts of up to 2M tx/day,
  22. so 500 concurrent RPC connections should be sufficient.
  23. */
  24. nearRPCConcurrentConnections = 500
  25. )
  26. type (
  27. NearRpc interface {
  28. Query(ctx context.Context, s string) ([]byte, error)
  29. }
  30. HttpNearRpc struct {
  31. nearRpc string
  32. nearHttpClient *http.Client
  33. }
  34. NearApi interface {
  35. GetBlock(ctx context.Context, blockId string) (Block, error)
  36. GetBlockByHeight(ctx context.Context, blockHeight uint64) (Block, error)
  37. GetFinalBlock(ctx context.Context) (Block, error)
  38. GetChunk(ctx context.Context, chunkHeader ChunkHeader) (Chunk, error)
  39. GetTxStatus(ctx context.Context, txHash string, senderAccountId string) ([]byte, error)
  40. GetVersion(ctx context.Context) (string, error)
  41. }
  42. NearApiImpl struct {
  43. nearRPC NearRpc
  44. }
  45. )
  46. func NewHttpNearRpc(nearRPC string) HttpNearRpc {
  47. // Customize the Transport to have larger connection pool (default is only 2 per host)
  48. //nolint:forcetypeassert // This should always succeed, and the function is only called on start-up.
  49. t := http.DefaultTransport.(*http.Transport).Clone()
  50. t.MaxConnsPerHost = nearRPCConcurrentConnections
  51. t.MaxIdleConnsPerHost = nearRPCConcurrentConnections
  52. var httpClient = &http.Client{
  53. Timeout: nearRPCTimeout,
  54. Transport: t,
  55. }
  56. return HttpNearRpc{nearRPC, httpClient}
  57. }
  58. func NewNearApiImpl(nearRpc NearRpc) NearApiImpl {
  59. return NearApiImpl{nearRpc}
  60. }
  61. func (n HttpNearRpc) Query(ctx context.Context, s string) ([]byte, error) {
  62. timeout, cancelFunc := context.WithTimeout(ctx, nearRPCTimeout)
  63. defer cancelFunc()
  64. timer := time.NewTimer(time.Nanosecond)
  65. var backoffMilliseconds int = 100
  66. for {
  67. select {
  68. case <-timeout.Done():
  69. return nil, errors.New("HTTP timeout")
  70. case <-timer.C:
  71. // perform HTTP request
  72. req, _ := http.NewRequestWithContext(timeout, http.MethodPost, n.nearRpc, bytes.NewBuffer([]byte(s)))
  73. req.Header.Add("Content-Type", "application/json")
  74. resp, err := n.nearHttpClient.Do(req)
  75. if err == nil {
  76. defer resp.Body.Close()
  77. result, err := common.SafeRead(resp.Body)
  78. if resp.StatusCode == 200 {
  79. return result, err
  80. }
  81. }
  82. // retry if there was a server error
  83. backoffMilliseconds += int((float64(backoffMilliseconds)) * (rand.Float64() * 2.5)) //#nosec G404 no CSPRNG needed here for jitter computation
  84. timer.Reset(time.Millisecond * time.Duration(backoffMilliseconds))
  85. }
  86. }
  87. }
  88. // getBlock calls the NEAR RPC API to retrieve a block by its hash (https://docs.near.org/api/rpc/block-chunk#block-details)
  89. func (n NearApiImpl) GetBlock(ctx context.Context, blockId string) (Block, error) {
  90. s := fmt.Sprintf(`{"id": "dontcare", "jsonrpc": "2.0", "method": "block", "params": {"block_id": "%s"}}`, blockId)
  91. blockBytes, err := n.nearRPC.Query(ctx, s)
  92. if err != nil {
  93. return Block{}, err
  94. }
  95. newBlock, err := NewBlockFromBytes(blockBytes)
  96. if err != nil {
  97. return Block{}, err
  98. }
  99. // SECURITY defense-in-depth
  100. if newBlock.Header.Hash != blockId {
  101. return Block{}, errors.New("returned block hash does not equal queried block hash")
  102. }
  103. return newBlock, err
  104. }
  105. // getBlockByHeight calls the NEAR RPC API to retrieve a block by its height (https://docs.near.org/api/rpc/block-chunk#block-details)
  106. func (n NearApiImpl) GetBlockByHeight(ctx context.Context, blockHeight uint64) (Block, error) {
  107. s := fmt.Sprintf(`{"id": "dontcare", "jsonrpc": "2.0", "method": "block", "params": {"block_id": %d}}`, blockHeight)
  108. blockBytes, err := n.nearRPC.Query(ctx, s)
  109. if err != nil {
  110. return Block{}, err
  111. }
  112. newBlock, err := NewBlockFromBytes(blockBytes)
  113. if err != nil {
  114. return Block{}, err
  115. }
  116. // SECURITY defense-in-depth
  117. if newBlock.Header.Height != blockHeight {
  118. return Block{}, errors.New("returned block height not equal queried block height")
  119. }
  120. return newBlock, nil
  121. }
  122. // getFinalBlock gets a finalized block from the NEAR RPC API using the parameter "finality": "final" (https://docs.near.org/api/rpc/block-chunk)
  123. func (n NearApiImpl) GetFinalBlock(ctx context.Context) (Block, error) {
  124. s := `{"id": "dontcare", "jsonrpc": "2.0", "method": "block", "params": {"finality": "final"}}`
  125. blockBytes, err := n.nearRPC.Query(ctx, s)
  126. if err != nil {
  127. return Block{}, err
  128. }
  129. return NewBlockFromBytes(blockBytes)
  130. }
  131. // getChunk gets a chunk from the NEAR RPC API: https://docs.near.org/api/rpc/block-chunk#chunk-details
  132. func (n NearApiImpl) GetChunk(ctx context.Context, chunkHeader ChunkHeader) (Chunk, error) {
  133. s := fmt.Sprintf(`{"id": "dontcare", "jsonrpc": "2.0", "method": "chunk", "params": {"chunk_id": "%s"}}`, chunkHeader.Hash)
  134. resBytes, err := n.nearRPC.Query(ctx, s)
  135. if err != nil {
  136. return Chunk{}, err
  137. }
  138. newChunk, err := NewChunkFromBytes(resBytes)
  139. if err != nil {
  140. return Chunk{}, err
  141. }
  142. // SECURITY defense-in-depth
  143. if newChunk.Hash != chunkHeader.Hash {
  144. fmt.Printf("queried hash=%s, return_hash=%s", chunkHeader.Hash, newChunk.Hash)
  145. return Chunk{}, errors.New("returned chunk hash does not equal queried chunk hash")
  146. }
  147. return newChunk, nil
  148. }
  149. // getTxStatus queries status of a transaction by hash, returning the transaction_outcomes and receipts_outcomes
  150. // sender_account_id is used to determine which shard to query for the transaction
  151. // See https://docs.near.org/api/rpc/transactions#transaction-status
  152. func (n NearApiImpl) GetTxStatus(ctx context.Context, txHash string, senderAccountId string) ([]byte, error) {
  153. s := fmt.Sprintf(`{"id": "dontcare", "jsonrpc": "2.0", "method": "tx", "params": ["%s", "%s"]}`, txHash, senderAccountId)
  154. return n.nearRPC.Query(ctx, s)
  155. }
  156. func (n NearApiImpl) GetVersion(ctx context.Context) (string, error) {
  157. s := `{"id": "dontcare", "jsonrpc": "2.0", "method": "status"}`
  158. versionBytes, err := n.nearRPC.Query(ctx, s)
  159. if err != nil {
  160. return "", err
  161. }
  162. version, err := VersionFromBytes(versionBytes)
  163. if err != nil {
  164. return "", err
  165. }
  166. return version, nil
  167. }
  168. func IsWellFormedHash(hash string) error {
  169. hashBytes, err := base58.Decode(hash)
  170. if err != nil {
  171. return err
  172. }
  173. if len(hashBytes) != 32 {
  174. return errors.New("base58-decoded hash is not 32 bytes")
  175. }
  176. return nil
  177. }