Merge branch 'main' into traceDB

pull/3799/head
Rongjian Lan 3 years ago committed by GitHub
commit 52da22f2e1
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 355
      .github/workflows/ci-tag.yaml
  2. 3
      .gitignore
  3. 2
      Dockerfile
  4. 3
      README.md
  5. 6
      api/service/blockproposal/service.go
  6. 6
      api/service/consensus/service.go
  7. 6
      api/service/explorer/service.go
  8. 51
      api/service/legacysync/syncing.go
  9. 5
      api/service/manager.go
  10. 243
      api/service/pprof/service.go
  11. 113
      api/service/pprof/service_test.go
  12. 6
      api/service/prometheus/service.go
  13. 6
      api/service/synchronize/service.go
  14. 11
      cmd/bootnode/main.go
  15. 57
      cmd/harmony/config_migrations.go
  16. 2
      cmd/harmony/config_test.go
  17. 12
      cmd/harmony/default.go
  18. 109
      cmd/harmony/flags.go
  19. 113
      cmd/harmony/flags_test.go
  20. 42
      cmd/harmony/main.go
  21. 6
      consensus/checks.go
  22. 4
      consensus/consensus.go
  23. 6
      consensus/consensus_v2.go
  24. 1
      consensus/construct.go
  25. 4
      consensus/fbft_log.go
  26. 2
      consensus/leader.go
  27. 25
      consensus/quorum/one-node-one-vote.go
  28. 18
      consensus/quorum/one-node-staked-vote.go
  29. 17
      consensus/quorum/quorum.go
  30. 6
      consensus/reward/rewarder.go
  31. 116
      consensus/validator.go
  32. 4
      consensus/votepower/roster.go
  33. 15
      core/blockchain.go
  34. 2
      core/events.go
  35. 41
      core/evm.go
  36. 21
      core/offchain.go
  37. 2
      core/rawdb/accessors_offchain.go
  38. 5
      core/state/statedb.go
  39. 303
      core/state/statedb_test.go
  40. 67
      core/state_processor.go
  41. 3
      core/state_transition.go
  42. 8
      core/tx_pool.go
  43. 7
      core/types.go
  44. 1
      core/vm/contract.go
  45. 79
      core/vm/contracts.go
  46. 64
      core/vm/contracts_test.go
  47. 33
      core/vm/evm.go
  48. 12
      core/vm/instructions.go
  49. 1
      core/vm/runtime/env.go
  50. 626
      eth/rpc/client.go
  51. 88
      eth/rpc/client_example_test.go
  52. 569
      eth/rpc/client_test.go
  53. 33
      eth/rpc/constants_unix.go
  54. 25
      eth/rpc/constants_unix_nocgo.go
  55. 110
      eth/rpc/doc.go
  56. 102
      eth/rpc/endpoints.go
  57. 65
      eth/rpc/errors.go
  58. 66
      eth/rpc/gzip.go
  59. 402
      eth/rpc/handler.go
  60. 360
      eth/rpc/http.go
  61. 54
      eth/rpc/http_test.go
  62. 33
      eth/rpc/inproc.go
  63. 56
      eth/rpc/ipc.go
  64. 37
      eth/rpc/ipc_js.go
  65. 54
      eth/rpc/ipc_unix.go
  66. 48
      eth/rpc/ipc_windows.go
  67. 329
      eth/rpc/json.go
  68. 68
      eth/rpc/metrics.go
  69. 147
      eth/rpc/server.go
  70. 152
      eth/rpc/server_test.go
  71. 261
      eth/rpc/service.go
  72. 66
      eth/rpc/stdio.go
  73. 328
      eth/rpc/subscription.go
  74. 207
      eth/rpc/subscription_test.go
  75. 7
      eth/rpc/testdata/invalid-badid.js
  76. 14
      eth/rpc/testdata/invalid-batch.js
  77. 7
      eth/rpc/testdata/invalid-idonly.js
  78. 4
      eth/rpc/testdata/invalid-nonobj.js
  79. 5
      eth/rpc/testdata/invalid-syntax.json
  80. 8
      eth/rpc/testdata/reqresp-batch.js
  81. 16
      eth/rpc/testdata/reqresp-echo.js
  82. 5
      eth/rpc/testdata/reqresp-namedparam.js
  83. 4
      eth/rpc/testdata/reqresp-noargsrets.js
  84. 4
      eth/rpc/testdata/reqresp-nomethod.js
  85. 4
      eth/rpc/testdata/reqresp-noparam.js
  86. 4
      eth/rpc/testdata/reqresp-paramsnull.js
  87. 6
      eth/rpc/testdata/revcall.js
  88. 7
      eth/rpc/testdata/revcall2.js
  89. 12
      eth/rpc/testdata/subscription.js
  90. 181
      eth/rpc/testservice_test.go
  91. 200
      eth/rpc/types.go
  92. 125
      eth/rpc/types_test.go
  93. 175
      eth/rpc/websocket.go
  94. 259
      eth/rpc/websocket_test.go
  95. 22
      go.mod
  96. 225
      hmy/gasprice.go
  97. 18
      hmy/hmy.go
  98. 5
      hmy/pool.go
  99. 2
      hmy/staking.go
  100. 4
      hmy/tracer.go
  101. Some files were not shown because too many files have changed in this diff Show More

@ -0,0 +1,355 @@
name: Manual release harmony (need tag)
on:
workflow_dispatch:
inputs:
tag:
decription: 'tag value to create the release'
required: true
jobs:
check:
name: Per-check for current tag
runs-on: ubuntu-18.04
continue-on-error: false
outputs:
tag_annotated: ${{ steps.check-tag-annotated.outputs.tag_annotated }}
steps:
- name: Checkout harmony core code
uses: actions/checkout@v2
with:
path: harmony
ref: ${{ github.event.inputs.tag }}
fetch-depth: 0
- name: Check tag annotated
id: check-tag-annotated
run: |
VERSION=$(git tag -l --sort=-v:refname | head -n 1)
if git rev-parse $VERSION^{tag} -- &>/dev/null
then
echo "::set-output name=tag_annotated::true"
else
echo "::set-output name=tag_annotated::false"
fi
working-directory: harmony
build:
name: Build harmony binary
needs: check
runs-on: ${{ matrix.os }}
if: needs.check.outputs.tag_annotated == 'true'
strategy:
matrix:
os: [ubuntu-18.04, macos-10.15, [self-hosted, linux, ARM64]]
steps:
- name: Import GPG key
if: join(matrix.os, '-') != 'self-hosted-linux-ARM64'
uses: crazy-max/ghaction-import-gpg@v3
with:
gpg-private-key: ${{ secrets.GPG_PRIVATE_KEY }}
passphrase: ${{ secrets.GPG_PRIVATE_KEY_PASS }}
- name: Set up Go 1.14
uses: actions/setup-go@v2
with:
go-version: 1.14.14
- name: Checkout dependence repo
uses: actions/checkout@v2
with:
repository: harmony-one/mcl
path: mcl
- name: Checkout dependence repo
uses: actions/checkout@v2
with:
repository: harmony-one/bls
path: bls
- name: Checkout harmony core code
uses: actions/checkout@v2
with:
path: harmony
ref: ${{ github.event.inputs.tag }}
fetch-depth: 0
- name: Get latest version and release
run: |
VERSION=$(git tag -l --sort=-v:refname | head -n 1 | tr -d v)
RELEASE=$(git describe --long | cut -f2 -d-)
echo "build_version=$VERSION" >> $GITHUB_ENV
echo "build_release=$RELEASE" >> $GITHUB_ENV
working-directory: harmony
- name: Build harmony binary and packages for Linux
if: matrix.os == 'ubuntu-18.04'
run: |
make linux_static
make deb
echo %_signature gpg >> $HOME/.rpmmacros && echo "%_gpg_name Harmony (harmony.one)" >> $HOME/.rpmmacros
make rpm
mv ./bin/harmony ./bin/harmony-amd64
mv $HOME/debbuild/harmony-$build_version-$build_release.deb ./bin/
mv $HOME/rpmbuild/RPMS/x86_64/harmony-$build_version-$build_release.x86_64.rpm ./bin/
working-directory: harmony
- name: Build harmony binary and packages for Linux on ARM64
if: join(matrix.os, '-') == 'self-hosted-linux-ARM64'
run: |
make linux_static
mv ./bin/harmony ./bin/harmony-arm64
working-directory: harmony
- name: Build harmony binary and packages for MacOS
if: matrix.os == 'macos-10.15'
run: |
brew install bash
sudo rm -f /usr/local/opt/openssl
sudo ln -sf /usr/local/opt/openssl@1.1 /usr/local/opt/openssl
make
cd ./bin && mkdir ./lib && mv ./*.dylib ./lib && rm -f ./bootnode
gpg --detach-sign harmony
zip -qr ./harmony-macos.zip ./*
rm -rf `ls * | egrep -v harmony-macos.zip`
working-directory: harmony
- name: Upload artifact
uses: actions/upload-artifact@v2
with:
name: harmony
path: harmony/bin/*
retention-days: 1
docker-build:
name: Build and push harmony docker image
needs: [check, build]
runs-on: ubuntu-18.04
if: needs.check.outputs.tag_annotated == 'true'
steps:
- name: Checkout harmony core code
uses: actions/checkout@v2
with:
path: harmony
ref: ${{ github.event.inputs.tag }}
fetch-depth: 0
- name: Get latest version
run: |
VERSION=$(git tag -l --sort=-v:refname | head -n 1 | tr -d v)
RELEASE=$(git describe --long | cut -f2 -d-)
echo "build_version=$VERSION" >> $GITHUB_ENV
echo "build_release=$RELEASE" >> $GITHUB_ENV
working-directory: harmony
- name: Download artifact
uses: actions/download-artifact@v2
with:
name: harmony
- name: Build preview works
run: |
mv $GITHUB_WORKSPACE/harmony-amd64 ./scripts/docker/harmony
working-directory: harmony
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v1
- name: Login to DockerHub
uses: docker/login-action@v1
with:
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_PASSWORD }}
- name: Build and push
uses: docker/build-push-action@v2
with:
context: ./harmony/scripts/docker
file: ./harmony/scripts/docker/Dockerfile
push: true
tags: |
harmonyone/harmony:${{ github.event.inputs.tag }}
harmonyone/harmony:${{ env.build_version }}-${{ env.build_release }}
release-page:
name: Sign binary and create and publish release page
needs: [check, build]
runs-on: ubuntu-18.04
if: needs.check.outputs.tag_annotated == 'true'
steps:
- name: Import GPG key
uses: crazy-max/ghaction-import-gpg@v3
with:
gpg-private-key: ${{ secrets.GPG_PRIVATE_KEY }}
passphrase: ${{ secrets.GPG_PRIVATE_KEY_PASS }}
- name: Checkout harmony core code
uses: actions/checkout@v2
with:
path: harmony
ref: ${{ github.event.inputs.tag }}
fetch-depth: 0
- name: Get latest version
run: |
VERSION=$(git tag -l --sort=-v:refname | head -n 1 | tr -d v)
VERSION_LONG=$(git describe --always --long --dirty)
RELEASE=$(git describe --long | cut -f2 -d-)
echo "build_version=$VERSION" >> $GITHUB_ENV
echo "build_version_long=$VERSION_LONG" >> $GITHUB_ENV
echo "build_release=$RELEASE" >> $GITHUB_ENV
working-directory: harmony
- name: Download artifact
uses: actions/download-artifact@v2
with:
name: harmony
- name: Signed amd64 harmony binary
run: |
gpg --detach-sign harmony-amd64
sha256sum harmony-amd64 >> harmony-amd64.sha256
- name: Signed arm64 harmony binary
run: |
gpg --detach-sign harmony-arm64
sha256sum harmony-arm64 >> harmony-arm64.sha256
- name: Signed amd64 harmony binary
run: |
shasum -a 256 harmony-macos.zip >> harmony-macos.zip.sha256
- name: Get tag message
env:
TAG_SHA: ${{ github.event.after }}
run: |
touch ./tag_message.md
TAG_MESSAGE=$(git cat-file tag v$build_version | tail -n+6)
echo -e "$TAG_MESSAGE\n\nThe released version: $build_version_long" >> ./tag_message.md
working-directory: harmony
- name: Create Release
id: create_release
uses: actions/create-release@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
tag_name: ${{ github.ref }}
release_name: Mainnet Release ${{ env.build_version }}
draft: true
prerelease: false
body_path: ./harmony/tag_message.md
- name: Upload harmony binary for Linux
uses: actions/upload-release-asset@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
upload_url: ${{ steps.create_release.outputs.upload_url }}
asset_path: ./harmony-amd64
asset_name: harmony
asset_content_type: application/octet-stream
- name: Upload harmony deb package for Linux
uses: actions/upload-release-asset@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
upload_url: ${{ steps.create_release.outputs.upload_url }}
asset_path: ./harmony-${{ env.build_version }}-${{ env.build_release }}.deb
asset_name: harmony-${{ env.build_version }}.deb
asset_content_type: application/x-deb
- name: Upload harmony rpm package for Linux
uses: actions/upload-release-asset@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
upload_url: ${{ steps.create_release.outputs.upload_url }}
asset_path: ./harmony-${{ env.build_version }}-${{ env.build_release }}.x86_64.rpm
asset_name: harmony-${{ env.build_version }}.x86_64.rpm
asset_content_type: application/x-rpm
- name: Upload harmony amd64 binary for Linux
uses: actions/upload-release-asset@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
upload_url: ${{ steps.create_release.outputs.upload_url }}
asset_path: ./harmony-amd64
asset_name: harmony-amd64
asset_content_type: application/octet-stream
- name: Upload sha256 signature of harmony amd64 binary for Linux
uses: actions/upload-release-asset@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
upload_url: ${{ steps.create_release.outputs.upload_url }}
asset_path: ./harmony-amd64.sha256
asset_name: harmony-amd64.sha256
asset_content_type: text/plain
- name: Upload gpg signature of harmony amd64 binary for Linux
uses: actions/upload-release-asset@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
upload_url: ${{ steps.create_release.outputs.upload_url }}
asset_path: ./harmony-amd64.sig
asset_name: harmony-amd64.sig
asset_content_type: application/octet-stream
- name: Upload harmony arm64 binary for Linux
uses: actions/upload-release-asset@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
upload_url: ${{ steps.create_release.outputs.upload_url }}
asset_path: ./harmony-arm64
asset_name: harmony-arm64
asset_content_type: application/octet-stream
- name: Upload sha256 signature of harmony arm64 binary for Linux
uses: actions/upload-release-asset@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
upload_url: ${{ steps.create_release.outputs.upload_url }}
asset_path: ./harmony-arm64.sha256
asset_name: harmony-arm64.sha256
asset_content_type: text/plain
- name: Upload gpg signature of harmony arm64 binary for Linux
uses: actions/upload-release-asset@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
upload_url: ${{ steps.create_release.outputs.upload_url }}
asset_path: ./harmony-arm64.sig
asset_name: harmony-arm64.sig
asset_content_type: application/octet-stream
- name: Upload harmony binary for MacOS
uses: actions/upload-release-asset@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
upload_url: ${{ steps.create_release.outputs.upload_url }}
asset_path: ./harmony-macos.zip
asset_name: harmony-macos-${{ env.build_version }}.zip
asset_content_type: application/zip
- name: Upload sha256 signature of harmony for MacOS
uses: actions/upload-release-asset@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
upload_url: ${{ steps.create_release.outputs.upload_url }}
asset_path: ./harmony-macos.zip.sha256
asset_name: harmony-macos.zip.sha256
asset_content_type: text/plain

3
.gitignore vendored

@ -91,3 +91,6 @@ explorer_storage_*
# local blskeys for testing
.hmy/blskeys
# pprof profiles
profiles/*.pb.gz

@ -74,6 +74,8 @@ WORKDIR ${HMY_PATH}/harmony
RUN eval "$(~/bin/gimme ${GIMME_GO_VERSION})" ; scripts/install_build_tools.sh
RUN go mod tidy
RUN eval "$(~/bin/gimme ${GIMME_GO_VERSION})" ; scripts/go_executable_build.sh -S
RUN cd ${HMY_PATH}/go-sdk && make -j8 && cp hmy /root/bin

@ -37,7 +37,7 @@ sudo yum install glibc-static gmp-devel gmp-static openssl-libs openssl-static g
On macOS:
```bash
brew cask install docker
brew install --cask docker
open /Applications/Docker.app
```
On Linux, reference official documentation [here](https://docs.docker.com/engine/install/).
@ -69,6 +69,7 @@ cd harmony
3. Build the harmony binary & dependent libs
```
go mod tidy
make
```
> Run `bash scripts/install_build_tools.sh` to ensure build tools are of correct versions.

@ -1,7 +1,6 @@
package blockproposal
import (
"github.com/ethereum/go-ethereum/rpc"
msg_pb "github.com/harmony-one/harmony/api/proto/message"
"github.com/harmony-one/harmony/consensus"
"github.com/harmony-one/harmony/internal/utils"
@ -43,8 +42,3 @@ func (s *Service) Stop() error {
utils.Logger().Info().Msg("Role conversion stopped.")
return nil
}
// APIs for the services.
func (s *Service) APIs() []rpc.API {
return nil
}

@ -1,7 +1,6 @@
package consensus
import (
"github.com/ethereum/go-ethereum/rpc"
msg_pb "github.com/harmony-one/harmony/api/proto/message"
"github.com/harmony-one/harmony/consensus"
"github.com/harmony-one/harmony/core/types"
@ -41,8 +40,3 @@ func (s *Service) Stop() error {
utils.Logger().Info().Msg("Consensus service stopped.")
return s.consensus.Close()
}
// APIs for the services.
func (s *Service) APIs() []rpc.API {
return nil
}

@ -14,7 +14,6 @@ import (
"github.com/harmony-one/harmony/core/types"
ethCommon "github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/rpc"
"github.com/gorilla/mux"
msg_pb "github.com/harmony-one/harmony/api/proto/message"
"github.com/harmony-one/harmony/core"
@ -304,11 +303,6 @@ func (s *Service) SetMessageChan(messageChan chan *msg_pb.Message) {
s.messageChan = messageChan
}
// APIs for the services.
func (s *Service) APIs() []rpc.API {
return nil
}
func defaultDBPath(ip, port string) string {
return path.Join(nodeconfig.GetDefaultConfig().DBDir, "explorer_storage_"+ip+"_"+port)
}

@ -162,6 +162,7 @@ func CreateStateSync(ip string, port string, peerHash [20]byte, isExplorer bool)
stateSync.lastMileBlocks = []*types.Block{}
stateSync.isExplorer = isExplorer
stateSync.syncConfig = &SyncConfig{}
stateSync.lastOutOfSyncResult = outOfSyncCheckResult{true, 0, 0}
return stateSync
}
@ -177,6 +178,15 @@ type StateSync struct {
stateSyncTaskQueue *queue.Queue
syncMux sync.Mutex
lastMileMux sync.Mutex
lastOutOfSyncResult outOfSyncCheckResult
outOfSyncResultLock sync.RWMutex
}
type outOfSyncCheckResult struct {
isOutOfSync bool
otherHeight uint64
heightDiff uint64
}
func (ss *StateSync) purgeAllBlocksFromCache() {
@ -836,9 +846,12 @@ func (ss *StateSync) UpdateBlockAndStatus(block *types.Block, bc *core.BlockChai
if err != nil {
return errors.Wrap(err, "parse commitSigAndBitmap")
}
startTime := time.Now()
if err := bc.Engine().VerifyHeaderSignature(bc, block.Header(), sig, bitmap); err != nil {
return errors.Wrapf(err, "verify header signature %v", block.Hash().String())
}
utils.Logger().Debug().Int64("elapsed time", time.Now().Sub(startTime).Milliseconds()).Msg("[Sync] VerifyHeaderSignature")
}
err := bc.Engine().VerifyHeader(bc, block.Header(), verifySeal)
if err == engine.ErrUnknownAncestor {
@ -1056,13 +1069,37 @@ func (ss *StateSync) GetMaxPeerHeight() uint64 {
// SyncStatus returns inSync, remote height, and difference between remote height and local height
func (ss *StateSync) SyncStatus(bc *core.BlockChain) (bool, uint64, uint64) {
outOfSync, remoteHeight, heightDiff := ss.IsOutOfSync(bc, false)
outOfSync, remoteHeight, heightDiff := ss.GetOutOfSyncStatus()
return !outOfSync, remoteHeight, heightDiff
}
// IsOutOfSync checks whether the node is out of sync from other peers
// It returns the sync status, remote height, and the difference betwen local blockchain and remote max height
func (ss *StateSync) IsOutOfSync(bc *core.BlockChain, doubleCheck bool) (bool, uint64, uint64) {
outOfSync, otherHeight, heightDiff := ss.isOutOfSync(bc, doubleCheck)
ss.outOfSyncResultLock.Lock()
ss.lastOutOfSyncResult = outOfSyncCheckResult{
isOutOfSync: outOfSync,
otherHeight: otherHeight,
heightDiff: heightDiff,
}
ss.outOfSyncResultLock.Unlock()
return outOfSync, otherHeight, heightDiff
}
// GetOutOfSyncStatus return the sync result of last isOutOfSync check.
// Return isOutofSync, otherHeight, and heightDifferent
func (ss *StateSync) GetOutOfSyncStatus() (bool, uint64, uint64) {
ss.outOfSyncResultLock.RLock()
defer ss.outOfSyncResultLock.RUnlock()
sr := ss.lastOutOfSyncResult
return sr.isOutOfSync, sr.otherHeight, sr.heightDiff
}
func (ss *StateSync) isOutOfSync(bc *core.BlockChain, doubleCheck bool) (bool, uint64, uint64) {
if ss.syncConfig == nil {
return true, 0, 0 // If syncConfig is not instantiated, return not in sync
}
@ -1071,11 +1108,15 @@ func (ss *StateSync) IsOutOfSync(bc *core.BlockChain, doubleCheck bool) (bool, u
wasOutOfSync := lastHeight+inSyncThreshold < otherHeight1
if !doubleCheck {
heightDiff := otherHeight1 - lastHeight
if otherHeight1 < lastHeight {
heightDiff = 0 //
}
utils.Logger().Info().
Uint64("OtherHeight", otherHeight1).
Uint64("lastHeight", lastHeight).
Msg("[SYNC] Checking sync status")
return wasOutOfSync, otherHeight1, otherHeight1 - lastHeight
return wasOutOfSync, otherHeight1, heightDiff
}
time.Sleep(1 * time.Second)
// double check the sync status after 1 second to confirm (avoid false alarm)
@ -1091,7 +1132,11 @@ func (ss *StateSync) IsOutOfSync(bc *core.BlockChain, doubleCheck bool) (bool, u
Uint64("currentHeight", currentHeight).
Msg("[SYNC] Checking sync status")
// Only confirm out of sync when the node has lower height and didn't move in heights for 2 consecutive checks
return wasOutOfSync && isOutOfSync && lastHeight == currentHeight, otherHeight2, otherHeight2 - currentHeight
heightDiff := otherHeight2 - lastHeight
if otherHeight2 < lastHeight {
heightDiff = 0 // overflow
}
return wasOutOfSync && isOutOfSync && lastHeight == currentHeight, otherHeight2, heightDiff
}
// SyncLoop will keep syncing with peers until catches up

@ -3,7 +3,6 @@ package service
import (
"fmt"
"github.com/ethereum/go-ethereum/rpc"
"github.com/harmony-one/harmony/internal/utils"
"github.com/pkg/errors"
"github.com/rs/zerolog"
@ -20,6 +19,7 @@ const (
Consensus
BlockProposal
NetworkInfo
Pprof
Prometheus
Synchronize
)
@ -36,6 +36,8 @@ func (t Type) String() string {
return "BlockProposal"
case NetworkInfo:
return "NetworkInfo"
case Pprof:
return "Pprof"
case Prometheus:
return "Prometheus"
case Synchronize:
@ -49,7 +51,6 @@ func (t Type) String() string {
type Service interface {
Start() error
Stop() error
APIs() []rpc.API // the list of RPC descriptors the service provides
}
// Manager stores all services for service manager.

@ -0,0 +1,243 @@
package pprof
import (
"fmt"
"log"
"net/http"
"os"
"path/filepath"
"runtime/pprof"
"sync"
"time"
"github.com/harmony-one/harmony/internal/utils"
)
// Config is the config for the pprof service
type Config struct {
Enabled bool
ListenAddr string
Folder string
ProfileNames []string
ProfileIntervals []int
ProfileDebugValues []int
}
type Profile struct {
Name string
Interval int
Debug int
ProfileRef *pprof.Profile
}
func (p Config) String() string {
return fmt.Sprintf("%v, %v, %v, %v/%v/%v", p.Enabled, p.ListenAddr, p.Folder, p.ProfileNames, p.ProfileIntervals, p.ProfileDebugValues)
}
// Constants for profile names
const (
CPU = "cpu"
)
// Service provides access to pprof profiles via HTTP and can save them to local disk periodically as user settings.
type Service struct {
config Config
profiles map[string]Profile
}
var (
initOnce sync.Once
svc = &Service{}
cpuFile *os.File
cpuLock sync.Mutex
)
// NewService creates the new pprof service
func NewService(cfg Config) *Service {
initOnce.Do(func() {
svc = newService(cfg)
})
return svc
}
func newService(cfg Config) *Service {
if !cfg.Enabled {
utils.Logger().Info().Msg("pprof service disabled...")
return nil
}
utils.Logger().Debug().Str("cfg", cfg.String()).Msg("pprof")
svc.config = cfg
profiles, err := cfg.unpackProfilesIntoMap()
if err != nil {
log.Fatal("could not unpack pprof profiles into map")
}
svc.profiles = profiles
return svc
}
// Start start the service
func (s *Service) Start() error {
dir, err := filepath.Abs(s.config.Folder)
if err != nil {
return err
}
err = os.MkdirAll(dir, os.FileMode(0755))
if err != nil {
return err
}
go func() {
utils.Logger().Info().Str("address", s.config.ListenAddr).Msg("starting pprof HTTP service")
http.ListenAndServe(s.config.ListenAddr, nil)
}()
if _, ok := s.profiles[CPU]; ok {
// The nature of the pprof CPU profile is fundamentally different to the other profiles, because it streams output to a file during profiling.
// Thus it has to be started outside of the defined interval.
go restartCpuProfile(dir)
}
for _, profile := range s.profiles {
scheduleProfile(profile, dir)
}
return nil
}
// Stop stop the service
func (s *Service) Stop() error {
dir, err := filepath.Abs(s.config.Folder)
if err != nil {
return err
}
for _, profile := range s.profiles {
if profile.Name == CPU {
stopCpuProfile()
} else {
err := saveProfile(profile, dir)
if err != nil {
utils.Logger().Error().Err(err).Msg(fmt.Sprintf("could not save pprof profile: %s", profile.Name))
}
}
}
return nil
}
// scheduleProfile schedules the provided profile based on the specified interval (e.g. saves the profile every x seconds)
func scheduleProfile(profile Profile, dir string) {
go func() {
if profile.Interval > 0 {
ticker := time.NewTicker(time.Second * time.Duration(profile.Interval))
defer ticker.Stop()
for {
select {
case <-ticker.C:
if profile.Name == CPU {
err := restartCpuProfile(dir)
if err != nil {
utils.Logger().Error().Err(err).Msg("could not start pprof CPU profile")
}
} else {
err := saveProfile(profile, dir)
if err != nil {
utils.Logger().Error().Err(err).Msg(fmt.Sprintf("could not save pprof profile: %s", profile.Name))
}
}
}
}
}
}()
}
// saveProfile saves the provided profile in the specified directory
func saveProfile(profile Profile, dir string) error {
f, err := newTempFile(dir, profile.Name, ".pb.gz")
if err != nil {
return err
}
defer f.Close()
if profile.ProfileRef == nil {
return nil
}
err = profile.ProfileRef.WriteTo(f, profile.Debug)
if err != nil {
return err
}
utils.Logger().Info().Msg(fmt.Sprintf("saved pprof profile in: %s", f.Name()))
return nil
}
// restartCpuProfile stops the current CPU profile, if any and then starts a new CPU profile. While profiling in the background, the profile will be buffered and written to a file.
func restartCpuProfile(dir string) error {
cpuLock.Lock()
defer cpuLock.Unlock()
stopCpuProfile()
f, err := newTempFile(dir, CPU, ".pb.gz")
if err != nil {
return err
}
pprof.StartCPUProfile(f)
cpuFile = f
utils.Logger().Info().Msg(fmt.Sprintf("saved pprof CPU profile in: %s", f.Name()))
return nil
}
// stopCpuProfile stops the current CPU profile, if any
func stopCpuProfile() {
pprof.StopCPUProfile()
if cpuFile != nil {
cpuFile.Close()
cpuFile = nil
}
}
// unpackProfilesIntoMap unpacks the profiles specified in the configuration into a map
func (config *Config) unpackProfilesIntoMap() (map[string]Profile, error) {
result := make(map[string]Profile, len(config.ProfileNames))
if len(config.ProfileNames) == 0 {
return nil, nil
}
for index, name := range config.ProfileNames {
profile := Profile{
Name: name,
Interval: 0, // 0 saves the profile when stopping the service
Debug: 0, // 0 writes the gzip-compressed protocol buffer
}
// Try set interval value
if len(config.ProfileIntervals) == len(config.ProfileNames) {
profile.Interval = config.ProfileIntervals[index]
} else if len(config.ProfileIntervals) > 0 {
profile.Interval = config.ProfileIntervals[0]
}
// Try set debug value
if len(config.ProfileDebugValues) == len(config.ProfileNames) {
profile.Debug = config.ProfileDebugValues[index]
} else if len(config.ProfileDebugValues) > 0 {
profile.Debug = config.ProfileDebugValues[0]
}
// Try set the profile reference
if profile.Name != CPU {
if p := pprof.Lookup(profile.Name); p == nil {
return nil, fmt.Errorf("pprof profile does not exist: %s", profile.Name)
} else {
profile.ProfileRef = p
}
}
result[name] = profile
}
return result, nil
}
// newTempFile returns a new output file in dir with the provided prefix and suffix.
func newTempFile(dir, name, suffix string) (*os.File, error) {
prefix := name + "."
currentTime := time.Now().Unix()
f, err := os.OpenFile(filepath.Join(dir, fmt.Sprintf("%s%d%s", prefix, currentTime, suffix)), os.O_RDWR|os.O_CREATE|os.O_EXCL, 0666)
if err != nil {
return nil, fmt.Errorf("could not create file of the form %s%d%s", prefix, currentTime, suffix)
}
return f, nil
}

@ -0,0 +1,113 @@
package pprof
import (
"errors"
"fmt"
"math/rand"
"os"
"path/filepath"
"reflect"
"runtime/pprof"
"strings"
"testing"
"time"
)
func TestUnpackProfilesIntoMap(t *testing.T) {
tests := []struct {
input *Config
expMap map[string]Profile
expErr error
}{
{
input: &Config{},
expMap: nil,
},
{
input: &Config{
ProfileNames: []string{"cpu", "cpu"},
},
expMap: map[string]Profile{
"cpu": {
Name: "cpu",
Interval: 0,
Debug: 0,
ProfileRef: pprof.Lookup("cpu"),
},
},
},
{
input: &Config{
ProfileNames: []string{"test"},
},
expMap: nil,
expErr: errors.New("pprof profile does not exist: test"),
},
{
input: &Config{
ProfileNames: []string{"cpu", "heap"},
ProfileIntervals: []int{0, 60},
ProfileDebugValues: []int{1},
},
expMap: map[string]Profile{
"cpu": {
Name: "cpu",
Interval: 0,
Debug: 1,
ProfileRef: pprof.Lookup("cpu"),
},
"heap": {
Name: "heap",
Interval: 60,
Debug: 1,
ProfileRef: pprof.Lookup("heap"),
},
},
},
}
for i, test := range tests {
actual, err := test.input.unpackProfilesIntoMap()
if assErr := assertError(err, test.expErr); assErr != nil {
t.Fatalf("Test %v: %v", i, assErr)
}
if !reflect.DeepEqual(actual, test.expMap) {
t.Errorf("Test %v: unexpected map\n\t%+v\n\t%+v", i, actual, test.expMap)
}
}
}
func TestStart(t *testing.T) {
input := &Config{
Enabled: true,
Folder: tempTestDir(),
ProfileNames: []string{"cpu"},
ProfileIntervals: []int{1},
}
defer os.RemoveAll(input.Folder)
s := NewService(*input)
err := s.Start()
if assErr := assertError(err, nil); assErr != nil {
t.Fatal(assErr)
}
time.Sleep(1 * time.Second)
}
func tempTestDir() string {
tempDir := os.TempDir()
testDir := filepath.Join(tempDir, fmt.Sprintf("pprof-service-test-%d-%d", os.Getpid(), rand.Int()))
os.RemoveAll(testDir)
return testDir
}
func assertError(gotErr, expErr error) error {
if (gotErr == nil) != (expErr == nil) {
return fmt.Errorf("error unexpected [%v] / [%v]", gotErr, expErr)
}
if gotErr == nil {
return nil
}
if !strings.Contains(gotErr.Error(), expErr.Error()) {
return fmt.Errorf("error unexpected [%v] / [%v]", gotErr, expErr)
}
return nil
}

@ -11,7 +11,6 @@ import (
"sync"
"time"
"github.com/ethereum/go-ethereum/rpc"
"github.com/harmony-one/harmony/internal/utils"
"github.com/prometheus/client_golang/prometheus"
"github.com/prometheus/client_golang/prometheus/promhttp"
@ -181,11 +180,6 @@ func (s *Service) Status() error {
return nil
}
// APIs returns the RPC apis of the prometheus service
func (s *Service) APIs() []rpc.API {
return nil
}
func (s *Service) getRegistry() *prometheus.Registry {
s.registryOnce.Do(func() {
if svc.registry == nil {

@ -1,7 +1,6 @@
package synchronize
import (
"github.com/ethereum/go-ethereum/rpc"
"github.com/harmony-one/harmony/core"
"github.com/harmony-one/harmony/hmy/downloader"
"github.com/harmony-one/harmony/p2p"
@ -30,8 +29,3 @@ func (s *Service) Stop() error {
s.Downloaders.Close()
return nil
}
// APIs return all APIs of the service
func (s *Service) APIs() []rpc.API {
return nil
}

@ -9,10 +9,11 @@ import (
"path"
"github.com/ethereum/go-ethereum/log"
"github.com/harmony-one/harmony/internal/utils"
"github.com/harmony-one/harmony/p2p"
net "github.com/libp2p/go-libp2p-core/network"
ma "github.com/multiformats/go-multiaddr"
"github.com/harmony-one/harmony/internal/utils"
"github.com/harmony-one/harmony/p2p"
)
// ConnLogger ..
@ -93,10 +94,13 @@ func main() {
port := flag.String("port", "9876", "port of the node.")
logFolder := flag.String("log_folder", "latest", "the folder collecting the logs of this execution")
logMaxSize := flag.Int("log_max_size", 100, "the max size in megabytes of the log file before it gets rotated")
logRotateCount := flag.Int("log_rotate_count", 0, "the number of rotated logs to keep. If set to 0 rotation is disabled")
logRotateMaxAge := flag.Int("log_rotate_max_age", 0, "the maximum number of days to retain old logs. If set to 0 rotation is disabled")
keyFile := flag.String("key", "./.bnkey", "the private key file of the bootnode")
versionFlag := flag.Bool("version", false, "Output version info")
verbosity := flag.Int("verbosity", 5, "Logging verbosity: 0=silent, 1=error, 2=warn, 3=info, 4=debug, 5=detail (default: 5)")
logConn := flag.Bool("log_conn", false, "log incoming/outgoing connections")
maxConnPerIP := flag.Int("max_conn_per_ip", 10, "max connections number for same ip")
flag.Parse()
@ -107,7 +111,7 @@ func main() {
// Logging setup
utils.SetLogContext(*port, *ip)
utils.SetLogVerbosity(log.Lvl(*verbosity))
utils.AddLogFile(fmt.Sprintf("%v/bootnode-%v-%v.log", *logFolder, *ip, *port), *logMaxSize)
utils.AddLogFile(fmt.Sprintf("%v/bootnode-%v-%v.log", *logFolder, *ip, *port), *logMaxSize, *logRotateCount, *logRotateMaxAge)
privKey, _, err := utils.LoadKeyFromFile(*keyFile)
if err != nil {
@ -122,6 +126,7 @@ func main() {
BLSKey: privKey,
BootNodes: nil, // Boot nodes have no boot nodes :) Will be connected when other nodes joined
DataStoreFile: &dataStorePath,
MaxConnPerIP: *maxConnPerIP,
})
if err != nil {
utils.FatalErrMsg(err, "cannot initialize network")

@ -4,11 +4,12 @@ import (
"errors"
"fmt"
goversion "github.com/hashicorp/go-version"
"github.com/pelletier/go-toml"
"github.com/harmony-one/harmony/api/service/legacysync"
harmonyconfig "github.com/harmony-one/harmony/internal/configs/harmony"
nodeconfig "github.com/harmony-one/harmony/internal/configs/node"
goversion "github.com/hashicorp/go-version"
"github.com/pelletier/go-toml"
)
const legacyConfigVersion = "1.0.4"
@ -167,4 +168,56 @@ func init() {
confTree.Set("Version", "2.1.0")
return confTree
}
migrations["2.1.0"] = func(confTree *toml.Tree) *toml.Tree {
// Legacy conf missing fields
if confTree.Get("Pprof.Enabled") == nil {
confTree.Set("Pprof.Enabled", true)
}
if confTree.Get("Pprof.Folder") == nil {
confTree.Set("Pprof.Folder", defaultConfig.Pprof.Folder)
}
if confTree.Get("Pprof.ProfileNames") == nil {
confTree.Set("Pprof.ProfileNames", defaultConfig.Pprof.ProfileNames)
}
if confTree.Get("Pprof.ProfileIntervals") == nil {
confTree.Set("Pprof.ProfileIntervals", defaultConfig.Pprof.ProfileIntervals)
}
if confTree.Get("Pprof.ProfileDebugValues") == nil {
confTree.Set("Pprof.ProfileDebugValues", defaultConfig.Pprof.ProfileDebugValues)
}
if confTree.Get("P2P.DiscConcurrency") == nil {
confTree.Set("P2P.DiscConcurrency", defaultConfig.P2P.DiscConcurrency)
}
confTree.Set("Version", "2.2.0")
return confTree
}
migrations["2.2.0"] = func(confTree *toml.Tree) *toml.Tree {
if confTree.Get("HTTP.AuthPort") == nil {
confTree.Set("HTTP.AuthPort", defaultConfig.HTTP.AuthPort)
}
confTree.Set("Version", "2.3.0")
return confTree
}
migrations["2.3.0"] = func(confTree *toml.Tree) *toml.Tree {
if confTree.Get("P2P.MaxConnsPerIP") == nil {
confTree.Set("P2P.MaxConnsPerIP", defaultConfig.P2P.MaxConnsPerIP)
}
confTree.Set("Version", "2.4.0")
return confTree
}
migrations["2.4.0"] = func(confTree *toml.Tree) *toml.Tree {
if confTree.Get("WS.AuthPort") == nil {
confTree.Set("WS.AuthPort", defaultConfig.WS.AuthPort)
}
confTree.Set("Version", "2.5.0")
return confTree
}
}

@ -62,6 +62,8 @@ Version = "1.0.4"
FileName = "harmony.log"
Folder = "./latest"
RotateSize = 100
RotateCount = 0
RotateMaxAge = 0
Verbosity = 3
[Network]

@ -5,7 +5,7 @@ import (
nodeconfig "github.com/harmony-one/harmony/internal/configs/node"
)
const tomlConfigVersion = "2.1.0"
const tomlConfigVersion = "2.5.0"
const (
defNetworkType = nodeconfig.Mainnet
@ -28,18 +28,22 @@ var defaultConfig = harmonyconfig.HarmonyConfig{
Port: nodeconfig.DefaultP2PPort,
IP: nodeconfig.DefaultPublicListenIP,
KeyFile: "./.hmykey",
DiscConcurrency: nodeconfig.DefaultP2PConcurrency,
MaxConnsPerIP: nodeconfig.DefaultMaxConnPerIP,
},
HTTP: harmonyconfig.HttpConfig{
Enabled: true,
RosettaEnabled: false,
IP: "127.0.0.1",
Port: nodeconfig.DefaultRPCPort,
AuthPort: nodeconfig.DefaultAuthRPCPort,
RosettaPort: nodeconfig.DefaultRosettaPort,
},
WS: harmonyconfig.WsConfig{
Enabled: true,
IP: "127.0.0.1",
Port: nodeconfig.DefaultWSPort,
AuthPort: nodeconfig.DefaultAuthWSPort,
},
RPCOpt: harmonyconfig.RpcOptConfig{
DebugEnabled: false,
@ -66,11 +70,17 @@ var defaultConfig = harmonyconfig.HarmonyConfig{
Pprof: harmonyconfig.PprofConfig{
Enabled: false,
ListenAddr: "127.0.0.1:6060",
Folder: "./profiles",
ProfileNames: []string{},
ProfileIntervals: []int{600},
ProfileDebugValues: []int{0},
},
Log: harmonyconfig.LogConfig{
Folder: "./latest",
FileName: "harmony.log",
RotateSize: 100,
RotateCount: 0,
RotateMaxAge: 0,
Verbosity: 3,
VerbosePrints: harmonyconfig.LogVerbosePrints{
Config: true,

@ -7,10 +7,11 @@ import (
harmonyconfig "github.com/harmony-one/harmony/internal/configs/harmony"
"github.com/spf13/cobra"
"github.com/harmony-one/harmony/api/service/legacysync"
"github.com/harmony-one/harmony/internal/cli"
nodeconfig "github.com/harmony-one/harmony/internal/configs/node"
"github.com/spf13/cobra"
)
var (
@ -57,8 +58,9 @@ var (
p2pIPFlag,
p2pKeyFileFlag,
p2pDHTDataStoreFlag,
p2pDiscoveryConcurrencyFlag,
legacyKeyFileFlag,
maxConnPerIPFlag,
}
httpFlags = []cli.Flag{
@ -66,6 +68,7 @@ var (
httpRosettaEnabledFlag,
httpIPFlag,
httpPortFlag,
httpAuthPortFlag,
httpRosettaPortFlag,
}
@ -73,6 +76,7 @@ var (
wsEnabledFlag,
wsIPFlag,
wsPortFlag,
wsAuthPortFlag,
}
rpcOptFlags = []cli.Flag{
@ -128,11 +132,17 @@ var (
pprofFlags = []cli.Flag{
pprofEnabledFlag,
pprofListenAddrFlag,
pprofFolderFlag,
pprofProfileNamesFlag,
pprofProfileIntervalFlag,
pprofProfileDebugFlag,
}
logFlags = []cli.Flag{
logFolderFlag,
logRotateSizeFlag,
logRotateCountFlag,
logRotateMaxAgeFlag,
logFileNameFlag,
logContextIPFlag,
logContextPortFlag,
@ -525,6 +535,16 @@ var (
DefValue: defaultConfig.P2P.KeyFile,
Deprecated: "use --p2p.keyfile",
}
p2pDiscoveryConcurrencyFlag = cli.IntFlag{
Name: "p2p.disc.concurrency",
Usage: "the pubsub's DHT discovery concurrency num (default with raw libp2p dht option)",
DefValue: defaultConfig.P2P.DiscConcurrency,
}
maxConnPerIPFlag = cli.IntFlag{
Name: "p2p.security.max-conn-per-ip",
Usage: "maximum number of connections allowed per node",
DefValue: defaultConfig.P2P.MaxConnsPerIP,
}
)
func applyP2PFlags(cmd *cobra.Command, config *harmonyconfig.HarmonyConfig) {
@ -548,6 +568,14 @@ func applyP2PFlags(cmd *cobra.Command, config *harmonyconfig.HarmonyConfig) {
ds := cli.GetStringFlagValue(cmd, p2pDHTDataStoreFlag)
config.P2P.DHTDataStore = &ds
}
if cli.IsFlagChanged(cmd, p2pDiscoveryConcurrencyFlag) {
config.P2P.DiscConcurrency = cli.GetIntFlagValue(cmd, p2pDiscoveryConcurrencyFlag)
}
if cli.IsFlagChanged(cmd, maxConnPerIPFlag) {
config.P2P.MaxConnsPerIP = cli.GetIntFlagValue(cmd, maxConnPerIPFlag)
}
}
// http flags
@ -567,6 +595,11 @@ var (
Usage: "rpc port to listen for HTTP requests",
DefValue: defaultConfig.HTTP.Port,
}
httpAuthPortFlag = cli.IntFlag{
Name: "http.auth-port",
Usage: "rpc port to listen for auth HTTP requests",
DefValue: defaultConfig.HTTP.AuthPort,
}
httpRosettaEnabledFlag = cli.BoolFlag{
Name: "http.rosetta",
Usage: "enable HTTP / Rosetta requests",
@ -592,6 +625,11 @@ func applyHTTPFlags(cmd *cobra.Command, config *harmonyconfig.HarmonyConfig) {
isRPCSpecified = true
}
if cli.IsFlagChanged(cmd, httpAuthPortFlag) {
config.HTTP.AuthPort = cli.GetIntFlagValue(cmd, httpAuthPortFlag)
isRPCSpecified = true
}
if cli.IsFlagChanged(cmd, httpRosettaPortFlag) {
config.HTTP.RosettaPort = cli.GetIntFlagValue(cmd, httpRosettaPortFlag)
isRosettaSpecified = true
@ -628,6 +666,11 @@ var (
Usage: "port for websocket endpoint",
DefValue: defaultConfig.WS.Port,
}
wsAuthPortFlag = cli.IntFlag{
Name: "ws.auth-port",
Usage: "port for websocket auth endpoint",
DefValue: defaultConfig.WS.AuthPort,
}
)
func applyWSFlags(cmd *cobra.Command, config *harmonyconfig.HarmonyConfig) {
@ -640,6 +683,9 @@ func applyWSFlags(cmd *cobra.Command, config *harmonyconfig.HarmonyConfig) {
if cli.IsFlagChanged(cmd, wsPortFlag) {
config.WS.Port = cli.GetIntFlagValue(cmd, wsPortFlag)
}
if cli.IsFlagChanged(cmd, wsAuthPortFlag) {
config.WS.AuthPort = cli.GetIntFlagValue(cmd, wsAuthPortFlag)
}
}
// rpc opt flags
@ -975,6 +1021,29 @@ var (
Usage: "listen address for pprof",
DefValue: defaultConfig.Pprof.ListenAddr,
}
pprofFolderFlag = cli.StringFlag{
Name: "pprof.folder",
Usage: "folder to put pprof profiles",
DefValue: defaultConfig.Pprof.Folder,
Hidden: true,
}
pprofProfileNamesFlag = cli.StringSliceFlag{
Name: "pprof.profile.names",
Usage: "a list of pprof profile names (separated by ,) e.g. cpu,heap,goroutine",
DefValue: defaultConfig.Pprof.ProfileNames,
}
pprofProfileIntervalFlag = cli.IntSliceFlag{
Name: "pprof.profile.intervals",
Usage: "a list of pprof profile interval integer values (separated by ,) e.g. 30 saves all profiles every 30 seconds or 0,10 saves the first profile on shutdown and the second profile every 10 seconds",
DefValue: defaultConfig.Pprof.ProfileIntervals,
Hidden: true,
}
pprofProfileDebugFlag = cli.IntSliceFlag{
Name: "pprof.profile.debug",
Usage: "a list of pprof profile debug integer values (separated by ,) e.g. 0 writes the gzip-compressed protocol buffer and 1 writes the legacy text format. Predefined profiles may assign meaning to other debug values: https://golang.org/pkg/runtime/pprof/",
DefValue: defaultConfig.Pprof.ProfileDebugValues,
Hidden: true,
}
)
func applyPprofFlags(cmd *cobra.Command, config *harmonyconfig.HarmonyConfig) {
@ -983,6 +1052,22 @@ func applyPprofFlags(cmd *cobra.Command, config *harmonyconfig.HarmonyConfig) {
config.Pprof.ListenAddr = cli.GetStringFlagValue(cmd, pprofListenAddrFlag)
pprofSet = true
}
if cli.IsFlagChanged(cmd, pprofFolderFlag) {
config.Pprof.Folder = cli.GetStringFlagValue(cmd, pprofFolderFlag)
pprofSet = true
}
if cli.IsFlagChanged(cmd, pprofProfileNamesFlag) {
config.Pprof.ProfileNames = cli.GetStringSliceFlagValue(cmd, pprofProfileNamesFlag)
pprofSet = true
}
if cli.IsFlagChanged(cmd, pprofProfileIntervalFlag) {
config.Pprof.ProfileIntervals = cli.GetIntSliceFlagValue(cmd, pprofProfileIntervalFlag)
pprofSet = true
}
if cli.IsFlagChanged(cmd, pprofProfileDebugFlag) {
config.Pprof.ProfileDebugValues = cli.GetIntSliceFlagValue(cmd, pprofProfileDebugFlag)
pprofSet = true
}
if cli.IsFlagChanged(cmd, pprofEnabledFlag) {
config.Pprof.Enabled = cli.GetBoolFlagValue(cmd, pprofEnabledFlag)
} else if pprofSet {
@ -1002,6 +1087,16 @@ var (
Usage: "rotation log size in megabytes",
DefValue: defaultConfig.Log.RotateSize,
}
logRotateCountFlag = cli.IntFlag{
Name: "log.rotate-count",
Usage: "maximum number of old log files to retain",
DefValue: defaultConfig.Log.RotateCount,
}
logRotateMaxAgeFlag = cli.IntFlag{
Name: "log.rotate-max-age",
Usage: "maximum number of days to retain old log files",
DefValue: defaultConfig.Log.RotateMaxAge,
}
logFileNameFlag = cli.StringFlag{
Name: "log.name",
Usage: "log file name (e.g. harmony.log)",
@ -1064,6 +1159,14 @@ func applyLogFlags(cmd *cobra.Command, config *harmonyconfig.HarmonyConfig) {
config.Log.RotateSize = cli.GetIntFlagValue(cmd, legacyLogRotateSizeFlag)
}
if cli.IsFlagChanged(cmd, logRotateCountFlag) {
config.Log.RotateCount = cli.GetIntFlagValue(cmd, logRotateCountFlag)
}
if cli.IsFlagChanged(cmd, logRotateMaxAgeFlag) {
config.Log.RotateMaxAge = cli.GetIntFlagValue(cmd, logRotateMaxAgeFlag)
}
if cli.IsFlagChanged(cmd, logFileNameFlag) {
config.Log.FileName = cli.GetStringFlagValue(cmd, logFileNameFlag)
}
@ -1296,8 +1399,10 @@ func applyLegacyMiscFlags(cmd *cobra.Command, config *harmonyconfig.HarmonyConfi
legacyPort := cli.GetIntFlagValue(cmd, legacyPortFlag)
config.P2P.Port = legacyPort
config.HTTP.Port = nodeconfig.GetRPCHTTPPortFromBase(legacyPort)
config.HTTP.AuthPort = nodeconfig.GetRPCAuthHTTPPortFromBase(legacyPort)
config.HTTP.RosettaPort = nodeconfig.GetRosettaHTTPPortFromBase(legacyPort)
config.WS.Port = nodeconfig.GetWSPortFromBase(legacyPort)
config.WS.AuthPort = nodeconfig.GetWSAuthPortFromBase(legacyPort)
legPortStr := strconv.Itoa(legacyPort)
syncPort, _ := strconv.Atoi(legacysync.GetSyncingPort(legPortStr))

@ -8,9 +8,10 @@ import (
harmonyconfig "github.com/harmony-one/harmony/internal/configs/harmony"
"github.com/spf13/cobra"
"github.com/harmony-one/harmony/internal/cli"
nodeconfig "github.com/harmony-one/harmony/internal/configs/node"
"github.com/spf13/cobra"
)
var (
@ -30,7 +31,7 @@ func TestHarmonyFlags(t *testing.T) {
"2p/QmRVbTpEYup8dSaURZfF6ByrMTSKa4UyUzJhSjahFzRqNj --ip 8.8.8.8 --port 9000 --network_type=mainn" +
"et --dns_zone=t.hmny.io --blacklist=./.hmy/blacklist.txt --min_peers=6 --max_bls_keys_per_node=" +
"10 --broadcast_invalid_tx=true --verbosity=3 --is_archival=false --shard_id=-1 --staking=true -" +
"-aws-config-source file:config.json",
"-aws-config-source file:config.json --p2p.disc.concurrency 5 --p2p.security.max-conn-per-ip 5",
expConfig: harmonyconfig.HarmonyConfig{
Version: tomlConfigVersion,
General: harmonyconfig.GeneralConfig{
@ -60,11 +61,14 @@ func TestHarmonyFlags(t *testing.T) {
Port: 9000,
IP: defaultConfig.P2P.IP,
KeyFile: defaultConfig.P2P.KeyFile,
DiscConcurrency: 5,
MaxConnsPerIP: 5,
},
HTTP: harmonyconfig.HttpConfig{
Enabled: true,
IP: "127.0.0.1",
Port: 9500,
AuthPort: 9501,
RosettaEnabled: false,
RosettaPort: 9700,
},
@ -77,6 +81,7 @@ func TestHarmonyFlags(t *testing.T) {
Enabled: true,
IP: "127.0.0.1",
Port: 9800,
AuthPort: 9801,
},
Consensus: &harmonyconfig.ConsensusConfig{
MinPeers: 6,
@ -100,11 +105,17 @@ func TestHarmonyFlags(t *testing.T) {
Pprof: harmonyconfig.PprofConfig{
Enabled: false,
ListenAddr: "127.0.0.1:6060",
Folder: "./profiles",
ProfileNames: []string{},
ProfileIntervals: []int{600},
ProfileDebugValues: []int{0},
},
Log: harmonyconfig.LogConfig{
Folder: "./latest",
FileName: "validator-8.8.8.8-9000.log",
RotateSize: 100,
RotateCount: 0,
RotateMaxAge: 0,
Verbosity: 3,
Context: &harmonyconfig.LogContext{
IP: "8.8.8.8",
@ -359,6 +370,7 @@ func TestP2PFlags(t *testing.T) {
IP: nodeconfig.DefaultPublicListenIP,
KeyFile: "./key.file",
DHTDataStore: &defDataStore,
MaxConnsPerIP: 10,
},
},
{
@ -367,6 +379,17 @@ func TestP2PFlags(t *testing.T) {
Port: 9001,
IP: nodeconfig.DefaultPublicListenIP,
KeyFile: "./key.file",
MaxConnsPerIP: 10,
},
},
{
args: []string{"--p2p.port", "9001", "--p2p.disc.concurrency", "5", "--p2p.security.max-conn-per-ip", "5"},
expConfig: harmonyconfig.P2pConfig{
Port: 9001,
IP: nodeconfig.DefaultPublicListenIP,
KeyFile: "./.hmykey",
DiscConcurrency: 5,
MaxConnsPerIP: 5,
},
},
}
@ -387,7 +410,7 @@ func TestP2PFlags(t *testing.T) {
continue
}
if !reflect.DeepEqual(got.P2P, test.expConfig) {
t.Errorf("Test %v: unexpected config: \n\t%+v\n\t%+v", i, got.Network, test.expConfig)
t.Errorf("Test %v: unexpected config: \n\t%+v\n\t%+v", i, got.P2P, test.expConfig)
}
ts.tearDown()
}
@ -410,6 +433,7 @@ func TestRPCFlags(t *testing.T) {
RosettaEnabled: false,
IP: defaultConfig.HTTP.IP,
Port: defaultConfig.HTTP.Port,
AuthPort: defaultConfig.HTTP.AuthPort,
RosettaPort: defaultConfig.HTTP.RosettaPort,
},
},
@ -420,6 +444,18 @@ func TestRPCFlags(t *testing.T) {
RosettaEnabled: false,
IP: "8.8.8.8",
Port: 9001,
AuthPort: defaultConfig.HTTP.AuthPort,
RosettaPort: defaultConfig.HTTP.RosettaPort,
},
},
{
args: []string{"--http.ip", "8.8.8.8", "--http.auth-port", "9001"},
expConfig: harmonyconfig.HttpConfig{
Enabled: true,
RosettaEnabled: false,
IP: "8.8.8.8",
Port: defaultConfig.HTTP.Port,
AuthPort: 9001,
RosettaPort: defaultConfig.HTTP.RosettaPort,
},
},
@ -430,6 +466,7 @@ func TestRPCFlags(t *testing.T) {
RosettaEnabled: true,
IP: "8.8.8.8",
Port: 9001,
AuthPort: defaultConfig.HTTP.AuthPort,
RosettaPort: 10001,
},
},
@ -440,6 +477,7 @@ func TestRPCFlags(t *testing.T) {
RosettaEnabled: true,
IP: "8.8.8.8",
Port: defaultConfig.HTTP.Port,
AuthPort: defaultConfig.HTTP.AuthPort,
RosettaPort: 10001,
},
},
@ -450,6 +488,7 @@ func TestRPCFlags(t *testing.T) {
RosettaEnabled: false,
IP: nodeconfig.DefaultPublicListenIP,
Port: 9501,
AuthPort: 9502,
RosettaPort: 9701,
},
},
@ -494,6 +533,7 @@ func TestWSFlags(t *testing.T) {
Enabled: false,
IP: defaultConfig.WS.IP,
Port: defaultConfig.WS.Port,
AuthPort: defaultConfig.WS.AuthPort,
},
},
{
@ -502,6 +542,16 @@ func TestWSFlags(t *testing.T) {
Enabled: true,
IP: "8.8.8.8",
Port: 9001,
AuthPort: defaultConfig.WS.AuthPort,
},
},
{
args: []string{"--ws", "--ws.ip", "8.8.8.8", "--ws.auth-port", "9001"},
expConfig: harmonyconfig.WsConfig{
Enabled: true,
IP: "8.8.8.8",
Port: defaultConfig.WS.Port,
AuthPort: 9001,
},
},
{
@ -510,6 +560,7 @@ func TestWSFlags(t *testing.T) {
Enabled: true,
IP: nodeconfig.DefaultPublicListenIP,
Port: 9801,
AuthPort: 9802,
},
},
}
@ -775,6 +826,10 @@ func TestPprofFlags(t *testing.T) {
expConfig: harmonyconfig.PprofConfig{
Enabled: true,
ListenAddr: defaultConfig.Pprof.ListenAddr,
Folder: defaultConfig.Pprof.Folder,
ProfileNames: defaultConfig.Pprof.ProfileNames,
ProfileIntervals: defaultConfig.Pprof.ProfileIntervals,
ProfileDebugValues: defaultConfig.Pprof.ProfileDebugValues,
},
},
{
@ -782,6 +837,10 @@ func TestPprofFlags(t *testing.T) {
expConfig: harmonyconfig.PprofConfig{
Enabled: true,
ListenAddr: "8.8.8.8:9001",
Folder: defaultConfig.Pprof.Folder,
ProfileNames: defaultConfig.Pprof.ProfileNames,
ProfileIntervals: defaultConfig.Pprof.ProfileIntervals,
ProfileDebugValues: defaultConfig.Pprof.ProfileDebugValues,
},
},
{
@ -789,6 +848,43 @@ func TestPprofFlags(t *testing.T) {
expConfig: harmonyconfig.PprofConfig{
Enabled: false,
ListenAddr: "8.8.8.8:9001",
Folder: defaultConfig.Pprof.Folder,
ProfileNames: defaultConfig.Pprof.ProfileNames,
ProfileIntervals: defaultConfig.Pprof.ProfileIntervals,
ProfileDebugValues: defaultConfig.Pprof.ProfileDebugValues,
},
},
{
args: []string{"--pprof.profile.names", "cpu,heap,mutex"},
expConfig: harmonyconfig.PprofConfig{
Enabled: true,
ListenAddr: defaultConfig.Pprof.ListenAddr,
Folder: defaultConfig.Pprof.Folder,
ProfileNames: []string{"cpu", "heap", "mutex"},
ProfileIntervals: defaultConfig.Pprof.ProfileIntervals,
ProfileDebugValues: defaultConfig.Pprof.ProfileDebugValues,
},
},
{
args: []string{"--pprof.profile.intervals", "0,1"},
expConfig: harmonyconfig.PprofConfig{
Enabled: true,
ListenAddr: defaultConfig.Pprof.ListenAddr,
Folder: defaultConfig.Pprof.Folder,
ProfileNames: defaultConfig.Pprof.ProfileNames,
ProfileIntervals: []int{0, 1},
ProfileDebugValues: defaultConfig.Pprof.ProfileDebugValues,
},
},
{
args: []string{"--pprof.profile.debug", "0,1,0"},
expConfig: harmonyconfig.PprofConfig{
Enabled: true,
ListenAddr: defaultConfig.Pprof.ListenAddr,
Folder: defaultConfig.Pprof.Folder,
ProfileNames: defaultConfig.Pprof.ProfileNames,
ProfileIntervals: defaultConfig.Pprof.ProfileIntervals,
ProfileDebugValues: []int{0, 1, 0},
},
},
}
@ -820,12 +916,15 @@ func TestLogFlags(t *testing.T) {
expConfig: defaultConfig.Log,
},
{
args: []string{"--log.dir", "latest_log", "--log.max-size", "10", "--log.name", "harmony.log",
"--log.verb", "5", "--log.verbose-prints", "config"},
args: []string{"--log.dir", "latest_log", "--log.max-size", "10", "--log.rotate-count", "3",
"--log.rotate-max-age", "0", "--log.name", "harmony.log", "--log.verb", "5",
"--log.verbose-prints", "config"},
expConfig: harmonyconfig.LogConfig{
Folder: "latest_log",
FileName: "harmony.log",
RotateSize: 10,
RotateCount: 3,
RotateMaxAge: 0,
Verbosity: 5,
VerbosePrints: harmonyconfig.LogVerbosePrints{
Config: true,
@ -839,6 +938,8 @@ func TestLogFlags(t *testing.T) {
Folder: defaultConfig.Log.Folder,
FileName: defaultConfig.Log.FileName,
RotateSize: defaultConfig.Log.RotateSize,
RotateCount: defaultConfig.Log.RotateCount,
RotateMaxAge: defaultConfig.Log.RotateMaxAge,
Verbosity: defaultConfig.Log.Verbosity,
VerbosePrints: defaultConfig.Log.VerbosePrints,
Context: &harmonyconfig.LogContext{
@ -854,6 +955,8 @@ func TestLogFlags(t *testing.T) {
Folder: "latest_log",
FileName: "validator-8.8.8.8-9001.log",
RotateSize: 10,
RotateCount: 0,
RotateMaxAge: 0,
Verbosity: 5,
VerbosePrints: defaultConfig.Log.VerbosePrints,
Context: &harmonyconfig.LogContext{

@ -5,7 +5,6 @@ import (
"io/ioutil"
"math/big"
"math/rand"
"net/http"
_ "net/http/pprof"
"os"
"os/signal"
@ -21,11 +20,13 @@ import (
ethCommon "github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/log"
"github.com/harmony-one/bls/ffi/go/bls"
"github.com/pkg/errors"
"github.com/spf13/cobra"
"github.com/harmony-one/bls/ffi/go/bls"
"github.com/harmony-one/harmony/api/service"
"github.com/harmony-one/harmony/api/service/pprof"
"github.com/harmony-one/harmony/api/service/prometheus"
"github.com/harmony-one/harmony/api/service/synchronize"
"github.com/harmony-one/harmony/common/fdlimit"
@ -134,7 +135,6 @@ func runHarmonyNode(cmd *cobra.Command, args []string) {
}
setupNodeLog(cfg)
setupPprof(cfg)
setupNodeAndRun(cfg)
}
@ -234,9 +234,11 @@ func applyRootFlags(cmd *cobra.Command, config *harmonyconfig.HarmonyConfig) {
func setupNodeLog(config harmonyconfig.HarmonyConfig) {
logPath := filepath.Join(config.Log.Folder, config.Log.FileName)
rotateSize := config.Log.RotateSize
rotateCount := config.Log.RotateCount
rotateMaxAge := config.Log.RotateMaxAge
verbosity := config.Log.Verbosity
utils.AddLogFile(logPath, rotateSize)
utils.AddLogFile(logPath, rotateSize, rotateCount, rotateMaxAge)
utils.SetLogVerbosity(log.Lvl(verbosity))
if config.Log.Context != nil {
ip := config.Log.Context.IP
@ -245,17 +247,6 @@ func setupNodeLog(config harmonyconfig.HarmonyConfig) {
}
}
func setupPprof(config harmonyconfig.HarmonyConfig) {
enabled := config.Pprof.Enabled
addr := config.Pprof.ListenAddr
if enabled {
go func() {
http.ListenAndServe(addr, nil)
}()
}
}
func setupNodeAndRun(hc harmonyconfig.HarmonyConfig) {
var err error
@ -325,9 +316,11 @@ func setupNodeAndRun(hc harmonyconfig.HarmonyConfig) {
HTTPEnabled: hc.HTTP.Enabled,
HTTPIp: hc.HTTP.IP,
HTTPPort: hc.HTTP.Port,
HTTPAuthPort: hc.HTTP.AuthPort,
WSEnabled: hc.WS.Enabled,
WSIp: hc.WS.IP,
WSPort: hc.WS.Port,
WSAuthPort: hc.WS.AuthPort,
DebugEnabled: hc.RPCOpt.DebugEnabled,
RateLimiterEnabled: hc.RPCOpt.RateLimterEnabled,
RequestsPerSecond: hc.RPCOpt.RequestsPerSecond,
@ -399,6 +392,9 @@ func setupNodeAndRun(hc harmonyconfig.HarmonyConfig) {
} else if currentNode.NodeConfig.Role() == nodeconfig.ExplorerNode {
currentNode.RegisterExplorerServices()
}
if hc.Pprof.Enabled {
setupPprofService(currentNode, hc)
}
if hc.Prometheus.Enabled {
setupPrometheusService(currentNode, hc, nodeConfig.ShardID)
}
@ -585,12 +581,13 @@ func createGlobalConfig(hc harmonyconfig.HarmonyConfig) (*nodeconfig.ConfigType,
Port: strconv.Itoa(hc.P2P.Port),
ConsensusPubKey: nodeConfig.ConsensusPriKey[0].Pub.Object,
}
myHost, err = p2p.NewHost(p2p.HostConfig{
Self: &selfPeer,
BLSKey: nodeConfig.P2PPriKey,
BootNodes: hc.Network.BootNodes,
DataStoreFile: hc.P2P.DHTDataStore,
DiscConcurrency: hc.P2P.DiscConcurrency,
MaxConnPerIP: hc.P2P.MaxConnsPerIP,
})
if err != nil {
return nil, errors.Wrap(err, "cannot create P2P network host")
@ -733,6 +730,19 @@ func processNodeType(hc harmonyconfig.HarmonyConfig, currentNode *node.Node, cur
}
}
func setupPprofService(node *node.Node, hc harmonyconfig.HarmonyConfig) {
pprofConfig := pprof.Config{
Enabled: hc.Pprof.Enabled,
ListenAddr: hc.Pprof.ListenAddr,
Folder: hc.Pprof.Folder,
ProfileNames: hc.Pprof.ProfileNames,
ProfileIntervals: hc.Pprof.ProfileIntervals,
ProfileDebugValues: hc.Pprof.ProfileDebugValues,
}
s := pprof.NewService(pprofConfig)
node.RegisterService(service.Pprof, s)
}
func setupPrometheusService(node *node.Node, hc harmonyconfig.HarmonyConfig, sid uint32) {
prometheusConfig := prometheus.Config{
Enabled: hc.Prometheus.Enabled,

@ -118,7 +118,7 @@ func (consensus *Consensus) isRightBlockNumCheck(recvMsg *FBFTMessage) bool {
return true
}
func (consensus *Consensus) onPreparedSanityChecks(
func (consensus *Consensus) newBlockSanityChecks(
blockObj *types.Block, recvMsg *FBFTMessage,
) bool {
if blockObj.NumberU64() != recvMsg.BlockNum ||
@ -126,7 +126,7 @@ func (consensus *Consensus) onPreparedSanityChecks(
consensus.getLogger().Warn().
Uint64("MsgBlockNum", recvMsg.BlockNum).
Uint64("blockNum", blockObj.NumberU64()).
Msg("[OnPrepared] BlockNum not match")
Msg("[newBlockSanityChecks] BlockNum not match")
return false
}
if blockObj.Header().Hash() != recvMsg.BlockHash {
@ -134,7 +134,7 @@ func (consensus *Consensus) onPreparedSanityChecks(
Uint64("MsgBlockNum", recvMsg.BlockNum).
Hex("MsgBlockHash", recvMsg.BlockHash[:]).
Str("blockObjHash", blockObj.Header().Hash().Hex()).
Msg("[OnPrepared] BlockHash not match")
Msg("[newBlockSanityChecks] BlockHash not match")
return false
}
return true

@ -84,6 +84,8 @@ type Consensus struct {
IgnoreViewIDCheck *abool.AtomicBool
// consensus mutex
mutex sync.Mutex
// mutex for verify new block
verifyBlockMutex sync.Mutex
// ViewChange struct
vc *viewChange
// Signal channel for proposing a new block and start new consensus
@ -136,7 +138,7 @@ type Consensus struct {
// VerifyBlock is a function used to verify the block and keep trace of verified blocks
func (consensus *Consensus) VerifyBlock(block *types.Block) error {
if !consensus.FBFTLog.IsBlockVerified(block) {
if !consensus.FBFTLog.IsBlockVerified(block.Hash()) {
if err := consensus.BlockVerifier(block); err != nil {
return errors.New("Block verification failed")
}

@ -495,7 +495,7 @@ func (iter *LastMileBlockIter) Next() *types.Block {
block := iter.blockCandidates[iter.curIndex]
iter.curIndex++
if !iter.fbftLog.IsBlockVerified(block) {
if !iter.fbftLog.IsBlockVerified(block.Hash()) {
if err := iter.verify(block); err != nil {
iter.logger.Debug().Err(err).Msg("block verification failed in consensus last mile block")
return nil
@ -558,7 +558,7 @@ func (consensus *Consensus) preCommitAndPropose(blk *types.Block) error {
go func() {
blk.SetCurrentCommitSig(bareMinimumCommit)
if _, err := consensus.Blockchain.InsertChain([]*types.Block{blk}, !consensus.FBFTLog.IsBlockVerified(blk)); err != nil {
if _, err := consensus.Blockchain.InsertChain([]*types.Block{blk}, !consensus.FBFTLog.IsBlockVerified(blk.Hash())); err != nil {
consensus.getLogger().Error().Err(err).Msg("[preCommitAndPropose] Failed to add block to chain")
return
}
@ -658,7 +658,7 @@ func (consensus *Consensus) tryCatchup() error {
func (consensus *Consensus) commitBlock(blk *types.Block, committedMsg *FBFTMessage) error {
if consensus.Blockchain.CurrentBlock().NumberU64() < blk.NumberU64() {
if _, err := consensus.Blockchain.InsertChain([]*types.Block{blk}, !consensus.FBFTLog.IsBlockVerified(blk)); err != nil {
if _, err := consensus.Blockchain.InsertChain([]*types.Block{blk}, !consensus.FBFTLog.IsBlockVerified(blk.Hash())); err != nil {
consensus.getLogger().Error().Err(err).Msg("[commitBlock] Failed to add block to chain")
return err
}

@ -99,6 +99,7 @@ func (consensus *Consensus) construct(
needMsgSig := true
switch p {
case msg_pb.MessageType_ANNOUNCE:
consensusMsg.Block = consensus.block
consensusMsg.Payload = consensus.blockHash[:]
case msg_pb.MessageType_PREPARE:
needMsgSig = false

@ -135,11 +135,11 @@ func (log *FBFTLog) MarkBlockVerified(block *types.Block) {
}
// IsBlockVerified checks whether the block is verified
func (log *FBFTLog) IsBlockVerified(block *types.Block) bool {
func (log *FBFTLog) IsBlockVerified(hash common.Hash) bool {
log.blockLock.RLock()
defer log.blockLock.RUnlock()
_, exist := log.verifiedBlocks[block.Hash()]
_, exist := log.verifiedBlocks[hash]
return exist
}

@ -29,7 +29,7 @@ func (consensus *Consensus) announce(block *types.Block) {
//// Lock Write - Start
consensus.mutex.Lock()
copy(consensus.blockHash[:], blockHash[:])
consensus.block = encodedBlock
consensus.block = encodedBlock // Must set block bytes before consensus.construct()
consensus.mutex.Unlock()
//// Lock Write - End

@ -4,6 +4,8 @@ import (
"encoding/json"
"math/big"
"github.com/pkg/errors"
bls_core "github.com/harmony-one/bls/ffi/go/bls"
"github.com/harmony-one/harmony/crypto/bls"
@ -20,6 +22,9 @@ type uniformVoteWeight struct {
DependencyInjectionWriter
DependencyInjectionReader
SignatureReader
lastPowerSignersCountCache map[Phase]int64
lastParticipantsCount int64
}
// Policy ..
@ -123,9 +128,29 @@ func (v *uniformVoteWeight) AmIMemberOfCommitee() bool {
}
func (v *uniformVoteWeight) ResetPrepareAndCommitVotes() {
v.lastPowerSignersCountCache[Prepare] = v.SignersCount(Prepare)
v.lastPowerSignersCountCache[Commit] = v.SignersCount(Commit)
v.lastParticipantsCount = v.ParticipantsCount()
v.reset([]Phase{Prepare, Commit})
}
func (v *uniformVoteWeight) ResetViewChangeVotes() {
v.lastPowerSignersCountCache[ViewChange] = v.SignersCount(ViewChange)
v.lastParticipantsCount = v.ParticipantsCount()
v.reset([]Phase{ViewChange})
}
func (v *uniformVoteWeight) CurrentTotalPower(p Phase) (*numeric.Dec, error) {
if v.lastParticipantsCount == 0 {
return nil, errors.New("uniformVoteWeight not cache last participants count")
}
if lastPowerSignersCount, ok := v.lastPowerSignersCountCache[p]; ok {
power := numeric.NewDec(lastPowerSignersCount).Quo(numeric.NewDec(v.lastParticipantsCount))
return &power, nil
} else {
return nil, errors.New("uniformVoteWeight not cache this phase")
}
}

@ -49,6 +49,7 @@ type stakedVoteWeight struct {
DependencyInjectionReader
roster votepower.Roster
voteTally VoteTally
lastPower map[Phase]numeric.Dec
}
// Policy ..
@ -267,7 +268,9 @@ func (v *stakedVoteWeight) MarshalJSON() ([]byte, error) {
i, externalCount := 0, 0
totalRaw := numeric.ZeroDec()
for identity, voter := range v.roster.Voters {
for _, slot := range v.roster.OrderedSlots {
identity := slot
voter := v.roster.Voters[slot]
member := u{
voter.IsHarmonyNode,
common2.MustAddressToBech32(voter.EarningAccount),
@ -323,12 +326,25 @@ func newVoteTally() VoteTally {
}
func (v *stakedVoteWeight) ResetPrepareAndCommitVotes() {
v.lastPower[Prepare] = v.voteTally.Prepare.tally
v.lastPower[Commit] = v.voteTally.Commit.tally
v.reset([]Phase{Prepare, Commit})
v.voteTally.Prepare = &tallyAndQuorum{numeric.NewDec(0), false}
v.voteTally.Commit = &tallyAndQuorum{numeric.NewDec(0), false}
}
func (v *stakedVoteWeight) ResetViewChangeVotes() {
v.lastPower[ViewChange] = v.voteTally.ViewChange.tally
v.reset([]Phase{ViewChange})
v.voteTally.ViewChange = &tallyAndQuorum{numeric.NewDec(0), false}
}
func (v *stakedVoteWeight) CurrentTotalPower(p Phase) (*numeric.Dec, error) {
if power, ok := v.lastPower[p]; ok {
return &power, nil
} else {
return nil, errors.New("stakedVoteWeight not cache this phase")
}
}

@ -132,6 +132,7 @@ type Decider interface {
IsAllSigsCollected() bool
ResetPrepareAndCommitVotes()
ResetViewChangeVotes()
CurrentTotalPower(p Phase) (*numeric.Dec, error)
}
// Registry ..
@ -408,15 +409,19 @@ func NewDecider(p Policy, shardID uint32) Decider {
switch p {
case SuperMajorityVote:
return &uniformVoteWeight{
c.DependencyInjectionWriter, c.DependencyInjectionReader, c,
DependencyInjectionWriter: c.DependencyInjectionWriter,
DependencyInjectionReader: c.DependencyInjectionReader,
SignatureReader: c,
lastPowerSignersCountCache: make(map[Phase]int64),
}
case SuperMajorityStake:
return &stakedVoteWeight{
c.SignatureReader,
c.DependencyInjectionWriter,
c.DependencyInjectionWriter.(DependencyInjectionReader),
*votepower.NewRoster(shardID),
newVoteTally(),
SignatureReader: c.SignatureReader,
DependencyInjectionWriter: c.DependencyInjectionWriter,
DependencyInjectionReader: c.DependencyInjectionWriter.(DependencyInjectionReader),
roster: *votepower.NewRoster(shardID),
voteTally: newVoteTally(),
lastPower: make(map[Phase]numeric.Dec),
}
default:
// Should not be possible

@ -6,12 +6,10 @@ import (
"github.com/harmony-one/harmony/crypto/bls"
"github.com/ethereum/go-ethereum/common"
"github.com/harmony-one/harmony/shard"
)
// Payout ..
type Payout struct {
ShardID uint32
Addr common.Address
NewlyEarned *big.Int
EarningKey bls.SerializedPublicKey
@ -20,12 +18,10 @@ type Payout struct {
// CompletedRound ..
type CompletedRound struct {
Total *big.Int
BeaconchainAward []Payout
ShardChainAward []Payout
Payouts []Payout
}
// Reader ..
type Reader interface {
ReadRoundResult() *CompletedRound
MissingSigners() shard.SlotList
}

@ -3,6 +3,8 @@ package consensus
import (
"encoding/hex"
"github.com/pkg/errors"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/rlp"
@ -31,7 +33,7 @@ func (consensus *Consensus) onAnnounce(msg *msg_pb.Message) {
}
consensus.StartFinalityCount()
consensus.getLogger().Debug().
consensus.getLogger().Info().
Uint64("MsgViewID", recvMsg.ViewID).
Uint64("MsgBlockNum", recvMsg.BlockNum).
Msg("[OnAnnounce] Announce message Added")
@ -58,6 +60,81 @@ func (consensus *Consensus) onAnnounce(msg *msg_pb.Message) {
}
consensus.prepare()
consensus.switchPhase("Announce", FBFTPrepare)
if len(recvMsg.Block) > 0 {
go func() {
// Best effort check, no need to error out.
_, err := consensus.validateNewBlock(recvMsg)
if err == nil {
consensus.getLogger().Info().
Msg("[Announce] Block verified")
}
}()
}
}
func (consensus *Consensus) validateNewBlock(recvMsg *FBFTMessage) (*types.Block, error) {
// Lock to prevent race condition between announce and prepare
consensus.verifyBlockMutex.Lock()
defer consensus.verifyBlockMutex.Unlock()
if consensus.FBFTLog.IsBlockVerified(recvMsg.BlockHash) {
var blockObj *types.Block
blockObj = consensus.FBFTLog.GetBlockByHash(recvMsg.BlockHash)
if blockObj == nil {
if err := rlp.DecodeBytes(recvMsg.Block, &blockObj); err != nil {
consensus.getLogger().Warn().
Err(err).
Uint64("MsgBlockNum", recvMsg.BlockNum).
Msg("[validateNewBlock] Unparseable block header data")
return nil, errors.New("Failed parsing new block")
}
}
consensus.getLogger().Info().
Msg("[validateNewBlock] Block Already verified")
return blockObj, nil
}
// check validity of block if any
var blockObj types.Block
if err := rlp.DecodeBytes(recvMsg.Block, &blockObj); err != nil {
consensus.getLogger().Warn().
Err(err).
Uint64("MsgBlockNum", recvMsg.BlockNum).
Msg("[validateNewBlock] Unparseable block header data")
return nil, errors.New("Failed parsing new block")
}
consensus.FBFTLog.AddBlock(&blockObj)
// let this handle it own logs
if !consensus.newBlockSanityChecks(&blockObj, recvMsg) {
return nil, errors.New("new block failed sanity checks")
}
// add block field
blockPayload := make([]byte, len(recvMsg.Block))
copy(blockPayload[:], recvMsg.Block[:])
consensus.block = blockPayload
recvMsg.Block = []byte{} // save memory space
consensus.FBFTLog.AddVerifiedMessage(recvMsg)
consensus.getLogger().Debug().
Uint64("MsgViewID", recvMsg.ViewID).
Uint64("MsgBlockNum", recvMsg.BlockNum).
Hex("blockHash", recvMsg.BlockHash[:]).
Msg("[validateNewBlock] Prepared message and block added")
if consensus.BlockVerifier == nil {
consensus.getLogger().Debug().Msg("[validateNewBlock] consensus received message before init. Ignoring")
return nil, errors.New("nil block verifier")
}
if err := consensus.VerifyBlock(&blockObj); err != nil {
consensus.getLogger().Error().Err(err).Msg("[validateNewBlock] Block verification failed")
return nil, errors.New("Block verification failed")
}
return &blockObj, nil
}
func (consensus *Consensus) prepare() {
@ -149,41 +226,12 @@ func (consensus *Consensus) onPrepared(recvMsg *FBFTMessage) {
return
}
// check validity of block
var blockObj types.Block
if err := rlp.DecodeBytes(recvMsg.Block, &blockObj); err != nil {
var blockObj *types.Block
if blockObj, err = consensus.validateNewBlock(recvMsg); err != nil {
consensus.getLogger().Warn().
Err(err).
Uint64("MsgBlockNum", recvMsg.BlockNum).
Msg("[OnPrepared] Unparseable block header data")
return
}
// let this handle it own logs
if !consensus.onPreparedSanityChecks(&blockObj, recvMsg) {
return
}
consensus.FBFTLog.AddBlock(&blockObj)
// add block field
blockPayload := make([]byte, len(recvMsg.Block))
copy(blockPayload[:], recvMsg.Block[:])
consensus.block = blockPayload
recvMsg.Block = []byte{} // save memory space
consensus.FBFTLog.AddVerifiedMessage(recvMsg)
consensus.getLogger().Debug().
Uint64("MsgViewID", recvMsg.ViewID).
Uint64("MsgBlockNum", recvMsg.BlockNum).
Hex("blockHash", recvMsg.BlockHash[:]).
Msg("[OnPrepared] Prepared message and block added")
if consensus.BlockVerifier == nil {
consensus.getLogger().Debug().Msg("[onPrepared] consensus received message before init. Ignoring")
return
}
if err := consensus.VerifyBlock(&blockObj); err != nil {
consensus.getLogger().Error().Err(err).Msg("[OnPrepared] Block verification failed")
return
Msg("[OnPrepared] failed to verify new block")
}
if consensus.checkViewID(recvMsg) != nil {
@ -212,7 +260,7 @@ func (consensus *Consensus) onPrepared(recvMsg *FBFTMessage) {
// tryCatchup is also run in onCommitted(), so need to lock with commitMutex.
if consensus.current.Mode() == Normal {
consensus.sendCommitMessages(&blockObj)
consensus.sendCommitMessages(blockObj)
consensus.switchPhase("onPrepared", FBFTCommit)
} else {
// don't sign the block that is not verified

@ -104,6 +104,7 @@ type Roster struct {
Voters map[bls.SerializedPublicKey]*AccommodateHarmonyVote
topLevelRegistry
ShardID uint32
OrderedSlots []bls.SerializedPublicKey
}
func (r Roster) String() string {
@ -245,6 +246,9 @@ func Compute(subComm *shard.Committee, epoch *big.Int) (*Roster, error) {
roster.OurVotingPowerTotalPercentage = ourPercentage
roster.TheirVotingPowerTotalPercentage = theirPercentage
for _, slot := range subComm.Slots {
roster.OrderedSlots = append(roster.OrderedSlots, slot.BLSPublicKey)
}
return roster, nil
}

@ -71,8 +71,8 @@ var (
)
const (
bodyCacheLimit = 256
blockCacheLimit = 256
bodyCacheLimit = 2048
blockCacheLimit = 2048
receiptsCacheLimit = 32
maxFutureBlocks = 256
maxTimeFutureBlocks = 30
@ -262,15 +262,15 @@ func NewBlockChain(
// ValidateNewBlock validates new block.
func (bc *BlockChain) ValidateNewBlock(block *types.Block) error {
state, err := state.New(bc.CurrentBlock().Root(), bc.stateCache)
if err != nil {
return err
}
// NOTE Order of mutating state here matters.
// Process block using the parent state as reference point.
receipts, cxReceipts, _, usedGas, _, err := bc.processor.Process(
block, state, bc.vmConfig,
// Do not read cache from processor.
receipts, cxReceipts, _, usedGas, _, _, err := bc.processor.Process(
block, state, bc.vmConfig, false,
)
if err != nil {
bc.reportBlock(block, receipts, err)
@ -1465,9 +1465,10 @@ func (bc *BlockChain) insertChain(chain types.Blocks, verifyHeaders bool) (int,
events = append(events, ev)
}
// Process block using the parent state as reference point.
receipts, cxReceipts, logs, usedGas, payout, err := bc.processor.Process(
block, state, vmConfig,
receipts, cxReceipts, logs, usedGas, payout, newState, err := bc.processor.Process(
block, state, vmConfig, true,
)
state = newState // update state in case the new state is cached.
if err != nil {
bc.reportBlock(block, receipts, err)
return i, events, coalescedLogs, err

@ -23,7 +23,7 @@ import (
)
// NewTxsEvent is posted when a batch of transactions enter the transaction pool.
type NewTxsEvent struct{ Txs []*types.Transaction }
type NewTxsEvent struct{ Txs []types.PoolTransaction }
// PendingLogsEvent is posted pre mining and notifies of pending logs.
type PendingLogsEvent struct {

@ -70,6 +70,7 @@ func NewEVMContext(msg Message, header *block.Header, chain ChainContext, author
Transfer: Transfer,
IsValidator: IsValidator,
GetHash: GetHashFn(header, chain),
GetVRF: GetVRFFn(header, chain),
Origin: msg.From(),
Coinbase: beneficiary,
BlockNumber: header.Number(),
@ -107,6 +108,46 @@ func GetHashFn(ref *block.Header, chain ChainContext) func(n uint64) common.Hash
}
}
// GetVRFFn returns a GetVRFFn which retrieves header vrf by number
func GetVRFFn(ref *block.Header, chain ChainContext) func(n uint64) common.Hash {
var cache map[uint64]common.Hash
return func(n uint64) common.Hash {
// If there's no hash cache yet, make one
if cache == nil {
curVRF := common.Hash{}
if len(ref.Vrf()) >= 32 {
vrfAndProof := ref.Vrf()
copy(curVRF[:], vrfAndProof[:32])
}
cache = map[uint64]common.Hash{
ref.Number().Uint64(): curVRF,
}
}
// Try to fulfill the request from the cache
if hash, ok := cache[n]; ok {
return hash
}
// Not cached, iterate the blocks and cache the hashes
for header := chain.GetHeader(ref.ParentHash(), ref.Number().Uint64()-1); header != nil; header = chain.GetHeader(header.ParentHash(), header.Number().Uint64()-1) {
curVRF := common.Hash{}
if len(header.Vrf()) >= 32 {
vrfAndProof := header.Vrf()
copy(curVRF[:], vrfAndProof[:32])
}
cache[header.Number().Uint64()] = curVRF
if n == header.Number().Uint64() {
return curVRF
}
}
return common.Hash{}
}
}
// CanTransfer checks whether there are enough funds in the address' account to make a transfer.
// This does not take the necessary gas in to account to make the transfer valid.
func CanTransfer(db vm.StateDB, addr common.Address, amount *big.Int) bool {

@ -228,32 +228,27 @@ func (bc *BlockChain) CommitOffChainData(
); err != nil {
return NonStatTy, err
}
for _, paid := range [...][]reward.Payout{
roundResult.BeaconchainAward, roundResult.ShardChainAward,
} {
for i := range paid {
stats, ok := tempValidatorStats[paid[i].Addr]
for _, paid := range roundResult.Payouts {
stats, ok := tempValidatorStats[paid.Addr]
if !ok {
stats, err = bc.ReadValidatorStats(paid[i].Addr)
stats, err = bc.ReadValidatorStats(paid.Addr)
if err != nil {
utils.Logger().Info().Err(err).
Str("addr", paid[i].Addr.Hex()).
Str("bls-earning-key", paid[i].EarningKey.Hex()).
Str("addr", paid.Addr.Hex()).
Str("bls-earning-key", paid.EarningKey.Hex()).
Msg("could not read validator stats to update for earning per key")
continue
}
tempValidatorStats[paid[i].Addr] = stats
tempValidatorStats[paid.Addr] = stats
}
for j := range stats.MetricsPerShard {
if stats.MetricsPerShard[j].Vote.Identity == paid[i].EarningKey {
if stats.MetricsPerShard[j].Vote.Identity == paid.EarningKey {
stats.MetricsPerShard[j].Earned.Add(
stats.MetricsPerShard[j].Earned,
paid[i].NewlyEarned,
paid.NewlyEarned,
)
}
}
}
}
bc.writeValidatorStats(tempValidatorStats, batch)

@ -89,7 +89,7 @@ func WritePendingSlashingCandidates(db DatabaseWriter, bytes []byte) error {
func ReadCXReceipts(db DatabaseReader, shardID uint32, number uint64, hash common.Hash) (types.CXReceipts, error) {
data, err := db.Get(cxReceiptKey(shardID, number, hash))
if err != nil || len(data) == 0 {
utils.Logger().Info().Err(err).Uint64("number", number).Int("dataLen", len(data)).Msg("ReadCXReceipts")
utils.Logger().Error().Err(err).Uint64("number", number).Int("dataLen", len(data)).Msg("ReadCXReceipts")
return nil, err
}
cxReceipts := types.CXReceipts{}

@ -37,6 +37,7 @@ import (
"github.com/harmony-one/harmony/staking"
"github.com/harmony-one/harmony/staking/effective"
stk "github.com/harmony-one/harmony/staking/types"
staketest "github.com/harmony-one/harmony/staking/types/test"
"github.com/pkg/errors"
)
@ -633,6 +634,10 @@ func (db *DB) Copy() *DB {
}
state.stateObjectsDirty[addr] = struct{}{}
}
for addr := range db.stateValidators {
validatorWrapper := staketest.CopyValidatorWrapper(*db.stateValidators[addr])
state.stateValidators[addr] = &validatorWrapper
}
for hash, logs := range db.logs {
cpy := make([]*types.Log, len(logs))
for i, l := range logs {

@ -33,6 +33,11 @@ import (
"github.com/ethereum/go-ethereum/common"
"github.com/harmony-one/harmony/core/types"
"github.com/harmony-one/harmony/crypto/bls"
"github.com/harmony-one/harmony/crypto/hash"
"github.com/harmony-one/harmony/numeric"
stk "github.com/harmony-one/harmony/staking/types"
)
// Tests that updating a state trie does not leak any database writes prior to
@ -156,6 +161,13 @@ func TestCopy(t *testing.T) {
obj := orig.GetOrNewStateObject(common.BytesToAddress([]byte{i}))
obj.AddBalance(big.NewInt(int64(i)))
orig.updateStateObject(obj)
validatorWrapper := makeValidValidatorWrapper(common.BytesToAddress([]byte{i}))
validatorWrapper.Description.Name = "Original"
err := orig.UpdateValidatorWrapper(common.BytesToAddress([]byte{i}), &validatorWrapper)
if err != nil {
t.Errorf("Couldn't update ValidatorWrapper %d with error %s", i, err)
}
}
orig.Finalise(false)
@ -178,6 +190,85 @@ func TestCopy(t *testing.T) {
orig.updateStateObject(origObj)
copy.updateStateObject(copyObj)
ccopy.updateStateObject(copyObj)
origValWrap, err := orig.ValidatorWrapper(common.BytesToAddress([]byte{i}))
if err != nil {
t.Errorf("Couldn't get validatorWrapper with error: %s", err)
}
copyValWrap, err := copy.ValidatorWrapper(common.BytesToAddress([]byte{i}))
if err != nil {
t.Errorf("Couldn't get validatorWrapper with error: %s", err)
}
ccopyValWrap, err := ccopy.ValidatorWrapper(common.BytesToAddress([]byte{i}))
if err != nil {
t.Errorf("Couldn't get validatorWrapper with error: %s", err)
}
origValWrap.LastEpochInCommittee.SetInt64(1)
copyValWrap.LastEpochInCommittee.SetInt64(2)
ccopyValWrap.LastEpochInCommittee.SetInt64(3)
origValWrap.MinSelfDelegation.Mul(big.NewInt(1e18), big.NewInt(10000))
copyValWrap.MinSelfDelegation.Mul(big.NewInt(1e18), big.NewInt(20000))
ccopyValWrap.MinSelfDelegation.Mul(big.NewInt(1e18), big.NewInt(30000))
origValWrap.MaxTotalDelegation.Mul(big.NewInt(1e18), big.NewInt(10000))
copyValWrap.MaxTotalDelegation.Mul(big.NewInt(1e18), big.NewInt(20000))
ccopyValWrap.MaxTotalDelegation.Mul(big.NewInt(1e18), big.NewInt(30000))
origValWrap.CreationHeight.SetInt64(1)
copyValWrap.CreationHeight.SetInt64(2)
ccopyValWrap.CreationHeight.SetInt64(3)
origValWrap.UpdateHeight.SetInt64(1)
copyValWrap.UpdateHeight.SetInt64(2)
ccopyValWrap.UpdateHeight.SetInt64(3)
origValWrap.Description.Name = "UpdatedOriginal" + string(i)
copyValWrap.Description.Name = "UpdatedCopy" + string(i)
ccopyValWrap.Description.Name = "UpdatedCCopy" + string(i)
origValWrap.Delegations[0].Amount.SetInt64(1)
copyValWrap.Delegations[0].Amount.SetInt64(2)
ccopyValWrap.Delegations[0].Amount.SetInt64(3)
origValWrap.Delegations[0].Reward.SetInt64(1)
copyValWrap.Delegations[0].Reward.SetInt64(2)
ccopyValWrap.Delegations[0].Reward.SetInt64(3)
origValWrap.Delegations[0].Undelegations[0].Amount.SetInt64(1)
copyValWrap.Delegations[0].Undelegations[0].Amount.SetInt64(2)
ccopyValWrap.Delegations[0].Undelegations[0].Amount.SetInt64(3)
origValWrap.Delegations[0].Undelegations[0].Epoch.SetInt64(1)
copyValWrap.Delegations[0].Undelegations[0].Epoch.SetInt64(2)
ccopyValWrap.Delegations[0].Undelegations[0].Epoch.SetInt64(3)
origValWrap.Counters.NumBlocksToSign.SetInt64(1)
copyValWrap.Counters.NumBlocksToSign.SetInt64(2)
ccopyValWrap.Counters.NumBlocksToSign.SetInt64(3)
origValWrap.Counters.NumBlocksSigned.SetInt64(1)
copyValWrap.Counters.NumBlocksSigned.SetInt64(2)
ccopyValWrap.Counters.NumBlocksSigned.SetInt64(3)
origValWrap.BlockReward.SetInt64(1)
copyValWrap.BlockReward.SetInt64(2)
ccopyValWrap.BlockReward.SetInt64(3)
err = orig.UpdateValidatorWrapper(common.BytesToAddress([]byte{i}), origValWrap)
if err != nil {
t.Errorf("Couldn't update ValidatorWrapper %d with error %s", i, err)
}
err = copy.UpdateValidatorWrapper(common.BytesToAddress([]byte{i}), copyValWrap)
if err != nil {
t.Errorf("Couldn't update ValidatorWrapper %d with error %s", i, err)
}
err = ccopy.UpdateValidatorWrapper(common.BytesToAddress([]byte{i}), ccopyValWrap)
if err != nil {
t.Errorf("Couldn't update ValidatorWrapper %d with error %s", i, err)
}
}
// Finalise the changes on all concurrently
@ -208,6 +299,149 @@ func TestCopy(t *testing.T) {
if want := big.NewInt(5 * int64(i)); ccopyObj.Balance().Cmp(want) != 0 {
t.Errorf("copy obj %d: balance mismatch: have %v, want %v", i, ccopyObj.Balance(), want)
}
origValWrap, err := orig.ValidatorWrapper(common.BytesToAddress([]byte{i}))
if err != nil {
t.Errorf("Couldn't get validatorWrapper %d with error: %s", i, err)
}
copyValWrap, err := copy.ValidatorWrapper(common.BytesToAddress([]byte{i}))
if err != nil {
t.Errorf("Couldn't get validatorWrapper %d with error: %s", i, err)
}
ccopyValWrap, err := ccopy.ValidatorWrapper(common.BytesToAddress([]byte{i}))
if err != nil {
t.Errorf("Couldn't get validatorWrapper %d with error: %s", i, err)
}
if origValWrap.LastEpochInCommittee.Cmp(big.NewInt(1)) != 0 {
t.Errorf("LastEpochInCommittee %d: balance mismatch: have %v, want %v", i, origValWrap.LastEpochInCommittee, big.NewInt(1))
}
if copyValWrap.LastEpochInCommittee.Cmp(big.NewInt(2)) != 0 {
t.Errorf("LastEpochInCommittee %d: balance mismatch: have %v, want %v", i, copyValWrap.LastEpochInCommittee, big.NewInt(2))
}
if ccopyValWrap.LastEpochInCommittee.Cmp(big.NewInt(3)) != 0 {
t.Errorf("LastEpochInCommittee %d: balance mismatch: have %v, want %v", i, ccopyValWrap.LastEpochInCommittee, big.NewInt(3))
}
if want := new(big.Int).Mul(big.NewInt(1e18), big.NewInt(10000)); origValWrap.MinSelfDelegation.Cmp(want) != 0 {
t.Errorf("MinSelfDelegation %d: balance mismatch: have %v, want %v", i, origValWrap.MinSelfDelegation, want)
}
if want := new(big.Int).Mul(big.NewInt(1e18), big.NewInt(20000)); copyValWrap.MinSelfDelegation.Cmp(want) != 0 {
t.Errorf("MinSelfDelegation %d: balance mismatch: have %v, want %v", i, copyValWrap.MinSelfDelegation, want)
}
if want := new(big.Int).Mul(big.NewInt(1e18), big.NewInt(30000)); ccopyValWrap.MinSelfDelegation.Cmp(want) != 0 {
t.Errorf("MinSelfDelegation %d: balance mismatch: have %v, want %v", i, ccopyValWrap.MinSelfDelegation, want)
}
if want := new(big.Int).Mul(big.NewInt(1e18), big.NewInt(10000)); origValWrap.MaxTotalDelegation.Cmp(want) != 0 {
t.Errorf("MaxTotalDelegation %d: balance mismatch: have %v, want %v", i, origValWrap.MaxTotalDelegation, want)
}
if want := new(big.Int).Mul(big.NewInt(1e18), big.NewInt(20000)); copyValWrap.MaxTotalDelegation.Cmp(want) != 0 {
t.Errorf("MaxTotalDelegation %d: balance mismatch: have %v, want %v", i, copyValWrap.MaxTotalDelegation, want)
}
if want := new(big.Int).Mul(big.NewInt(1e18), big.NewInt(30000)); ccopyValWrap.MaxTotalDelegation.Cmp(want) != 0 {
t.Errorf("MaxTotalDelegation %d: balance mismatch: have %v, want %v", i, ccopyValWrap.MaxTotalDelegation, want)
}
if origValWrap.CreationHeight.Cmp(big.NewInt(1)) != 0 {
t.Errorf("CreationHeight %d: balance mismatch: have %v, want %v", i, origValWrap.CreationHeight, big.NewInt(1))
}
if copyValWrap.CreationHeight.Cmp(big.NewInt(2)) != 0 {
t.Errorf("CreationHeight %d: balance mismatch: have %v, want %v", i, copyValWrap.CreationHeight, big.NewInt(2))
}
if ccopyValWrap.CreationHeight.Cmp(big.NewInt(3)) != 0 {
t.Errorf("CreationHeight %d: balance mismatch: have %v, want %v", i, ccopyValWrap.CreationHeight, big.NewInt(3))
}
if origValWrap.UpdateHeight.Cmp(big.NewInt(1)) != 0 {
t.Errorf("UpdateHeight %d: balance mismatch: have %v, want %v", i, origValWrap.UpdateHeight, big.NewInt(1))
}
if copyValWrap.UpdateHeight.Cmp(big.NewInt(2)) != 0 {
t.Errorf("UpdateHeight %d: balance mismatch: have %v, want %v", i, copyValWrap.UpdateHeight, big.NewInt(2))
}
if ccopyValWrap.UpdateHeight.Cmp(big.NewInt(3)) != 0 {
t.Errorf("UpdateHeight %d: balance mismatch: have %v, want %v", i, ccopyValWrap.UpdateHeight, big.NewInt(3))
}
if want := "UpdatedOriginal" + string(i); origValWrap.Description.Name != want {
t.Errorf("originalValWrap %d: Incorrect Name: have %s, want %s", i, origValWrap.Description.Name, want)
}
if want := "UpdatedCopy" + string(i); copyValWrap.Description.Name != want {
t.Errorf("originalValWrap %d: Incorrect Name: have %s, want %s", i, copyValWrap.Description.Name, want)
}
if want := "UpdatedCCopy" + string(i); ccopyValWrap.Description.Name != want {
t.Errorf("originalValWrap %d: Incorrect Name: have %s, want %s", i, ccopyValWrap.Description.Name, want)
}
if origValWrap.Delegations[0].Amount.Cmp(big.NewInt(1)) != 0 {
t.Errorf("Delegations[0].Amount %d: balance mismatch: have %v, want %v", i, origValWrap.Delegations[0].Amount, big.NewInt(1))
}
if copyValWrap.Delegations[0].Amount.Cmp(big.NewInt(2)) != 0 {
t.Errorf("Delegations[0].Amount %d: balance mismatch: have %v, want %v", i, copyValWrap.Delegations[0].Amount, big.NewInt(2))
}
if ccopyValWrap.Delegations[0].Amount.Cmp(big.NewInt(3)) != 0 {
t.Errorf("Delegations[0].Amount %d: balance mismatch: have %v, want %v", i, ccopyValWrap.Delegations[0].Amount, big.NewInt(3))
}
if origValWrap.Delegations[0].Reward.Cmp(big.NewInt(1)) != 0 {
t.Errorf("Delegations[0].Reward %d: balance mismatch: have %v, want %v", i, origValWrap.Delegations[0].Reward, big.NewInt(1))
}
if copyValWrap.Delegations[0].Reward.Cmp(big.NewInt(2)) != 0 {
t.Errorf("Delegations[0].Reward %d: balance mismatch: have %v, want %v", i, copyValWrap.Delegations[0].Reward, big.NewInt(2))
}
if ccopyValWrap.Delegations[0].Reward.Cmp(big.NewInt(3)) != 0 {
t.Errorf("Delegations[0].Reward %d: balance mismatch: have %v, want %v", i, ccopyValWrap.Delegations[0].Reward, big.NewInt(3))
}
if origValWrap.Delegations[0].Undelegations[0].Amount.Cmp(big.NewInt(1)) != 0 {
t.Errorf("Delegations[0].Undelegations[0].Amount %d: balance mismatch: have %v, want %v", i, origValWrap.Delegations[0].Undelegations[0].Amount, big.NewInt(1))
}
if copyValWrap.Delegations[0].Undelegations[0].Amount.Cmp(big.NewInt(2)) != 0 {
t.Errorf("Delegations[0].Undelegations[0].Amount %d: balance mismatch: have %v, want %v", i, copyValWrap.Delegations[0].Undelegations[0].Amount, big.NewInt(2))
}
if ccopyValWrap.Delegations[0].Undelegations[0].Amount.Cmp(big.NewInt(3)) != 0 {
t.Errorf("Delegations[0].Undelegations[0].Amount %d: balance mismatch: have %v, want %v", i, ccopyValWrap.Delegations[0].Undelegations[0].Amount, big.NewInt(3))
}
if origValWrap.Delegations[0].Undelegations[0].Epoch.Cmp(big.NewInt(1)) != 0 {
t.Errorf("CreationHeight %d: balance mismatch: have %v, want %v", i, origValWrap.Delegations[0].Undelegations[0].Epoch, big.NewInt(1))
}
if copyValWrap.Delegations[0].Undelegations[0].Epoch.Cmp(big.NewInt(2)) != 0 {
t.Errorf("CreationHeight %d: balance mismatch: have %v, want %v", i, copyValWrap.Delegations[0].Undelegations[0].Epoch, big.NewInt(2))
}
if ccopyValWrap.Delegations[0].Undelegations[0].Epoch.Cmp(big.NewInt(3)) != 0 {
t.Errorf("CreationHeight %d: balance mismatch: have %v, want %v", i, ccopyValWrap.Delegations[0].Undelegations[0].Epoch, big.NewInt(3))
}
if origValWrap.Counters.NumBlocksToSign.Cmp(big.NewInt(1)) != 0 {
t.Errorf("Counters.NumBlocksToSign %d: balance mismatch: have %v, want %v", i, origValWrap.Counters.NumBlocksToSign, big.NewInt(1))
}
if copyValWrap.Counters.NumBlocksToSign.Cmp(big.NewInt(2)) != 0 {
t.Errorf("Counters.NumBlocksToSign %d: balance mismatch: have %v, want %v", i, copyValWrap.Counters.NumBlocksToSign, big.NewInt(2))
}
if ccopyValWrap.Counters.NumBlocksToSign.Cmp(big.NewInt(3)) != 0 {
t.Errorf("Counters.NumBlocksToSign %d: balance mismatch: have %v, want %v", i, ccopyValWrap.Counters.NumBlocksToSign, big.NewInt(3))
}
if origValWrap.Counters.NumBlocksSigned.Cmp(big.NewInt(1)) != 0 {
t.Errorf("Counters.NumBlocksSigned %d: balance mismatch: have %v, want %v", i, origValWrap.Counters.NumBlocksSigned, big.NewInt(1))
}
if copyValWrap.Counters.NumBlocksSigned.Cmp(big.NewInt(2)) != 0 {
t.Errorf("Counters.NumBlocksSigned %d: balance mismatch: have %v, want %v", i, copyValWrap.Counters.NumBlocksSigned, big.NewInt(2))
}
if ccopyValWrap.Counters.NumBlocksSigned.Cmp(big.NewInt(3)) != 0 {
t.Errorf("Counters.NumBlocksSigned %d: balance mismatch: have %v, want %v", i, ccopyValWrap.Counters.NumBlocksSigned, big.NewInt(3))
}
if origValWrap.BlockReward.Cmp(big.NewInt(1)) != 0 {
t.Errorf("Block Reward %d: balance mismatch: have %v, want %v", i, origValWrap.BlockReward, big.NewInt(1))
}
if copyValWrap.BlockReward.Cmp(big.NewInt(2)) != 0 {
t.Errorf("Block Reward %d: balance mismatch: have %v, want %v", i, copyValWrap.BlockReward, big.NewInt(2))
}
if ccopyValWrap.BlockReward.Cmp(big.NewInt(3)) != 0 {
t.Errorf("Block Reward %d: balance mismatch: have %v, want %v", i, ccopyValWrap.BlockReward, big.NewInt(3))
}
}
}
@ -681,3 +915,72 @@ func TestDeleteCreateRevert(t *testing.T) {
t.Fatalf("self-destructed contract came alive")
}
}
func makeValidValidatorWrapper(addr common.Address) stk.ValidatorWrapper {
cr := stk.CommissionRates{
Rate: numeric.ZeroDec(),
MaxRate: numeric.ZeroDec(),
MaxChangeRate: numeric.ZeroDec(),
}
c := stk.Commission{cr, big.NewInt(300)}
d := stk.Description{
Name: "Wayne",
Identity: "wen",
Website: "harmony.one.wen",
Details: "best",
}
v := stk.Validator{
Address: addr,
SlotPubKeys: []bls.SerializedPublicKey{makeBLSPubSigPair().pub},
LastEpochInCommittee: big.NewInt(20),
MinSelfDelegation: new(big.Int).Mul(big.NewInt(10000), big.NewInt(1e18)),
MaxTotalDelegation: new(big.Int).Mul(big.NewInt(12000), big.NewInt(1e18)),
Commission: c,
Description: d,
CreationHeight: big.NewInt(12306),
}
ds := stk.Delegations{
stk.Delegation{
DelegatorAddress: v.Address,
Amount: big.NewInt(0),
Reward: big.NewInt(0),
Undelegations: stk.Undelegations{
stk.Undelegation{
Amount: big.NewInt(0),
Epoch: big.NewInt(0),
},
},
},
}
br := big.NewInt(1)
w := stk.ValidatorWrapper{
Validator: v,
Delegations: ds,
BlockReward: br,
}
w.Counters.NumBlocksSigned = big.NewInt(0)
w.Counters.NumBlocksToSign = big.NewInt(0)
return w
}
type blsPubSigPair struct {
pub bls.SerializedPublicKey
sig bls.SerializedSignature
}
func makeBLSPubSigPair() blsPubSigPair {
blsPriv := bls.RandPrivateKey()
blsPub := blsPriv.GetPublicKey()
msgHash := hash.Keccak256([]byte(stk.BLSVerificationStr))
sig := blsPriv.SignHash(msgHash)
var shardPub bls.SerializedPublicKey
copy(shardPub[:], blsPub.Serialize())
var shardSig bls.SerializedSignature
copy(shardSig[:], sig.Serialize())
return blsPubSigPair{shardPub, shardSig}
}

@ -18,6 +18,9 @@ package core
import (
"math/big"
"time"
lru "github.com/hashicorp/golang-lru"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/crypto"
@ -36,6 +39,10 @@ import (
"github.com/pkg/errors"
)
const (
resultCacheLimit = 64 // The number of cached results from processing blocks
)
// StateProcessor is a basic Processor, which takes care of transitioning
// state from one point to another.
//
@ -44,16 +51,28 @@ type StateProcessor struct {
config *params.ChainConfig // Chain configuration options
bc *BlockChain // Canonical block chain
engine consensus_engine.Engine // Consensus engine used for block rewards
resultCache *lru.Cache // Cache for result after a certain block is processed
}
type ProcessorResult struct {
Receipts types.Receipts
CxReceipts types.CXReceipts
Logs []*types.Log
UsedGas uint64
Reward reward.Reader
State *state.DB
}
// NewStateProcessor initialises a new StateProcessor.
func NewStateProcessor(
config *params.ChainConfig, bc *BlockChain, engine consensus_engine.Engine,
) *StateProcessor {
resultCache, _ := lru.New(resultCacheLimit)
return &StateProcessor{
config: config,
bc: bc,
engine: engine,
resultCache: resultCache,
}
}
@ -65,11 +84,22 @@ func NewStateProcessor(
// returns the amount of gas that was used in the process. If any of the
// transactions failed to execute due to insufficient gas it will return an error.
func (p *StateProcessor) Process(
block *types.Block, statedb *state.DB, cfg vm.Config,
block *types.Block, statedb *state.DB, cfg vm.Config, readCache bool,
) (
types.Receipts, types.CXReceipts,
[]*types.Log, uint64, reward.Reader, error,
[]*types.Log, uint64, reward.Reader, *state.DB, error,
) {
cacheKey := block.Hash()
if readCache {
if cached, ok := p.resultCache.Get(cacheKey); ok {
// Return the cached result to avoid process the same block again.
// Only the successful results are cached in case for retry.
result := cached.(*ProcessorResult)
utils.Logger().Info().Str("block num", block.Number().String()).Msg("result cache hit.")
return result.Receipts, result.CxReceipts, result.Logs, result.UsedGas, result.Reward, result.State, nil
}
}
var (
receipts types.Receipts
outcxs types.CXReceipts
@ -82,9 +112,10 @@ func (p *StateProcessor) Process(
beneficiary, err := p.bc.GetECDSAFromCoinbase(header)
if err != nil {
return nil, nil, nil, 0, nil, err
return nil, nil, nil, 0, nil, statedb, err
}
startTime := time.Now()
// Iterate over and process the individual transactions
for i, tx := range block.Transactions() {
statedb.Prepare(tx.Hash(), block.Hash(), i)
@ -92,7 +123,7 @@ func (p *StateProcessor) Process(
p.config, p.bc, &beneficiary, gp, statedb, header, tx, usedGas, cfg,
)
if err != nil {
return nil, nil, nil, 0, nil, err
return nil, nil, nil, 0, nil, statedb, err
}
receipts = append(receipts, receipt)
if cxReceipt != nil {
@ -100,6 +131,9 @@ func (p *StateProcessor) Process(
}
allLogs = append(allLogs, receipt.Logs...)
}
utils.Logger().Debug().Int64("elapsed time", time.Now().Sub(startTime).Milliseconds()).Msg("Process Normal Txns")
startTime = time.Now()
// Iterate over and process the staking transactions
L := len(block.Transactions())
for i, tx := range block.StakingTransactions() {
@ -108,11 +142,12 @@ func (p *StateProcessor) Process(
p.config, p.bc, &beneficiary, gp, statedb, header, tx, usedGas, cfg,
)
if err != nil {
return nil, nil, nil, 0, nil, err
return nil, nil, nil, 0, nil, statedb, err
}
receipts = append(receipts, receipt)
allLogs = append(allLogs, receipt.Logs...)
}
utils.Logger().Debug().Int64("elapsed time", time.Now().Sub(startTime).Milliseconds()).Msg("Process Staking Txns")
// incomingReceipts should always be processed
// after transactions (to be consistent with the block proposal)
@ -121,14 +156,14 @@ func (p *StateProcessor) Process(
p.config, statedb, header, cx,
); err != nil {
return nil, nil,
nil, 0, nil, errors.New("[Process] Cannot apply incoming receipts")
nil, 0, nil, statedb, errors.New("[Process] Cannot apply incoming receipts")
}
}
slashes := slash.Records{}
if s := header.Slashes(); len(s) > 0 {
if err := rlp.DecodeBytes(s, &slashes); err != nil {
return nil, nil, nil, 0, nil, errors.New(
return nil, nil, nil, 0, nil, statedb, errors.New(
"[Process] Cannot finalize block",
)
}
@ -145,10 +180,24 @@ func (p *StateProcessor) Process(
receipts, outcxs, incxs, block.StakingTransactions(), slashes, sigsReady, func() uint64 { return header.ViewID().Uint64() },
)
if err != nil {
return nil, nil, nil, 0, nil, errors.New("[Process] Cannot finalize block")
return nil, nil, nil, 0, nil, statedb, errors.New("[Process] Cannot finalize block")
}
result := &ProcessorResult{
Receipts: receipts,
CxReceipts: outcxs,
Logs: allLogs,
UsedGas: *usedGas,
Reward: payout,
State: statedb,
}
p.resultCache.Add(cacheKey, result)
return receipts, outcxs, allLogs, *usedGas, payout, statedb, nil
}
return receipts, outcxs, allLogs, *usedGas, payout, nil
// CacheProcessorResult caches the process result on the cache key.
func (p *StateProcessor) CacheProcessorResult(cacheKey interface{}, result *ProcessorResult) {
p.resultCache.Add(cacheKey, result)
}
// return true if it is valid

@ -18,6 +18,7 @@ package core
import (
"bytes"
"fmt"
"math"
"math/big"
"sort"
@ -268,7 +269,7 @@ func (st *StateTransition) TransitionDb() (ExecutionResult, error) {
return ExecutionResult{}, err
}
if err = st.useGas(gas); err != nil {
return ExecutionResult{}, err
return ExecutionResult{}, fmt.Errorf("%w: have %d, want %d", ErrIntrinsicGas, st.gas, gas)
}
evm := st.evm

@ -1290,7 +1290,7 @@ func (pool *TxPool) removeTx(hash common.Hash, outofbound bool) {
// invalidated transactions (low nonce, low balance) are deleted.
func (pool *TxPool) promoteExecutables(accounts []common.Address) {
// Track the promoted transactions to broadcast them at once
var promoted types.PoolTransactions
var promoted []types.PoolTransaction
logger := utils.Logger().With().Stack().Logger()
// Gather all the accounts potentially needing updates
@ -1353,9 +1353,9 @@ func (pool *TxPool) promoteExecutables(accounts []common.Address) {
}
}
// Notify subsystem for new promoted transactions.
//if len(promoted) > 0 {
// go pool.txFeed.Send(NewTxsEvent{promoted})
//}
if len(promoted) > 0 {
go pool.txFeed.Send(NewTxsEvent{promoted})
}
// If the pending limit is overflown, start equalizing allowances
pending := uint64(0)
for _, list := range pool.pending {

@ -54,9 +54,12 @@ type Validator interface {
// initial state is based. It should return the receipts generated, amount
// of gas used in the process and return an error if any of the internal rules
// failed.
// Process will cache the result of successfully processed blocks.
// readCache decides whether the method will try reading from result cache.
type Processor interface {
Process(block *types.Block, statedb *state.DB, cfg vm.Config) (
Process(block *types.Block, statedb *state.DB, cfg vm.Config, readCache bool) (
types.Receipts, types.CXReceipts,
[]*types.Log, uint64, reward.Reader, error,
[]*types.Log, uint64, reward.Reader, *state.DB, error,
)
CacheProcessorResult(cacheKey interface{}, result *ProcessorResult)
}

@ -59,6 +59,7 @@ type Contract struct {
Gas uint64
value *big.Int
WithDataCopyFix bool // with fix for https://github.com/ethereum/go-ethereum/pull/23446
}
// NewContract returns a new contract environment for the execution of EVM.

@ -30,6 +30,11 @@ import (
"github.com/ethereum/go-ethereum/crypto/bn256"
"github.com/harmony-one/harmony/internal/params"
"golang.org/x/crypto/ripemd160"
//Needed for SHA3-256 FIPS202
"encoding/hex"
"golang.org/x/crypto/sha3"
)
// PrecompiledContract is the basic interface for native Go contracts. The implementation
@ -91,6 +96,24 @@ var PrecompiledContractsVRF = map[common.Address]PrecompiledContract{
common.BytesToAddress([]byte{255}): &vrf{},
}
// PrecompiledContractsSHA3FIPS contains the default set of pre-compiled Ethereum
// contracts used in the Istanbul release. plus VRF and SHA3FIPS-202 standard
var PrecompiledContractsSHA3FIPS = map[common.Address]PrecompiledContract{
common.BytesToAddress([]byte{1}): &ecrecover{},
common.BytesToAddress([]byte{2}): &sha256hash{},
common.BytesToAddress([]byte{3}): &ripemd160hash{},
common.BytesToAddress([]byte{4}): &dataCopy{},
common.BytesToAddress([]byte{5}): &bigModExp{},
common.BytesToAddress([]byte{6}): &bn256AddIstanbul{},
common.BytesToAddress([]byte{7}): &bn256ScalarMulIstanbul{},
common.BytesToAddress([]byte{8}): &bn256PairingIstanbul{},
common.BytesToAddress([]byte{9}): &blake2F{},
common.BytesToAddress([]byte{255}): &vrf{},
common.BytesToAddress([]byte{253}): &sha3fip{},
common.BytesToAddress([]byte{254}): &ecrecoverPublicKey{},
}
// RunPrecompiledContract runs and evaluates the output of a precompiled contract.
func RunPrecompiledContract(p PrecompiledContract, input []byte, contract *Contract) (ret []byte, err error) {
gas := p.RequiredGas(input)
@ -533,3 +556,59 @@ func (c *vrf) Run(input []byte) ([]byte, error) {
// So here we simply return it
return append([]byte{}, input...), nil
}
// SHA3-256 FIPS 202 standard implemented as a native contract.
type sha3fip struct{}
// TODO Check if the gas price calculation needs modification
// RequiredGas returns the gas required to execute the pre-compiled contract.
//
// This method does not require any overflow checking as the input size gas costs
// required for anything significant is so high it's impossible to pay for.
func (c *sha3fip) RequiredGas(input []byte) uint64 {
return uint64(len(input)+31)/32*params.Sha3FipsWordGas + params.Sha3FipsGas
}
func (c *sha3fip) Run(input []byte) ([]byte, error) {
hexStr := common.Bytes2Hex(input)
pub, _ := hex.DecodeString(hexStr)
h := sha3.Sum256(pub[:])
return h[:], nil
}
// ECRECOVER implemented as a native contract.
type ecrecoverPublicKey struct{}
func (c *ecrecoverPublicKey) RequiredGas(input []byte) uint64 {
return params.EcrecoverGas
}
func (c *ecrecoverPublicKey) Run(input []byte) ([]byte, error) {
const ecrecoverPublicKeyInputLength = 128
input = common.RightPadBytes(input, ecrecoverPublicKeyInputLength)
// "input" is (hash, v, r, s), each 32 bytes
// but for ecrecover we want (r, s, v)
r := new(big.Int).SetBytes(input[64:96])
s := new(big.Int).SetBytes(input[96:128])
v := input[63]
// tighter sig s values input homestead only apply to tx sigs
if !allZero(input[32:63]) || !crypto.ValidateSignatureValues(v, r, s, false) {
return nil, nil
}
// We must make sure not to modify the 'input', so placing the 'v' along with
// the signature needs to be done on a new allocation
sig := make([]byte, 65)
copy(sig, input[64:128])
sig[64] = v
// v needs to be at the end for libsecp256k1
pubKey, err := crypto.Ecrecover(input[:32], sig)
// make sure the public key is a valid one
if err != nil {
return nil, nil
}
return pubKey, nil
}

@ -399,7 +399,7 @@ var blake2FTests = []precompiledTest{
}
func testPrecompiled(addr string, test precompiledTest, t *testing.T) {
p := PrecompiledContractsIstanbul[common.HexToAddress(addr)]
p := PrecompiledContractsSHA3FIPS[common.HexToAddress(addr)]
in := common.Hex2Bytes(test.input)
contract := NewContract(AccountRef(common.HexToAddress("1337")),
nil, new(big.Int), p.RequiredGas(in))
@ -418,7 +418,7 @@ func testPrecompiled(addr string, test precompiledTest, t *testing.T) {
}
func testPrecompiledOOG(addr string, test precompiledTest, t *testing.T) {
p := PrecompiledContractsIstanbul[common.HexToAddress(addr)]
p := PrecompiledContractsSHA3FIPS[common.HexToAddress(addr)]
in := common.Hex2Bytes(test.input)
contract := NewContract(AccountRef(common.HexToAddress("1337")),
nil, new(big.Int), p.RequiredGas(in)-1)
@ -436,7 +436,7 @@ func testPrecompiledOOG(addr string, test precompiledTest, t *testing.T) {
}
func testPrecompiledFailure(addr string, test precompiledFailureTest, t *testing.T) {
p := PrecompiledContractsIstanbul[common.HexToAddress(addr)]
p := PrecompiledContractsSHA3FIPS[common.HexToAddress(addr)]
in := common.Hex2Bytes(test.input)
contract := NewContract(AccountRef(common.HexToAddress("31337")),
nil, new(big.Int), p.RequiredGas(in))
@ -458,7 +458,7 @@ func benchmarkPrecompiled(addr string, test precompiledTest, bench *testing.B) {
if test.noBenchmark {
return
}
p := PrecompiledContractsIstanbul[common.HexToAddress(addr)]
p := PrecompiledContractsSHA3FIPS[common.HexToAddress(addr)]
in := common.Hex2Bytes(test.input)
reqGas := p.RequiredGas(in)
contract := NewContract(AccountRef(common.HexToAddress("1337")),
@ -661,3 +661,59 @@ func TestPrecompiledEcrecover(t *testing.T) {
}
}
// sha3fip test vectors
var sha3fipTests = []precompiledTest{
{
input: "0448250ebe88d77e0a12bcf530fe6a2cf1ac176945638d309b840d631940c93b78c2bd6d16f227a8877e3f1604cd75b9c5a8ab0cac95174a8a0a0f8ea9e4c10bca",
expected: "c7647f7e251bf1bd70863c8693e93a4e77dd0c9a689073e987d51254317dc704",
name: "sha3fip",
},
{
input: "1234",
expected: "19becdc0e8d6dd4aa2c9c2983dbb9c61956a8ade69b360d3e6019f0bcd5557a9",
name: "sha3fip",
},
}
func TestPrecompiledSHA3fip(t *testing.T) {
for _, test := range sha3fipTests {
testPrecompiled("FD", test, t)
}
}
// EcRecover test vectors
var ecRecoverPublicKeyTests = []precompiledTest{
{
input: "c5d6c454e4d7a8e8a654f5ef96e8efe41d21a65b171b298925414aa3dc061e37" +
"0000000000000000000000000000000000000000000000000000000000000000" +
"4011de30c04302a2352400df3d1459d6d8799580dceb259f45db1d99243a8d0c" +
"64f548b7776cb93e37579b830fc3efce41e12e0958cda9f8c5fcad682c610795",
expected: "0448250ebe88d77e0a12bcf530fe6a2cf1ac176945638d309b840d631940c93b78c2bd6d16f227a8877e3f1604cd75b9c5a8ab0cac95174a8a0a0f8ea9e4c10bca",
name: "CallEcrecoverrecoverable Key",
},
{
input: "c5d6c454e4d7a8e8a654f5ef96e8efe41d21a65b171b298925414aa3dc061e37" +
"000000000000000000000000000000000000000000000000000000000000001b" +
"4011de30c04302a2352400df3d1459d6d8799580dceb259f45db1d99243a8d0c" +
"64f548b7776cb93e37579b830fc3efce41e12e0958cda9f8c5fcad682c610795",
expected: "",
name: "InvalidLowV-bits-1",
}, {
input: "c5d6c454e4d7a8e8a654f5ef96e8efe41d21a65b171b298925414aa3dc061e37" +
"000000000000000000000000000000000000000000000000000000000000001c" +
"4011de30c04302a2352400df3d1459d6d8799580dceb259f45db1d99243a8d0c" +
"64f548b7776cb93e37579b830fc3efce41e12e0958cda9f8c5fcad682c610795",
expected: "",
name: "InvalidLowV-bits-1",
},
}
func TestPrecompiledEcrecoverPublicKey(t *testing.T) {
for _, test := range ecRecoverPublicKeyTests {
testPrecompiled("FE", test, t)
}
}

@ -42,6 +42,9 @@ type (
// GetHashFunc returns the nth block hash in the blockchain
// and is used by the BLOCKHASH EVM op code.
GetHashFunc func(uint64) common.Hash
// GetVRFFunc returns the nth block vrf in the blockchain
// and is used by the precompile VRF contract.
GetVRFFunc func(uint64) common.Hash
)
// run runs the given contract and takes care of running precompiles with a fallback to the byte code interpreter.
@ -57,11 +60,30 @@ func run(evm *EVM, contract *Contract, input []byte, readOnly bool) ([]byte, err
if evm.chainRules.IsVRF {
precompiles = PrecompiledContractsVRF
}
if evm.chainRules.IsSHA3 {
precompiles = PrecompiledContractsSHA3FIPS
}
if p := precompiles[*contract.CodeAddr]; p != nil {
if _, ok := p.(*vrf); ok {
// Override the input with vrf data of the current block so it can be returned to the contract program.
if evm.chainRules.IsPrevVRF {
requestedBlockNum := big.NewInt(0).SetBytes(input)
minBlockNum := big.NewInt(0).Sub(evm.BlockNumber, common.Big257)
if requestedBlockNum.Cmp(evm.BlockNumber) == 0 {
input = evm.Context.VRF.Bytes()
} else if requestedBlockNum.Cmp(minBlockNum) > 0 && requestedBlockNum.Cmp(evm.BlockNumber) < 0 {
// requested block number is in range
input = evm.GetVRF(requestedBlockNum.Uint64()).Bytes()
} else {
// else default to the current block's VRF
input = evm.Context.VRF.Bytes()
}
} else {
// Override the input with vrf data of the requested block so it can be returned to the contract program.
input = evm.Context.VRF.Bytes()
}
}
return RunPrecompiledContract(p, input, contract)
}
}
@ -75,6 +97,10 @@ func run(evm *EVM, contract *Contract, input []byte, readOnly bool) ([]byte, err
}(evm.interpreter)
evm.interpreter = interpreter
}
if evm.ChainConfig().IsDataCopyFixEpoch(evm.EpochNumber) {
contract.WithDataCopyFix = true
}
return interpreter.Run(contract, input, readOnly)
}
@ -92,6 +118,8 @@ type Context struct {
Transfer TransferFunc
// GetHash returns the hash corresponding to n
GetHash GetHashFunc
// GetVRF returns the VRF corresponding to n
GetVRF GetVRFFunc
// IsValidator determines whether the address corresponds to a validator or a smart contract
// true: is a validator address; false: is smart contract address
@ -237,6 +265,9 @@ func (evm *EVM) Call(caller ContractRef, addr common.Address, input []byte, gas
if evm.chainRules.IsVRF {
precompiles = PrecompiledContractsVRF
}
if evm.chainRules.IsSHA3 {
precompiles = PrecompiledContractsSHA3FIPS
}
if precompiles[addr] == nil && evm.ChainConfig().IsS3(evm.EpochNumber) && value.Sign() == 0 {
// Calling a non existing account, don't do anything, but ping the tracer

@ -767,6 +767,9 @@ func opCall(pc *uint64, interpreter *EVMInterpreter, contract *Contract, memory
stack.push(interpreter.intPool.get().SetUint64(1))
}
if err == nil || err == ErrExecutionReverted {
if contract.WithDataCopyFix {
ret = common.CopyBytes(ret)
}
memory.Set(retOffset.Uint64(), retSize.Uint64(), ret)
}
contract.Gas += returnGas
@ -796,6 +799,9 @@ func opCallCode(pc *uint64, interpreter *EVMInterpreter, contract *Contract, mem
stack.push(interpreter.intPool.get().SetUint64(1))
}
if err == nil || err == ErrExecutionReverted {
if contract.WithDataCopyFix {
ret = common.CopyBytes(ret)
}
memory.Set(retOffset.Uint64(), retSize.Uint64(), ret)
}
contract.Gas += returnGas
@ -821,6 +827,9 @@ func opDelegateCall(pc *uint64, interpreter *EVMInterpreter, contract *Contract,
stack.push(interpreter.intPool.get().SetUint64(1))
}
if err == nil || err == ErrExecutionReverted {
if contract.WithDataCopyFix {
ret = common.CopyBytes(ret)
}
memory.Set(retOffset.Uint64(), retSize.Uint64(), ret)
}
contract.Gas += returnGas
@ -846,6 +855,9 @@ func opStaticCall(pc *uint64, interpreter *EVMInterpreter, contract *Contract, m
stack.push(interpreter.intPool.get().SetUint64(1))
}
if err == nil || err == ErrExecutionReverted {
if contract.WithDataCopyFix {
ret = common.CopyBytes(ret)
}
memory.Set(retOffset.Uint64(), retSize.Uint64(), ret)
}
contract.Gas += returnGas

@ -29,6 +29,7 @@ func NewEnv(cfg *Config) *vm.EVM {
Transfer: core.Transfer,
IsValidator: core.IsValidator,
GetHash: func(uint64) common.Hash { return common.Hash{} },
GetVRF: func(uint64) common.Hash { return common.Hash{} },
Origin: cfg.Origin,
Coinbase: cfg.Coinbase,

@ -0,0 +1,626 @@
// Copyright 2016 The go-ethereum Authors
// This file is part of the go-ethereum library.
//
// The go-ethereum library is free software: you can redistribute it and/or modify
// it under the terms of the GNU Lesser General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// The go-ethereum library is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Lesser General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public License
// along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>.
package rpc
import (
"bytes"
"context"
"errors"
"fmt"
"net/url"
"reflect"
"strconv"
"sync/atomic"
"time"
"github.com/goccy/go-json"
"github.com/ethereum/go-ethereum/log"
)
var (
ErrClientQuit = errors.New("client is closed")
ErrNoResult = errors.New("no result in JSON-RPC response")
ErrSubscriptionQueueOverflow = errors.New("subscription queue overflow")
errClientReconnected = errors.New("client reconnected")
errDead = errors.New("connection lost")
)
const (
// Timeouts
defaultDialTimeout = 10 * time.Second // used if context has no deadline
subscribeTimeout = 5 * time.Second // overall timeout eth_subscribe, rpc_modules calls
)
const (
// Subscriptions are removed when the subscriber cannot keep up.
//
// This can be worked around by supplying a channel with sufficiently sized buffer,
// but this can be inconvenient and hard to explain in the docs. Another issue with
// buffered channels is that the buffer is static even though it might not be needed
// most of the time.
//
// The approach taken here is to maintain a per-subscription linked list buffer
// shrinks on demand. If the buffer reaches the size below, the subscription is
// dropped.
maxClientSubscriptionBuffer = 20000
)
// BatchElem is an element in a batch request.
type BatchElem struct {
Method string
Args []interface{}
// The result is unmarshaled into this field. Result must be set to a
// non-nil pointer value of the desired type, otherwise the response will be
// discarded.
Result interface{}
// Error is set if the server returns an error for this request, or if
// unmarshaling into Result fails. It is not set for I/O errors.
Error error
}
// Client represents a connection to an RPC server.
type Client struct {
idgen func() ID // for subscriptions
isHTTP bool
services *serviceRegistry
idCounter uint32
// This function, if non-nil, is called when the connection is lost.
reconnectFunc reconnectFunc
// writeConn is used for writing to the connection on the caller's goroutine. It should
// only be accessed outside of dispatch, with the write lock held. The write lock is
// taken by sending on requestOp and released by sending on sendDone.
writeConn jsonWriter
// for dispatch
close chan struct{}
closing chan struct{} // closed when client is quitting
didClose chan struct{} // closed when client quits
reconnected chan ServerCodec // where write/reconnect sends the new connection
readOp chan readOp // read messages
readErr chan error // errors from read
reqInit chan *requestOp // register response IDs, takes write lock
reqSent chan error // signals write completion, releases write lock
reqTimeout chan *requestOp // removes response IDs when call timeout expires
}
type reconnectFunc func(ctx context.Context) (ServerCodec, error)
type clientContextKey struct{}
type clientConn struct {
codec ServerCodec
handler *handler
}
func (c *Client) newClientConn(conn ServerCodec) *clientConn {
ctx := context.WithValue(context.Background(), clientContextKey{}, c)
handler := newHandler(ctx, conn, c.idgen, c.services)
return &clientConn{conn, handler}
}
func (cc *clientConn) close(err error, inflightReq *requestOp) {
cc.handler.close(err, inflightReq)
cc.codec.close()
}
type readOp struct {
msgs []*jsonrpcMessage
batch bool
}
type requestOp struct {
ids []json.RawMessage
err error
resp chan *jsonrpcMessage // receives up to len(ids) responses
sub *ClientSubscription // only set for EthSubscribe requests
}
func (op *requestOp) wait(ctx context.Context, c *Client) (*jsonrpcMessage, error) {
select {
case <-ctx.Done():
// Send the timeout to dispatch so it can remove the request IDs.
if !c.isHTTP {
select {
case c.reqTimeout <- op:
case <-c.closing:
}
}
return nil, ctx.Err()
case resp := <-op.resp:
return resp, op.err
}
}
// Dial creates a new client for the given URL.
//
// The currently supported URL schemes are "http", "https", "ws" and "wss". If rawurl is a
// file name with no URL scheme, a local socket connection is established using UNIX
// domain sockets on supported platforms and named pipes on Windows. If you want to
// configure transport options, use DialHTTP, DialWebsocket or DialIPC instead.
//
// For websocket connections, the origin is set to the local host name.
//
// The client reconnects automatically if the connection is lost.
func Dial(rawurl string) (*Client, error) {
return DialContext(context.Background(), rawurl)
}
// DialContext creates a new RPC client, just like Dial.
//
// The context is used to cancel or time out the initial connection establishment. It does
// not affect subsequent interactions with the client.
func DialContext(ctx context.Context, rawurl string) (*Client, error) {
u, err := url.Parse(rawurl)
if err != nil {
return nil, err
}
switch u.Scheme {
case "http", "https":
return DialHTTP(rawurl)
case "ws", "wss":
return DialWebsocket(ctx, rawurl, "")
case "stdio":
return DialStdIO(ctx)
case "":
return DialIPC(ctx, rawurl)
default:
return nil, fmt.Errorf("no known transport for URL scheme %q", u.Scheme)
}
}
// Client retrieves the client from the context, if any. This can be used to perform
// 'reverse calls' in a handler method.
func ClientFromContext(ctx context.Context) (*Client, bool) {
client, ok := ctx.Value(clientContextKey{}).(*Client)
return client, ok
}
func newClient(initctx context.Context, connect reconnectFunc) (*Client, error) {
conn, err := connect(initctx)
if err != nil {
return nil, err
}
c := initClient(conn, randomIDGenerator(), new(serviceRegistry))
c.reconnectFunc = connect
return c, nil
}
func initClient(conn ServerCodec, idgen func() ID, services *serviceRegistry) *Client {
_, isHTTP := conn.(*httpConn)
c := &Client{
idgen: idgen,
isHTTP: isHTTP,
services: services,
writeConn: conn,
close: make(chan struct{}),
closing: make(chan struct{}),
didClose: make(chan struct{}),
reconnected: make(chan ServerCodec),
readOp: make(chan readOp),
readErr: make(chan error),
reqInit: make(chan *requestOp),
reqSent: make(chan error, 1),
reqTimeout: make(chan *requestOp),
}
if !isHTTP {
go c.dispatch(conn)
}
return c
}
// RegisterName creates a service for the given receiver type under the given name. When no
// methods on the given receiver match the criteria to be either a RPC method or a
// subscription an error is returned. Otherwise a new service is created and added to the
// service collection this client provides to the server.
func (c *Client) RegisterName(name string, receiver interface{}) error {
return c.services.registerName(name, receiver)
}
func (c *Client) nextID() json.RawMessage {
id := atomic.AddUint32(&c.idCounter, 1)
return strconv.AppendUint(nil, uint64(id), 10)
}
// SupportedModules calls the rpc_modules method, retrieving the list of
// APIs that are available on the server.
func (c *Client) SupportedModules() (map[string]string, error) {
var result map[string]string
ctx, cancel := context.WithTimeout(context.Background(), subscribeTimeout)
defer cancel()
err := c.CallContext(ctx, &result, "rpc_modules")
return result, err
}
// Close closes the client, aborting any in-flight requests.
func (c *Client) Close() {
if c.isHTTP {
return
}
select {
case c.close <- struct{}{}:
<-c.didClose
case <-c.didClose:
}
}
// Call performs a JSON-RPC call with the given arguments and unmarshals into
// result if no error occurred.
//
// The result must be a pointer so that package json can unmarshal into it. You
// can also pass nil, in which case the result is ignored.
func (c *Client) Call(result interface{}, method string, args ...interface{}) error {
ctx := context.Background()
return c.CallContext(ctx, result, method, args...)
}
// CallContext performs a JSON-RPC call with the given arguments. If the context is
// canceled before the call has successfully returned, CallContext returns immediately.
//
// The result must be a pointer so that package json can unmarshal into it. You
// can also pass nil, in which case the result is ignored.
func (c *Client) CallContext(ctx context.Context, result interface{}, method string, args ...interface{}) error {
msg, err := c.newMessage(method, args...)
if err != nil {
return err
}
op := &requestOp{ids: []json.RawMessage{msg.ID}, resp: make(chan *jsonrpcMessage, 1)}
if c.isHTTP {
err = c.sendHTTP(ctx, op, msg)
} else {
err = c.send(ctx, op, msg)
}
if err != nil {
return err
}
// dispatch has accepted the request and will close the channel when it quits.
switch resp, err := op.wait(ctx, c); {
case err != nil:
return err
case resp.Error != nil:
return resp.Error
case len(resp.Result) == 0:
return ErrNoResult
default:
return json.Unmarshal(resp.Result, result)
}
}
// BatchCall sends all given requests as a single batch and waits for the server
// to return a response for all of them.
//
// In contrast to Call, BatchCall only returns I/O errors. Any error specific to
// a request is reported through the Error field of the corresponding BatchElem.
//
// Note that batch calls may not be executed atomically on the server side.
func (c *Client) BatchCall(b []BatchElem) error {
ctx := context.Background()
return c.BatchCallContext(ctx, b)
}
// BatchCall sends all given requests as a single batch and waits for the server
// to return a response for all of them. The wait duration is bounded by the
// context's deadline.
//
// In contrast to CallContext, BatchCallContext only returns errors that have occurred
// while sending the request. Any error specific to a request is reported through the
// Error field of the corresponding BatchElem.
//
// Note that batch calls may not be executed atomically on the server side.
func (c *Client) BatchCallContext(ctx context.Context, b []BatchElem) error {
msgs := make([]*jsonrpcMessage, len(b))
op := &requestOp{
ids: make([]json.RawMessage, len(b)),
resp: make(chan *jsonrpcMessage, len(b)),
}
for i, elem := range b {
msg, err := c.newMessage(elem.Method, elem.Args...)
if err != nil {
return err
}
msgs[i] = msg
op.ids[i] = msg.ID
}
var err error
if c.isHTTP {
err = c.sendBatchHTTP(ctx, op, msgs)
} else {
err = c.send(ctx, op, msgs)
}
// Wait for all responses to come back.
for n := 0; n < len(b) && err == nil; n++ {
var resp *jsonrpcMessage
resp, err = op.wait(ctx, c)
if err != nil {
break
}
// Find the element corresponding to this response.
// The element is guaranteed to be present because dispatch
// only sends valid IDs to our channel.
var elem *BatchElem
for i := range msgs {
if bytes.Equal(msgs[i].ID, resp.ID) {
elem = &b[i]
break
}
}
if resp.Error != nil {
elem.Error = resp.Error
continue
}
if len(resp.Result) == 0 {
elem.Error = ErrNoResult
continue
}
elem.Error = json.Unmarshal(resp.Result, elem.Result)
}
return err
}
// Notify sends a notification, i.e. a method call that doesn't expect a response.
func (c *Client) Notify(ctx context.Context, method string, args ...interface{}) error {
op := new(requestOp)
msg, err := c.newMessage(method, args...)
if err != nil {
return err
}
msg.ID = nil
if c.isHTTP {
return c.sendHTTP(ctx, op, msg)
} else {
return c.send(ctx, op, msg)
}
}
// EthSubscribe registers a subscripion under the "eth" namespace.
func (c *Client) EthSubscribe(ctx context.Context, channel interface{}, args ...interface{}) (*ClientSubscription, error) {
return c.Subscribe(ctx, "eth", channel, args...)
}
// ShhSubscribe registers a subscripion under the "shh" namespace.
func (c *Client) ShhSubscribe(ctx context.Context, channel interface{}, args ...interface{}) (*ClientSubscription, error) {
return c.Subscribe(ctx, "shh", channel, args...)
}
// Subscribe calls the "<namespace>_subscribe" method with the given arguments,
// registering a subscription. Server notifications for the subscription are
// sent to the given channel. The element type of the channel must match the
// expected type of content returned by the subscription.
//
// The context argument cancels the RPC request that sets up the subscription but has no
// effect on the subscription after Subscribe has returned.
//
// Slow subscribers will be dropped eventually. Client buffers up to 20000 notifications
// before considering the subscriber dead. The subscription Err channel will receive
// ErrSubscriptionQueueOverflow. Use a sufficiently large buffer on the channel or ensure
// that the channel usually has at least one reader to prevent this issue.
func (c *Client) Subscribe(ctx context.Context, namespace string, channel interface{}, args ...interface{}) (*ClientSubscription, error) {
// Check type of channel first.
chanVal := reflect.ValueOf(channel)
if chanVal.Kind() != reflect.Chan || chanVal.Type().ChanDir()&reflect.SendDir == 0 {
panic("first argument to Subscribe must be a writable channel")
}
if chanVal.IsNil() {
panic("channel given to Subscribe must not be nil")
}
if c.isHTTP {
return nil, ErrNotificationsUnsupported
}
msg, err := c.newMessage(namespace+subscribeMethodSuffix, args...)
if err != nil {
return nil, err
}
op := &requestOp{
ids: []json.RawMessage{msg.ID},
resp: make(chan *jsonrpcMessage),
sub: newClientSubscription(c, namespace, chanVal),
}
// Send the subscription request.
// The arrival and validity of the response is signaled on sub.quit.
if err := c.send(ctx, op, msg); err != nil {
return nil, err
}
if _, err := op.wait(ctx, c); err != nil {
return nil, err
}
return op.sub, nil
}
func (c *Client) newMessage(method string, paramsIn ...interface{}) (*jsonrpcMessage, error) {
msg := &jsonrpcMessage{Version: vsn, ID: c.nextID(), Method: method}
if paramsIn != nil { // prevent sending "params":null
var err error
if msg.Params, err = json.Marshal(paramsIn); err != nil {
return nil, err
}
}
return msg, nil
}
// send registers op with the dispatch loop, then sends msg on the connection.
// if sending fails, op is deregistered.
func (c *Client) send(ctx context.Context, op *requestOp, msg interface{}) error {
select {
case c.reqInit <- op:
err := c.write(ctx, msg)
c.reqSent <- err
return err
case <-ctx.Done():
// This can happen if the client is overloaded or unable to keep up with
// subscription notifications.
return ctx.Err()
case <-c.closing:
return ErrClientQuit
}
}
func (c *Client) write(ctx context.Context, msg interface{}) error {
// The previous write failed. Try to establish a new connection.
if c.writeConn == nil {
if err := c.reconnect(ctx); err != nil {
return err
}
}
err := c.writeConn.writeJSON(ctx, msg)
if err != nil {
c.writeConn = nil
}
return err
}
func (c *Client) reconnect(ctx context.Context) error {
if c.reconnectFunc == nil {
return errDead
}
if _, ok := ctx.Deadline(); !ok {
var cancel func()
ctx, cancel = context.WithTimeout(ctx, defaultDialTimeout)
defer cancel()
}
newconn, err := c.reconnectFunc(ctx)
if err != nil {
log.Trace("RPC client reconnect failed", "err", err)
return err
}
select {
case c.reconnected <- newconn:
c.writeConn = newconn
return nil
case <-c.didClose:
newconn.close()
return ErrClientQuit
}
}
// dispatch is the main loop of the client.
// It sends read messages to waiting calls to Call and BatchCall
// and subscription notifications to registered subscriptions.
func (c *Client) dispatch(codec ServerCodec) {
var (
lastOp *requestOp // tracks last send operation
reqInitLock = c.reqInit // nil while the send lock is held
conn = c.newClientConn(codec)
reading = true
)
defer func() {
close(c.closing)
if reading {
conn.close(ErrClientQuit, nil)
c.drainRead()
}
close(c.didClose)
}()
// Spawn the initial read loop.
go c.read(codec)
for {
select {
case <-c.close:
return
// Read path:
case op := <-c.readOp:
if op.batch {
conn.handler.handleBatch(op.msgs)
} else {
conn.handler.handleMsg(op.msgs[0])
}
case err := <-c.readErr:
conn.handler.log.Debug("RPC connection read error", "err", err)
conn.close(err, lastOp)
reading = false
// Reconnect:
case newcodec := <-c.reconnected:
log.Debug("RPC client reconnected", "reading", reading, "conn", newcodec.remoteAddr())
if reading {
// Wait for the previous read loop to exit. This is a rare case which
// happens if this loop isn't notified in time after the connection breaks.
// In those cases the caller will notice first and reconnect. Closing the
// handler terminates all waiting requests (closing op.resp) except for
// lastOp, which will be transferred to the new handler.
conn.close(errClientReconnected, lastOp)
c.drainRead()
}
go c.read(newcodec)
reading = true
conn = c.newClientConn(newcodec)
// Re-register the in-flight request on the new handler
// because that's where it will be sent.
conn.handler.addRequestOp(lastOp)
// Send path:
case op := <-reqInitLock:
// Stop listening for further requests until the current one has been sent.
reqInitLock = nil
lastOp = op
conn.handler.addRequestOp(op)
case err := <-c.reqSent:
if err != nil {
// Remove response handlers for the last send. When the read loop
// goes down, it will signal all other current operations.
conn.handler.removeRequestOp(lastOp)
}
// Let the next request in.
reqInitLock = c.reqInit
lastOp = nil
case op := <-c.reqTimeout:
conn.handler.removeRequestOp(op)
}
}
}
// drainRead drops read messages until an error occurs.
func (c *Client) drainRead() {
for {
select {
case <-c.readOp:
case <-c.readErr:
return
}
}
}
// read decodes RPC messages from a codec, feeding them into dispatch.
func (c *Client) read(codec ServerCodec) {
for {
msgs, batch, err := codec.readBatch()
if _, ok := err.(*json.SyntaxError); ok {
codec.writeJSON(context.Background(), errorMessage(&parseError{err.Error()}))
}
if err != nil {
c.readErr <- err
return
}
c.readOp <- readOp{msgs, batch}
}
}

@ -0,0 +1,88 @@
// Copyright 2016 The go-ethereum Authors
// This file is part of the go-ethereum library.
//
// The go-ethereum library is free software: you can redistribute it and/or modify
// it under the terms of the GNU Lesser General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// The go-ethereum library is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Lesser General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public License
// along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>.
package rpc_test
import (
"context"
"fmt"
"math/big"
"time"
"github.com/harmony-one/harmony/eth/rpc"
)
// In this example, our client wishes to track the latest 'block number'
// known to the server. The server supports two methods:
//
// eth_getBlockByNumber("latest", {})
// returns the latest block object.
//
// eth_subscribe("newBlocks")
// creates a subscription which fires block objects when new blocks arrive.
type Block struct {
Number *big.Int
}
func ExampleClientSubscription() {
// Connect the client.
client, _ := rpc.Dial("ws://127.0.0.1:8485")
subch := make(chan Block)
// Ensure that subch receives the latest block.
go func() {
for i := 0; ; i++ {
if i > 0 {
time.Sleep(2 * time.Second)
}
subscribeBlocks(client, subch)
}
}()
// Print events from the subscription as they arrive.
for block := range subch {
fmt.Println("latest block:", block.Number)
}
}
// subscribeBlocks runs in its own goroutine and maintains
// a subscription for new blocks.
func subscribeBlocks(client *rpc.Client, subch chan Block) {
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
// Subscribe to new blocks.
sub, err := client.EthSubscribe(ctx, subch, "newHeads")
if err != nil {
fmt.Println("subscribe error:", err)
return
}
// The connection is established now.
// Update the channel with the current block.
var lastBlock Block
if err := client.CallContext(ctx, &lastBlock, "eth_getBlockByNumber", "latest"); err != nil {
fmt.Println("can't get latest block:", err)
return
}
subch <- lastBlock
// The subscription will deliver events to the channel. Wait for the
// subscription to end for any reason, then loop around to re-establish
// the connection.
fmt.Println("connection lost: ", <-sub.Err())
}

@ -0,0 +1,569 @@
// Copyright 2016 The go-ethereum Authors
// This file is part of the go-ethereum library.
//
// The go-ethereum library is free software: you can redistribute it and/or modify
// it under the terms of the GNU Lesser General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// The go-ethereum library is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Lesser General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public License
// along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>.
package rpc
import (
"context"
"fmt"
"math/rand"
"net"
"net/http"
"net/http/httptest"
"os"
"reflect"
"runtime"
"sync"
"testing"
"time"
"github.com/davecgh/go-spew/spew"
"github.com/ethereum/go-ethereum/log"
)
func TestClientRequest(t *testing.T) {
server := newTestServer()
defer server.Stop()
client := DialInProc(server)
defer client.Close()
var resp echoResult
if err := client.Call(&resp, "test_echo", "hello", 10, &echoArgs{"world"}); err != nil {
t.Fatal(err)
}
if !reflect.DeepEqual(resp, echoResult{"hello", 10, &echoArgs{"world"}}) {
t.Errorf("incorrect result %#v", resp)
}
}
func TestClientBatchRequest(t *testing.T) {
server := newTestServer()
defer server.Stop()
client := DialInProc(server)
defer client.Close()
batch := []BatchElem{
{
Method: "test_echo",
Args: []interface{}{"hello", 10, &echoArgs{"world"}},
Result: new(echoResult),
},
{
Method: "test_echo",
Args: []interface{}{"hello2", 11, &echoArgs{"world"}},
Result: new(echoResult),
},
{
Method: "no_such_method",
Args: []interface{}{1, 2, 3},
Result: new(int),
},
}
if err := client.BatchCall(batch); err != nil {
t.Fatal(err)
}
wantResult := []BatchElem{
{
Method: "test_echo",
Args: []interface{}{"hello", 10, &echoArgs{"world"}},
Result: &echoResult{"hello", 10, &echoArgs{"world"}},
},
{
Method: "test_echo",
Args: []interface{}{"hello2", 11, &echoArgs{"world"}},
Result: &echoResult{"hello2", 11, &echoArgs{"world"}},
},
{
Method: "no_such_method",
Args: []interface{}{1, 2, 3},
Result: new(int),
Error: &jsonError{Code: -32601, Message: "the method no_such_method does not exist/is not available"},
},
}
if !reflect.DeepEqual(batch, wantResult) {
t.Errorf("batch results mismatch:\ngot %swant %s", spew.Sdump(batch), spew.Sdump(wantResult))
}
}
func TestClientNotify(t *testing.T) {
server := newTestServer()
defer server.Stop()
client := DialInProc(server)
defer client.Close()
if err := client.Notify(context.Background(), "test_echo", "hello", 10, &echoArgs{"world"}); err != nil {
t.Fatal(err)
}
}
// func TestClientCancelInproc(t *testing.T) { testClientCancel("inproc", t) }
func TestClientCancelWebsocket(t *testing.T) { testClientCancel("ws", t) }
func TestClientCancelHTTP(t *testing.T) { testClientCancel("http", t) }
func TestClientCancelIPC(t *testing.T) { testClientCancel("ipc", t) }
// This test checks that requests made through CallContext can be canceled by canceling
// the context.
func testClientCancel(transport string, t *testing.T) {
// These tests take a lot of time, run them all at once.
// You probably want to run with -parallel 1 or comment out
// the call to t.Parallel if you enable the logging.
t.Parallel()
server := newTestServer()
defer server.Stop()
// What we want to achieve is that the context gets canceled
// at various stages of request processing. The interesting cases
// are:
// - cancel during dial
// - cancel while performing a HTTP request
// - cancel while waiting for a response
//
// To trigger those, the times are chosen such that connections
// are killed within the deadline for every other call (maxKillTimeout
// is 2x maxCancelTimeout).
//
// Once a connection is dead, there is a fair chance it won't connect
// successfully because the accept is delayed by 1s.
maxContextCancelTimeout := 300 * time.Millisecond
fl := &flakeyListener{
maxAcceptDelay: 1 * time.Second,
maxKillTimeout: 600 * time.Millisecond,
}
var client *Client
switch transport {
case "ws", "http":
c, hs := httpTestClient(server, transport, fl)
defer hs.Close()
client = c
case "ipc":
c, l := ipcTestClient(server, fl)
defer l.Close()
client = c
default:
panic("unknown transport: " + transport)
}
// The actual test starts here.
var (
wg sync.WaitGroup
nreqs = 10
ncallers = 6
)
caller := func(index int) {
defer wg.Done()
for i := 0; i < nreqs; i++ {
var (
ctx context.Context
cancel func()
timeout = time.Duration(rand.Int63n(int64(maxContextCancelTimeout)))
)
if index < ncallers/2 {
// For half of the callers, create a context without deadline
// and cancel it later.
ctx, cancel = context.WithCancel(context.Background())
time.AfterFunc(timeout, cancel)
} else {
// For the other half, create a context with a deadline instead. This is
// different because the context deadline is used to set the socket write
// deadline.
ctx, cancel = context.WithTimeout(context.Background(), timeout)
}
// Now perform a call with the context.
// The key thing here is that no call will ever complete successfully.
sleepTime := maxContextCancelTimeout + 20*time.Millisecond
err := client.CallContext(ctx, nil, "test_sleep", sleepTime)
if err != nil {
log.Debug(fmt.Sprint("got expected error:", err))
} else {
t.Errorf("no error for call with %v wait time", timeout)
}
cancel()
}
}
wg.Add(ncallers)
for i := 0; i < ncallers; i++ {
go caller(i)
}
wg.Wait()
}
func TestClientSubscribeInvalidArg(t *testing.T) {
server := newTestServer()
defer server.Stop()
client := DialInProc(server)
defer client.Close()
check := func(shouldPanic bool, arg interface{}) {
defer func() {
err := recover()
if shouldPanic && err == nil {
t.Errorf("EthSubscribe should've panicked for %#v", arg)
}
if !shouldPanic && err != nil {
t.Errorf("EthSubscribe shouldn't have panicked for %#v", arg)
buf := make([]byte, 1024*1024)
buf = buf[:runtime.Stack(buf, false)]
t.Error(err)
t.Error(string(buf))
}
}()
client.EthSubscribe(context.Background(), arg, "foo_bar")
}
check(true, nil)
check(true, 1)
check(true, (chan int)(nil))
check(true, make(<-chan int))
check(false, make(chan int))
check(false, make(chan<- int))
}
func TestClientSubscribe(t *testing.T) {
server := newTestServer()
defer server.Stop()
client := DialInProc(server)
defer client.Close()
nc := make(chan int)
count := 10
sub, err := client.Subscribe(context.Background(), "nftest", nc, "someSubscription", count, 0)
if err != nil {
t.Fatal("can't subscribe:", err)
}
for i := 0; i < count; i++ {
if val := <-nc; val != i {
t.Fatalf("value mismatch: got %d, want %d", val, i)
}
}
sub.Unsubscribe()
select {
case v := <-nc:
t.Fatal("received value after unsubscribe:", v)
case err := <-sub.Err():
if err != nil {
t.Fatalf("Err returned a non-nil error after explicit unsubscribe: %q", err)
}
case <-time.After(1 * time.Second):
t.Fatalf("subscription not closed within 1s after unsubscribe")
}
}
// In this test, the connection drops while Subscribe is waiting for a response.
func TestClientSubscribeClose(t *testing.T) {
server := newTestServer()
service := &notificationTestService{
gotHangSubscriptionReq: make(chan struct{}),
unblockHangSubscription: make(chan struct{}),
}
if err := server.RegisterName("nftest2", service); err != nil {
t.Fatal(err)
}
defer server.Stop()
client := DialInProc(server)
defer client.Close()
var (
nc = make(chan int)
errc = make(chan error)
sub *ClientSubscription
err error
)
go func() {
sub, err = client.Subscribe(context.Background(), "nftest2", nc, "hangSubscription", 999)
errc <- err
}()
<-service.gotHangSubscriptionReq
client.Close()
service.unblockHangSubscription <- struct{}{}
select {
case err := <-errc:
if err == nil {
t.Errorf("Subscribe returned nil error after Close")
}
if sub != nil {
t.Error("Subscribe returned non-nil subscription after Close")
}
case <-time.After(1 * time.Second):
t.Fatalf("Subscribe did not return within 1s after Close")
}
}
// This test reproduces https://github.com/ethereum/go-ethereum/issues/17837 where the
// client hangs during shutdown when Unsubscribe races with Client.Close.
func TestClientCloseUnsubscribeRace(t *testing.T) {
server := newTestServer()
defer server.Stop()
for i := 0; i < 20; i++ {
client := DialInProc(server)
nc := make(chan int)
sub, err := client.Subscribe(context.Background(), "nftest", nc, "someSubscription", 3, 1)
if err != nil {
t.Fatal(err)
}
go client.Close()
go sub.Unsubscribe()
select {
case <-sub.Err():
case <-time.After(5 * time.Second):
t.Fatal("subscription not closed within timeout")
}
}
}
// This test checks that Client doesn't lock up when a single subscriber
// doesn't read subscription events.
func TestClientNotificationStorm(t *testing.T) {
server := newTestServer()
defer server.Stop()
doTest := func(count int, wantError bool) {
client := DialInProc(server)
defer client.Close()
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
// Subscribe on the server. It will start sending many notifications
// very quickly.
nc := make(chan int)
sub, err := client.Subscribe(ctx, "nftest", nc, "someSubscription", count, 0)
if err != nil {
t.Fatal("can't subscribe:", err)
}
defer sub.Unsubscribe()
// Process each notification, try to run a call in between each of them.
for i := 0; i < count; i++ {
select {
case val := <-nc:
if val != i {
t.Fatalf("(%d/%d) unexpected value %d", i, count, val)
}
case err := <-sub.Err():
if wantError && err != ErrSubscriptionQueueOverflow {
t.Fatalf("(%d/%d) got error %q, want %q", i, count, err, ErrSubscriptionQueueOverflow)
} else if !wantError {
t.Fatalf("(%d/%d) got unexpected error %q", i, count, err)
}
return
}
var r int
err := client.CallContext(ctx, &r, "nftest_echo", i)
if err != nil {
if !wantError {
t.Fatalf("(%d/%d) call error: %v", i, count, err)
}
return
}
}
if wantError {
t.Fatalf("didn't get expected error")
}
}
doTest(8000, false)
doTest(23000, true)
}
func TestClientHTTP(t *testing.T) {
server := newTestServer()
defer server.Stop()
client, hs := httpTestClient(server, "http", nil)
defer hs.Close()
defer client.Close()
// Launch concurrent requests.
var (
results = make([]echoResult, 100)
errc = make(chan error)
wantResult = echoResult{"a", 1, new(echoArgs)}
)
defer client.Close()
for i := range results {
i := i
go func() {
errc <- client.Call(&results[i], "test_echo",
wantResult.String, wantResult.Int, wantResult.Args)
}()
}
// Wait for all of them to complete.
timeout := time.NewTimer(5 * time.Second)
defer timeout.Stop()
for i := range results {
select {
case err := <-errc:
if err != nil {
t.Fatal(err)
}
case <-timeout.C:
t.Fatalf("timeout (got %d/%d) results)", i+1, len(results))
}
}
// Check results.
for i := range results {
if !reflect.DeepEqual(results[i], wantResult) {
t.Errorf("result %d mismatch: got %#v, want %#v", i, results[i], wantResult)
}
}
}
func TestClientReconnect(t *testing.T) {
startServer := func(addr string) (*Server, net.Listener) {
srv := newTestServer()
l, err := net.Listen("tcp", addr)
if err != nil {
t.Fatal("can't listen:", err)
}
go http.Serve(l, srv.WebsocketHandler([]string{"*"}))
return srv, l
}
ctx, cancel := context.WithTimeout(context.Background(), 12*time.Second)
defer cancel()
// Start a server and corresponding client.
s1, l1 := startServer("127.0.0.1:0")
client, err := DialContext(ctx, "ws://"+l1.Addr().String())
if err != nil {
t.Fatal("can't dial", err)
}
// Perform a call. This should work because the server is up.
var resp echoResult
if err := client.CallContext(ctx, &resp, "test_echo", "", 1, nil); err != nil {
t.Fatal(err)
}
// Shut down the server and allow for some cool down time so we can listen on the same
// address again.
l1.Close()
s1.Stop()
time.Sleep(2 * time.Second)
// Try calling again. It shouldn't work.
if err := client.CallContext(ctx, &resp, "test_echo", "", 2, nil); err == nil {
t.Error("successful call while the server is down")
t.Logf("resp: %#v", resp)
}
// Start it up again and call again. The connection should be reestablished.
// We spawn multiple calls here to check whether this hangs somehow.
s2, l2 := startServer(l1.Addr().String())
defer l2.Close()
defer s2.Stop()
start := make(chan struct{})
errors := make(chan error, 20)
for i := 0; i < cap(errors); i++ {
go func() {
<-start
var resp echoResult
errors <- client.CallContext(ctx, &resp, "test_echo", "", 3, nil)
}()
}
close(start)
errcount := 0
for i := 0; i < cap(errors); i++ {
if err = <-errors; err != nil {
errcount++
}
}
t.Logf("%d errors, last error: %v", errcount, err)
if errcount > 1 {
t.Errorf("expected one error after disconnect, got %d", errcount)
}
}
func httpTestClient(srv *Server, transport string, fl *flakeyListener) (*Client, *httptest.Server) {
// Create the HTTP server.
var hs *httptest.Server
switch transport {
case "ws":
hs = httptest.NewUnstartedServer(srv.WebsocketHandler([]string{"*"}))
case "http":
hs = httptest.NewUnstartedServer(srv)
default:
panic("unknown HTTP transport: " + transport)
}
// Wrap the listener if required.
if fl != nil {
fl.Listener = hs.Listener
hs.Listener = fl
}
// Connect the client.
hs.Start()
client, err := Dial(transport + "://" + hs.Listener.Addr().String())
if err != nil {
panic(err)
}
return client, hs
}
func ipcTestClient(srv *Server, fl *flakeyListener) (*Client, net.Listener) {
// Listen on a random endpoint.
endpoint := fmt.Sprintf("go-ethereum-test-ipc-%d-%d", os.Getpid(), rand.Int63())
if runtime.GOOS == "windows" {
endpoint = `\\.\pipe\` + endpoint
} else {
endpoint = os.TempDir() + "/" + endpoint
}
l, err := ipcListen(endpoint)
if err != nil {
panic(err)
}
// Connect the listener to the server.
if fl != nil {
fl.Listener = l
l = fl
}
go srv.ServeListener(l)
// Connect the client.
client, err := Dial(endpoint)
if err != nil {
panic(err)
}
return client, l
}
// flakeyListener kills accepted connections after a random timeout.
type flakeyListener struct {
net.Listener
maxKillTimeout time.Duration
maxAcceptDelay time.Duration
}
func (l *flakeyListener) Accept() (net.Conn, error) {
delay := time.Duration(rand.Int63n(int64(l.maxAcceptDelay)))
time.Sleep(delay)
c, err := l.Listener.Accept()
if err == nil {
timeout := time.Duration(rand.Int63n(int64(l.maxKillTimeout)))
time.AfterFunc(timeout, func() {
log.Debug(fmt.Sprintf("killing conn %v after %v", c.LocalAddr(), timeout))
c.Close()
})
}
return c, err
}

@ -0,0 +1,33 @@
// Copyright 2019 The go-ethereum Authors
// This file is part of the go-ethereum library.
//
// The go-ethereum library is free software: you can redistribute it and/or modify
// it under the terms of the GNU Lesser General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// The go-ethereum library is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Lesser General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public License
// along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>.
// +build darwin dragonfly freebsd linux nacl netbsd openbsd solaris
package rpc
/*
#include <sys/un.h>
int maximum_socket_path_size() {
struct sockaddr_un s;
return sizeof(s.sun_path);
}
*/
import "C"
var (
max_path_size = C.maximum_socket_path_size()
)

@ -0,0 +1,25 @@
// Copyright 2019 The go-ethereum Authors
// This file is part of the go-ethereum library.
//
// The go-ethereum library is free software: you can redistribute it and/or modify
// it under the terms of the GNU Lesser General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// The go-ethereum library is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Lesser General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public License
// along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>.
// +build !cgo,!windows
package rpc
var (
// On Linux, sun_path is 108 bytes in size
// see http://man7.org/linux/man-pages/man7/unix.7.html
max_path_size = 108
)

@ -0,0 +1,110 @@
// Copyright 2015 The go-ethereum Authors
// This file is part of the go-ethereum library.
//
// The go-ethereum library is free software: you can redistribute it and/or modify
// it under the terms of the GNU Lesser General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// The go-ethereum library is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Lesser General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public License
// along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>.
/*
Package rpc implements bi-directional JSON-RPC 2.0 on multiple transports.
It provides access to the exported methods of an object across a network or other I/O
connection. After creating a server or client instance, objects can be registered to make
them visible as 'services'. Exported methods that follow specific conventions can be
called remotely. It also has support for the publish/subscribe pattern.
RPC Methods
Methods that satisfy the following criteria are made available for remote access:
- method must be exported
- method returns 0, 1 (response or error) or 2 (response and error) values
An example method:
func (s *CalcService) Add(a, b int) (int, error)
When the returned error isn't nil the returned integer is ignored and the error is sent
back to the client. Otherwise the returned integer is sent back to the client.
Optional arguments are supported by accepting pointer values as arguments. E.g. if we want
to do the addition in an optional finite field we can accept a mod argument as pointer
value.
func (s *CalcService) Add(a, b int, mod *int) (int, error)
This RPC method can be called with 2 integers and a null value as third argument. In that
case the mod argument will be nil. Or it can be called with 3 integers, in that case mod
will be pointing to the given third argument. Since the optional argument is the last
argument the RPC package will also accept 2 integers as arguments. It will pass the mod
argument as nil to the RPC method.
The server offers the ServeCodec method which accepts a ServerCodec instance. It will read
requests from the codec, process the request and sends the response back to the client
using the codec. The server can execute requests concurrently. Responses can be sent back
to the client out of order.
An example server which uses the JSON codec:
type CalculatorService struct {}
func (s *CalculatorService) Add(a, b int) int {
return a + b
}
func (s *CalculatorService) Div(a, b int) (int, error) {
if b == 0 {
return 0, errors.New("divide by zero")
}
return a/b, nil
}
calculator := new(CalculatorService)
server := NewServer()
server.RegisterName("calculator", calculator)
l, _ := net.ListenUnix("unix", &net.UnixAddr{Net: "unix", Name: "/tmp/calculator.sock"})
server.ServeListener(l)
Subscriptions
The package also supports the publish subscribe pattern through the use of subscriptions.
A method that is considered eligible for notifications must satisfy the following
criteria:
- method must be exported
- first method argument type must be context.Context
- method must have return types (rpc.Subscription, error)
An example method:
func (s *BlockChainService) NewBlocks(ctx context.Context) (rpc.Subscription, error) {
...
}
When the service containing the subscription method is registered to the server, for
example under the "blockchain" namespace, a subscription is created by calling the
"blockchain_subscribe" method.
Subscriptions are deleted when the user sends an unsubscribe request or when the
connection which was used to create the subscription is closed. This can be initiated by
the client and server. The server will close the connection for any write error.
For more information about subscriptions, see https://github.com/ethereum/go-ethereum/wiki/RPC-PUB-SUB.
Reverse Calls
In any method handler, an instance of rpc.Client can be accessed through the
ClientFromContext method. Using this client instance, server-to-client method calls can be
performed on the RPC connection.
*/
package rpc

@ -0,0 +1,102 @@
// Copyright 2018 The go-ethereum Authors
// This file is part of the go-ethereum library.
//
// The go-ethereum library is free software: you can redistribute it and/or modify
// it under the terms of the GNU Lesser General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// The go-ethereum library is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Lesser General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public License
// along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>.
package rpc
import (
"net"
"github.com/ethereum/go-ethereum/log"
)
// StartHTTPEndpoint starts the HTTP RPC endpoint, configured with cors/vhosts/modules
func StartHTTPEndpoint(endpoint string, apis []API, modules []string, cors []string, vhosts []string, timeouts HTTPTimeouts) (net.Listener, *Server, error) {
// Generate the whitelist based on the allowed modules
whitelist := make(map[string]bool)
for _, module := range modules {
whitelist[module] = true
}
// Register all the APIs exposed by the services
handler := NewServer()
for _, api := range apis {
if whitelist[api.Namespace] || (len(whitelist) == 0 && api.Public) {
if err := handler.RegisterName(api.Namespace, api.Service); err != nil {
return nil, nil, err
}
log.Debug("HTTP registered", "namespace", api.Namespace)
}
}
// All APIs registered, start the HTTP listener
var (
listener net.Listener
err error
)
if listener, err = net.Listen("tcp", endpoint); err != nil {
return nil, nil, err
}
go NewHTTPServer(cors, vhosts, timeouts, handler).Serve(listener)
return listener, handler, err
}
// StartWSEndpoint starts a websocket endpoint
func StartWSEndpoint(endpoint string, apis []API, modules []string, wsOrigins []string, exposeAll bool) (net.Listener, *Server, error) {
// Generate the whitelist based on the allowed modules
whitelist := make(map[string]bool)
for _, module := range modules {
whitelist[module] = true
}
// Register all the APIs exposed by the services
handler := NewServer()
for _, api := range apis {
if exposeAll || whitelist[api.Namespace] || (len(whitelist) == 0 && api.Public) {
if err := handler.RegisterName(api.Namespace, api.Service); err != nil {
return nil, nil, err
}
log.Debug("WebSocket registered", "service", api.Service, "namespace", api.Namespace)
}
}
// All APIs registered, start the HTTP listener
var (
listener net.Listener
err error
)
if listener, err = net.Listen("tcp", endpoint); err != nil {
return nil, nil, err
}
go NewWSServer(wsOrigins, handler).Serve(listener)
return listener, handler, err
}
// StartIPCEndpoint starts an IPC endpoint.
func StartIPCEndpoint(ipcEndpoint string, apis []API) (net.Listener, *Server, error) {
// Register all the APIs exposed by the services.
handler := NewServer()
for _, api := range apis {
if err := handler.RegisterName(api.Namespace, api.Service); err != nil {
return nil, nil, err
}
log.Debug("IPC registered", "namespace", api.Namespace)
}
// All APIs registered, start the IPC listener.
listener, err := ipcListen(ipcEndpoint)
if err != nil {
return nil, nil, err
}
go handler.ServeListener(listener)
return listener, handler, nil
}

@ -0,0 +1,65 @@
// Copyright 2015 The go-ethereum Authors
// This file is part of the go-ethereum library.
//
// The go-ethereum library is free software: you can redistribute it and/or modify
// it under the terms of the GNU Lesser General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// The go-ethereum library is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Lesser General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public License
// along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>.
package rpc
import "fmt"
const defaultErrorCode = -32000
type methodNotFoundError struct{ method string }
func (e *methodNotFoundError) ErrorCode() int { return -32601 }
func (e *methodNotFoundError) Error() string {
return fmt.Sprintf("the method %s does not exist/is not available", e.method)
}
type subscriptionNotFoundError struct{ namespace, subscription string }
func (e *subscriptionNotFoundError) ErrorCode() int { return -32601 }
func (e *subscriptionNotFoundError) Error() string {
return fmt.Sprintf("no %q subscription in %s namespace", e.subscription, e.namespace)
}
// Invalid JSON was received by the server.
type parseError struct{ message string }
func (e *parseError) ErrorCode() int { return -32700 }
func (e *parseError) Error() string { return e.message }
// received message isn't a valid request
type invalidRequestError struct{ message string }
func (e *invalidRequestError) ErrorCode() int { return -32600 }
func (e *invalidRequestError) Error() string { return e.message }
// received message is invalid
type invalidMessageError struct{ message string }
func (e *invalidMessageError) ErrorCode() int { return -32700 }
func (e *invalidMessageError) Error() string { return e.message }
// unable to decode supplied params, or an invalid number of parameters
type invalidParamsError struct{ message string }
func (e *invalidParamsError) ErrorCode() int { return -32602 }
func (e *invalidParamsError) Error() string { return e.message }

@ -0,0 +1,66 @@
// Copyright 2019 The go-ethereum Authors
// This file is part of the go-ethereum library.
//
// The go-ethereum library is free software: you can redistribute it and/or modify
// it under the terms of the GNU Lesser General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// The go-ethereum library is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Lesser General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public License
// along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>.
package rpc
import (
"compress/gzip"
"io"
"io/ioutil"
"net/http"
"strings"
"sync"
)
var gzPool = sync.Pool{
New: func() interface{} {
w := gzip.NewWriter(ioutil.Discard)
return w
},
}
type gzipResponseWriter struct {
io.Writer
http.ResponseWriter
}
func (w *gzipResponseWriter) WriteHeader(status int) {
w.Header().Del("Content-Length")
w.ResponseWriter.WriteHeader(status)
}
func (w *gzipResponseWriter) Write(b []byte) (int, error) {
return w.Writer.Write(b)
}
func newGzipHandler(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if !strings.Contains(r.Header.Get("Accept-Encoding"), "gzip") {
next.ServeHTTP(w, r)
return
}
w.Header().Set("Content-Encoding", "gzip")
gz := gzPool.Get().(*gzip.Writer)
defer gzPool.Put(gz)
gz.Reset(w)
defer gz.Close()
next.ServeHTTP(&gzipResponseWriter{ResponseWriter: w, Writer: gz}, r)
})
}

@ -0,0 +1,402 @@
// Copyright 2019 The go-ethereum Authors
// This file is part of the go-ethereum library.
//
// The go-ethereum library is free software: you can redistribute it and/or modify
// it under the terms of the GNU Lesser General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// The go-ethereum library is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Lesser General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public License
// along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>.
package rpc
import (
"context"
"reflect"
"strconv"
"strings"
"sync"
"time"
"github.com/goccy/go-json"
"github.com/ethereum/go-ethereum/log"
)
// handler handles JSON-RPC messages. There is one handler per connection. Note that
// handler is not safe for concurrent use. Message handling never blocks indefinitely
// because RPCs are processed on background goroutines launched by handler.
//
// The entry points for incoming messages are:
//
// h.handleMsg(message)
// h.handleBatch(message)
//
// Outgoing calls use the requestOp struct. Register the request before sending it
// on the connection:
//
// op := &requestOp{ids: ...}
// h.addRequestOp(op)
//
// Now send the request, then wait for the reply to be delivered through handleMsg:
//
// if err := op.wait(...); err != nil {
// h.removeRequestOp(op) // timeout, etc.
// }
//
type handler struct {
reg *serviceRegistry
unsubscribeCb *callback
idgen func() ID // subscription ID generator
respWait map[string]*requestOp // active client requests
clientSubs map[string]*ClientSubscription // active client subscriptions
callWG sync.WaitGroup // pending call goroutines
rootCtx context.Context // canceled by close()
cancelRoot func() // cancel function for rootCtx
conn jsonWriter // where responses will be sent
log log.Logger
allowSubscribe bool
subLock sync.Mutex
serverSubs map[ID]*Subscription
}
type callProc struct {
ctx context.Context
notifiers []*Notifier
}
func newHandler(connCtx context.Context, conn jsonWriter, idgen func() ID, reg *serviceRegistry) *handler {
rootCtx, cancelRoot := context.WithCancel(connCtx)
h := &handler{
reg: reg,
idgen: idgen,
conn: conn,
respWait: make(map[string]*requestOp),
clientSubs: make(map[string]*ClientSubscription),
rootCtx: rootCtx,
cancelRoot: cancelRoot,
allowSubscribe: true,
serverSubs: make(map[ID]*Subscription),
log: log.Root(),
}
if conn.remoteAddr() != "" {
h.log = h.log.New("conn", conn.remoteAddr())
}
h.unsubscribeCb = newCallback(reflect.Value{}, reflect.ValueOf(h.unsubscribe))
return h
}
// handleBatch executes all messages in a batch and returns the responses.
func (h *handler) handleBatch(msgs []*jsonrpcMessage) {
// Emit error response for empty batches:
if len(msgs) == 0 {
h.startCallProc(func(cp *callProc) {
h.conn.writeJSON(cp.ctx, errorMessage(&invalidRequestError{"empty batch"}))
})
return
}
// Handle non-call messages first:
calls := make([]*jsonrpcMessage, 0, len(msgs))
for _, msg := range msgs {
if handled := h.handleImmediate(msg); !handled {
calls = append(calls, msg)
}
}
if len(calls) == 0 {
return
}
// Process calls on a goroutine because they may block indefinitely:
h.startCallProc(func(cp *callProc) {
answers := make([]*jsonrpcMessage, 0, len(msgs))
for _, msg := range calls {
if answer := h.handleCallMsg(cp, msg); answer != nil {
answers = append(answers, answer)
}
}
h.addSubscriptions(cp.notifiers)
if len(answers) > 0 {
h.conn.writeJSON(cp.ctx, answers)
}
for _, n := range cp.notifiers {
n.activate()
}
})
}
// handleMsg handles a single message.
func (h *handler) handleMsg(msg *jsonrpcMessage) {
if ok := h.handleImmediate(msg); ok {
return
}
h.startCallProc(func(cp *callProc) {
answer := h.handleCallMsg(cp, msg)
h.addSubscriptions(cp.notifiers)
if answer != nil {
h.conn.writeJSON(cp.ctx, answer)
}
for _, n := range cp.notifiers {
n.activate()
}
})
}
// close cancels all requests except for inflightReq and waits for
// call goroutines to shut down.
func (h *handler) close(err error, inflightReq *requestOp) {
h.cancelAllRequests(err, inflightReq)
h.callWG.Wait()
h.cancelRoot()
h.cancelServerSubscriptions(err)
}
// addRequestOp registers a request operation.
func (h *handler) addRequestOp(op *requestOp) {
for _, id := range op.ids {
h.respWait[string(id)] = op
}
}
// removeRequestOps stops waiting for the given request IDs.
func (h *handler) removeRequestOp(op *requestOp) {
for _, id := range op.ids {
delete(h.respWait, string(id))
}
}
// cancelAllRequests unblocks and removes pending requests and active subscriptions.
func (h *handler) cancelAllRequests(err error, inflightReq *requestOp) {
didClose := make(map[*requestOp]bool)
if inflightReq != nil {
didClose[inflightReq] = true
}
for id, op := range h.respWait {
// Remove the op so that later calls will not close op.resp again.
delete(h.respWait, id)
if !didClose[op] {
op.err = err
close(op.resp)
didClose[op] = true
}
}
for id, sub := range h.clientSubs {
delete(h.clientSubs, id)
sub.quitWithError(false, err)
}
}
func (h *handler) addSubscriptions(nn []*Notifier) {
h.subLock.Lock()
defer h.subLock.Unlock()
for _, n := range nn {
if sub := n.takeSubscription(); sub != nil {
h.serverSubs[sub.ID] = sub
}
}
}
// cancelServerSubscriptions removes all subscriptions and closes their error channels.
func (h *handler) cancelServerSubscriptions(err error) {
h.subLock.Lock()
defer h.subLock.Unlock()
for id, s := range h.serverSubs {
s.err <- err
close(s.err)
delete(h.serverSubs, id)
}
}
// startCallProc runs fn in a new goroutine and starts tracking it in the h.calls wait group.
func (h *handler) startCallProc(fn func(*callProc)) {
h.callWG.Add(1)
go func() {
ctx, cancel := context.WithCancel(h.rootCtx)
defer h.callWG.Done()
defer cancel()
fn(&callProc{ctx: ctx})
}()
}
// handleImmediate executes non-call messages. It returns false if the message is a
// call or requires a reply.
func (h *handler) handleImmediate(msg *jsonrpcMessage) bool {
start := time.Now()
switch {
case msg.isNotification():
if strings.HasSuffix(msg.Method, notificationMethodSuffix) {
h.handleSubscriptionResult(msg)
return true
}
return false
case msg.isResponse():
h.handleResponse(msg)
h.log.Trace("Handled RPC response", "reqid", idForLog{msg.ID}, "t", time.Since(start))
return true
default:
return false
}
}
// handleSubscriptionResult processes subscription notifications.
func (h *handler) handleSubscriptionResult(msg *jsonrpcMessage) {
var result subscriptionResult
if err := json.Unmarshal(msg.Params, &result); err != nil {
h.log.Debug("Dropping invalid subscription message")
return
}
if h.clientSubs[result.ID] != nil {
h.clientSubs[result.ID].deliver(result.Result)
}
}
// handleResponse processes method call responses.
func (h *handler) handleResponse(msg *jsonrpcMessage) {
op := h.respWait[string(msg.ID)]
if op == nil {
h.log.Debug("Unsolicited RPC response", "reqid", idForLog{msg.ID})
return
}
delete(h.respWait, string(msg.ID))
// For normal responses, just forward the reply to Call/BatchCall.
if op.sub == nil {
op.resp <- msg
return
}
// For subscription responses, start the subscription if the server
// indicates success. EthSubscribe gets unblocked in either case through
// the op.resp channel.
defer close(op.resp)
if msg.Error != nil {
op.err = msg.Error
return
}
if op.err = json.Unmarshal(msg.Result, &op.sub.subid); op.err == nil {
go op.sub.start()
h.clientSubs[op.sub.subid] = op.sub
}
}
// handleCallMsg executes a call message and returns the answer.
func (h *handler) handleCallMsg(ctx *callProc, msg *jsonrpcMessage) *jsonrpcMessage {
start := time.Now()
switch {
case msg.isNotification():
h.handleCall(ctx, msg)
h.log.Debug("Served "+msg.Method, "t", time.Since(start))
return nil
case msg.isCall():
resp := h.handleCall(ctx, msg)
if resp.Error != nil {
h.log.Warn("Served "+msg.Method, "reqid", idForLog{msg.ID}, "t", time.Since(start), "err", resp.Error.Message)
} else {
h.log.Debug("Served "+msg.Method, "reqid", idForLog{msg.ID}, "t", time.Since(start))
}
return resp
case msg.hasValidID():
return msg.errorResponse(&invalidRequestError{"invalid request"})
default:
return errorMessage(&invalidRequestError{"invalid request"})
}
}
// handleCall processes method calls.
func (h *handler) handleCall(cp *callProc, msg *jsonrpcMessage) *jsonrpcMessage {
if msg.isSubscribe() {
return h.handleSubscribe(cp, msg)
}
var callb *callback
if msg.isUnsubscribe() {
callb = h.unsubscribeCb
} else {
callb = h.reg.callback(msg.Method)
}
if callb == nil {
return msg.errorResponse(&methodNotFoundError{method: msg.Method})
}
args, err := parsePositionalArguments(msg.Params, callb.argTypes)
if err != nil {
return msg.errorResponse(&invalidParamsError{err.Error()})
}
return h.runMethod(cp.ctx, msg, callb, args)
}
// handleSubscribe processes *_subscribe method calls.
func (h *handler) handleSubscribe(cp *callProc, msg *jsonrpcMessage) *jsonrpcMessage {
if !h.allowSubscribe {
return msg.errorResponse(ErrNotificationsUnsupported)
}
// Subscription method name is first argument.
name, err := parseSubscriptionName(msg.Params)
if err != nil {
return msg.errorResponse(&invalidParamsError{err.Error()})
}
namespace := msg.namespace()
callb := h.reg.subscription(namespace, name)
if callb == nil {
return msg.errorResponse(&subscriptionNotFoundError{namespace, name})
}
// Parse subscription name arg too, but remove it before calling the callback.
argTypes := append([]reflect.Type{stringType}, callb.argTypes...)
args, err := parsePositionalArguments(msg.Params, argTypes)
if err != nil {
return msg.errorResponse(&invalidParamsError{err.Error()})
}
args = args[1:]
// Install notifier in context so the subscription handler can find it.
n := &Notifier{h: h, namespace: namespace}
cp.notifiers = append(cp.notifiers, n)
ctx := context.WithValue(cp.ctx, notifierKey{}, n)
return h.runMethod(ctx, msg, callb, args)
}
// runMethod runs the Go callback for an RPC method.
func (h *handler) runMethod(ctx context.Context, msg *jsonrpcMessage, callb *callback, args []reflect.Value) *jsonrpcMessage {
timer := doMetricRequest(msg.Method)
defer doMetricDelayHist(timer)
result, err := callb.call(ctx, msg.Method, args)
if err != nil {
doMetricErroredRequest(msg.Method)
return msg.errorResponse(err)
}
return msg.response(result)
}
// unsubscribe is the callback function for all *_unsubscribe calls.
func (h *handler) unsubscribe(ctx context.Context, id ID) (bool, error) {
h.subLock.Lock()
defer h.subLock.Unlock()
s := h.serverSubs[id]
if s == nil {
return false, ErrSubscriptionNotFound
}
close(s.err)
delete(h.serverSubs, id)
return true, nil
}
type idForLog struct{ json.RawMessage }
func (id idForLog) String() string {
if s, err := strconv.Unquote(string(id.RawMessage)); err == nil {
return s
}
return string(id.RawMessage)
}

@ -0,0 +1,360 @@
// Copyright 2015 The go-ethereum Authors
// This file is part of the go-ethereum library.
//
// The go-ethereum library is free software: you can redistribute it and/or modify
// it under the terms of the GNU Lesser General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// The go-ethereum library is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Lesser General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public License
// along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>.
package rpc
import (
"bytes"
"context"
"errors"
"fmt"
"io"
"io/ioutil"
"mime"
"net"
"net/http"
"strings"
"sync"
"time"
"github.com/goccy/go-json"
"github.com/ethereum/go-ethereum/log"
"github.com/rs/cors"
)
const (
maxRequestContentLength = 1024 * 1024 * 5
contentType = "application/json"
)
// https://www.jsonrpc.org/historical/json-rpc-over-http.html#id13
var acceptedContentTypes = []string{contentType, "application/json-rpc", "application/jsonrequest"}
type httpConn struct {
client *http.Client
req *http.Request
closeOnce sync.Once
closeCh chan interface{}
}
// httpConn is treated specially by Client.
func (hc *httpConn) writeJSON(context.Context, interface{}) error {
panic("writeJSON called on httpConn")
}
func (hc *httpConn) remoteAddr() string {
return hc.req.URL.String()
}
func (hc *httpConn) readBatch() ([]*jsonrpcMessage, bool, error) {
<-hc.closeCh
return nil, false, io.EOF
}
func (hc *httpConn) close() {
hc.closeOnce.Do(func() { close(hc.closeCh) })
}
func (hc *httpConn) closed() <-chan interface{} {
return hc.closeCh
}
// HTTPTimeouts represents the configuration params for the HTTP RPC server.
type HTTPTimeouts struct {
// ReadTimeout is the maximum duration for reading the entire
// request, including the body.
//
// Because ReadTimeout does not let Handlers make per-request
// decisions on each request body's acceptable deadline or
// upload rate, most users will prefer to use
// ReadHeaderTimeout. It is valid to use them both.
ReadTimeout time.Duration
// WriteTimeout is the maximum duration before timing out
// writes of the response. It is reset whenever a new
// request's header is read. Like ReadTimeout, it does not
// let Handlers make decisions on a per-request basis.
WriteTimeout time.Duration
// IdleTimeout is the maximum amount of time to wait for the
// next request when keep-alives are enabled. If IdleTimeout
// is zero, the value of ReadTimeout is used. If both are
// zero, ReadHeaderTimeout is used.
IdleTimeout time.Duration
}
// DefaultHTTPTimeouts represents the default timeout values used if further
// configuration is not provided.
var DefaultHTTPTimeouts = HTTPTimeouts{
ReadTimeout: 30 * time.Second,
WriteTimeout: 30 * time.Second,
IdleTimeout: 120 * time.Second,
}
// DialHTTPWithClient creates a new RPC client that connects to an RPC server over HTTP
// using the provided HTTP Client.
func DialHTTPWithClient(endpoint string, client *http.Client) (*Client, error) {
req, err := http.NewRequest(http.MethodPost, endpoint, nil)
if err != nil {
return nil, err
}
req.Header.Set("Content-Type", contentType)
req.Header.Set("Accept", contentType)
initctx := context.Background()
return newClient(initctx, func(context.Context) (ServerCodec, error) {
return &httpConn{client: client, req: req, closeCh: make(chan interface{})}, nil
})
}
// DialHTTP creates a new RPC client that connects to an RPC server over HTTP.
func DialHTTP(endpoint string) (*Client, error) {
return DialHTTPWithClient(endpoint, new(http.Client))
}
func (c *Client) sendHTTP(ctx context.Context, op *requestOp, msg interface{}) error {
hc := c.writeConn.(*httpConn)
respBody, err := hc.doRequest(ctx, msg)
if respBody != nil {
defer respBody.Close()
}
if err != nil {
if respBody != nil {
buf := new(bytes.Buffer)
if _, err2 := buf.ReadFrom(respBody); err2 == nil {
return fmt.Errorf("%v %v", err, buf.String())
}
}
return err
}
var respmsg jsonrpcMessage
if err := json.NewDecoder(respBody).Decode(&respmsg); err != nil {
return err
}
op.resp <- &respmsg
return nil
}
func (c *Client) sendBatchHTTP(ctx context.Context, op *requestOp, msgs []*jsonrpcMessage) error {
hc := c.writeConn.(*httpConn)
respBody, err := hc.doRequest(ctx, msgs)
if err != nil {
return err
}
defer respBody.Close()
var respmsgs []jsonrpcMessage
if err := json.NewDecoder(respBody).Decode(&respmsgs); err != nil {
return err
}
for i := 0; i < len(respmsgs); i++ {
op.resp <- &respmsgs[i]
}
return nil
}
func (hc *httpConn) doRequest(ctx context.Context, msg interface{}) (io.ReadCloser, error) {
body, err := json.Marshal(msg)
if err != nil {
return nil, err
}
req := hc.req.WithContext(ctx)
req.Body = ioutil.NopCloser(bytes.NewReader(body))
req.ContentLength = int64(len(body))
resp, err := hc.client.Do(req)
if err != nil {
return nil, err
}
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
return resp.Body, errors.New(resp.Status)
}
return resp.Body, nil
}
// httpServerConn turns a HTTP connection into a Conn.
type httpServerConn struct {
io.Reader
io.Writer
r *http.Request
}
func newHTTPServerConn(r *http.Request, w http.ResponseWriter) ServerCodec {
body := io.LimitReader(r.Body, maxRequestContentLength)
conn := &httpServerConn{Reader: body, Writer: w, r: r}
return NewCodec(conn)
}
// Close does nothing and always returns nil.
func (t *httpServerConn) Close() error { return nil }
// RemoteAddr returns the peer address of the underlying connection.
func (t *httpServerConn) RemoteAddr() string {
return t.r.RemoteAddr
}
// SetWriteDeadline does nothing and always returns nil.
func (t *httpServerConn) SetWriteDeadline(time.Time) error { return nil }
// NewHTTPServer creates a new HTTP RPC server around an API provider.
//
// Deprecated: Server implements http.Handler
func NewHTTPServer(cors []string, vhosts []string, timeouts HTTPTimeouts, srv http.Handler) *http.Server {
// Wrap the CORS-handler within a host-handler
handler := newCorsHandler(srv, cors)
handler = newVHostHandler(vhosts, handler)
handler = newGzipHandler(handler)
// Make sure timeout values are meaningful
if timeouts.ReadTimeout < time.Second {
log.Warn("Sanitizing invalid HTTP read timeout", "provided", timeouts.ReadTimeout, "updated", DefaultHTTPTimeouts.ReadTimeout)
timeouts.ReadTimeout = DefaultHTTPTimeouts.ReadTimeout
}
if timeouts.WriteTimeout < time.Second {
log.Warn("Sanitizing invalid HTTP write timeout", "provided", timeouts.WriteTimeout, "updated", DefaultHTTPTimeouts.WriteTimeout)
timeouts.WriteTimeout = DefaultHTTPTimeouts.WriteTimeout
}
if timeouts.IdleTimeout < time.Second {
log.Warn("Sanitizing invalid HTTP idle timeout", "provided", timeouts.IdleTimeout, "updated", DefaultHTTPTimeouts.IdleTimeout)
timeouts.IdleTimeout = DefaultHTTPTimeouts.IdleTimeout
}
// Bundle and start the HTTP server
return &http.Server{
Handler: handler,
ReadTimeout: timeouts.ReadTimeout,
WriteTimeout: timeouts.WriteTimeout,
IdleTimeout: timeouts.IdleTimeout,
}
}
// ServeHTTP serves JSON-RPC requests over HTTP.
func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
// Permit dumb empty requests for remote health-checks (AWS)
if r.Method == http.MethodGet && r.ContentLength == 0 && r.URL.RawQuery == "" {
return
}
if code, err := validateRequest(r); err != nil {
http.Error(w, err.Error(), code)
return
}
// All checks passed, create a codec that reads direct from the request body
// untilEOF and writes the response to w and order the server to process a
// single request.
ctx := r.Context()
ctx = context.WithValue(ctx, "remote", r.RemoteAddr)
ctx = context.WithValue(ctx, "scheme", r.Proto)
ctx = context.WithValue(ctx, "local", r.Host)
if ua := r.Header.Get("User-Agent"); ua != "" {
ctx = context.WithValue(ctx, "User-Agent", ua)
}
if origin := r.Header.Get("Origin"); origin != "" {
ctx = context.WithValue(ctx, "Origin", origin)
}
w.Header().Set("content-type", contentType)
codec := newHTTPServerConn(r, w)
defer codec.close()
s.serveSingleRequest(ctx, codec)
}
// validateRequest returns a non-zero response code and error message if the
// request is invalid.
func validateRequest(r *http.Request) (int, error) {
if r.Method == http.MethodPut || r.Method == http.MethodDelete {
return http.StatusMethodNotAllowed, errors.New("method not allowed")
}
if r.ContentLength > maxRequestContentLength {
err := fmt.Errorf("content length too large (%d>%d)", r.ContentLength, maxRequestContentLength)
return http.StatusRequestEntityTooLarge, err
}
// Allow OPTIONS (regardless of content-type)
if r.Method == http.MethodOptions {
return 0, nil
}
// Check content-type
if mt, _, err := mime.ParseMediaType(r.Header.Get("content-type")); err == nil {
for _, accepted := range acceptedContentTypes {
if accepted == mt {
return 0, nil
}
}
}
// Invalid content-type
err := fmt.Errorf("invalid content type, only %s is supported", contentType)
return http.StatusUnsupportedMediaType, err
}
func newCorsHandler(srv http.Handler, allowedOrigins []string) http.Handler {
// disable CORS support if user has not specified a custom CORS configuration
if len(allowedOrigins) == 0 {
return srv
}
c := cors.New(cors.Options{
AllowedOrigins: allowedOrigins,
AllowedMethods: []string{http.MethodPost, http.MethodGet},
MaxAge: 600,
AllowedHeaders: []string{"*"},
})
return c.Handler(srv)
}
// virtualHostHandler is a handler which validates the Host-header of incoming requests.
// The virtualHostHandler can prevent DNS rebinding attacks, which do not utilize CORS-headers,
// since they do in-domain requests against the RPC api. Instead, we can see on the Host-header
// which domain was used, and validate that against a whitelist.
type virtualHostHandler struct {
vhosts map[string]struct{}
next http.Handler
}
// ServeHTTP serves JSON-RPC requests over HTTP, implements http.Handler
func (h *virtualHostHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
// if r.Host is not set, we can continue serving since a browser would set the Host header
if r.Host == "" {
h.next.ServeHTTP(w, r)
return
}
host, _, err := net.SplitHostPort(r.Host)
if err != nil {
// Either invalid (too many colons) or no port specified
host = r.Host
}
if ipAddr := net.ParseIP(host); ipAddr != nil {
// It's an IP address, we can serve that
h.next.ServeHTTP(w, r)
return
}
// Not an ip address, but a hostname. Need to validate
if _, exist := h.vhosts["*"]; exist {
h.next.ServeHTTP(w, r)
return
}
if _, exist := h.vhosts[host]; exist {
h.next.ServeHTTP(w, r)
return
}
http.Error(w, "invalid host specified", http.StatusForbidden)
}
func newVHostHandler(vhosts []string, next http.Handler) http.Handler {
vhostMap := make(map[string]struct{})
for _, allowedHost := range vhosts {
vhostMap[strings.ToLower(allowedHost)] = struct{}{}
}
return &virtualHostHandler{vhostMap, next}
}

@ -0,0 +1,54 @@
// Copyright 2017 The go-ethereum Authors
// This file is part of the go-ethereum library.
//
// The go-ethereum library is free software: you can redistribute it and/or modify
// it under the terms of the GNU Lesser General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// The go-ethereum library is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Lesser General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public License
// along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>.
package rpc
import (
"net/http"
"net/http/httptest"
"strings"
"testing"
)
func TestHTTPErrorResponseWithDelete(t *testing.T) {
testHTTPErrorResponse(t, http.MethodDelete, contentType, "", http.StatusMethodNotAllowed)
}
func TestHTTPErrorResponseWithPut(t *testing.T) {
testHTTPErrorResponse(t, http.MethodPut, contentType, "", http.StatusMethodNotAllowed)
}
func TestHTTPErrorResponseWithMaxContentLength(t *testing.T) {
body := make([]rune, maxRequestContentLength+1)
testHTTPErrorResponse(t,
http.MethodPost, contentType, string(body), http.StatusRequestEntityTooLarge)
}
func TestHTTPErrorResponseWithEmptyContentType(t *testing.T) {
testHTTPErrorResponse(t, http.MethodPost, "", "", http.StatusUnsupportedMediaType)
}
func TestHTTPErrorResponseWithValidRequest(t *testing.T) {
testHTTPErrorResponse(t, http.MethodPost, contentType, "", 0)
}
func testHTTPErrorResponse(t *testing.T, method, contentType, body string, expected int) {
request := httptest.NewRequest(method, "http://url.com", strings.NewReader(body))
request.Header.Set("content-type", contentType)
if code, _ := validateRequest(request); code != expected {
t.Fatalf("response code should be %d not %d", expected, code)
}
}

@ -0,0 +1,33 @@
// Copyright 2016 The go-ethereum Authors
// This file is part of the go-ethereum library.
//
// The go-ethereum library is free software: you can redistribute it and/or modify
// it under the terms of the GNU Lesser General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// The go-ethereum library is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Lesser General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public License
// along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>.
package rpc
import (
"context"
"net"
)
// DialInProc attaches an in-process connection to the given RPC server.
func DialInProc(handler *Server) *Client {
initctx := context.Background()
c, _ := newClient(initctx, func(context.Context) (ServerCodec, error) {
p1, p2 := net.Pipe()
go handler.ServeCodec(NewCodec(p1), 0)
return NewCodec(p2), nil
})
return c
}

@ -0,0 +1,56 @@
// Copyright 2015 The go-ethereum Authors
// This file is part of the go-ethereum library.
//
// The go-ethereum library is free software: you can redistribute it and/or modify
// it under the terms of the GNU Lesser General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// The go-ethereum library is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Lesser General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public License
// along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>.
package rpc
import (
"context"
"net"
"github.com/ethereum/go-ethereum/log"
"github.com/ethereum/go-ethereum/p2p/netutil"
)
// ServeListener accepts connections on l, serving JSON-RPC on them.
func (s *Server) ServeListener(l net.Listener) error {
for {
conn, err := l.Accept()
if netutil.IsTemporaryError(err) {
log.Warn("RPC accept error", "err", err)
continue
} else if err != nil {
return err
}
log.Trace("Accepted RPC connection", "conn", conn.RemoteAddr())
go s.ServeCodec(NewCodec(conn), 0)
}
}
// DialIPC create a new IPC client that connects to the given endpoint. On Unix it assumes
// the endpoint is the full path to a unix socket, and Windows the endpoint is an
// identifier for a named pipe.
//
// The context is used for the initial connection establishment. It does not
// affect subsequent interactions with the client.
func DialIPC(ctx context.Context, endpoint string) (*Client, error) {
return newClient(ctx, func(ctx context.Context) (ServerCodec, error) {
conn, err := newIPCConnection(ctx, endpoint)
if err != nil {
return nil, err
}
return NewCodec(conn), err
})
}

@ -0,0 +1,37 @@
// Copyright 2018 The go-ethereum Authors
// This file is part of the go-ethereum library.
//
// The go-ethereum library is free software: you can redistribute it and/or modify
// it under the terms of the GNU Lesser General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// The go-ethereum library is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Lesser General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public License
// along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>.
// +build js
package rpc
import (
"context"
"errors"
"net"
)
var errNotSupported = errors.New("rpc: not supported")
// ipcListen will create a named pipe on the given endpoint.
func ipcListen(endpoint string) (net.Listener, error) {
return nil, errNotSupported
}
// newIPCConnection will connect to a named pipe with the given endpoint as name.
func newIPCConnection(ctx context.Context, endpoint string) (net.Conn, error) {
return nil, errNotSupported
}

@ -0,0 +1,54 @@
// Copyright 2015 The go-ethereum Authors
// This file is part of the go-ethereum library.
//
// The go-ethereum library is free software: you can redistribute it and/or modify
// it under the terms of the GNU Lesser General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// The go-ethereum library is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Lesser General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public License
// along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>.
// +build darwin dragonfly freebsd linux nacl netbsd openbsd solaris
package rpc
import (
"context"
"fmt"
"net"
"os"
"path/filepath"
"github.com/ethereum/go-ethereum/log"
)
// ipcListen will create a Unix socket on the given endpoint.
func ipcListen(endpoint string) (net.Listener, error) {
if len(endpoint) > int(max_path_size) {
log.Warn(fmt.Sprintf("The ipc endpoint is longer than %d characters. ", max_path_size),
"endpoint", endpoint)
}
// Ensure the IPC path exists and remove any previous leftover
if err := os.MkdirAll(filepath.Dir(endpoint), 0751); err != nil {
return nil, err
}
os.Remove(endpoint)
l, err := net.Listen("unix", endpoint)
if err != nil {
return nil, err
}
os.Chmod(endpoint, 0600)
return l, nil
}
// newIPCConnection will connect to a Unix socket on the given endpoint.
func newIPCConnection(ctx context.Context, endpoint string) (net.Conn, error) {
return new(net.Dialer).DialContext(ctx, "unix", endpoint)
}

@ -0,0 +1,48 @@
// Copyright 2015 The go-ethereum Authors
// This file is part of the go-ethereum library.
//
// The go-ethereum library is free software: you can redistribute it and/or modify
// it under the terms of the GNU Lesser General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// The go-ethereum library is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Lesser General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public License
// along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>.
// +build windows
package rpc
import (
"context"
"net"
"time"
"gopkg.in/natefinch/npipe.v2"
)
// This is used if the dialing context has no deadline. It is much smaller than the
// defaultDialTimeout because named pipes are local and there is no need to wait so long.
const defaultPipeDialTimeout = 2 * time.Second
// ipcListen will create a named pipe on the given endpoint.
func ipcListen(endpoint string) (net.Listener, error) {
return npipe.Listen(endpoint)
}
// newIPCConnection will connect to a named pipe with the given endpoint as name.
func newIPCConnection(ctx context.Context, endpoint string) (net.Conn, error) {
timeout := defaultPipeDialTimeout
if deadline, ok := ctx.Deadline(); ok {
timeout = deadline.Sub(time.Now())
if timeout < 0 {
timeout = 0
}
}
return npipe.DialTimeout(endpoint, timeout)
}

@ -0,0 +1,329 @@
// Copyright 2015 The go-ethereum Authors
// This file is part of the go-ethereum library.
//
// The go-ethereum library is free software: you can redistribute it and/or modify
// it under the terms of the GNU Lesser General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// The go-ethereum library is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Lesser General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public License
// along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>.
package rpc
import (
"bytes"
"context"
std_json "encoding/json"
"errors"
"fmt"
"io"
"reflect"
"strings"
"sync"
"time"
"github.com/goccy/go-json"
)
const (
vsn = "2.0"
serviceMethodSeparator = "_"
subscribeMethodSuffix = "_subscribe"
unsubscribeMethodSuffix = "_unsubscribe"
notificationMethodSuffix = "_subscription"
defaultWriteTimeout = 10 * time.Second // used if context has no deadline
)
var null = json.RawMessage("null")
type subscriptionResult struct {
ID string `json:"subscription"`
Result json.RawMessage `json:"result,omitempty"`
}
// A value of this type can a JSON-RPC request, notification, successful response or
// error response. Which one it is depends on the fields.
type jsonrpcMessage struct {
Version string `json:"jsonrpc,omitempty"`
ID json.RawMessage `json:"id,omitempty"`
Method string `json:"method,omitempty"`
Params json.RawMessage `json:"params,omitempty"`
Error *jsonError `json:"error,omitempty"`
Result json.RawMessage `json:"result,omitempty"`
}
func (msg *jsonrpcMessage) isNotification() bool {
return msg.ID == nil && msg.Method != ""
}
func (msg *jsonrpcMessage) isCall() bool {
return msg.hasValidID() && msg.Method != ""
}
func (msg *jsonrpcMessage) isResponse() bool {
return msg.hasValidID() && msg.Method == "" && msg.Params == nil && (msg.Result != nil || msg.Error != nil)
}
func (msg *jsonrpcMessage) hasValidID() bool {
return len(msg.ID) > 0 && msg.ID[0] != '{' && msg.ID[0] != '['
}
func (msg *jsonrpcMessage) isSubscribe() bool {
return strings.HasSuffix(msg.Method, subscribeMethodSuffix)
}
func (msg *jsonrpcMessage) isUnsubscribe() bool {
return strings.HasSuffix(msg.Method, unsubscribeMethodSuffix)
}
func (msg *jsonrpcMessage) namespace() string {
elem := strings.SplitN(msg.Method, serviceMethodSeparator, 2)
return elem[0]
}
func (msg *jsonrpcMessage) String() string {
b, _ := json.Marshal(msg)
return string(b)
}
func (msg *jsonrpcMessage) errorResponse(err error) *jsonrpcMessage {
resp := errorMessage(err)
resp.ID = msg.ID
return resp
}
func (msg *jsonrpcMessage) response(result interface{}) *jsonrpcMessage {
enc, err := json.Marshal(result)
if err != nil {
// TODO: wrap with 'internal server error'
return msg.errorResponse(err)
}
return &jsonrpcMessage{Version: vsn, ID: msg.ID, Result: enc}
}
func errorMessage(err error) *jsonrpcMessage {
msg := &jsonrpcMessage{Version: vsn, ID: null, Error: &jsonError{
Code: defaultErrorCode,
Message: err.Error(),
}}
ec, ok := err.(Error)
if ok {
msg.Error.Code = ec.ErrorCode()
}
return msg
}
type jsonError struct {
Code int `json:"code"`
Message string `json:"message"`
Data interface{} `json:"data,omitempty"`
}
func (err *jsonError) Error() string {
if err.Message == "" {
return fmt.Sprintf("json-rpc error %d", err.Code)
}
return err.Message
}
func (err *jsonError) ErrorCode() int {
return err.Code
}
// Conn is a subset of the methods of net.Conn which are sufficient for ServerCodec.
type Conn interface {
io.ReadWriteCloser
SetWriteDeadline(time.Time) error
}
type deadlineCloser interface {
io.Closer
SetWriteDeadline(time.Time) error
}
// ConnRemoteAddr wraps the RemoteAddr operation, which returns a description
// of the peer address of a connection. If a Conn also implements ConnRemoteAddr, this
// description is used in log messages.
type ConnRemoteAddr interface {
RemoteAddr() string
}
// jsonCodec reads and writes JSON-RPC messages to the underlying connection. It also has
// support for parsing arguments and serializing (result) objects.
type jsonCodec struct {
remote string
closer sync.Once // close closed channel once
closeCh chan interface{} // closed on Close
decode func(v interface{}) error // decoder to allow multiple transports
encMu sync.Mutex // guards the encoder
encode func(v interface{}) error // encoder to allow multiple transports
conn deadlineCloser
}
// NewFuncCodec creates a codec which uses the given functions to read and write. If conn
// implements ConnRemoteAddr, log messages will use it to include the remote address of
// the connection.
func NewFuncCodec(conn deadlineCloser, encode, decode func(v interface{}) error) ServerCodec {
codec := &jsonCodec{
closeCh: make(chan interface{}),
encode: encode,
decode: decode,
conn: conn,
}
if ra, ok := conn.(ConnRemoteAddr); ok {
codec.remote = ra.RemoteAddr()
}
return codec
}
// NewCodec creates a codec on the given connection. If conn implements ConnRemoteAddr, log
// messages will use it to include the remote address of the connection.
func NewCodec(conn Conn) ServerCodec {
enc := json.NewEncoder(conn)
dec := json.NewDecoder(conn)
dec.UseNumber()
return NewFuncCodec(conn, enc.Encode, dec.Decode)
}
func (c *jsonCodec) remoteAddr() string {
return c.remote
}
func (c *jsonCodec) readBatch() (msg []*jsonrpcMessage, batch bool, err error) {
// Decode the next JSON object in the input stream.
// This verifies basic syntax, etc.
var rawmsg json.RawMessage
if err := c.decode(&rawmsg); err != nil {
return nil, false, err
}
msg, batch = parseMessage(rawmsg)
return msg, batch, nil
}
func (c *jsonCodec) writeJSON(ctx context.Context, v interface{}) error {
c.encMu.Lock()
defer c.encMu.Unlock()
deadline, ok := ctx.Deadline()
if !ok {
deadline = time.Now().Add(defaultWriteTimeout)
}
c.conn.SetWriteDeadline(deadline)
return c.encode(v)
}
func (c *jsonCodec) close() {
c.closer.Do(func() {
close(c.closeCh)
c.conn.Close()
})
}
// Closed returns a channel which will be closed when Close is called
func (c *jsonCodec) closed() <-chan interface{} {
return c.closeCh
}
// parseMessage parses raw bytes as a (batch of) JSON-RPC message(s). There are no error
// checks in this function because the raw message has already been syntax-checked when it
// is called. Any non-JSON-RPC messages in the input return the zero value of
// jsonrpcMessage.
func parseMessage(raw json.RawMessage) ([]*jsonrpcMessage, bool) {
if !isBatch(raw) {
msgs := []*jsonrpcMessage{{}}
json.Unmarshal(raw, &msgs[0])
return msgs, false
}
dec := std_json.NewDecoder(bytes.NewReader(raw))
dec.Token() // skip '['
var msgs []*jsonrpcMessage
for dec.More() {
msgs = append(msgs, new(jsonrpcMessage))
dec.Decode(&msgs[len(msgs)-1])
}
return msgs, true
}
// isBatch returns true when the first non-whitespace characters is '['
func isBatch(raw json.RawMessage) bool {
for _, c := range raw {
// skip insignificant whitespace (http://www.ietf.org/rfc/rfc4627.txt)
if c == 0x20 || c == 0x09 || c == 0x0a || c == 0x0d {
continue
}
return c == '['
}
return false
}
// parsePositionalArguments tries to parse the given args to an array of values with the
// given types. It returns the parsed values or an error when the args could not be
// parsed. Missing optional arguments are returned as reflect.Zero values.
func parsePositionalArguments(rawArgs json.RawMessage, types []reflect.Type) ([]reflect.Value, error) {
dec := json.NewDecoder(bytes.NewReader(rawArgs))
var args []reflect.Value
tok, err := dec.Token()
switch {
case err == io.EOF || tok == nil && err == nil:
// "params" is optional and may be empty. Also allow "params":null even though it's
// not in the spec because our own client used to send it.
case err != nil:
return nil, err
case tok == json.Delim('['):
// Read argument array.
if args, err = parseArgumentArray(dec, types); err != nil {
return nil, err
}
default:
return nil, errors.New("non-array args")
}
// Set any missing args to nil.
for i := len(args); i < len(types); i++ {
if types[i].Kind() != reflect.Ptr {
return nil, fmt.Errorf("missing value for required argument %d", i)
}
args = append(args, reflect.Zero(types[i]))
}
return args, nil
}
func parseArgumentArray(dec *json.Decoder, types []reflect.Type) ([]reflect.Value, error) {
args := make([]reflect.Value, 0, len(types))
for i := 0; dec.More(); i++ {
if i >= len(types) {
return args, fmt.Errorf("too many arguments, want at most %d", len(types))
}
argval := reflect.New(types[i])
if err := dec.Decode(argval.Interface()); err != nil {
return args, fmt.Errorf("invalid argument %d: %v", i, err)
}
if argval.IsNil() && types[i].Kind() != reflect.Ptr {
return args, fmt.Errorf("missing value for required argument %d", i)
}
args = append(args, argval.Elem())
}
// Read end of args array.
_, err := dec.Token()
return args, err
}
// parseSubscriptionName extracts the subscription name from an encoded argument array.
func parseSubscriptionName(rawArgs json.RawMessage) (string, error) {
dec := json.NewDecoder(bytes.NewReader(rawArgs))
if tok, _ := dec.Token(); tok != json.Delim('[') {
return "", errors.New("non-array args")
}
v, _ := dec.Token()
method, ok := v.(string)
if !ok {
return "", errors.New("expected subscription name as first argument")
}
return method, nil
}

@ -0,0 +1,68 @@
package rpc
import (
prom "github.com/harmony-one/harmony/api/service/prometheus"
"github.com/prometheus/client_golang/prometheus"
)
func init() {
prom.PromRegistry().MustRegister(
requestCounterVec,
requestErroredCounterVec,
requestDurationHistVec,
)
}
var (
requestCounterVec = prometheus.NewCounterVec(
prometheus.CounterOpts{
Namespace: "hmy",
Subsystem: "rpc2",
Name: "request_count",
Help: "counters for each RPC method",
},
[]string{"method"},
)
requestErroredCounterVec = prometheus.NewCounterVec(
prometheus.CounterOpts{
Namespace: "hmy",
Subsystem: "rpc2",
Name: "err_count",
Help: "counters of errored RPC method",
},
[]string{"method"},
)
requestDurationHistVec = prometheus.NewHistogramVec(
prometheus.HistogramOpts{
Namespace: "hmy",
Subsystem: "rpc2",
Name: "delay_histogram",
Help: "delays histogram in seconds",
// buckets: 50ms, 100ms, 200ms, 400ms, 800ms, 1600ms, 3200ms, +INF
Buckets: prometheus.ExponentialBuckets(0.05, 2, 8),
},
[]string{"method"},
)
)
func doMetricRequest(method string) *prometheus.Timer {
pLabel := prometheus.Labels{
"method": method,
}
requestCounterVec.With(pLabel).Inc()
timer := prometheus.NewTimer(requestDurationHistVec.With(pLabel))
return timer
}
func doMetricErroredRequest(method string) {
pLabel := prometheus.Labels{
"method": method,
}
requestErroredCounterVec.With(pLabel).Inc()
}
func doMetricDelayHist(timer *prometheus.Timer) {
timer.ObserveDuration()
}

@ -0,0 +1,147 @@
// Copyright 2015 The go-ethereum Authors
// This file is part of the go-ethereum library.
//
// The go-ethereum library is free software: you can redistribute it and/or modify
// it under the terms of the GNU Lesser General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// The go-ethereum library is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Lesser General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public License
// along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>.
package rpc
import (
"context"
"io"
"sync/atomic"
mapset "github.com/deckarep/golang-set"
"github.com/ethereum/go-ethereum/log"
)
const MetadataApi = "rpc"
// CodecOption specifies which type of messages a codec supports.
//
// Deprecated: this option is no longer honored by Server.
type CodecOption int
const (
// OptionMethodInvocation is an indication that the codec supports RPC method calls
OptionMethodInvocation CodecOption = 1 << iota
// OptionSubscriptions is an indication that the codec suports RPC notifications
OptionSubscriptions = 1 << iota // support pub sub
)
// Server is an RPC server.
type Server struct {
services serviceRegistry
idgen func() ID
run int32
codecs mapset.Set
}
// NewServer creates a new server instance with no registered handlers.
func NewServer() *Server {
server := &Server{idgen: randomIDGenerator(), codecs: mapset.NewSet(), run: 1}
// Register the default service providing meta information about the RPC service such
// as the services and methods it offers.
rpcService := &RPCService{server}
server.RegisterName(MetadataApi, rpcService)
return server
}
// RegisterName creates a service for the given receiver type under the given name. When no
// methods on the given receiver match the criteria to be either a RPC method or a
// subscription an error is returned. Otherwise a new service is created and added to the
// service collection this server provides to clients.
func (s *Server) RegisterName(name string, receiver interface{}) error {
return s.services.registerName(name, receiver)
}
// ServeCodec reads incoming requests from codec, calls the appropriate callback and writes
// the response back using the given codec. It will block until the codec is closed or the
// server is stopped. In either case the codec is closed.
//
// Note that codec options are no longer supported.
func (s *Server) ServeCodec(codec ServerCodec, options CodecOption) {
defer codec.close()
// Don't serve if server is stopped.
if atomic.LoadInt32(&s.run) == 0 {
return
}
// Add the codec to the set so it can be closed by Stop.
s.codecs.Add(codec)
defer s.codecs.Remove(codec)
c := initClient(codec, s.idgen, &s.services)
<-codec.closed()
c.Close()
}
// serveSingleRequest reads and processes a single RPC request from the given codec. This
// is used to serve HTTP connections. Subscriptions and reverse calls are not allowed in
// this mode.
func (s *Server) serveSingleRequest(ctx context.Context, codec ServerCodec) {
// Don't serve if server is stopped.
if atomic.LoadInt32(&s.run) == 0 {
return
}
h := newHandler(ctx, codec, s.idgen, &s.services)
h.allowSubscribe = false
defer h.close(io.EOF, nil)
reqs, batch, err := codec.readBatch()
if err != nil {
if err != io.EOF {
codec.writeJSON(ctx, errorMessage(&invalidMessageError{"parse error"}))
}
return
}
if batch {
h.handleBatch(reqs)
} else {
h.handleMsg(reqs[0])
}
}
// Stop stops reading new requests, waits for stopPendingRequestTimeout to allow pending
// requests to finish, then closes all codecs which will cancel pending requests and
// subscriptions.
func (s *Server) Stop() {
if atomic.CompareAndSwapInt32(&s.run, 1, 0) {
log.Debug("RPC server shutting down")
s.codecs.Each(func(c interface{}) bool {
c.(ServerCodec).close()
return true
})
}
}
// RPCService gives meta information about the server.
// e.g. gives information about the loaded modules.
type RPCService struct {
server *Server
}
// Modules returns the list of RPC services with their version number
func (s *RPCService) Modules() map[string]string {
s.server.services.mu.Lock()
defer s.server.services.mu.Unlock()
modules := make(map[string]string)
for name := range s.server.services.services {
modules[name] = "1.0"
}
return modules
}

@ -0,0 +1,152 @@
// Copyright 2015 The go-ethereum Authors
// This file is part of the go-ethereum library.
//
// The go-ethereum library is free software: you can redistribute it and/or modify
// it under the terms of the GNU Lesser General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// The go-ethereum library is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Lesser General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public License
// along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>.
package rpc
import (
"bufio"
"bytes"
"io"
"io/ioutil"
"net"
"path/filepath"
"strings"
"testing"
"time"
)
func TestServerRegisterName(t *testing.T) {
server := NewServer()
service := new(testService)
if err := server.RegisterName("test", service); err != nil {
t.Fatalf("%v", err)
}
if len(server.services.services) != 2 {
t.Fatalf("Expected 2 service entries, got %d", len(server.services.services))
}
svc, ok := server.services.services["test"]
if !ok {
t.Fatalf("Expected service calc to be registered")
}
wantCallbacks := 7
if len(svc.callbacks) != wantCallbacks {
t.Errorf("Expected %d callbacks for service 'service', got %d", wantCallbacks, len(svc.callbacks))
}
}
func TestServer(t *testing.T) {
files, err := ioutil.ReadDir("testdata")
if err != nil {
t.Fatal("where'd my testdata go?")
}
for _, f := range files {
if f.IsDir() || strings.HasPrefix(f.Name(), ".") {
continue
}
path := filepath.Join("testdata", f.Name())
name := strings.TrimSuffix(f.Name(), filepath.Ext(f.Name()))
t.Run(name, func(t *testing.T) {
runTestScript(t, path)
})
}
}
func runTestScript(t *testing.T, file string) {
server := newTestServer()
content, err := ioutil.ReadFile(file)
if err != nil {
t.Fatal(err)
}
clientConn, serverConn := net.Pipe()
defer clientConn.Close()
go server.ServeCodec(NewCodec(serverConn), 0)
readbuf := bufio.NewReader(clientConn)
for _, line := range strings.Split(string(content), "\n") {
line = strings.TrimSpace(line)
switch {
case len(line) == 0 || strings.HasPrefix(line, "//"):
// skip comments, blank lines
continue
case strings.HasPrefix(line, "--> "):
t.Log(line)
// write to connection
clientConn.SetWriteDeadline(time.Now().Add(5 * time.Second))
if _, err := io.WriteString(clientConn, line[4:]+"\n"); err != nil {
t.Fatalf("write error: %v", err)
}
case strings.HasPrefix(line, "<-- "):
t.Log(line)
want := line[4:]
// read line from connection and compare text
clientConn.SetReadDeadline(time.Now().Add(5 * time.Second))
sent, err := readbuf.ReadString('\n')
if err != nil {
t.Fatalf("read error: %v", err)
}
sent = strings.TrimRight(sent, "\r\n")
if sent != want {
t.Errorf("wrong line from server\ngot: %s\nwant: %s", sent, want)
}
default:
panic("invalid line in test script: " + line)
}
}
}
// This test checks that responses are delivered for very short-lived connections that
// only carry a single request.
func TestServerShortLivedConn(t *testing.T) {
server := newTestServer()
defer server.Stop()
listener, err := net.Listen("tcp", "127.0.0.1:0")
if err != nil {
t.Fatal("can't listen:", err)
}
defer listener.Close()
go server.ServeListener(listener)
var (
request = `{"jsonrpc":"2.0","id":1,"method":"rpc_modules"}` + "\n"
wantResp = `{"jsonrpc":"2.0","id":1,"result":{"nftest":"1.0","rpc":"1.0","test":"1.0"}}` + "\n"
deadline = time.Now().Add(10 * time.Second)
)
for i := 0; i < 20; i++ {
conn, err := net.Dial("tcp", listener.Addr().String())
if err != nil {
t.Fatal("can't dial:", err)
}
defer conn.Close()
conn.SetDeadline(deadline)
// Write the request, then half-close the connection so the server stops reading.
conn.Write([]byte(request))
conn.(*net.TCPConn).CloseWrite()
// Now try to get the response.
buf := make([]byte, 2000)
n, err := conn.Read(buf)
if err != nil {
t.Fatal("read error:", err)
}
if !bytes.Equal(buf[:n], []byte(wantResp)) {
t.Fatalf("wrong response: %s", buf[:n])
}
}
}

@ -0,0 +1,261 @@
// Copyright 2019 The go-ethereum Authors
// This file is part of the go-ethereum library.
//
// The go-ethereum library is free software: you can redistribute it and/or modify
// it under the terms of the GNU Lesser General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// The go-ethereum library is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Lesser General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public License
// along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>.
package rpc
import (
"context"
"errors"
"fmt"
"reflect"
"runtime"
"strings"
"sync"
"unicode"
"github.com/ethereum/go-ethereum/log"
)
var (
contextType = reflect.TypeOf((*context.Context)(nil)).Elem()
errorType = reflect.TypeOf((*error)(nil)).Elem()
subscriptionType = reflect.TypeOf(Subscription{})
stringType = reflect.TypeOf("")
)
type serviceRegistry struct {
mu sync.Mutex
services map[string]service
}
// service represents a registered object.
type service struct {
name string // name for service
callbacks map[string]*callback // registered handlers
subscriptions map[string]*callback // available subscriptions/notifications
}
// callback is a method callback which was registered in the server
type callback struct {
fn reflect.Value // the function
rcvr reflect.Value // receiver object of method, set if fn is method
argTypes []reflect.Type // input argument types
hasCtx bool // method's first argument is a context (not included in argTypes)
errPos int // err return idx, of -1 when method cannot return error
isSubscribe bool // true if this is a subscription callback
}
func (r *serviceRegistry) registerName(name string, rcvr interface{}) error {
rcvrVal := reflect.ValueOf(rcvr)
if name == "" {
return fmt.Errorf("no service name for type %s", rcvrVal.Type().String())
}
callbacks := suitableCallbacks(rcvrVal)
if len(callbacks) == 0 {
return fmt.Errorf("service %T doesn't have any suitable methods/subscriptions to expose", rcvr)
}
r.mu.Lock()
defer r.mu.Unlock()
if r.services == nil {
r.services = make(map[string]service)
}
svc, ok := r.services[name]
if !ok {
svc = service{
name: name,
callbacks: make(map[string]*callback),
subscriptions: make(map[string]*callback),
}
r.services[name] = svc
}
for name, cb := range callbacks {
if cb.isSubscribe {
svc.subscriptions[name] = cb
} else {
svc.callbacks[name] = cb
}
}
return nil
}
// callback returns the callback corresponding to the given RPC method name.
func (r *serviceRegistry) callback(method string) *callback {
elem := strings.SplitN(method, serviceMethodSeparator, 2)
if len(elem) != 2 {
return nil
}
r.mu.Lock()
defer r.mu.Unlock()
return r.services[elem[0]].callbacks[elem[1]]
}
// subscription returns a subscription callback in the given service.
func (r *serviceRegistry) subscription(service, name string) *callback {
r.mu.Lock()
defer r.mu.Unlock()
return r.services[service].subscriptions[name]
}
// suitableCallbacks iterates over the methods of the given type. It determines if a method
// satisfies the criteria for a RPC callback or a subscription callback and adds it to the
// collection of callbacks. See server documentation for a summary of these criteria.
func suitableCallbacks(receiver reflect.Value) map[string]*callback {
typ := receiver.Type()
callbacks := make(map[string]*callback)
for m := 0; m < typ.NumMethod(); m++ {
method := typ.Method(m)
if method.PkgPath != "" {
continue // method not exported
}
cb := newCallback(receiver, method.Func)
if cb == nil {
continue // function invalid
}
name := formatName(method.Name)
callbacks[name] = cb
}
return callbacks
}
// newCallback turns fn (a function) into a callback object. It returns nil if the function
// is unsuitable as an RPC callback.
func newCallback(receiver, fn reflect.Value) *callback {
fntype := fn.Type()
c := &callback{fn: fn, rcvr: receiver, errPos: -1, isSubscribe: isPubSub(fntype)}
// Determine parameter types. They must all be exported or builtin types.
c.makeArgTypes()
// Verify return types. The function must return at most one error
// and/or one other non-error value.
outs := make([]reflect.Type, fntype.NumOut())
for i := 0; i < fntype.NumOut(); i++ {
outs[i] = fntype.Out(i)
}
if len(outs) > 2 {
return nil
}
// If an error is returned, it must be the last returned value.
switch {
case len(outs) == 1 && isErrorType(outs[0]):
c.errPos = 0
case len(outs) == 2:
if isErrorType(outs[0]) || !isErrorType(outs[1]) {
return nil
}
c.errPos = 1
}
return c
}
// makeArgTypes composes the argTypes list.
func (c *callback) makeArgTypes() {
fntype := c.fn.Type()
// Skip receiver and context.Context parameter (if present).
firstArg := 0
if c.rcvr.IsValid() {
firstArg++
}
if fntype.NumIn() > firstArg && fntype.In(firstArg) == contextType {
c.hasCtx = true
firstArg++
}
// Add all remaining parameters.
c.argTypes = make([]reflect.Type, fntype.NumIn()-firstArg)
for i := firstArg; i < fntype.NumIn(); i++ {
c.argTypes[i-firstArg] = fntype.In(i)
}
}
// call invokes the callback.
func (c *callback) call(ctx context.Context, method string, args []reflect.Value) (res interface{}, errRes error) {
// Create the argument slice.
fullargs := make([]reflect.Value, 0, 2+len(args))
if c.rcvr.IsValid() {
fullargs = append(fullargs, c.rcvr)
}
if c.hasCtx {
fullargs = append(fullargs, reflect.ValueOf(ctx))
}
fullargs = append(fullargs, args...)
// Catch panic while running the callback.
defer func() {
if err := recover(); err != nil {
const size = 64 << 10
buf := make([]byte, size)
buf = buf[:runtime.Stack(buf, false)]
log.Error("RPC method " + method + " crashed: " + fmt.Sprintf("%v\n%s", err, buf))
errRes = errors.New("method handler crashed")
}
}()
// Run the callback.
results := c.fn.Call(fullargs)
if len(results) == 0 {
return nil, nil
}
if c.errPos >= 0 && !results[c.errPos].IsNil() {
// Method has returned non-nil error value.
err := results[c.errPos].Interface().(error)
return reflect.Value{}, err
}
return results[0].Interface(), nil
}
// Is t context.Context or *context.Context?
func isContextType(t reflect.Type) bool {
for t.Kind() == reflect.Ptr {
t = t.Elem()
}
return t == contextType
}
// Does t satisfy the error interface?
func isErrorType(t reflect.Type) bool {
for t.Kind() == reflect.Ptr {
t = t.Elem()
}
return t.Implements(errorType)
}
// Is t Subscription or *Subscription?
func isSubscriptionType(t reflect.Type) bool {
for t.Kind() == reflect.Ptr {
t = t.Elem()
}
return t == subscriptionType
}
// isPubSub tests whether the given method has as as first argument a context.Context and
// returns the pair (Subscription, error).
func isPubSub(methodType reflect.Type) bool {
// numIn(0) is the receiver type
if methodType.NumIn() < 2 || methodType.NumOut() != 2 {
return false
}
return isContextType(methodType.In(1)) &&
isSubscriptionType(methodType.Out(0)) &&
isErrorType(methodType.Out(1))
}
// formatName converts to first character of name to lowercase.
func formatName(name string) string {
ret := []rune(name)
if len(ret) > 0 {
ret[0] = unicode.ToLower(ret[0])
}
return string(ret)
}

@ -0,0 +1,66 @@
// Copyright 2018 The go-ethereum Authors
// This file is part of the go-ethereum library.
//
// The go-ethereum library is free software: you can redistribute it and/or modify
// it under the terms of the GNU Lesser General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// The go-ethereum library is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Lesser General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public License
// along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>.
package rpc
import (
"context"
"errors"
"io"
"net"
"os"
"time"
)
// DialStdIO creates a client on stdin/stdout.
func DialStdIO(ctx context.Context) (*Client, error) {
return DialIO(ctx, os.Stdin, os.Stdout)
}
// DialIO creates a client which uses the given IO channels
func DialIO(ctx context.Context, in io.Reader, out io.Writer) (*Client, error) {
return newClient(ctx, func(_ context.Context) (ServerCodec, error) {
return NewCodec(stdioConn{
in: in,
out: out,
}), nil
})
}
type stdioConn struct {
in io.Reader
out io.Writer
}
func (io stdioConn) Read(b []byte) (n int, err error) {
return io.in.Read(b)
}
func (io stdioConn) Write(b []byte) (n int, err error) {
return io.out.Write(b)
}
func (io stdioConn) Close() error {
return nil
}
func (io stdioConn) RemoteAddr() string {
return "/dev/stdin"
}
func (io stdioConn) SetWriteDeadline(t time.Time) error {
return &net.OpError{Op: "set", Net: "stdio", Source: nil, Addr: nil, Err: errors.New("deadline not supported")}
}

@ -0,0 +1,328 @@
// Copyright 2016 The go-ethereum Authors
// This file is part of the go-ethereum library.
//
// The go-ethereum library is free software: you can redistribute it and/or modify
// it under the terms of the GNU Lesser General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// The go-ethereum library is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Lesser General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public License
// along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>.
package rpc
import (
"bufio"
"container/list"
"context"
crand "crypto/rand"
"encoding/binary"
"encoding/hex"
"errors"
"math/rand"
"reflect"
"strings"
"sync"
"time"
"github.com/goccy/go-json"
)
var (
// ErrNotificationsUnsupported is returned when the connection doesn't support notifications
ErrNotificationsUnsupported = errors.New("notifications not supported")
// ErrNotificationNotFound is returned when the notification for the given id is not found
ErrSubscriptionNotFound = errors.New("subscription not found")
)
var globalGen = randomIDGenerator()
// ID defines a pseudo random number that is used to identify RPC subscriptions.
type ID string
// NewID returns a new, random ID.
func NewID() ID {
return globalGen()
}
// randomIDGenerator returns a function generates a random IDs.
func randomIDGenerator() func() ID {
seed, err := binary.ReadVarint(bufio.NewReader(crand.Reader))
if err != nil {
seed = int64(time.Now().Nanosecond())
}
var (
mu sync.Mutex
rng = rand.New(rand.NewSource(seed))
)
return func() ID {
mu.Lock()
defer mu.Unlock()
id := make([]byte, 16)
rng.Read(id)
return encodeID(id)
}
}
func encodeID(b []byte) ID {
id := hex.EncodeToString(b)
id = strings.TrimLeft(id, "0")
if id == "" {
id = "0" // ID's are RPC quantities, no leading zero's and 0 is 0x0.
}
return ID("0x" + id)
}
type notifierKey struct{}
// NotifierFromContext returns the Notifier value stored in ctx, if any.
func NotifierFromContext(ctx context.Context) (*Notifier, bool) {
n, ok := ctx.Value(notifierKey{}).(*Notifier)
return n, ok
}
// Notifier is tied to a RPC connection that supports subscriptions.
// Server callbacks use the notifier to send notifications.
type Notifier struct {
h *handler
namespace string
mu sync.Mutex
sub *Subscription
buffer []json.RawMessage
callReturned bool
activated bool
}
// CreateSubscription returns a new subscription that is coupled to the
// RPC connection. By default subscriptions are inactive and notifications
// are dropped until the subscription is marked as active. This is done
// by the RPC server after the subscription ID is send to the client.
func (n *Notifier) CreateSubscription() *Subscription {
n.mu.Lock()
defer n.mu.Unlock()
if n.sub != nil {
panic("can't create multiple subscriptions with Notifier")
} else if n.callReturned {
panic("can't create subscription after subscribe call has returned")
}
n.sub = &Subscription{ID: n.h.idgen(), namespace: n.namespace, err: make(chan error, 1)}
return n.sub
}
// Notify sends a notification to the client with the given data as payload.
// If an error occurs the RPC connection is closed and the error is returned.
func (n *Notifier) Notify(id ID, data interface{}) error {
enc, err := json.Marshal(data)
if err != nil {
return err
}
n.mu.Lock()
defer n.mu.Unlock()
if n.sub == nil {
panic("can't Notify before subscription is created")
} else if n.sub.ID != id {
panic("Notify with wrong ID")
}
if n.activated {
return n.send(n.sub, enc)
}
n.buffer = append(n.buffer, enc)
return nil
}
// Closed returns a channel that is closed when the RPC connection is closed.
// Deprecated: use subscription error channel
func (n *Notifier) Closed() <-chan interface{} {
return n.h.conn.closed()
}
// takeSubscription returns the subscription (if one has been created). No subscription can
// be created after this call.
func (n *Notifier) takeSubscription() *Subscription {
n.mu.Lock()
defer n.mu.Unlock()
n.callReturned = true
return n.sub
}
// acticate is called after the subscription ID was sent to client. Notifications are
// buffered before activation. This prevents notifications being sent to the client before
// the subscription ID is sent to the client.
func (n *Notifier) activate() error {
n.mu.Lock()
defer n.mu.Unlock()
for _, data := range n.buffer {
if err := n.send(n.sub, data); err != nil {
return err
}
}
n.activated = true
return nil
}
func (n *Notifier) send(sub *Subscription, data json.RawMessage) error {
params, _ := json.Marshal(&subscriptionResult{ID: string(sub.ID), Result: data})
ctx := context.Background()
return n.h.conn.writeJSON(ctx, &jsonrpcMessage{
Version: vsn,
Method: n.namespace + notificationMethodSuffix,
Params: params,
})
}
// A Subscription is created by a notifier and tight to that notifier. The client can use
// this subscription to wait for an unsubscribe request for the client, see Err().
type Subscription struct {
ID ID
namespace string
err chan error // closed on unsubscribe
}
// Err returns a channel that is closed when the client send an unsubscribe request.
func (s *Subscription) Err() <-chan error {
return s.err
}
// MarshalJSON marshals a subscription as its ID.
func (s *Subscription) MarshalJSON() ([]byte, error) {
return json.Marshal(s.ID)
}
// ClientSubscription is a subscription established through the Client's Subscribe or
// EthSubscribe methods.
type ClientSubscription struct {
client *Client
etype reflect.Type
channel reflect.Value
namespace string
subid string
in chan json.RawMessage
quitOnce sync.Once // ensures quit is closed once
quit chan struct{} // quit is closed when the subscription exits
errOnce sync.Once // ensures err is closed once
err chan error
}
func newClientSubscription(c *Client, namespace string, channel reflect.Value) *ClientSubscription {
sub := &ClientSubscription{
client: c,
namespace: namespace,
etype: channel.Type().Elem(),
channel: channel,
quit: make(chan struct{}),
err: make(chan error, 1),
in: make(chan json.RawMessage),
}
return sub
}
// Err returns the subscription error channel. The intended use of Err is to schedule
// resubscription when the client connection is closed unexpectedly.
//
// The error channel receives a value when the subscription has ended due
// to an error. The received error is nil if Close has been called
// on the underlying client and no other error has occurred.
//
// The error channel is closed when Unsubscribe is called on the subscription.
func (sub *ClientSubscription) Err() <-chan error {
return sub.err
}
// Unsubscribe unsubscribes the notification and closes the error channel.
// It can safely be called more than once.
func (sub *ClientSubscription) Unsubscribe() {
sub.quitWithError(true, nil)
sub.errOnce.Do(func() { close(sub.err) })
}
func (sub *ClientSubscription) quitWithError(unsubscribeServer bool, err error) {
sub.quitOnce.Do(func() {
// The dispatch loop won't be able to execute the unsubscribe call
// if it is blocked on deliver. Close sub.quit first because it
// unblocks deliver.
close(sub.quit)
if unsubscribeServer {
sub.requestUnsubscribe()
}
if err != nil {
if err == ErrClientQuit {
err = nil // Adhere to subscription semantics.
}
sub.err <- err
}
})
}
func (sub *ClientSubscription) deliver(result json.RawMessage) (ok bool) {
select {
case sub.in <- result:
return true
case <-sub.quit:
return false
}
}
func (sub *ClientSubscription) start() {
sub.quitWithError(sub.forward())
}
func (sub *ClientSubscription) forward() (unsubscribeServer bool, err error) {
cases := []reflect.SelectCase{
{Dir: reflect.SelectRecv, Chan: reflect.ValueOf(sub.quit)},
{Dir: reflect.SelectRecv, Chan: reflect.ValueOf(sub.in)},
{Dir: reflect.SelectSend, Chan: sub.channel},
}
buffer := list.New()
defer buffer.Init()
for {
var chosen int
var recv reflect.Value
if buffer.Len() == 0 {
// Idle, omit send case.
chosen, recv, _ = reflect.Select(cases[:2])
} else {
// Non-empty buffer, send the first queued item.
cases[2].Send = reflect.ValueOf(buffer.Front().Value)
chosen, recv, _ = reflect.Select(cases)
}
switch chosen {
case 0: // <-sub.quit
return false, nil
case 1: // <-sub.in
val, err := sub.unmarshal(recv.Interface().(json.RawMessage))
if err != nil {
return true, err
}
if buffer.Len() == maxClientSubscriptionBuffer {
return true, ErrSubscriptionQueueOverflow
}
buffer.PushBack(val)
case 2: // sub.channel<-
cases[2].Send = reflect.Value{} // Don't hold onto the value.
buffer.Remove(buffer.Front())
}
}
}
func (sub *ClientSubscription) unmarshal(result json.RawMessage) (interface{}, error) {
val := reflect.New(sub.etype)
err := json.Unmarshal(result, val.Interface())
return val.Elem().Interface(), err
}
func (sub *ClientSubscription) requestUnsubscribe() error {
var result interface{}
return sub.client.Call(&result, sub.namespace+unsubscribeMethodSuffix, sub.subid)
}

@ -0,0 +1,207 @@
// Copyright 2016 The go-ethereum Authors
// This file is part of the go-ethereum library.
//
// The go-ethereum library is free software: you can redistribute it and/or modify
// it under the terms of the GNU Lesser General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// The go-ethereum library is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Lesser General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public License
// along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>.
package rpc
import (
"fmt"
"net"
"strings"
"testing"
"time"
"github.com/goccy/go-json"
)
func TestNewID(t *testing.T) {
hexchars := "0123456789ABCDEFabcdef"
for i := 0; i < 100; i++ {
id := string(NewID())
if !strings.HasPrefix(id, "0x") {
t.Fatalf("invalid ID prefix, want '0x...', got %s", id)
}
id = id[2:]
if len(id) == 0 || len(id) > 32 {
t.Fatalf("invalid ID length, want len(id) > 0 && len(id) <= 32), got %d", len(id))
}
for i := 0; i < len(id); i++ {
if strings.IndexByte(hexchars, id[i]) == -1 {
t.Fatalf("unexpected byte, want any valid hex char, got %c", id[i])
}
}
}
}
func TestSubscriptions(t *testing.T) {
var (
namespaces = []string{"eth", "shh", "bzz"}
service = &notificationTestService{}
subCount = len(namespaces)
notificationCount = 3
server = NewServer()
clientConn, serverConn = net.Pipe()
out = json.NewEncoder(clientConn)
in = json.NewDecoder(clientConn)
successes = make(chan subConfirmation)
notifications = make(chan subscriptionResult)
errors = make(chan error, subCount*notificationCount+1)
)
// setup and start server
for _, namespace := range namespaces {
if err := server.RegisterName(namespace, service); err != nil {
t.Fatalf("unable to register test service %v", err)
}
}
go server.ServeCodec(NewCodec(serverConn), 0)
defer server.Stop()
// wait for message and write them to the given channels
go waitForMessages(in, successes, notifications, errors)
// create subscriptions one by one
for i, namespace := range namespaces {
request := map[string]interface{}{
"id": i,
"method": fmt.Sprintf("%s_subscribe", namespace),
"version": "2.0",
"params": []interface{}{"someSubscription", notificationCount, i},
}
if err := out.Encode(&request); err != nil {
t.Fatalf("Could not create subscription: %v", err)
}
}
timeout := time.After(30 * time.Second)
subids := make(map[string]string, subCount)
count := make(map[string]int, subCount)
allReceived := func() bool {
done := len(count) == subCount
for _, c := range count {
if c < notificationCount {
done = false
}
}
return done
}
for !allReceived() {
select {
case confirmation := <-successes: // subscription created
subids[namespaces[confirmation.reqid]] = string(confirmation.subid)
case notification := <-notifications:
count[notification.ID]++
case err := <-errors:
t.Fatal(err)
case <-timeout:
for _, namespace := range namespaces {
subid, found := subids[namespace]
if !found {
t.Errorf("subscription for %q not created", namespace)
continue
}
if count, found := count[subid]; !found || count < notificationCount {
t.Errorf("didn't receive all notifications (%d<%d) in time for namespace %q", count, notificationCount, namespace)
}
}
t.Fatal("timed out")
}
}
}
// This test checks that unsubscribing works.
func TestServerUnsubscribe(t *testing.T) {
// Start the server.
server := newTestServer()
service := &notificationTestService{unsubscribed: make(chan string)}
server.RegisterName("nftest2", service)
p1, p2 := net.Pipe()
go server.ServeCodec(NewCodec(p1), 0)
p2.SetDeadline(time.Now().Add(10 * time.Second))
// Subscribe.
p2.Write([]byte(`{"jsonrpc":"2.0","id":1,"method":"nftest2_subscribe","params":["someSubscription",0,10]}`))
// Handle received messages.
resps := make(chan subConfirmation)
notifications := make(chan subscriptionResult)
errors := make(chan error)
go waitForMessages(json.NewDecoder(p2), resps, notifications, errors)
// Receive the subscription ID.
var sub subConfirmation
select {
case sub = <-resps:
case err := <-errors:
t.Fatal(err)
}
// Unsubscribe and check that it is handled on the server side.
p2.Write([]byte(`{"jsonrpc":"2.0","method":"nftest2_unsubscribe","params":["` + sub.subid + `"]}`))
for {
select {
case id := <-service.unsubscribed:
if id != string(sub.subid) {
t.Errorf("wrong subscription ID unsubscribed")
}
return
case err := <-errors:
t.Fatal(err)
case <-notifications:
// drop notifications
}
}
}
type subConfirmation struct {
reqid int
subid ID
}
func waitForMessages(in *json.Decoder, successes chan subConfirmation, notifications chan subscriptionResult, errors chan error) {
for {
var msg jsonrpcMessage
if err := in.Decode(&msg); err != nil {
errors <- fmt.Errorf("decode error: %v", err)
return
}
switch {
case msg.isNotification():
var res subscriptionResult
if err := json.Unmarshal(msg.Params, &res); err != nil {
errors <- fmt.Errorf("invalid subscription result: %v", err)
} else {
notifications <- res
}
case msg.isResponse():
var c subConfirmation
if msg.Error != nil {
errors <- msg.Error
} else if err := json.Unmarshal(msg.Result, &c.subid); err != nil {
errors <- fmt.Errorf("invalid response: %v", err)
} else {
json.Unmarshal(msg.ID, &c.reqid)
successes <- c
}
default:
errors <- fmt.Errorf("unrecognized message: %v", msg)
return
}
}
}

@ -0,0 +1,7 @@
// This test checks processing of messages with invalid ID.
--> {"id":[],"method":"test_foo"}
<-- {"jsonrpc":"2.0","id":null,"error":{"code":-32600,"message":"invalid request"}}
--> {"id":{},"method":"test_foo"}
<-- {"jsonrpc":"2.0","id":null,"error":{"code":-32600,"message":"invalid request"}}

@ -0,0 +1,14 @@
// This test checks the behavior of batches with invalid elements.
// Empty batches are not allowed. Batches may contain junk.
--> []
<-- {"jsonrpc":"2.0","id":null,"error":{"code":-32600,"message":"empty batch"}}
--> [1]
<-- [{"jsonrpc":"2.0","id":null,"error":{"code":-32600,"message":"invalid request"}}]
--> [1,2,3]
<-- [{"jsonrpc":"2.0","id":null,"error":{"code":-32600,"message":"invalid request"}},{"jsonrpc":"2.0","id":null,"error":{"code":-32600,"message":"invalid request"}},{"jsonrpc":"2.0","id":null,"error":{"code":-32600,"message":"invalid request"}}]
--> [{"jsonrpc":"2.0","id":1,"method":"test_echo","params":["foo",1]},55,{"jsonrpc":"2.0","id":2,"method":"unknown_method"},{"foo":"bar"}]
<-- [{"jsonrpc":"2.0","id":1,"result":{"String":"foo","Int":1,"Args":null}},{"jsonrpc":"2.0","id":null,"error":{"code":-32600,"message":"invalid request"}},{"jsonrpc":"2.0","id":2,"error":{"code":-32601,"message":"the method unknown_method does not exist/is not available"}},{"jsonrpc":"2.0","id":null,"error":{"code":-32600,"message":"invalid request"}}]

@ -0,0 +1,7 @@
// This test checks processing of messages that contain just the ID and nothing else.
--> {"id":1}
<-- {"jsonrpc":"2.0","id":1,"error":{"code":-32600,"message":"invalid request"}}
--> {"jsonrpc":"2.0","id":1}
<-- {"jsonrpc":"2.0","id":1,"error":{"code":-32600,"message":"invalid request"}}

@ -0,0 +1,4 @@
// This test checks behavior for invalid requests.
--> 1
<-- {"jsonrpc":"2.0","id":null,"error":{"code":-32600,"message":"invalid request"}}

@ -0,0 +1,5 @@
// This test checks that an error is written for invalid JSON requests.
--> 'f
<-- {"jsonrpc":"2.0","id":null,"error":{"code":-32700,"message":"json: invalid character \n as bool(false)"}}

@ -0,0 +1,8 @@
// There is no response for all-notification batches.
--> [{"jsonrpc":"2.0","method":"test_echo","params":["x",99]}]
// This test checks regular batch calls.
--> [{"jsonrpc":"2.0","id":2,"method":"test_echo","params":[]}, {"jsonrpc":"2.0","id": 3,"method":"test_echo","params":["x",3]}]
<-- [{"jsonrpc":"2.0","id":2,"error":{"code":-32602,"message":"missing value for required argument 0"}},{"jsonrpc":"2.0","id":3,"result":{"String":"x","Int":3,"Args":null}}]

@ -0,0 +1,16 @@
// This test calls the test_echo method.
--> {"jsonrpc": "2.0", "id": 2, "method": "test_echo", "params": []}
<-- {"jsonrpc":"2.0","id":2,"error":{"code":-32602,"message":"missing value for required argument 0"}}
--> {"jsonrpc": "2.0", "id": 2, "method": "test_echo", "params": ["x"]}
<-- {"jsonrpc":"2.0","id":2,"error":{"code":-32602,"message":"missing value for required argument 1"}}
--> {"jsonrpc": "2.0", "id": 2, "method": "test_echo", "params": ["x", 3]}
<-- {"jsonrpc":"2.0","id":2,"result":{"String":"x","Int":3,"Args":null}}
--> {"jsonrpc": "2.0", "id": 2, "method": "test_echo", "params": ["x", 3, {"S": "foo"}]}
<-- {"jsonrpc":"2.0","id":2,"result":{"String":"x","Int":3,"Args":{"S":"foo"}}}
--> {"jsonrpc": "2.0", "id": 2, "method": "test_echoWithCtx", "params": ["x", 3, {"S": "foo"}]}
<-- {"jsonrpc":"2.0","id":2,"result":{"String":"x","Int":3,"Args":{"S":"foo"}}}

@ -0,0 +1,5 @@
// This test checks that an error response is sent for calls
// with named parameters.
--> {"jsonrpc":"2.0","method":"test_echo","params":{"int":23},"id":3}
<-- {"jsonrpc":"2.0","id":3,"error":{"code":-32602,"message":"non-array args"}}

@ -0,0 +1,4 @@
// This test calls the test_noArgsRets method.
--> {"jsonrpc": "2.0", "id": "foo", "method": "test_noArgsRets", "params": []}
<-- {"jsonrpc":"2.0","id":"foo","result":null}

@ -0,0 +1,4 @@
// This test calls a method that doesn't exist.
--> {"jsonrpc": "2.0", "id": 2, "method": "invalid_method", "params": [2, 3]}
<-- {"jsonrpc":"2.0","id":2,"error":{"code":-32601,"message":"the method invalid_method does not exist/is not available"}}

@ -0,0 +1,4 @@
// This test checks that calls with no parameters work.
--> {"jsonrpc":"2.0","method":"test_noArgsRets","id":3}
<-- {"jsonrpc":"2.0","id":3,"result":null}

@ -0,0 +1,4 @@
// This test checks that calls with "params":null work.
--> {"jsonrpc":"2.0","method":"test_noArgsRets","params":null,"id":3}
<-- {"jsonrpc":"2.0","id":3,"result":null}

@ -0,0 +1,6 @@
// This test checks reverse calls.
--> {"jsonrpc":"2.0","id":2,"method":"test_callMeBack","params":["foo",[1]]}
<-- {"jsonrpc":"2.0","id":1,"method":"foo","params":[1]}
--> {"jsonrpc":"2.0","id":1,"result":"my result"}
<-- {"jsonrpc":"2.0","id":2,"result":"my result"}

@ -0,0 +1,7 @@
// This test checks reverse calls.
--> {"jsonrpc":"2.0","id":2,"method":"test_callMeBackLater","params":["foo",[1]]}
<-- {"jsonrpc":"2.0","id":2,"result":null}
<-- {"jsonrpc":"2.0","id":1,"method":"foo","params":[1]}
--> {"jsonrpc":"2.0","id":1,"result":"my result"}

@ -0,0 +1,12 @@
// This test checks basic subscription support.
--> {"jsonrpc":"2.0","id":1,"method":"nftest_subscribe","params":["someSubscription",5,1]}
<-- {"jsonrpc":"2.0","id":1,"result":"0x1"}
<-- {"jsonrpc":"2.0","method":"nftest_subscription","params":{"subscription":"0x1","result":1}}
<-- {"jsonrpc":"2.0","method":"nftest_subscription","params":{"subscription":"0x1","result":2}}
<-- {"jsonrpc":"2.0","method":"nftest_subscription","params":{"subscription":"0x1","result":3}}
<-- {"jsonrpc":"2.0","method":"nftest_subscription","params":{"subscription":"0x1","result":4}}
<-- {"jsonrpc":"2.0","method":"nftest_subscription","params":{"subscription":"0x1","result":5}}
--> {"jsonrpc":"2.0","id":2,"method":"nftest_echo","params":[11]}
<-- {"jsonrpc":"2.0","id":2,"result":11}

@ -0,0 +1,181 @@
// Copyright 2019 The go-ethereum Authors
// This file is part of the go-ethereum library.
//
// The go-ethereum library is free software: you can redistribute it and/or modify
// it under the terms of the GNU Lesser General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// The go-ethereum library is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Lesser General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public License
// along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>.
package rpc
import (
"context"
"encoding/binary"
"errors"
"sync"
"time"
)
func newTestServer() *Server {
server := NewServer()
server.idgen = sequentialIDGenerator()
if err := server.RegisterName("test", new(testService)); err != nil {
panic(err)
}
if err := server.RegisterName("nftest", new(notificationTestService)); err != nil {
panic(err)
}
return server
}
func sequentialIDGenerator() func() ID {
var (
mu sync.Mutex
counter uint64
)
return func() ID {
mu.Lock()
defer mu.Unlock()
counter++
id := make([]byte, 8)
binary.BigEndian.PutUint64(id, counter)
return encodeID(id)
}
}
type testService struct{}
type echoArgs struct {
S string
}
type echoResult struct {
String string
Int int
Args *echoArgs
}
func (s *testService) NoArgsRets() {}
func (s *testService) Echo(str string, i int, args *echoArgs) echoResult {
return echoResult{str, i, args}
}
func (s *testService) EchoWithCtx(ctx context.Context, str string, i int, args *echoArgs) echoResult {
return echoResult{str, i, args}
}
func (s *testService) Sleep(ctx context.Context, duration time.Duration) {
time.Sleep(duration)
}
func (s *testService) Rets() (string, error) {
return "", nil
}
//lint:ignore ST1008 returns error first on purpose.
func (s *testService) InvalidRets1() (error, string) {
return nil, ""
}
func (s *testService) InvalidRets2() (string, string) {
return "", ""
}
func (s *testService) InvalidRets3() (string, string, error) {
return "", "", nil
}
func (s *testService) CallMeBack(ctx context.Context, method string, args []interface{}) (interface{}, error) {
c, ok := ClientFromContext(ctx)
if !ok {
return nil, errors.New("no client")
}
var result interface{}
err := c.Call(&result, method, args...)
return result, err
}
func (s *testService) CallMeBackLater(ctx context.Context, method string, args []interface{}) error {
c, ok := ClientFromContext(ctx)
if !ok {
return errors.New("no client")
}
go func() {
<-ctx.Done()
var result interface{}
c.Call(&result, method, args...)
}()
return nil
}
func (s *testService) Subscription(ctx context.Context) (*Subscription, error) {
return nil, nil
}
type notificationTestService struct {
unsubscribed chan string
gotHangSubscriptionReq chan struct{}
unblockHangSubscription chan struct{}
}
func (s *notificationTestService) Echo(i int) int {
return i
}
func (s *notificationTestService) Unsubscribe(subid string) {
if s.unsubscribed != nil {
s.unsubscribed <- subid
}
}
func (s *notificationTestService) SomeSubscription(ctx context.Context, n, val int) (*Subscription, error) {
notifier, supported := NotifierFromContext(ctx)
if !supported {
return nil, ErrNotificationsUnsupported
}
// By explicitly creating an subscription we make sure that the subscription id is send
// back to the client before the first subscription.Notify is called. Otherwise the
// events might be send before the response for the *_subscribe method.
subscription := notifier.CreateSubscription()
go func() {
for i := 0; i < n; i++ {
if err := notifier.Notify(subscription.ID, val+i); err != nil {
return
}
}
select {
case <-notifier.Closed():
case <-subscription.Err():
}
if s.unsubscribed != nil {
s.unsubscribed <- string(subscription.ID)
}
}()
return subscription, nil
}
// HangSubscription blocks on s.unblockHangSubscription before sending anything.
func (s *notificationTestService) HangSubscription(ctx context.Context, val int) (*Subscription, error) {
notifier, supported := NotifierFromContext(ctx)
if !supported {
return nil, ErrNotificationsUnsupported
}
s.gotHangSubscriptionReq <- struct{}{}
<-s.unblockHangSubscription
subscription := notifier.CreateSubscription()
go func() {
notifier.Notify(subscription.ID, val)
}()
return subscription, nil
}

@ -0,0 +1,200 @@
// Copyright 2015 The go-ethereum Authors
// This file is part of the go-ethereum library.
//
// The go-ethereum library is free software: you can redistribute it and/or modify
// it under the terms of the GNU Lesser General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// The go-ethereum library is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Lesser General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public License
// along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>.
package rpc
import (
"context"
"fmt"
"math"
"strings"
"github.com/goccy/go-json"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/common/hexutil"
)
// API describes the set of methods offered over the RPC interface
type API struct {
Namespace string // namespace under which the rpc methods of Service are exposed
Version string // api version for DApp's
Service interface{} // receiver instance which holds the methods
Public bool // indication if the methods must be considered safe for public use
}
// Error wraps RPC errors, which contain an error code in addition to the message.
type Error interface {
Error() string // returns the message
ErrorCode() int // returns the code
}
// ServerCodec implements reading, parsing and writing RPC messages for the server side of
// a RPC session. Implementations must be go-routine safe since the codec can be called in
// multiple go-routines concurrently.
type ServerCodec interface {
readBatch() (msgs []*jsonrpcMessage, isBatch bool, err error)
close()
jsonWriter
}
// jsonWriter can write JSON messages to its underlying connection.
// Implementations must be safe for concurrent use.
type jsonWriter interface {
writeJSON(context.Context, interface{}) error
// Closed returns a channel which is closed when the connection is closed.
closed() <-chan interface{}
// RemoteAddr returns the peer address of the connection.
remoteAddr() string
}
type BlockNumber int64
const (
PendingBlockNumber = BlockNumber(-2)
LatestBlockNumber = BlockNumber(-1)
EarliestBlockNumber = BlockNumber(0)
)
// UnmarshalJSON parses the given JSON fragment into a BlockNumber. It supports:
// - "latest", "earliest" or "pending" as string arguments
// - the block number
// Returned errors:
// - an invalid block number error when the given argument isn't a known strings
// - an out of range error when the given block number is either too little or too large
func (bn *BlockNumber) UnmarshalJSON(data []byte) error {
input := strings.TrimSpace(string(data))
if len(input) >= 2 && input[0] == '"' && input[len(input)-1] == '"' {
input = input[1 : len(input)-1]
}
switch input {
case "earliest":
*bn = EarliestBlockNumber
return nil
case "latest":
*bn = LatestBlockNumber
return nil
case "pending":
*bn = PendingBlockNumber
return nil
}
blckNum, err := hexutil.DecodeUint64(input)
if err != nil {
return err
}
if blckNum > math.MaxInt64 {
return fmt.Errorf("block number larger than int64")
}
*bn = BlockNumber(blckNum)
return nil
}
func (bn BlockNumber) Int64() int64 {
return (int64)(bn)
}
type BlockNumberOrHash struct {
BlockNumber *BlockNumber `json:"blockNumber,omitempty"`
BlockHash *common.Hash `json:"blockHash,omitempty"`
RequireCanonical bool `json:"requireCanonical,omitempty"`
}
func (bnh *BlockNumberOrHash) UnmarshalJSON(data []byte) error {
type erased BlockNumberOrHash
e := erased{}
err := json.Unmarshal(data, &e)
if err == nil {
if e.BlockNumber != nil && e.BlockHash != nil {
return fmt.Errorf("cannot specify both BlockHash and BlockNumber, choose one or the other")
}
bnh.BlockNumber = e.BlockNumber
bnh.BlockHash = e.BlockHash
bnh.RequireCanonical = e.RequireCanonical
return nil
}
var input string
err = json.Unmarshal(data, &input)
if err != nil {
return err
}
switch input {
case "earliest":
bn := EarliestBlockNumber
bnh.BlockNumber = &bn
return nil
case "latest":
bn := LatestBlockNumber
bnh.BlockNumber = &bn
return nil
case "pending":
bn := PendingBlockNumber
bnh.BlockNumber = &bn
return nil
default:
if len(input) == 66 {
hash := common.Hash{}
err := hash.UnmarshalText([]byte(input))
if err != nil {
return err
}
bnh.BlockHash = &hash
return nil
} else {
blckNum, err := hexutil.DecodeUint64(input)
if err != nil {
return err
}
if blckNum > math.MaxInt64 {
return fmt.Errorf("blocknumber too high")
}
bn := BlockNumber(blckNum)
bnh.BlockNumber = &bn
return nil
}
}
}
func (bnh *BlockNumberOrHash) Number() (BlockNumber, bool) {
if bnh.BlockNumber != nil {
return *bnh.BlockNumber, true
}
return BlockNumber(0), false
}
func (bnh *BlockNumberOrHash) Hash() (common.Hash, bool) {
if bnh.BlockHash != nil {
return *bnh.BlockHash, true
}
return common.Hash{}, false
}
func BlockNumberOrHashWithNumber(blockNr BlockNumber) BlockNumberOrHash {
return BlockNumberOrHash{
BlockNumber: &blockNr,
BlockHash: nil,
RequireCanonical: false,
}
}
func BlockNumberOrHashWithHash(hash common.Hash, canonical bool) BlockNumberOrHash {
return BlockNumberOrHash{
BlockNumber: nil,
BlockHash: &hash,
RequireCanonical: canonical,
}
}

@ -0,0 +1,125 @@
// Copyright 2015 The go-ethereum Authors
// This file is part of the go-ethereum library.
//
// The go-ethereum library is free software: you can redistribute it and/or modify
// it under the terms of the GNU Lesser General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// The go-ethereum library is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Lesser General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public License
// along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>.
package rpc
import (
"testing"
"github.com/goccy/go-json"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/common/math"
)
func TestBlockNumberJSONUnmarshal(t *testing.T) {
tests := []struct {
input string
mustFail bool
expected BlockNumber
}{
0: {`"0x"`, true, BlockNumber(0)},
1: {`"0x0"`, false, BlockNumber(0)},
2: {`"0X1"`, false, BlockNumber(1)},
3: {`"0x00"`, true, BlockNumber(0)},
4: {`"0x01"`, true, BlockNumber(0)},
5: {`"0x1"`, false, BlockNumber(1)},
6: {`"0x12"`, false, BlockNumber(18)},
7: {`"0x7fffffffffffffff"`, false, BlockNumber(math.MaxInt64)},
8: {`"0x8000000000000000"`, true, BlockNumber(0)},
9: {"0", true, BlockNumber(0)},
10: {`"ff"`, true, BlockNumber(0)},
11: {`"pending"`, false, PendingBlockNumber},
12: {`"latest"`, false, LatestBlockNumber},
13: {`"earliest"`, false, EarliestBlockNumber},
14: {`someString`, true, BlockNumber(0)},
15: {`""`, true, BlockNumber(0)},
16: {``, true, BlockNumber(0)},
}
for i, test := range tests {
var num BlockNumber
err := json.Unmarshal([]byte(test.input), &num)
if test.mustFail && err == nil {
t.Errorf("Test %d should fail", i)
continue
}
if !test.mustFail && err != nil {
t.Errorf("Test %d should pass but got err: %v", i, err)
continue
}
if num != test.expected {
t.Errorf("Test %d got unexpected value, want %d, got %d", i, test.expected, num)
}
}
}
func TestBlockNumberOrHash_UnmarshalJSON(t *testing.T) {
tests := []struct {
input string
mustFail bool
expected BlockNumberOrHash
}{
0: {`"0x"`, true, BlockNumberOrHash{}},
1: {`"0x0"`, false, BlockNumberOrHashWithNumber(0)},
2: {`"0X1"`, false, BlockNumberOrHashWithNumber(1)},
3: {`"0x00"`, true, BlockNumberOrHash{}},
4: {`"0x01"`, true, BlockNumberOrHash{}},
5: {`"0x1"`, false, BlockNumberOrHashWithNumber(1)},
6: {`"0x12"`, false, BlockNumberOrHashWithNumber(18)},
7: {`"0x7fffffffffffffff"`, false, BlockNumberOrHashWithNumber(math.MaxInt64)},
8: {`"0x8000000000000000"`, true, BlockNumberOrHash{}},
9: {"0", true, BlockNumberOrHash{}},
10: {`"ff"`, true, BlockNumberOrHash{}},
11: {`"pending"`, false, BlockNumberOrHashWithNumber(PendingBlockNumber)},
12: {`"latest"`, false, BlockNumberOrHashWithNumber(LatestBlockNumber)},
13: {`"earliest"`, false, BlockNumberOrHashWithNumber(EarliestBlockNumber)},
14: {`someString`, true, BlockNumberOrHash{}},
15: {`""`, true, BlockNumberOrHash{}},
16: {``, true, BlockNumberOrHash{}},
17: {`"0x0000000000000000000000000000000000000000000000000000000000000000"`, false, BlockNumberOrHashWithHash(common.HexToHash("0x0000000000000000000000000000000000000000000000000000000000000000"), false)},
18: {`{"blockHash":"0x0000000000000000000000000000000000000000000000000000000000000000"}`, false, BlockNumberOrHashWithHash(common.HexToHash("0x0000000000000000000000000000000000000000000000000000000000000000"), false)},
19: {`{"blockHash":"0x0000000000000000000000000000000000000000000000000000000000000000","requireCanonical":false}`, false, BlockNumberOrHashWithHash(common.HexToHash("0x0000000000000000000000000000000000000000000000000000000000000000"), false)},
20: {`{"blockHash":"0x0000000000000000000000000000000000000000000000000000000000000000","requireCanonical":true}`, false, BlockNumberOrHashWithHash(common.HexToHash("0x0000000000000000000000000000000000000000000000000000000000000000"), true)},
21: {`{"blockNumber":"0x1"}`, false, BlockNumberOrHashWithNumber(1)},
22: {`{"blockNumber":"pending"}`, false, BlockNumberOrHashWithNumber(PendingBlockNumber)},
23: {`{"blockNumber":"latest"}`, false, BlockNumberOrHashWithNumber(LatestBlockNumber)},
24: {`{"blockNumber":"earliest"}`, false, BlockNumberOrHashWithNumber(EarliestBlockNumber)},
25: {`{"blockNumber":"0x1", "blockHash":"0x0000000000000000000000000000000000000000000000000000000000000000"}`, true, BlockNumberOrHash{}},
}
for i, test := range tests {
var bnh BlockNumberOrHash
err := json.Unmarshal([]byte(test.input), &bnh)
if test.mustFail && err == nil {
t.Errorf("Test %d should fail", i)
continue
}
if !test.mustFail && err != nil {
t.Errorf("Test %d should pass but got err: %v", i, err)
continue
}
hash, hashOk := bnh.Hash()
expectedHash, expectedHashOk := test.expected.Hash()
num, numOk := bnh.Number()
expectedNum, expectedNumOk := test.expected.Number()
if bnh.RequireCanonical != test.expected.RequireCanonical ||
hash != expectedHash || hashOk != expectedHashOk ||
num != expectedNum || numOk != expectedNumOk {
t.Errorf("Test %d got unexpected value, want %v, got %v", i, test.expected, bnh)
}
}
}

@ -0,0 +1,175 @@
// Copyright 2015 The go-ethereum Authors
// This file is part of the go-ethereum library.
//
// The go-ethereum library is free software: you can redistribute it and/or modify
// it under the terms of the GNU Lesser General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// The go-ethereum library is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Lesser General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public License
// along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>.
package rpc
import (
"context"
"encoding/base64"
"fmt"
"net/http"
"net/url"
"os"
"strings"
"sync"
mapset "github.com/deckarep/golang-set"
"github.com/ethereum/go-ethereum/log"
"github.com/gorilla/websocket"
)
const (
wsReadBuffer = 1024
wsWriteBuffer = 1024
)
var wsBufferPool = new(sync.Pool)
// NewWSServer creates a new websocket RPC server around an API provider.
//
// Deprecated: use Server.WebsocketHandler
func NewWSServer(allowedOrigins []string, srv *Server) *http.Server {
return &http.Server{Handler: srv.WebsocketHandler(allowedOrigins)}
}
// WebsocketHandler returns a handler that serves JSON-RPC to WebSocket connections.
//
// allowedOrigins should be a comma-separated list of allowed origin URLs.
// To allow connections with any origin, pass "*".
func (s *Server) WebsocketHandler(allowedOrigins []string) http.Handler {
var upgrader = websocket.Upgrader{
ReadBufferSize: wsReadBuffer,
WriteBufferSize: wsWriteBuffer,
WriteBufferPool: wsBufferPool,
CheckOrigin: wsHandshakeValidator(allowedOrigins),
}
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
conn, err := upgrader.Upgrade(w, r, nil)
if err != nil {
log.Debug("WebSocket upgrade failed", "err", err)
return
}
codec := newWebsocketCodec(conn)
s.ServeCodec(codec, 0)
})
}
// wsHandshakeValidator returns a handler that verifies the origin during the
// websocket upgrade process. When a '*' is specified as an allowed origins all
// connections are accepted.
func wsHandshakeValidator(allowedOrigins []string) func(*http.Request) bool {
origins := mapset.NewSet()
allowAllOrigins := false
for _, origin := range allowedOrigins {
if origin == "*" {
allowAllOrigins = true
}
if origin != "" {
origins.Add(strings.ToLower(origin))
}
}
// allow localhost if no allowedOrigins are specified.
if len(origins.ToSlice()) == 0 {
origins.Add("http://localhost")
if hostname, err := os.Hostname(); err == nil {
origins.Add("http://" + strings.ToLower(hostname))
}
}
log.Debug(fmt.Sprintf("Allowed origin(s) for WS RPC interface %v", origins.ToSlice()))
f := func(req *http.Request) bool {
// Skip origin verification if no Origin header is present. The origin check
// is supposed to protect against browser based attacks. Browsers always set
// Origin. Non-browser software can put anything in origin and checking it doesn't
// provide additional security.
if _, ok := req.Header["Origin"]; !ok {
return true
}
// Verify origin against whitelist.
origin := strings.ToLower(req.Header.Get("Origin"))
if allowAllOrigins || origins.Contains(origin) {
return true
}
log.Warn("Rejected WebSocket connection", "origin", origin)
return false
}
return f
}
type wsHandshakeError struct {
err error
status string
}
func (e wsHandshakeError) Error() string {
s := e.err.Error()
if e.status != "" {
s += " (HTTP status " + e.status + ")"
}
return s
}
// DialWebsocket creates a new RPC client that communicates with a JSON-RPC server
// that is listening on the given endpoint.
//
// The context is used for the initial connection establishment. It does not
// affect subsequent interactions with the client.
func DialWebsocket(ctx context.Context, endpoint, origin string) (*Client, error) {
endpoint, header, err := wsClientHeaders(endpoint, origin)
if err != nil {
return nil, err
}
dialer := websocket.Dialer{
ReadBufferSize: wsReadBuffer,
WriteBufferSize: wsWriteBuffer,
WriteBufferPool: wsBufferPool,
}
return newClient(ctx, func(ctx context.Context) (ServerCodec, error) {
conn, resp, err := dialer.DialContext(ctx, endpoint, header)
if err != nil {
hErr := wsHandshakeError{err: err}
if resp != nil {
hErr.status = resp.Status
}
return nil, hErr
}
return newWebsocketCodec(conn), nil
})
}
func wsClientHeaders(endpoint, origin string) (string, http.Header, error) {
endpointURL, err := url.Parse(endpoint)
if err != nil {
return endpoint, nil, err
}
header := make(http.Header)
if origin != "" {
header.Add("origin", origin)
}
if endpointURL.User != nil {
b64auth := base64.StdEncoding.EncodeToString([]byte(endpointURL.User.String()))
header.Add("authorization", "Basic "+b64auth)
endpointURL.User = nil
}
return endpointURL.String(), header, nil
}
func newWebsocketCodec(conn *websocket.Conn) ServerCodec {
conn.SetReadLimit(maxRequestContentLength)
return NewFuncCodec(conn, conn.WriteJSON, conn.ReadJSON)
}

@ -0,0 +1,259 @@
// Copyright 2018 The go-ethereum Authors
// This file is part of the go-ethereum library.
//
// The go-ethereum library is free software: you can redistribute it and/or modify
// it under the terms of the GNU Lesser General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// The go-ethereum library is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Lesser General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public License
// along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>.
package rpc
import (
"context"
"net"
"net/http"
"net/http/httptest"
"reflect"
"strings"
"testing"
"time"
"github.com/gorilla/websocket"
)
func TestWebsocketClientHeaders(t *testing.T) {
t.Parallel()
endpoint, header, err := wsClientHeaders("wss://testuser:test-PASS_01@example.com:1234", "https://example.com")
if err != nil {
t.Fatalf("wsGetConfig failed: %s", err)
}
if endpoint != "wss://example.com:1234" {
t.Fatal("User should have been stripped from the URL")
}
if header.Get("authorization") != "Basic dGVzdHVzZXI6dGVzdC1QQVNTXzAx" {
t.Fatal("Basic auth header is incorrect")
}
if header.Get("origin") != "https://example.com" {
t.Fatal("Origin not set")
}
}
// This test checks that the server rejects connections from disallowed origins.
func TestWebsocketOriginCheck(t *testing.T) {
t.Parallel()
var (
srv = newTestServer()
httpsrv = httptest.NewServer(srv.WebsocketHandler([]string{"http://example.com"}))
wsURL = "ws:" + strings.TrimPrefix(httpsrv.URL, "http:")
)
defer srv.Stop()
defer httpsrv.Close()
client, err := DialWebsocket(context.Background(), wsURL, "http://ekzample.com")
if err == nil {
client.Close()
t.Fatal("no error for wrong origin")
}
wantErr := wsHandshakeError{websocket.ErrBadHandshake, "403 Forbidden"}
if !reflect.DeepEqual(err, wantErr) {
t.Fatalf("wrong error for wrong origin: %q", err)
}
// Connections without origin header should work.
client, err = DialWebsocket(context.Background(), wsURL, "")
if err != nil {
t.Fatal("error for empty origin")
}
client.Close()
}
// This test checks whether calls exceeding the request size limit are rejected.
func TestWebsocketLargeCall(t *testing.T) {
t.Parallel()
var (
srv = newTestServer()
httpsrv = httptest.NewServer(srv.WebsocketHandler([]string{"*"}))
wsURL = "ws:" + strings.TrimPrefix(httpsrv.URL, "http:")
)
defer srv.Stop()
defer httpsrv.Close()
client, err := DialWebsocket(context.Background(), wsURL, "")
if err != nil {
t.Fatalf("can't dial: %v", err)
}
defer client.Close()
// This call sends slightly less than the limit and should work.
var result echoResult
arg := strings.Repeat("x", maxRequestContentLength-200)
if err := client.Call(&result, "test_echo", arg, 1); err != nil {
t.Fatalf("valid call didn't work: %v", err)
}
if result.String != arg {
t.Fatal("wrong string echoed")
}
// This call sends twice the allowed size and shouldn't work.
arg = strings.Repeat("x", maxRequestContentLength*2)
err = client.Call(&result, "test_echo", arg)
if err == nil {
t.Fatal("no error for too large call")
}
}
// This test checks that client handles WebSocket ping frames correctly.
func TestClientWebsocketPing(t *testing.T) {
t.Parallel()
var (
sendPing = make(chan struct{})
server = wsPingTestServer(t, sendPing)
ctx, cancel = context.WithTimeout(context.Background(), 1*time.Second)
)
defer cancel()
defer server.Shutdown(ctx)
client, err := DialContext(ctx, "ws://"+server.Addr)
if err != nil {
t.Fatalf("client dial error: %v", err)
}
resultChan := make(chan int)
sub, err := client.EthSubscribe(ctx, resultChan, "foo")
if err != nil {
t.Fatalf("client subscribe error: %v", err)
}
// Wait for the context's deadline to be reached before proceeding.
// This is important for reproducing https://github.com/ethereum/go-ethereum/issues/19798
<-ctx.Done()
close(sendPing)
// Wait for the subscription result.
timeout := time.NewTimer(5 * time.Second)
for {
select {
case err := <-sub.Err():
t.Error("client subscription error:", err)
case result := <-resultChan:
t.Log("client got result:", result)
return
case <-timeout.C:
t.Error("didn't get any result within the test timeout")
return
}
}
}
// wsPingTestServer runs a WebSocket server which accepts a single subscription request.
// When a value arrives on sendPing, the server sends a ping frame, waits for a matching
// pong and finally delivers a single subscription result.
func wsPingTestServer(t *testing.T, sendPing <-chan struct{}) *http.Server {
var srv http.Server
shutdown := make(chan struct{})
srv.RegisterOnShutdown(func() {
close(shutdown)
})
srv.Handler = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// Upgrade to WebSocket.
upgrader := websocket.Upgrader{
CheckOrigin: func(r *http.Request) bool { return true },
}
conn, err := upgrader.Upgrade(w, r, nil)
if err != nil {
t.Errorf("server WS upgrade error: %v", err)
return
}
defer conn.Close()
// Handle the connection.
wsPingTestHandler(t, conn, shutdown, sendPing)
})
// Start the server.
listener, err := net.Listen("tcp", "127.0.0.1:0")
if err != nil {
t.Fatal("can't listen:", err)
}
srv.Addr = listener.Addr().String()
go srv.Serve(listener)
return &srv
}
func wsPingTestHandler(t *testing.T, conn *websocket.Conn, shutdown, sendPing <-chan struct{}) {
// Canned responses for the eth_subscribe call in TestClientWebsocketPing.
const (
subResp = `{"jsonrpc":"2.0","id":1,"result":"0x00"}`
subNotify = `{"jsonrpc":"2.0","method":"eth_subscription","params":{"subscription":"0x00","result":1}}`
)
// Handle subscribe request.
if _, _, err := conn.ReadMessage(); err != nil {
t.Errorf("server read error: %v", err)
return
}
if err := conn.WriteMessage(websocket.TextMessage, []byte(subResp)); err != nil {
t.Errorf("server write error: %v", err)
return
}
// Read from the connection to process control messages.
var pongCh = make(chan string)
conn.SetPongHandler(func(d string) error {
t.Logf("server got pong: %q", d)
pongCh <- d
return nil
})
go func() {
for {
typ, msg, err := conn.ReadMessage()
if err != nil {
return
}
t.Logf("server got message (%d): %q", typ, msg)
}
}()
// Write messages.
var (
sendResponse <-chan time.Time
wantPong string
)
for {
select {
case _, open := <-sendPing:
if !open {
sendPing = nil
}
t.Logf("server sending ping")
conn.WriteMessage(websocket.PingMessage, []byte("ping"))
wantPong = "ping"
case data := <-pongCh:
if wantPong == "" {
t.Errorf("unexpected pong")
} else if data != wantPong {
t.Errorf("got pong with wrong data %q", data)
}
wantPong = ""
sendResponse = time.NewTimer(200 * time.Millisecond).C
case <-sendResponse:
t.Logf("server sending response")
conn.WriteMessage(websocket.TextMessage, []byte(subNotify))
sendResponse = nil
case <-shutdown:
conn.Close()
return
}
}
}

@ -17,10 +17,12 @@ require (
github.com/ethereum/go-ethereum v1.9.25
github.com/fjl/memsize v0.0.0-20180929194037-2a09253e352a // indirect
github.com/garslo/gogen v0.0.0-20170307003452-d6ebae628c7c // indirect
github.com/goccy/go-json v0.7.10
github.com/golang/mock v1.4.4
github.com/golang/protobuf v1.4.3
github.com/golangci/golangci-lint v1.22.2
github.com/gorilla/mux v1.8.0
github.com/gorilla/websocket v1.4.2
github.com/harmony-ek/gencodec v0.0.0-20190215044613-e6740dbdd846
github.com/harmony-one/abool v1.0.1
github.com/harmony-one/bls v0.0.6
@ -30,22 +32,22 @@ require (
github.com/hashicorp/golang-lru v0.5.4
github.com/ipfs/go-ds-badger v0.2.4
github.com/json-iterator/go v1.1.10
github.com/libp2p/go-libp2p v0.14.2
github.com/libp2p/go-libp2p-core v0.8.5
github.com/libp2p/go-libp2p v0.14.0
github.com/libp2p/go-libp2p-core v0.8.6
github.com/libp2p/go-libp2p-crypto v0.1.0
github.com/libp2p/go-libp2p-discovery v0.5.0
github.com/libp2p/go-libp2p-kad-dht v0.12.2
github.com/libp2p/go-libp2p-pubsub v0.4.1
github.com/multiformats/go-multiaddr v0.3.1
github.com/libp2p/go-libp2p-kad-dht v0.11.1
github.com/libp2p/go-libp2p-pubsub v0.4.0
github.com/multiformats/go-multiaddr v0.3.3
github.com/multiformats/go-multiaddr-dns v0.3.1
github.com/natefinch/lumberjack v2.0.0+incompatible
github.com/pborman/uuid v1.2.0
github.com/pelletier/go-toml v1.2.0
github.com/pelletier/go-toml v1.9.3
github.com/pkg/errors v0.9.1
github.com/prometheus/client_golang v1.8.0
github.com/prometheus/client_golang v1.10.0
github.com/rcrowley/go-metrics v0.0.0-20200313005456-10cdbea86bc0
github.com/rjeczalik/notify v0.9.2
github.com/rs/cors v1.7.0 // indirect
github.com/rs/cors v1.7.0
github.com/rs/zerolog v1.18.0
github.com/spf13/cobra v0.0.5
github.com/spf13/pflag v1.0.5
@ -55,10 +57,10 @@ require (
go.uber.org/ratelimit v0.1.0
go.uber.org/zap v1.16.0
golang.org/x/crypto v0.0.0-20210322153248-0c34fe9e7dc2
golang.org/x/lint v0.0.0-20200302205851-738671d3881b
golang.org/x/lint v0.0.0-20210508222113-6edffad5e616
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c
golang.org/x/time v0.0.0-20210220033141-f8bda1e9f3ba
golang.org/x/tools v0.0.0-20210106214847-113979e3529a
golang.org/x/tools v0.1.7
google.golang.org/grpc v1.33.2
google.golang.org/protobuf v1.25.0
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c

@ -0,0 +1,225 @@
// Copyright 2015 The go-ethereum Authors
// This file is part of the go-ethereum library.
//
// The go-ethereum library is free software: you can redistribute it and/or modify
// it under the terms of the GNU Lesser General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// The go-ethereum library is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Lesser General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public License
// along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>.
package hmy
import (
"context"
"fmt"
"math/big"
"sort"
"sync"
"github.com/ethereum/go-ethereum/rpc"
"github.com/harmony-one/harmony/block"
"github.com/harmony-one/harmony/internal/utils"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/params"
"github.com/harmony-one/harmony/core/types"
)
const sampleNumber = 3 // Number of transactions sampled in a block
var DefaultMaxPrice = big.NewInt(1 * params.Ether)
type GasPriceConfig struct {
Blocks int
Percentile int
Default *big.Int `toml:",omitempty"`
MaxPrice *big.Int `toml:",omitempty"`
}
// OracleBackend includes all necessary background APIs for oracle.
type OracleBackend interface {
HeaderByNumber(ctx context.Context, number rpc.BlockNumber) (*block.Header, error)
BlockByNumber(ctx context.Context, number rpc.BlockNumber) (*types.Block, error)
ChainConfig() *params.ChainConfig
}
// Oracle recommends gas prices based on the content of recent
// blocks. Suitable for both light and full clients.
type Oracle struct {
backend *Harmony
lastHead common.Hash
lastPrice *big.Int
maxPrice *big.Int
cacheLock sync.RWMutex
fetchLock sync.Mutex
checkBlocks int
percentile int
}
// NewOracle returns a new gasprice oracle which can recommend suitable
// gasprice for newly created transaction.
func NewOracle(backend *Harmony, params GasPriceConfig) *Oracle {
blocks := params.Blocks
if blocks < 1 {
blocks = 1
utils.Logger().Warn().Msg(fmt.Sprint("Sanitizing invalid gasprice oracle sample blocks", "provided", params.Blocks, "updated", blocks))
}
percent := params.Percentile
if percent < 0 {
percent = 0
utils.Logger().Warn().Msg(fmt.Sprint("Sanitizing invalid gasprice oracle sample percentile", "provided", params.Percentile, "updated", percent))
}
if percent > 100 {
percent = 100
utils.Logger().Warn().Msg(fmt.Sprint("Sanitizing invalid gasprice oracle sample percentile", "provided", params.Percentile, "updated", percent))
}
maxPrice := params.MaxPrice
if maxPrice == nil || maxPrice.Int64() <= 0 {
maxPrice = DefaultMaxPrice
utils.Logger().Warn().Msg(fmt.Sprint("Sanitizing invalid gasprice oracle price cap", "provided", params.MaxPrice, "updated", maxPrice))
}
return &Oracle{
backend: backend,
lastPrice: params.Default,
maxPrice: maxPrice,
checkBlocks: blocks,
percentile: percent,
}
}
// SuggestPrice returns a gasprice so that newly created transaction can
// have a very high chance to be included in the following blocks.
func (gpo *Oracle) SuggestPrice(ctx context.Context) (*big.Int, error) {
head, _ := gpo.backend.HeaderByNumber(ctx, rpc.LatestBlockNumber)
headHash := head.Hash()
// If the latest gasprice is still available, return it.
gpo.cacheLock.RLock()
lastHead, lastPrice := gpo.lastHead, gpo.lastPrice
gpo.cacheLock.RUnlock()
if headHash == lastHead {
return lastPrice, nil
}
gpo.fetchLock.Lock()
defer gpo.fetchLock.Unlock()
// Try checking the cache again, maybe the last fetch fetched what we need
gpo.cacheLock.RLock()
lastHead, lastPrice = gpo.lastHead, gpo.lastPrice
gpo.cacheLock.RUnlock()
if headHash == lastHead {
return lastPrice, nil
}
var (
sent, exp int
number = head.Number().Uint64()
result = make(chan getBlockPricesResult, gpo.checkBlocks)
quit = make(chan struct{})
txPrices []*big.Int
)
for sent < gpo.checkBlocks && number > 0 {
go gpo.getBlockPrices(ctx, types.MakeSigner(gpo.backend.ChainConfig(), big.NewInt(int64(number))), number, sampleNumber, result, quit)
sent++
exp++
number--
}
for exp > 0 {
res := <-result
if res.err != nil {
close(quit)
return lastPrice, res.err
}
exp--
// Nothing returned. There are two special cases here:
// - The block is empty
// - All the transactions included are sent by the miner itself.
// In these cases, use the latest calculated price for samping.
if len(res.prices) == 0 {
res.prices = []*big.Int{lastPrice}
}
// Besides, in order to collect enough data for sampling, if nothing
// meaningful returned, try to query more blocks. But the maximum
// is 2*checkBlocks.
if len(res.prices) == 1 && len(txPrices)+1+exp < gpo.checkBlocks*2 && number > 0 {
go gpo.getBlockPrices(ctx, types.MakeSigner(gpo.backend.ChainConfig(), big.NewInt(int64(number))), number, sampleNumber, result, quit)
sent++
exp++
number--
}
txPrices = append(txPrices, res.prices...)
}
price := lastPrice
if len(txPrices) > 0 {
sort.Sort(bigIntArray(txPrices))
price = txPrices[(len(txPrices)-1)*gpo.percentile/100]
}
if price.Cmp(gpo.maxPrice) > 0 {
price = new(big.Int).Set(gpo.maxPrice)
}
gpo.cacheLock.Lock()
gpo.lastHead = headHash
gpo.lastPrice = price
gpo.cacheLock.Unlock()
return price, nil
}
type getBlockPricesResult struct {
prices []*big.Int
err error
}
type transactionsByGasPrice []*types.Transaction
func (t transactionsByGasPrice) Len() int { return len(t) }
func (t transactionsByGasPrice) Swap(i, j int) { t[i], t[j] = t[j], t[i] }
func (t transactionsByGasPrice) Less(i, j int) bool {
return t[i].GasPrice().Cmp(t[j].GasPrice()) < 0
}
// getBlockPrices calculates the lowest transaction gas price in a given block
// and sends it to the result channel. If the block is empty or all transactions
// are sent by the miner itself(it doesn't make any sense to include this kind of
// transaction prices for sampling), nil gasprice is returned.
func (gpo *Oracle) getBlockPrices(ctx context.Context, signer types.Signer, blockNum uint64, limit int, result chan getBlockPricesResult, quit chan struct{}) {
block, err := gpo.backend.BlockByNumber(ctx, rpc.BlockNumber(blockNum))
if block == nil {
select {
case result <- getBlockPricesResult{nil, err}:
case <-quit:
}
return
}
blockTxs := block.Transactions()
txs := make([]*types.Transaction, len(blockTxs))
copy(txs, blockTxs)
sort.Sort(transactionsByGasPrice(txs))
var prices []*big.Int
for _, tx := range txs {
sender, err := types.Sender(signer, tx)
if err == nil && sender != block.Coinbase() {
prices = append(prices, tx.GasPrice())
if len(prices) >= limit {
break
}
}
}
select {
case result <- getBlockPricesResult{prices, nil}:
case <-quit:
}
}
type bigIntArray []*big.Int
func (s bigIntArray) Len() int { return len(s) }
func (s bigIntArray) Less(i, j int) bool { return s[i].Cmp(s[j]) < 0 }
func (s bigIntArray) Swap(i, j int) { s[i], s[j] = s[j], s[i] }

@ -62,6 +62,9 @@ type Harmony struct {
RPCGasCap *big.Int `toml:",omitempty"`
ShardID uint32
// Gas price suggestion oracle
gpo *Oracle
// Internals
eventMux *event.TypeMux
chainDb ethdb.Database // Block chain database
@ -112,6 +115,7 @@ type NodeAPI interface {
GetConsensusCurViewID() uint64
GetConfig() commonRPC.Config
ShutDown()
GetLastSigningPower() (float64, error)
}
// New creates a new Harmony object (including the
@ -126,7 +130,8 @@ func New(
totalStakeCache := newTotalStakeCache(totalStakeCacheDuration)
bloomIndexer := NewBloomIndexer(chainDb, params.BloomBitsBlocks, params.BloomConfirms)
bloomIndexer.Start(nodeAPI.Blockchain())
return &Harmony{
backend := &Harmony{
ShutdownChan: make(chan bool),
BloomRequests: make(chan chan *bloombits.Retrieval),
BloomIndexer: bloomIndexer,
@ -145,6 +150,17 @@ func New(
undelegationPayoutsCache: undelegationPayoutsCache,
preStakingBlockRewardsCache: preStakingBlockRewardsCache,
}
// Setup gas price oracle
gpoParams := GasPriceConfig{
Blocks: 20,
Percentile: 60,
Default: big.NewInt(1e10),
}
gpo := NewOracle(backend, gpoParams)
backend.gpo = gpo
return backend
}
// SingleFlightRequest ..

@ -2,6 +2,7 @@ package hmy
import (
"context"
"math/big"
"github.com/ethereum/go-ethereum/common"
"github.com/harmony-one/harmony/core/types"
@ -46,3 +47,7 @@ func (hmy *Harmony) GetPoolTransactions() (types.PoolTransactions, error) {
}
return txs, nil
}
func (hmy *Harmony) SuggestPrice(ctx context.Context) (*big.Int, error) {
return hmy.gpo.SuggestPrice(ctx)
}

@ -357,7 +357,7 @@ func (hmy *Harmony) GetValidatorInformation(
// b.apiCache.Forget(prevKey)
// calculate last APRHistoryLength epochs for averaging APR
// epochFrom := bc.Config().StakingEpoch
// epochFrom := bc.GasPriceConfig().StakingEpoch
// nowMinus := big.NewInt(0).Sub(now, big.NewInt(staking.APRHistoryLength))
// if nowMinus.Cmp(epochFrom) > 0 {
// epochFrom = nowMinus

@ -280,7 +280,7 @@ func (hmy *Harmony) TraceChain(ctx context.Context, start, end *types.Block, con
traced += uint64(len(txs))
}
// Generate the next state snapshot fast without tracing
_, _, _, _, _, err := hmy.BlockChain.Processor().Process(block, statedb, vm.Config{})
_, _, _, _, _, _, err := hmy.BlockChain.Processor().Process(block, statedb, vm.Config{}, false)
if err != nil {
failed = err
break
@ -674,7 +674,7 @@ func (hmy *Harmony) ComputeStateDB(block *types.Block, reexec uint64) (*state.DB
if block = hmy.BlockChain.GetBlockByNumber(block.NumberU64() + 1); block == nil {
return nil, fmt.Errorf("block #%d not found", block.NumberU64()+1)
}
_, _, _, _, _, err := hmy.BlockChain.Processor().Process(block, statedb, vm.Config{})
_, _, _, _, _, _, err := hmy.BlockChain.Processor().Process(block, statedb, vm.Config{}, false)
if err != nil {
return nil, fmt.Errorf("processing block %d failed: %v", block.NumberU64(), err)
}

Some files were not shown because too many files have changed in this diff Show More

Loading…
Cancel
Save