diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..30368ef --- /dev/null +++ b/Dockerfile @@ -0,0 +1,73 @@ +ARG GO_VERSION=alpine +ARG BASE=golang:alpine + +FROM ${BASE} as builder + +RUN apk update \ + && apk add --no-cache \ + linux-headers \ + gcc \ + libtool \ + openssl-dev \ + libffi \ + tini \ + git \ + 'su-exec>=0.2' \ + && apk add --no-cache --virtual .build_deps build-base libffi-dev + +# add our user and group first to make sure their IDs get assigned consistently, regardless of whatever dependencies get added +RUN addgroup -g 502 -S bloxroute \ + && adduser -u 502 -S -G bloxroute bloxroute \ + && mkdir -p /app/bloxroute/logs \ + && chown -R bloxroute:bloxroute /app/bloxroute + +# Move to working directory +WORKDIR /app/bloxroute + +# Download dependency using go mod +COPY go.mod . +COPY go.sum . +RUN go mod download +COPY --chown=bloxroute:bloxroute Makefile . +RUN make third_party_utils + +COPY --chown=bloxroute:bloxroute . . +RUN make gateway +RUN chown bloxroute:bloxroute ./bin/gateway +RUN chown bloxroute:bloxroute ./bin/bxcli + +FROM golang:${GO_VERSION} + +RUN apk update \ + && apk add --no-cache \ + linux-headers \ + gcc \ + libtool \ + openssl-dev \ + libffi \ + tini \ + git \ + 'su-exec>=0.2' \ + && apk add --no-cache --virtual .build_deps build-base libffi-dev + +# add our user and group first to make sure their IDs get assigned consistently, regardless of whatever dependencies get added +RUN addgroup -g 502 -S bloxroute \ + && adduser -u 502 -S -G bloxroute bloxroute \ + && mkdir -p /app/bloxroute/logs \ + && chown -R bloxroute:bloxroute /app/bloxroute + +# Move to working directory +WORKDIR /app/bloxroute +RUN chmod +s /bin/ping +RUN chmod +s /bin/busybox + +COPY --from=builder /app/bloxroute/bin/bxcli /app/bloxroute/bin/bxcli +COPY --from=builder /app/bloxroute/bin/gateway /app/bloxroute/bin/gateway + +COPY docker-entrypoint.sh /usr/local/bin/ + +ENV PATH="/app/bloxroute/bin:${PATH}" + +EXPOSE 1801 5001 + +ENTRYPOINT ["/sbin/tini", "--", "/bin/sh", "/usr/local/bin/docker-entrypoint.sh"] diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..160e941 --- /dev/null +++ b/Makefile @@ -0,0 +1,109 @@ +MODULE = $(shell env GO111MODULE=on $(GO) list -m) +DATE ?= $(shell date +%FT%T%z) +VERSION ?= $(shell git describe --tags --always --dirty --match=v2* 2> /dev/null || \ + cat $(CURDIR)/.version 2> /dev/null || echo v2.1.1.2) +PKGS = $(or $(PKG),$(shell env GO111MODULE=on $(GO) list ./...)) +TESTPKGS = $(shell env GO111MODULE=on $(GO) list -f \ + '{{ if or .TestGoFiles .XTestGoFiles }}{{ .ImportPath }}{{ end }}' \ + $(PKGS)) +BIN = $(CURDIR)/bin + +GO = go +TIMEOUT = 15 +V = 0 +Q = $(if $(filter 1,$V),,@) +M = $(shell printf "\033[34;1mâ–¶\033[0m") + +export GO111MODULE=on + +.PHONY: all +all: gateway +gateway: third_party_utils ; $(info $(M) building gateway executable) @ ## Build program binary + $Q $(GO) build \ + -tags release \ + -ldflags '-X $(MODULE)/version.BuildVersion=$(VERSION) -X $(MODULE)/version.BuildDate=$(DATE)' \ + -o $(BIN) ./cmd/... + +# Tools +third_party_utils: $(BIN)/golint + +$(BIN): + @mkdir -p $@ +$(BIN)/%: | $(BIN) ; $(info $(M) building $(PACKAGE)) + $Q tmp=$$(mktemp -d); \ + env GO111MODULE=off GOPATH=$$tmp GOBIN=$(BIN) $(GO) get $(PACKAGE) \ + || ret=$$?; \ + rm -rf $$tmp ; exit $$ret + +GOLINT = $(BIN)/golint +$(BIN)/golint: PACKAGE=golang.org/x/lint/golint + +GOCOV = $(BIN)/gocov +$(BIN)/gocov: PACKAGE=github.com/axw/gocov/... + +GOCOVXML = $(BIN)/gocov-xml +$(BIN)/gocov-xml: PACKAGE=github.com/AlekSi/gocov-xml + +GO2XUNIT = $(BIN)/go2xunit +$(BIN)/go2xunit: PACKAGE=github.com/tebeka/go2xunit + +# Tests + +TEST_TARGETS := test-default test-bench test-short test-verbose test-race test-integration +.PHONY: $(TEST_TARGETS) test-xml check test tests +test-bench: ARGS=-run=__absolutelynothing__ -bench=. ## Run benchmarks +test-short: ARGS=-short ## Run only short tests +test-verbose: ARGS=-v ## Run tests in verbose mode with coverage reporting +test-race: ARGS=-race ## Run tests with race detector +test-integration: ARGS=-tags="integration" ./... +$(TEST_TARGETS): NAME=$(MAKECMDGOALS:test-%=%) +$(TEST_TARGETS): test +check test tests: fmt lint ; $(info $(M) running $(NAME:%=% )tests) @ ## Run tests + $Q $(GO) test -timeout $(TIMEOUT)s $(ARGS) $(TESTPKGS) + +test-xml: fmt lint | $(GO2XUNIT) ; $(info $(M) running xUnit tests) @ ## Run tests with xUnit output + $Q mkdir -p test + $Q 2>&1 $(GO) test -timeout $(TIMEOUT)s -v $(TESTPKGS) | tee test/tests.output + $(GO2XUNIT) -fail -input test/tests.output -output test/tests.xml + +COVERAGE_MODE = atomic +COVERAGE_PROFILE = $(COVERAGE_DIR)/profile.out +COVERAGE_XML = $(COVERAGE_DIR)/coverage.xml +COVERAGE_HTML = $(COVERAGE_DIR)/index.html +.PHONY: test-coverage test-coverage-tools +test-coverage-tools: | $(GOCOV) $(GOCOVXML) +test-coverage: COVERAGE_DIR := $(CURDIR)/test/coverage.$(shell date -u +"%Y-%m-%dT%H:%M:%SZ") +test-coverage: fmt lint test-coverage-tools ; $(info $(M) running coverage tests) @ ## Run coverage tests + $Q mkdir -p $(COVERAGE_DIR) + $Q $(GO) test \ + -coverpkg=$$($(GO) list -f '{{ join .Deps "\n" }}' $(TESTPKGS) | \ + grep '^$(MODULE)/' | \ + tr '\n' ',' | sed 's/,$$//') \ + -covermode=$(COVERAGE_MODE) \ + -coverprofile="$(COVERAGE_PROFILE)" $(TESTPKGS) + $Q $(GO) tool cover -html=$(COVERAGE_PROFILE) -o $(COVERAGE_HTML) + $Q $(GOCOV) convert $(COVERAGE_PROFILE) | $(GOCOVXML) > $(COVERAGE_XML) + +.PHONY: lint +lint: | $(GOLINT) ; $(info $(M) running golint) @ ## Run golint + $Q $(GOLINT) -set_exit_status $(PKGS) + +.PHONY: fmt +fmt: ; $(info $(M) running gofmt) @ ## Run gofmt on all source files + $Q $(GO) fmt $(PKGS) + +# Misc + +.PHONY: clean +clean: ; $(info $(M) cleaning) @ ## Cleanup everything + @rm -rf $(BIN) + @rm -rf test/tests.* test/coverage.* + +.PHONY: help +help: + @grep -hE '^[ a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | \ + awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-17s\033[0m %s\n", $$1, $$2}' + +.PHONY: version +version: + @echo $(VERSION) diff --git a/README.md b/README.md index a43d211..028e217 100644 --- a/README.md +++ b/README.md @@ -1 +1 @@ -# gateway +# gateway \ No newline at end of file diff --git a/bin/bxcli b/bin/bxcli new file mode 100755 index 0000000..b32bfe4 Binary files /dev/null and b/bin/bxcli differ diff --git a/bin/gateway b/bin/gateway new file mode 100755 index 0000000..f612bb0 Binary files /dev/null and b/bin/gateway differ diff --git a/bin/golint b/bin/golint new file mode 100755 index 0000000..d2986cb Binary files /dev/null and b/bin/golint differ diff --git a/blockchain/bridge.go b/blockchain/bridge.go new file mode 100644 index 0000000..c6b8ffb --- /dev/null +++ b/blockchain/bridge.go @@ -0,0 +1,228 @@ +package blockchain + +import ( + "errors" + "github.com/bloXroute-Labs/gateway/blockchain/network" + "github.com/bloXroute-Labs/gateway/types" +) + +// NoActiveBlockchainPeersAlert is used to send an alert to the gateway on initial liveliness check if no active blockchain peers +type NoActiveBlockchainPeersAlert struct { +} + +// TransactionAnnouncement represents an available transaction from a given peer that can be requested +type TransactionAnnouncement struct { + Hashes types.SHA256HashList + PeerID string +} + +// TransactionsFromNode is used to pass transactions from a node to the BDN +type TransactionsFromNode struct { + Transactions []*types.BxTransaction + PeerEndpoint types.NodeEndpoint +} + +// BlockFromNode is used to pass blocks from a node to the BDN +type BlockFromNode struct { + Block *types.BxBlock + PeerEndpoint types.NodeEndpoint +} + +// BlockAnnouncement represents an available block from a given peer that can be requested +type BlockAnnouncement struct { + Hash types.SHA256Hash + PeerID string + PeerEndpoint types.NodeEndpoint +} + +// Converter defines an interface for converting between blockchain and BDN transactions +type Converter interface { + TransactionBlockchainToBDN(interface{}) (*types.BxTransaction, error) + TransactionBDNToBlockchain(*types.BxTransaction) (interface{}, error) + BlockBlockchainToBDN(interface{}) (*types.BxBlock, error) + BlockBDNtoBlockchain(block *types.BxBlock) (interface{}, error) + BxBlockToCanonicFormat(*types.BxBlock) (*types.BlockNotification, error) +} + +// constants for transaction channel buffer sizes +const ( + transactionBacklog = 500 + transactionHashesBacklog = 1000 + blockBacklog = 100 +) + +// Bridge represents the application interface over which messages are passed between the blockchain node and the BDN +type Bridge interface { + Converter + + ReceiveNetworkConfigUpdates() <-chan network.EthConfig + UpdateNetworkConfig(network.EthConfig) error + + AnnounceTransactionHashes(string, types.SHA256HashList) error + SendTransactionsFromBDN([]*types.BxTransaction) error + SendTransactionsToBDN(txs []*types.BxTransaction, peerEndpoint types.NodeEndpoint) error + RequestTransactionsFromNode(string, types.SHA256HashList) error + + ReceiveNodeTransactions() <-chan TransactionsFromNode + ReceiveBDNTransactions() <-chan []*types.BxTransaction + ReceiveTransactionHashesAnnouncement() <-chan TransactionAnnouncement + ReceiveTransactionHashesRequest() <-chan TransactionAnnouncement + + SendBlockToBDN(*types.BxBlock, types.NodeEndpoint) error + SendBlockToNode(*types.BxBlock) error + + ReceiveBlockFromBDN() <-chan *types.BxBlock + ReceiveBlockFromNode() <-chan BlockFromNode + + ReceiveNoActiveBlockchainPeersAlert() <-chan NoActiveBlockchainPeersAlert + SendNoActiveBlockchainPeersAlert() error +} + +// ErrChannelFull is a special error for identifying overflowing channel buffers +var ErrChannelFull = errors.New("channel full") + +// BxBridge is a channel based implementation of the Bridge interface +type BxBridge struct { + Converter + config chan network.EthConfig + transactionsFromNode chan TransactionsFromNode + transactionsFromBDN chan []*types.BxTransaction + transactionHashesFromNode chan TransactionAnnouncement + transactionHashesRequests chan TransactionAnnouncement + + blocksFromNode chan BlockFromNode + blocksFromBDN chan *types.BxBlock + + noActiveBlockchainPeers chan NoActiveBlockchainPeersAlert +} + +// NewBxBridge returns a BxBridge instance +func NewBxBridge(converter Converter) Bridge { + return &BxBridge{ + config: make(chan network.EthConfig), + transactionsFromNode: make(chan TransactionsFromNode, transactionBacklog), + transactionsFromBDN: make(chan []*types.BxTransaction, transactionBacklog), + transactionHashesFromNode: make(chan TransactionAnnouncement, transactionHashesBacklog), + transactionHashesRequests: make(chan TransactionAnnouncement, transactionHashesBacklog), + blocksFromNode: make(chan BlockFromNode, blockBacklog), + blocksFromBDN: make(chan *types.BxBlock, blockBacklog), + noActiveBlockchainPeers: make(chan NoActiveBlockchainPeersAlert), + Converter: converter, + } +} + +// ReceiveNetworkConfigUpdates provides a channel with network config updates +func (b *BxBridge) ReceiveNetworkConfigUpdates() <-chan network.EthConfig { + return b.config +} + +// UpdateNetworkConfig pushes a new Ethereum configuration update +func (b *BxBridge) UpdateNetworkConfig(config network.EthConfig) error { + b.config <- config + return nil +} + +// AnnounceTransactionHashes pushes a series of transaction announcements onto the announcements channel +func (b BxBridge) AnnounceTransactionHashes(peerID string, hashes types.SHA256HashList) error { + select { + case b.transactionHashesFromNode <- TransactionAnnouncement{Hashes: hashes, PeerID: peerID}: + return nil + default: + return ErrChannelFull + } +} + +// RequestTransactionsFromNode requests a series of transactions that a peer node has announced +func (b BxBridge) RequestTransactionsFromNode(peerID string, hashes types.SHA256HashList) error { + select { + case b.transactionHashesRequests <- TransactionAnnouncement{Hashes: hashes, PeerID: peerID}: + return nil + default: + return ErrChannelFull + } +} + +// SendTransactionsFromBDN sends a set of transactions from the BDN for distribution to nodes +func (b BxBridge) SendTransactionsFromBDN(transactions []*types.BxTransaction) error { + select { + case b.transactionsFromBDN <- transactions: + return nil + default: + return ErrChannelFull + } +} + +// SendTransactionsToBDN sends a set of transactions from a node to the BDN for propagation +func (b BxBridge) SendTransactionsToBDN(txs []*types.BxTransaction, peerEndpoint types.NodeEndpoint) error { + select { + case b.transactionsFromNode <- TransactionsFromNode{Transactions: txs, PeerEndpoint: peerEndpoint}: + return nil + default: + return ErrChannelFull + } +} + +// ReceiveNodeTransactions provides a channel that pushes transactions as they come in from nodes +func (b BxBridge) ReceiveNodeTransactions() <-chan TransactionsFromNode { + return b.transactionsFromNode +} + +// ReceiveBDNTransactions provides a channel that pushes transactions as they arrive from the BDN +func (b BxBridge) ReceiveBDNTransactions() <-chan []*types.BxTransaction { + return b.transactionsFromBDN +} + +// ReceiveTransactionHashesAnnouncement provides a channel that pushes announcements as nodes announce them +func (b BxBridge) ReceiveTransactionHashesAnnouncement() <-chan TransactionAnnouncement { + return b.transactionHashesFromNode +} + +// ReceiveTransactionHashesRequest provides a channel that pushes requests for transaction hashes from the BDN +func (b BxBridge) ReceiveTransactionHashesRequest() <-chan TransactionAnnouncement { + return b.transactionHashesRequests +} + +// SendBlockToBDN sends a block from a node to the BDN +func (b BxBridge) SendBlockToBDN(block *types.BxBlock, peerEndpoint types.NodeEndpoint) error { + select { + case b.blocksFromNode <- BlockFromNode{Block: block, PeerEndpoint: peerEndpoint}: + return nil + default: + return ErrChannelFull + } +} + +// SendBlockToNode sends a block from the BDN for distribution to nodes +func (b BxBridge) SendBlockToNode(block *types.BxBlock) error { + select { + case b.blocksFromBDN <- block: + return nil + default: + return ErrChannelFull + } +} + +// ReceiveBlockFromNode provides a channel that pushes blocks as they come in from nodes +func (b BxBridge) ReceiveBlockFromNode() <-chan BlockFromNode { + return b.blocksFromNode +} + +// ReceiveBlockFromBDN provides a channel that pushes new blocks from the BDN +func (b BxBridge) ReceiveBlockFromBDN() <-chan *types.BxBlock { + return b.blocksFromBDN +} + +// SendNoActiveBlockchainPeersAlert sends alerts to the BDN when there is no active blockchain peer +func (b BxBridge) SendNoActiveBlockchainPeersAlert() error { + select { + case b.noActiveBlockchainPeers <- NoActiveBlockchainPeersAlert{}: + return nil + default: + return ErrChannelFull + } +} + +// ReceiveNoActiveBlockchainPeersAlert provides a channel that pushes no active blockchain peer alerts +func (b BxBridge) ReceiveNoActiveBlockchainPeersAlert() <-chan NoActiveBlockchainPeersAlert { + return b.noActiveBlockchainPeers +} diff --git a/blockchain/eth/backend.go b/blockchain/eth/backend.go new file mode 100644 index 0000000..5bcb96f --- /dev/null +++ b/blockchain/eth/backend.go @@ -0,0 +1,656 @@ +package eth + +import ( + "context" + "errors" + "fmt" + "github.com/bloXroute-Labs/gateway" + "github.com/bloXroute-Labs/gateway/blockchain" + "github.com/bloXroute-Labs/gateway/blockchain/network" + "github.com/bloXroute-Labs/gateway/types" + ethcommon "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/common/hexutil" + ethtypes "github.com/ethereum/go-ethereum/core/types" + "github.com/ethereum/go-ethereum/eth/protocols/eth" + "github.com/ethereum/go-ethereum/p2p" + "github.com/ethereum/go-ethereum/rlp" + "github.com/ethereum/go-ethereum/rpc" + log "github.com/sirupsen/logrus" + "math/big" + "time" +) + +const ( + checkpointTimeout = 5 * time.Second + maxFutureBlockNumber = 100 +) + +// Backend represents the interface to which any stateful message handling (e.g. looking up tx pool items or block headers) will be passed to for processing +type Backend interface { + NetworkConfig() *network.EthConfig + RunPeer(peer *Peer, handler func(*Peer) error) error + Handle(peer *Peer, packet eth.Packet) error + GetHeaders(start eth.HashOrNumber, count int, skip int, reverse bool) ([]*ethtypes.Header, error) + GetBodies(hashes []ethcommon.Hash) ([]*ethtypes.Body, error) +} + +// Handler is the Ethereum backend implementation. It passes transactions and blocks to the BDN bridge and tracks received blocks and transactions from peers. +type Handler struct { + bridge blockchain.Bridge + peers *peerSet + cancel context.CancelFunc + config *network.EthConfig + + chain *Chain + activeWebsocket bool +} + +// NewHandler returns a new Handler and starts its processing go routines +func NewHandler(parent context.Context, bridge blockchain.Bridge, config *network.EthConfig, wsProvider blockchain.WSProvider) *Handler { + ctx, cancel := context.WithCancel(parent) + h := &Handler{ + bridge: bridge, + peers: newPeerSet(), + cancel: cancel, + config: config, + chain: NewChain(ctx), + } + go h.checkInitialBlockchainLiveliness(100 * time.Second) + go h.handleBDNBridge(ctx) + if wsProvider != nil { + go h.runEthSub(wsProvider) + } + return h +} + +func (h *Handler) handleFeeds(ws blockchain.WSProvider, newHeadsRespCh chan *ethtypes.Header, nptResponseCh chan ethcommon.Hash, nhSub *rpc.ClientSubscription, nptSub *rpc.ClientSubscription) { + activeFeeds := true + activeFeedsCheckTicker := time.NewTicker(time.Second * 10) + defer activeFeedsCheckTicker.Stop() + for { + select { + case err := <-nhSub.Err(): + ws.Log().Errorf("failed to get notification from newHeads: %v", err) + ws.UpdateNodeSyncStatus(blockchain.Unsynced) + return + case newHeader := <-newHeadsRespCh: + ws.Log().Tracef("received header for block %v (height %v)", newHeader.Hash(), newHeader.Number) + h.confirmBlockForAll(newHeader.Hash(), newHeader.Number) + case err := <-nptSub.Err(): + ws.Log().Errorf("failed to get notification from newPendingTransactions: %v", err) + ws.UpdateNodeSyncStatus(blockchain.Unsynced) + return + case newPendingTx := <-nptResponseCh: + activeFeeds = true + txHash, err := types.NewSHA256Hash(newPendingTx[:]) + ws.Log().Tracef("received new pending tx %v", txHash) + hashes := types.SHA256HashList{txHash} + err = h.bridge.AnnounceTransactionHashes(bxgateway.WSConnectionID, hashes) + if err != nil { + ws.Log().Errorf("failed to send transaction %v to the gateway: %v", txHash, err) + } + case <-activeFeedsCheckTicker.C: + if !activeFeeds { + ws.UpdateNodeSyncStatus(blockchain.Unsynced) + } else { + ws.UpdateNodeSyncStatus(blockchain.Synced) + } + activeFeeds = false + } + } +} + +func (h *Handler) runEthSub(ws blockchain.WSProvider) { + newHeadsRespCh := make(chan *ethtypes.Header) + nptResponseCh := make(chan ethcommon.Hash) + + for { + nhSub, err := ws.Subscribe(newHeadsRespCh, "newHeads") + if err != nil { + log.Error(err) + ws.Close() + ws.Connect() + continue + } + + nptSub, err := ws.Subscribe(nptResponseCh, "newPendingTransactions") + if err != nil { + log.Error(err) + ws.Close() + ws.Connect() + continue + } + + h.activeWebsocket = true + + // process feeds + h.handleFeeds(ws, newHeadsRespCh, nptResponseCh, nhSub.Sub.(*rpc.ClientSubscription), nptSub.Sub.(*rpc.ClientSubscription)) + + // force all peers to manually request block confirmations while disconnected + for _, peer := range h.peers.getAll() { + peer.RequestConfirmations = true + } + h.activeWebsocket = false + + // closing connection and trying reconnection + ws.Close() + ws.Connect() + } +} + +// Stop shutdowns down all the handler goroutines +func (h *Handler) Stop() { + h.cancel() +} + +// NetworkConfig returns the backend's network configuration +func (h *Handler) NetworkConfig() *network.EthConfig { + return h.config +} + +// RunPeer registers a peer within the peer set and starts handling all its messages +func (h *Handler) RunPeer(ep *Peer, handler func(*Peer) error) error { + ep.RequestConfirmations = !h.activeWebsocket + if err := h.peers.register(ep); err != nil { + return err + } + defer func() { + ep.Stop() + _ = h.peers.unregister(ep.ID()) + }() + + ep.Start() + time.AfterFunc(checkpointTimeout, func() { + ep.checkpointPassed = true + }) + return handler(ep) +} + +func (h *Handler) handleBDNBridge(ctx context.Context) { + for { + select { + case bdnTxs := <-h.bridge.ReceiveBDNTransactions(): + h.processBDNTransactions(bdnTxs) + case request := <-h.bridge.ReceiveTransactionHashesRequest(): + h.processBDNTransactionRequests(request) + case bdnBlock := <-h.bridge.ReceiveBlockFromBDN(): + h.processBDNBlock(bdnBlock) + case config := <-h.bridge.ReceiveNetworkConfigUpdates(): + h.config.Update(config) + case <-ctx.Done(): + return + } + } +} + +func (h *Handler) processBDNTransactions(bdnTxs []*types.BxTransaction) { + ethTxs := make([]*ethtypes.Transaction, 0, len(bdnTxs)) + + for _, bdnTx := range bdnTxs { + blockchainTx, err := h.bridge.TransactionBDNToBlockchain(bdnTx) + if err != nil { + logTransactionConverterFailure(err, bdnTx) + continue + } + + ethTx, ok := blockchainTx.(*ethtypes.Transaction) + if !ok { + logTransactionConverterFailure(err, bdnTx) + continue + } + + ethTxs = append(ethTxs, ethTx) + } + + h.broadcastTransactions(ethTxs) +} + +func (h *Handler) processBDNTransactionRequests(request blockchain.TransactionAnnouncement) { + ethTxHashes := make([]ethcommon.Hash, 0, len(request.Hashes)) + + for _, txHash := range request.Hashes { + ethTxHashes = append(ethTxHashes, ethcommon.BytesToHash(txHash[:])) + } + + peer, ok := h.peers.get(request.PeerID) + if !ok { + log.Warnf("peer %v announced %v hashes, but is not available for querying", request.PeerID, len(ethTxHashes)) + return + } + + err := peer.RequestTransactions(ethTxHashes) + if err != nil { + peer.Log().Errorf("could not request %v transactions: %v", len(ethTxHashes), err) + } +} + +func (h *Handler) processBDNBlock(bdnBlock *types.BxBlock) { + ethBlockInfo, err := h.storeBDNBlock(bdnBlock) + if err != nil { + logBlockConverterFailure(err, bdnBlock) + return + } + + // nil if block is a duplicate and does not need processing + if ethBlockInfo == nil { + return + } + + ethBlock := ethBlockInfo.Block + err = h.chain.SetTotalDifficulty(ethBlockInfo) + if err != nil { + log.Debugf("could not resolve difficulty for block %v, announcing instead", ethBlock.Hash()) + h.broadcastBlockAnnouncement(ethBlock) + } else { + h.broadcastBlock(ethBlock, ethBlockInfo.TotalDifficulty()) + } +} + +func (h *Handler) awaitBlockResponse(peer *Peer, blockHash ethcommon.Hash, headersCh chan eth.Packet, bodiesCh chan eth.Packet, fetchResponse func(peer *Peer, blockHash ethcommon.Hash, headersCh chan eth.Packet, bodiesCh chan eth.Packet) (*eth.BlockHeadersPacket, *eth.BlockBodiesPacket, error)) { + startTime := time.Now() + headers, bodies, err := fetchResponse(peer, blockHash, headersCh, bodiesCh) + + if err != nil { + if peer.disconnected { + peer.Log().Tracef("block %v response timed out for disconnected peer", blockHash) + return + } + + if err == ErrInvalidPacketType { + // message is already logged + } else if err == ErrResponseTimeout { + peer.Log().Errorf("did not receive block header and body for block %v before timeout", blockHash) + } else { + peer.Log().Errorf("could not fetch block header and body for block %v: %v", blockHash, err) + } + peer.Disconnect(p2p.DiscUselessPeer) + return + } + + elapsedTime := time.Now().Sub(startTime) + peer.Log().Debugf("took %v to fetch block %v header and body", elapsedTime, blockHash) + + if err := h.processBlockComponents(peer, headers, bodies); err != nil { + log.Errorf("error processing block components for hash %v: %v", blockHash, err) + } +} + +func (h *Handler) fetchBlockResponse66(peer *Peer, blockHash ethcommon.Hash, headersCh chan eth.Packet, bodiesCh chan eth.Packet) (*eth.BlockHeadersPacket, *eth.BlockBodiesPacket, error) { + var ( + headers *eth.BlockHeadersPacket + bodies *eth.BlockBodiesPacket + errCh = make(chan error, 2) + ) + + go func() { + var ok bool + + select { + case rawHeaders := <-headersCh: + headers, ok = rawHeaders.(*eth.BlockHeadersPacket) + peer.Log().Debugf("received header for block %v", blockHash) + if !ok { + log.Errorf("could not convert headers for block %v to the expected packet type, got %T", blockHash, rawHeaders) + errCh <- ErrInvalidPacketType + } else { + errCh <- nil + } + case <-time.After(responseTimeout): + errCh <- ErrResponseTimeout + } + }() + + go func() { + var ok bool + + select { + case rawBodies := <-bodiesCh: + bodies, ok = rawBodies.(*eth.BlockBodiesPacket) + peer.Log().Debugf("received body for block %v", blockHash) + if !ok { + log.Errorf("could not convert bodies for block %v to the expected packet type, got %T", blockHash, rawBodies) + errCh <- ErrInvalidPacketType + } else { + errCh <- nil + } + case <-time.After(responseTimeout): + errCh <- ErrResponseTimeout + } + }() + + for i := 0; i < 2; i++ { + err := <-errCh + if err != nil { + return nil, nil, err + } + } + + return headers, bodies, nil +} + +func (h *Handler) fetchBlockResponse(peer *Peer, blockHash ethcommon.Hash, headersCh chan eth.Packet, bodiesCh chan eth.Packet) (*eth.BlockHeadersPacket, *eth.BlockBodiesPacket, error) { + var ( + headers *eth.BlockHeadersPacket + bodies *eth.BlockBodiesPacket + ok bool + ) + + select { + case rawHeaders := <-headersCh: + headers, ok = rawHeaders.(*eth.BlockHeadersPacket) + if !ok { + log.Errorf("could not convert headers for block %v to the expected packet type, got %T", blockHash, rawHeaders) + return nil, nil, ErrInvalidPacketType + } + case <-time.After(responseTimeout): + return nil, nil, ErrResponseTimeout + } + + peer.Log().Debugf("received header for block %v", blockHash) + + select { + case rawBodies := <-bodiesCh: + bodies, ok = rawBodies.(*eth.BlockBodiesPacket) + if !ok { + log.Errorf("could not convert headers for block %v to the expected packet type, got %T", blockHash, rawBodies) + return nil, nil, ErrInvalidPacketType + } + case <-time.After(responseTimeout): + return nil, nil, ErrResponseTimeout + } + + peer.Log().Debugf("received body for block %v", blockHash) + + return headers, bodies, nil +} + +func (h *Handler) processBlockComponents(peer *Peer, headers *eth.BlockHeadersPacket, bodies *eth.BlockBodiesPacket) error { + if len(*headers) != 1 && len(*bodies) != 1 { + return fmt.Errorf("received %v headers and %v bodies, instead of 1 of each", len(*headers), len(*bodies)) + } + + header := (*headers)[0] + body := (*bodies)[0] + block := ethtypes.NewBlockWithHeader(header).WithBody(body.Transactions, body.Uncles) + blockInfo := NewBlockInfo(block, nil) + _ = h.chain.SetTotalDifficulty(blockInfo) + return h.processBlock(peer, blockInfo) +} + +// Handle processes Ethereum message packets that update internal backend state. In general, messages that require responses should not reach this function. +func (h *Handler) Handle(peer *Peer, packet eth.Packet) error { + switch p := packet.(type) { + case *eth.StatusPacket: + h.chain.InitializeDifficulty(p.Head, p.TD) + return nil + case *eth.TransactionsPacket: + return h.processTransactions(peer, *p) + case *eth.PooledTransactionsPacket: + return h.processTransactions(peer, *p) + case *eth.NewPooledTransactionHashesPacket: + return h.processTransactionHashes(peer, *p) + case *eth.NewBlockPacket: + return h.processBlock(peer, NewBlockInfo(p.Block, p.TD)) + case *eth.NewBlockHashesPacket: + return h.processBlockAnnouncement(peer, *p) + case *eth.BlockHeadersPacket: + return h.processBlockHeaders(peer, *p) + default: + return fmt.Errorf("unexpected eth packet type: %v", packet) + } +} + +func (h *Handler) createBxBlockFromEthHeader(header *ethtypes.Header) (*types.BxBlock, error) { + body, ok := h.chain.getBlockBody(header.Hash()) + if !ok { + return nil, ErrBodyNotFound + } + ethBlock := ethtypes.NewBlockWithHeader(header).WithBody(body.Transactions, body.Uncles) + blockInfo := BlockInfo{ethBlock, header.Difficulty} + bxBlock, err := h.bridge.BlockBlockchainToBDN(&blockInfo) + if err != nil { + return nil, fmt.Errorf("failed to convert block %v to BDN format: %v", header.Hash(), err) + } + return bxBlock, nil +} + +func (h *Handler) broadcastTransactions(txs ethtypes.Transactions) { + for _, peer := range h.peers.getAll() { + if err := peer.SendTransactions(txs); err != nil { + peer.Log().Errorf("could not send %v transactions: %v", len(txs), err) + } + } +} + +func (h *Handler) broadcastBlock(block *ethtypes.Block, totalDifficulty *big.Int) { + for _, peer := range h.peers.getAll() { + peer.QueueNewBlock(block, totalDifficulty) + peer.Log().Debugf("queuing block %v from BDN", block.Hash()) + } +} + +func (h *Handler) broadcastBlockAnnouncement(block *ethtypes.Block) { + blockHash := block.Hash() + number := block.NumberU64() + for _, peer := range h.peers.getAll() { + if err := peer.AnnounceBlock(blockHash, number); err != nil { + peer.Log().Errorf("could not announce block %v: %v", block.Hash(), err) + } + } +} + +func (h *Handler) processTransactions(peer *Peer, txs []*ethtypes.Transaction) error { + bdnTxs := make([]*types.BxTransaction, 0, len(txs)) + for _, tx := range txs { + bdnTx, err := h.bridge.TransactionBlockchainToBDN(tx) + if err != nil { + return err + } + bdnTxs = append(bdnTxs, bdnTx) + } + err := h.bridge.SendTransactionsToBDN(bdnTxs, peer.IPEndpoint()) + + if err == blockchain.ErrChannelFull { + log.Warnf("transaction channel for sending to the BDN is full; dropping %v transactions...", len(txs)) + return nil + } + + return err +} + +func (h *Handler) processTransactionHashes(peer *Peer, txHashes []ethcommon.Hash) error { + sha256Hashes := make([]types.SHA256Hash, 0, len(txHashes)) + for _, hash := range txHashes { + sha256Hashes = append(sha256Hashes, NewSHA256Hash(hash)) + } + + err := h.bridge.AnnounceTransactionHashes(peer.ID(), sha256Hashes) + + if err == blockchain.ErrChannelFull { + log.Warnf("transaction announcement channel for sending to the BDN is full; dropping %v hashes...", len(txHashes)) + return nil + } + + return err +} + +func (h *Handler) processBlock(peer *Peer, blockInfo *BlockInfo) error { + block := blockInfo.Block + blockHash := block.Hash() + blockHeight := block.Number() + + if err := h.validateBlock(blockHash, blockHeight.Int64(), int64(block.Time())); err != nil { + if err == ErrAlreadySeen { + peer.Log().Debugf("skipping block %v (height %v): %v", blockHash, blockHeight, err) + } else { + peer.Log().Warnf("skipping block %v (height %v): %v", blockHash, blockHeight, err) + } + return nil + } + + peer.Log().Debugf("processing new block %v (height %v)", blockHash, blockHeight) + newHeadCount := h.chain.AddBlock(blockInfo, BSBlockchain) + h.sendConfirmedBlocksToBDN(newHeadCount, peer.IPEndpoint()) + return nil +} + +func (h *Handler) validateBlock(blockHash ethcommon.Hash, blockHeight int64, blockTime int64) error { + if err := h.chain.ValidateBlock(blockHash, blockHeight); err != nil { + return err + } + + minTimestamp := time.Now().Add(-h.config.IgnoreBlockTimeout) + if minTimestamp.Unix() > blockTime { + return errors.New("timestamp too old") + } + return nil +} + +func (h *Handler) processBlockAnnouncement(peer *Peer, newBlocks eth.NewBlockHashesPacket) error { + for _, block := range newBlocks { + peer.Log().Debugf("processing new block announcement %v (height %v)", block.Hash, block.Number) + + if !h.chain.HasBlock(block.Hash) { + headersCh := make(chan eth.Packet) + bodiesCh := make(chan eth.Packet) + + if peer.isVersion66() { + err := peer.RequestBlock66(block.Hash, headersCh, bodiesCh) + if err != nil { + peer.Log().Errorf("could not request block %v: %v", block.Hash, err) + return err + } + go h.awaitBlockResponse(peer, block.Hash, headersCh, bodiesCh, h.fetchBlockResponse66) + } else { + err := peer.RequestBlock(block.Hash, headersCh, bodiesCh) + if err != nil { + peer.Log().Errorf("could not request block %v: %v", block.Hash, err) + return err + } + go h.awaitBlockResponse(peer, block.Hash, headersCh, bodiesCh, h.fetchBlockResponse) + } + } else { + h.confirmBlock(block.Hash, peer.IPEndpoint()) + } + } + + return nil +} + +func (h *Handler) processBlockHeaders(peer *Peer, blockHeaders eth.BlockHeadersPacket) error { + // expected to only be called when header is not expected to be part of a get block headers in response to a new block hashes message + for _, blockHeader := range blockHeaders { + h.confirmBlock(blockHeader.Hash(), peer.IPEndpoint()) + } + return nil +} + +// confirmedBlockForAll is called when the websocket connection indicates that a block has been accepted +// - this function is a replacement for requesting manual confirmations. +// - this function will not work correct once multiple blockchain peers are connected, since the websocket connection only represents one of them +func (h *Handler) confirmBlockForAll(hash ethcommon.Hash, height *big.Int) { + h.confirmBlock(hash, types.NodeEndpoint{}) + for _, peer := range h.peers.getAll() { + peer.UpdateHead(height.Uint64(), hash) + } +} + +func (h *Handler) confirmBlock(hash ethcommon.Hash, peerEndpoint types.NodeEndpoint) { + newHeads := h.chain.ConfirmBlock(hash) + h.sendConfirmedBlocksToBDN(newHeads, peerEndpoint) +} + +func (h *Handler) sendConfirmedBlocksToBDN(count int, peerEndpoint types.NodeEndpoint) { + newHeads, err := h.chain.GetNewHeadsForBDN(count) + if err != nil { + log.Errorf("could not fetch chainstate: %v", err) + } + + // iterate in reverse to send all new heads in ascending order to BDN + for i := len(newHeads) - 1; i >= 0; i-- { + newHead := newHeads[i] + + bdnBlock, err := h.bridge.BlockBlockchainToBDN(newHead) + if err != nil { + log.Errorf("could not convert block: %v", err) + continue + } + err = h.bridge.SendBlockToBDN(bdnBlock, peerEndpoint) + if err != nil { + log.Errorf("could not send block to BDN: %v", err) + continue + } + h.chain.MarkSentToBDN(newHead.Block.Hash()) + } +} + +// GetBodies assembles and returns a set of block bodies +func (h *Handler) GetBodies(hashes []ethcommon.Hash) ([]*ethtypes.Body, error) { + return h.chain.GetBodies(hashes) +} + +// GetHeaders assembles and returns a set of headers +func (h *Handler) GetHeaders(start eth.HashOrNumber, count int, skip int, reverse bool) ([]*ethtypes.Header, error) { + return h.chain.GetHeaders(start, count, skip, reverse) +} + +// storeBDNBlock will return a nil block and no error if block is a duplicate +func (h *Handler) storeBDNBlock(bdnBlock *types.BxBlock) (*BlockInfo, error) { + blockHash := ethcommon.BytesToHash(bdnBlock.Hash().Bytes()) + if h.chain.HasBlock(blockHash) { + log.Debugf("duplicate block %v from BDN, skipping", blockHash) + return nil, nil + } + + blockchainBlock, err := h.bridge.BlockBDNtoBlockchain(bdnBlock) + if err != nil { + logBlockConverterFailure(err, bdnBlock) + return nil, errors.New("could not convert BDN block to Ethereum block") + } + + ethBlockInfo, ok := blockchainBlock.(*BlockInfo) + if !ok { + logBlockConverterFailure(err, bdnBlock) + return nil, errors.New("could not convert BDN block to Ethereum block") + } + + h.chain.AddBlock(ethBlockInfo, BSBDN) + return ethBlockInfo, nil +} + +func (h *Handler) checkInitialBlockchainLiveliness(initialLivelinessCheckDelay time.Duration) { + ticker := time.NewTicker(initialLivelinessCheckDelay) + for { + select { + case <-ticker.C: + if len(h.peers.getAll()) == 0 { + err := h.bridge.SendNoActiveBlockchainPeersAlert() + if err == blockchain.ErrChannelFull { + log.Warnf("no active blockchain peers alert channel is full") + } + } + ticker.Stop() + } + } +} + +func logTransactionConverterFailure(err error, bdnTx *types.BxTransaction) { + transactionHex := "" + if log.IsLevelEnabled(log.TraceLevel) { + transactionHex = hexutil.Encode(bdnTx.Content()) + } + log.Errorf("could not convert transaction (hash: %v) from BDN to Ethereum transaction: %v. contents: %v", bdnTx.Hash(), err, transactionHex) +} + +func logBlockConverterFailure(err error, bdnBlock *types.BxBlock) { + blockHex := "" + if log.IsLevelEnabled(log.TraceLevel) { + b, err := rlp.EncodeToBytes(bdnBlock) + if err != nil { + log.Error("bad block from BDN could not be encoded to RLP bytes") + return + } + blockHex = hexutil.Encode(b) + } + log.Errorf("could not convert block (hash: %v) from BDN to Ethereum block: %v. contents: %v", bdnBlock.Hash(), err, blockHex) +} diff --git a/blockchain/eth/backend_test.go b/blockchain/eth/backend_test.go new file mode 100644 index 0000000..3e6a656 --- /dev/null +++ b/blockchain/eth/backend_test.go @@ -0,0 +1,631 @@ +package eth + +import ( + "context" + "github.com/bloXroute-Labs/gateway/blockchain" + "github.com/bloXroute-Labs/gateway/blockchain/eth/test" + "github.com/bloXroute-Labs/gateway/blockchain/network" + "github.com/bloXroute-Labs/gateway/test/bxmock" + "github.com/bloXroute-Labs/gateway/types" + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/core/forkid" + ethtypes "github.com/ethereum/go-ethereum/core/types" + "github.com/ethereum/go-ethereum/crypto" + "github.com/ethereum/go-ethereum/eth/protocols/eth" + "github.com/ethereum/go-ethereum/p2p" + "github.com/ethereum/go-ethereum/rlp" + "github.com/stretchr/testify/assert" + "math/big" + "testing" + "time" +) + +const expectTimeout = time.Millisecond + +func setup(writeChannelSize int) (blockchain.Bridge, *Handler, *Peer) { + bridge := blockchain.NewBxBridge(Converter{}) + config, _ := network.NewEthereumPreset("Mainnet") + handler := NewHandler(context.Background(), bridge, &config, bxmock.NewMockWSProvider()) + peer, _ := testPeer(writeChannelSize) + _ = handler.peers.register(peer) + return bridge, handler, peer +} + +func TestHandler_HandleStatus(t *testing.T) { + _, handler, peer := setup(-1) + head := common.Hash{1, 2, 3} + headDifficulty := big.NewInt(100) + nextBlock := bxmock.NewEthBlock(2, head) + + err := handler.Handle(peer, ð.StatusPacket{ + ProtocolVersion: eth.ETH66, + NetworkID: 1, + TD: headDifficulty, + Head: head, + Genesis: common.Hash{2, 3, 4}, + ForkID: forkid.ID{}, + }) + assert.Nil(t, err) + + // new difficulty is stored + storedHeadDifficulty, ok := handler.chain.getBlockDifficulty(head) + assert.True(t, ok) + assert.Equal(t, headDifficulty, storedHeadDifficulty) + + // future blocks uses this difficulty + nextBlockInfo := NewBlockInfo(nextBlock, nil) + err = handler.chain.SetTotalDifficulty(nextBlockInfo) + assert.Nil(t, err) + assert.Equal(t, new(big.Int).Add(headDifficulty, nextBlock.Difficulty()), nextBlockInfo.TotalDifficulty()) +} + +func TestHandler_HandleTransactions(t *testing.T) { + privateKey, _ := crypto.GenerateKey() + bridge, handler, peer := setup(-1) + + txs := []*ethtypes.Transaction{ + bxmock.NewSignedEthTx(ethtypes.LegacyTxType, 1, privateKey), + bxmock.NewSignedEthTx(ethtypes.LegacyTxType, 2, privateKey), + } + + txsPacket := eth.TransactionsPacket(txs) + + err := handler.Handle(peer, &txsPacket) + assert.Nil(t, err) + + bxTxs := <-bridge.ReceiveNodeTransactions() + assert.Equal(t, len(txs), len(bxTxs.Transactions)) + + for i, bxTx := range bxTxs.Transactions { + tx := txs[i] + assert.Equal(t, NewSHA256Hash(tx.Hash()), bxTx.Hash()) + + encodedTx, _ := rlp.EncodeToBytes(tx) + assert.Equal(t, encodedTx, []uint8(bxTx.Content())) + } + + // pooled txs should have exact same behavior + pooledTxsPacket := eth.PooledTransactionsPacket(txs) + err = handler.Handle(peer, &pooledTxsPacket) + assert.Nil(t, err) + + bxTxs2 := <-bridge.ReceiveNodeTransactions() + assert.Equal(t, bxTxs, bxTxs2) +} + +func TestHandler_HandleTransactionHashes(t *testing.T) { + bridge, handler, peer := setup(-1) + + txHashes := []types.SHA256Hash{ + types.GenerateSHA256Hash(), + types.GenerateSHA256Hash(), + } + txHashesPacket := make(eth.NewPooledTransactionHashesPacket, 0) + for _, txHash := range txHashes { + txHashesPacket = append(txHashesPacket, common.BytesToHash(txHash[:])) + } + + err := handler.Handle(peer, &txHashesPacket) + assert.Nil(t, err) + + txAnnouncements := <-bridge.ReceiveTransactionHashesAnnouncement() + assert.Equal(t, peer.ID(), txAnnouncements.PeerID) + + for i, announcedHash := range txAnnouncements.Hashes { + assert.Equal(t, txHashes[i], announcedHash) + } +} + +func TestHandler_HandleNewBlock(t *testing.T) { + bridge, handler, peer := setup(-1) + blockHeight := uint64(1) + + block := bxmock.NewEthBlock(blockHeight, common.Hash{}) + header := block.Header() + td := big.NewInt(10000) + + err := testHandleNewBlock(handler, peer, block, td) + assert.Nil(t, err) + + assertBlockSentToBDN(t, bridge, block.Hash()) + + storedHeaderByHash, err := handler.GetHeaders(eth.HashOrNumber{ + Hash: block.Hash(), + }, 1, 0, false) + assert.Nil(t, err) + assert.Equal(t, 1, len(storedHeaderByHash)) + assert.Equal(t, header, storedHeaderByHash[0]) + + storedHeaderByHeight, err := handler.GetHeaders(eth.HashOrNumber{ + Number: blockHeight, + }, 1, 0, false) + assert.Nil(t, err) + assert.Equal(t, 1, len(storedHeaderByHeight)) + assert.Equal(t, header, storedHeaderByHeight[0]) +} + +func TestHandler_HandleNewBlock_TooOld(t *testing.T) { + bridge, handler, peer := setup(-1) + blockHeight := uint64(1) + + // oldest block that won't be sent to BDN + header := bxmock.NewEthBlockHeader(blockHeight, common.Hash{}) + header.Time = uint64(time.Now().Add(-handler.config.IgnoreBlockTimeout).Add(-time.Second).Unix()) + block := bxmock.NewEthBlockWithHeader(header) + td := big.NewInt(10000) + + err := testHandleNewBlock(handler, peer, block, td) + assert.Nil(t, err) + + assertNoBlockSentToBDN(t, bridge) + + // oldest block that will be sent to BDN + header = bxmock.NewEthBlockHeader(blockHeight, common.Hash{}) + header.Time = uint64(time.Now().Add(-handler.config.IgnoreBlockTimeout).Add(time.Second).Unix()) + block = bxmock.NewEthBlockWithHeader(header) + td = big.NewInt(10000) + + err = testHandleNewBlock(handler, peer, block, td) + assert.Nil(t, err) + + assertBlockSentToBDN(t, bridge, block.Hash()) +} + +func TestHandler_HandleNewBlock_TooFarInFuture(t *testing.T) { + bridge, handler, peer := setup(-1) + blockHeight := uint64(maxFutureBlockNumber + 100) + + block := bxmock.NewEthBlock(blockHeight, common.Hash{}) + td := big.NewInt(10000) + + err := testHandleNewBlock(handler, peer, block, td) + assert.Nil(t, err) + + // ok, always send initial block + assertBlockSentToBDN(t, bridge, block.Hash()) + + block = bxmock.NewEthBlock(blockHeight*2, block.Hash()) + err = testHandleNewBlock(handler, peer, block, td) + assert.Nil(t, err) + + assertNoBlockSentToBDN(t, bridge) +} + +func TestHandler_HandleNewBlockHashes(t *testing.T) { + bridge, handler, peer := setup(2) + peer.Start() + peerRW := peer.rw.(*test.MsgReadWriter) + + // process parent block for calculating difficulty + parentBlock := bxmock.NewEthBlock(9, common.Hash{}) + parentHash := parentBlock.Hash() + parentTD := big.NewInt(1000) + + err := handler.processBlock(peer, NewBlockInfo(parentBlock, parentTD)) + assert.Nil(t, err) + + assertBlockSentToBDN(t, bridge, parentHash) + + // start message handling goroutines + go func() { + for { + if err := handleMessage(handler, peer); err != nil { + assert.Fail(t, "unexpected message handling failure") + } + } + }() + + // process new block request + blockHeight := uint64(10) + block := bxmock.NewEthBlock(blockHeight, parentHash) + + err = testHandleNewBlockHashes(handler, peer, block.Hash(), blockHeight) + assert.Nil(t, err) + + // expect get headers + get bodies request to peer + assert.True(t, peerRW.ExpectWrite(time.Millisecond)) + assert.True(t, peerRW.ExpectWrite(time.Millisecond)) + assert.Equal(t, 2, len(peerRW.WriteMessages)) + + getHeadersMsg := peerRW.WriteMessages[0] + getBodiesMsg := peerRW.WriteMessages[1] + + assert.Equal(t, uint64(eth.GetBlockHeadersMsg), getHeadersMsg.Code) + assert.Equal(t, uint64(eth.GetBlockBodiesMsg), getBodiesMsg.Code) + + var getHeaders eth.GetBlockHeadersPacket + err = getHeadersMsg.Decode(&getHeaders) + assert.Nil(t, err) + assert.Equal(t, block.Hash(), getHeaders.Origin.Hash) + assert.Equal(t, uint64(1), getHeaders.Amount) + assert.Equal(t, uint64(0), getHeaders.Skip) + assert.Equal(t, false, getHeaders.Reverse) + + assert.Nil(t, err) + + var getBodies eth.GetBlockBodiesPacket + err = getBodiesMsg.Decode(&getBodies) + assert.Nil(t, err) + assert.Equal(t, 1, len(getBodies)) + assert.Equal(t, block.Hash(), getBodies[0]) + + // send header/body from peer + peerRW.QueueIncomingMessage(uint64(eth.BlockHeadersMsg), eth.BlockHeadersPacket{block.Header()}) + peerRW.QueueIncomingMessage(uint64(eth.BlockBodiesMsg), eth.BlockBodiesPacket{ð.BlockBody{ + Transactions: block.Transactions(), + Uncles: block.Uncles(), + }}) + + expectedDifficulty := new(big.Int).Add(parentTD, block.Difficulty()) + expectedBlock, _ := bridge.BlockBlockchainToBDN(NewBlockInfo(block, expectedDifficulty)) + newBlock := assertBlockSentToBDN(t, bridge, block.Hash()) + assert.True(t, expectedBlock.Equals(newBlock.Block)) + + // difficulty is unknown from header/body handling, but still set by stored difficulties + assert.Equal(t, expectedDifficulty, newBlock.Block.TotalDifficulty) + + // check peer internal state is cleaned up + assert.Equal(t, 0, len(peer.responseQueue)) +} + +func TestHandler_HandleNewBlockHashes_HandlingError(t *testing.T) { + bridge, handler, peer := setup(2) + peer.Start() + peerRW := peer.rw.(*test.MsgReadWriter) + + blockHeight := uint64(1) + block := bxmock.NewEthBlock(blockHeight, common.Hash{}) + err := testHandleNewBlockHashes(handler, peer, block.Hash(), blockHeight) + + // expect get headers + get bodies request to peer + assert.True(t, peerRW.ExpectWrite(time.Millisecond)) + assert.True(t, peerRW.ExpectWrite(time.Millisecond)) + assert.Equal(t, 2, len(peerRW.WriteMessages)) + + getHeadersMsg := peerRW.WriteMessages[0] + getBodiesMsg := peerRW.WriteMessages[1] + + assert.Equal(t, uint64(eth.GetBlockHeadersMsg), getHeadersMsg.Code) + assert.Equal(t, uint64(eth.GetBlockBodiesMsg), getBodiesMsg.Code) + + peerRW.QueueIncomingMessage(uint64(eth.BlockBodiesMsg), eth.BlockBodiesPacket{ð.BlockBody{ + Transactions: block.Transactions(), + Uncles: block.Uncles(), + }}) + + // handle bad block body + err = handleMessage(handler, peer) + assert.Nil(t, err) + + assertNoBlockSentToBDN(t, bridge) + + time.Sleep(1 * time.Millisecond) + assert.True(t, peer.disconnected) +} + +func TestHandler_HandleNewBlockHashes66(t *testing.T) { + bridge, handler, peer := setup(2) + peer.version = eth.ETH66 + peer.Start() + peerRW := peer.rw.(*test.MsgReadWriter) + + blockHeight := uint64(1) + block := bxmock.NewEthBlock(blockHeight, common.Hash{}) + err := testHandleNewBlockHashes(handler, peer, block.Hash(), blockHeight) + + // expect get headers + get bodies request to peer + assert.True(t, peerRW.ExpectWrite(time.Millisecond)) + assert.True(t, peerRW.ExpectWrite(time.Millisecond)) + assert.Equal(t, 2, len(peerRW.WriteMessages)) + + getHeadersMsg := peerRW.WriteMessages[0] + getBodiesMsg := peerRW.WriteMessages[1] + + assert.Equal(t, uint64(eth.GetBlockHeadersMsg), getHeadersMsg.Code) + assert.Equal(t, uint64(eth.GetBlockBodiesMsg), getBodiesMsg.Code) + + var getHeaders eth.GetBlockHeadersPacket66 + err = getHeadersMsg.Decode(&getHeaders) + assert.Nil(t, err) + assert.Equal(t, block.Hash(), getHeaders.Origin.Hash) + assert.Equal(t, uint64(1), getHeaders.Amount) + assert.Equal(t, uint64(0), getHeaders.Skip) + assert.Equal(t, false, getHeaders.Reverse) + + var getBodies eth.GetBlockBodiesPacket66 + err = getBodiesMsg.Decode(&getBodies) + assert.Nil(t, err) + assert.Equal(t, 1, len(getBodies.GetBlockBodiesPacket)) + assert.Equal(t, block.Hash(), getBodies.GetBlockBodiesPacket[0]) + + headersID := getHeaders.RequestId + bodiesID := getBodies.RequestId + + // send header/body from peer (out of order with request IDs) + peerRW.QueueIncomingMessage(uint64(eth.BlockBodiesMsg), eth.BlockBodiesPacket66{ + RequestId: bodiesID, + BlockBodiesPacket: eth.BlockBodiesPacket{ð.BlockBody{ + Transactions: block.Transactions(), + Uncles: block.Uncles(), + }}, + }) + peerRW.QueueIncomingMessage(uint64(eth.BlockHeadersMsg), eth.BlockHeadersPacket66{ + RequestId: headersID, + BlockHeadersPacket: eth.BlockHeadersPacket{block.Header()}, + }) + + // expect bodies message, then peer message + err = handleMessage(handler, peer) + assert.Nil(t, err) + + err = handleMessage(handler, peer) + assert.Nil(t, err) + + expectedBlock, _ := bridge.BlockBlockchainToBDN(NewBlockInfo(block, nil)) + newBlock := assertBlockSentToBDN(t, bridge, block.Hash()) + assert.True(t, expectedBlock.Equals(newBlock.Block)) + + // difficulty is unknown from header/body handling + assert.Equal(t, big.NewInt(0), newBlock.Block.TotalDifficulty) + + // check peer state is cleaned up + assert.Equal(t, 0, len(peer.responseQueue66.Keys())) +} + +func TestHandler_processBDNBlock(t *testing.T) { + bridge, handler, peer := setup(1) + peer.Start() + peerRW := peer.rw.(*test.MsgReadWriter) + + // generate bx block for processing + ethBlock := bxmock.NewEthBlock(10, common.Hash{}) + blockHash := ethBlock.Hash() + td := big.NewInt(10000) + bxBlock, _ := bridge.BlockBlockchainToBDN(NewBlockInfo(ethBlock, td)) + + // indicate previous head from status message + peer.confirmedHead = blockRef{hash: ethBlock.ParentHash()} + + handler.processBDNBlock(bxBlock) + + // expect message to be sent to a peer + blockPacket := assertBlockSentToBlockchain(t, peerRW, ethBlock.Hash()) + assert.Equal(t, blockHash, blockPacket.Block.Hash()) + assert.Equal(t, len(ethBlock.Transactions()), len(blockPacket.Block.Transactions())) + assert.Equal(t, td, blockPacket.TD) + + // contents stored in cache + assert.True(t, handler.chain.HasBlock(blockHash)) + + storedHeader, ok := handler.chain.getBlockHeader(10, blockHash) + assert.True(t, ok) + assert.Equal(t, ethBlock.Header(), storedHeader) + + storedBody, ok := handler.chain.getBlockBody(blockHash) + assert.True(t, ok) + assert.Equal(t, ethBlock.Body().Uncles, storedBody.Uncles) + for i, tx := range ethBlock.Body().Transactions { + assert.Equal(t, tx.Hash(), storedBody.Transactions[i].Hash()) + } + + // confirm block, should send back to BDN and update head + err := handler.Handle(peer, ð.BlockHeadersPacket{ethBlock.Header()}) + assert.Nil(t, err) + assertBlockSentToBDN(t, bridge, ethBlock.Hash()) +} + +func TestHandler_processBDNBlockResolveDifficulty(t *testing.T) { + bridge, handler, peer := setup(1) + peer.Start() + peerRW := peer.rw.(*test.MsgReadWriter) + + // preprocess a parent for calculating difficulty + parentBlock := bxmock.NewEthBlock(9, common.Hash{}) + parentHash := parentBlock.Hash() + parentTD := big.NewInt(1000) + + err := testHandleNewBlock(handler, peer, parentBlock, parentTD) + assert.Nil(t, err) + + // generate bx block for processing + ethBlock := bxmock.NewEthBlock(10, parentHash) + blockHash := ethBlock.Hash() + bxBlock, _ := bridge.BlockBlockchainToBDN(NewBlockInfo(ethBlock, nil)) + + handler.processBDNBlock(bxBlock) + + blockPacket := assertBlockSentToBlockchain(t, peerRW, blockHash) + assert.Equal(t, blockHash, blockPacket.Block.Hash()) + assert.Equal(t, len(ethBlock.Transactions()), len(blockPacket.Block.Transactions())) + assert.Equal(t, new(big.Int).Add(parentTD, ethBlock.Difficulty()), blockPacket.TD) +} + +func TestHandler_processBDNBlockUnresolvableDifficulty(t *testing.T) { + bridge, handler, peer := setup(1) + peer.Start() + peerRW := peer.rw.(*test.MsgReadWriter) + + // generate bx block for processing + height := uint64(10) + ethBlock := bxmock.NewEthBlock(height, common.Hash{}) + blockHash := ethBlock.Hash() + bxBlock, _ := bridge.BlockBlockchainToBDN(NewBlockInfo(ethBlock, nil)) + + handler.processBDNBlock(bxBlock) + + // expect message to be sent to a peer + assert.True(t, peerRW.ExpectWrite(time.Millisecond)) + assert.Equal(t, 1, len(peerRW.WriteMessages)) + msg := peerRW.WriteMessages[0] + assert.Equal(t, uint64(eth.NewBlockHashesMsg), msg.Code) + + var blockHashesPacket eth.NewBlockHashesPacket + err := msg.Decode(&blockHashesPacket) + assert.Nil(t, err) + assert.Equal(t, 1, len(blockHashesPacket)) + assert.Equal(t, height, blockHashesPacket[0].Number) + assert.Equal(t, blockHash, blockHashesPacket[0].Hash) +} + +func TestHandler_blockForks(t *testing.T) { + var err error + bridge, handler, peer := setup(1) + peer.Start() + peerRW := peer.rw.(*test.MsgReadWriter) + + block1 := bxmock.NewEthBlock(uint64(1), common.Hash{}) + block2a := bxmock.NewEthBlock(uint64(2), block1.Hash()) + block2b := bxmock.NewEthBlock(uint64(2), block1.Hash()) + block3a := bxmock.NewEthBlock(uint64(3), block2a.Hash()) + block3b := bxmock.NewEthBlock(uint64(3), block2b.Hash()) + block4b := bxmock.NewEthBlock(uint64(4), block3b.Hash()) + + // sequence of blocks received from blockchain + newBlock1 := eth.NewBlockPacket{Block: block1, TD: big.NewInt(100)} + newBlock2a := eth.NewBlockPacket{Block: block2a, TD: big.NewInt(201)} + // newBlock3a sent as NewBlockHashes instead of block packet + newBlock4b := eth.NewBlockPacket{Block: block4b, TD: big.NewInt(402)} + + // sequence of blocks received from BDN + bxBlock1, _ := bridge.BlockBlockchainToBDN(NewBlockInfo(block1, big.NewInt(100))) + bxBlock2a, _ := bridge.BlockBlockchainToBDN(NewBlockInfo(block2a, big.NewInt(201))) + bxBlock2b, _ := bridge.BlockBlockchainToBDN(NewBlockInfo(block2b, big.NewInt(202))) + bxBlock3a, _ := bridge.BlockBlockchainToBDN(NewBlockInfo(block3a, big.NewInt(301))) + bxBlock3b, _ := bridge.BlockBlockchainToBDN(NewBlockInfo(block3b, big.NewInt(302))) + bxBlock4b, _ := bridge.BlockBlockchainToBDN(NewBlockInfo(block4b, big.NewInt(402))) + + /* + sending sequence: + 1 blockchain + 2a BDN + 2a blockchain + 2b BDN + 3a BDN + 3a blockchain + 3b BDN + 4b BDN + 4b blockchain (maybe this should be a confirmation instead of a full block?) + */ + + // expectation: block is sent to BDN + err = testHandleNewBlock(handler, peer, newBlock1.Block, newBlock1.TD) + assert.Nil(t, err) + assertBlockSentToBDN(t, bridge, block1.Hash()) + assertNoBlockSentToBlockchain(t, peerRW) + + // expectation: duplicate, nothing new + handler.processBDNBlock(bxBlock1) + assertNoBlockSentToBDN(t, bridge) + assertNoBlockSentToBlockchain(t, peerRW) + + // expectation: sent to blockchain node (next in confirmed chain) + handler.processBDNBlock(bxBlock2a) + assertNoBlockSentToBDN(t, bridge) + assertBlockSentToBlockchain(t, peerRW, block2a.Hash()) + + // expectation: sent to gateway as confirmation + _ = testHandleNewBlock(handler, peer, newBlock2a.Block, newBlock2a.TD) + assertBlockSentToBDN(t, bridge, block2a.Hash()) + assertNoBlockSentToBlockchain(t, peerRW) + + // expectation: nothing sent anywhere (parked for blockchain, unconfirmed for BDN) + handler.processBDNBlock(bxBlock2b) + assertNoBlockSentToBDN(t, bridge) + assertNoBlockSentToBlockchain(t, peerRW) + + // expectation: block sent to blockchain node + handler.processBDNBlock(bxBlock3a) + assertNoBlockSentToBDN(t, bridge) + assertBlockSentToBlockchain(t, peerRW, block3a.Hash()) + + // expectation: new block hashes confirms blocks, and sends to BDN + _ = testHandleNewBlockHashes(handler, peer, block3a.Hash(), block3a.NumberU64()) + assertBlockSentToBDN(t, bridge, block3a.Hash()) + assertNoBlockSentToBlockchain(t, peerRW) + + // expectation: nothing sent anywhere (parked + unconfirmed) + handler.processBDNBlock(bxBlock3b) + assertNoBlockSentToBDN(t, bridge) + assertNoBlockSentToBlockchain(t, peerRW) + + // expectation: nothing sent anywhere (unconfirmed, blockchain node is on 3a/4a path) + handler.processBDNBlock(bxBlock4b) + assertNoBlockSentToBDN(t, bridge) + assertNoBlockSentToBlockchain(t, peerRW) + + // expectation: send 2b, 3b, 4b to BDN (confirmed now) + err = handler.Handle(peer, &newBlock4b) + _ = testHandleNewBlock(handler, peer, newBlock4b.Block, newBlock4b.TD) + assertBlockSentToBDN(t, bridge, block2b.Hash()) + assertBlockSentToBDN(t, bridge, block3b.Hash()) + assertBlockSentToBDN(t, bridge, block4b.Hash()) + assertNoBlockSentToBlockchain(t, peerRW) +} + +// variety of handling functions here to trigger handlers in handlers.go instead of directly invoking the handler (useful for setting state on Peer during handling) +func testHandleNewBlock(handler *Handler, peer *Peer, block *ethtypes.Block, td *big.Int) error { + newBlockPacket := eth.NewBlockPacket{ + Block: block, + TD: td, + } + return handleNewBlockMsg(handler, encodeRLP(eth.NewBlockMsg, newBlockPacket), peer) +} + +func testHandleNewBlockHashes(handler *Handler, peer *Peer, hash common.Hash, height uint64) error { + newBlockHashesPacket := eth.NewBlockHashesPacket{ + { + Hash: hash, + Number: height, + }, + } + return handleNewBlockHashes(handler, encodeRLP(eth.NewBlockHashesMsg, newBlockHashesPacket), peer) +} + +func encodeRLP(code uint64, data interface{}) Decoder { + size, r, err := rlp.EncodeToReader(data) + if err != nil { + panic(err) + } + return p2p.Msg{ + Code: code, + Size: uint32(size), + Payload: r, + } +} + +func assertBlockSentToBDN(t *testing.T, bridge blockchain.Bridge, hash common.Hash) blockchain.BlockFromNode { + select { + case sentBlock := <-bridge.ReceiveBlockFromNode(): + assert.Equal(t, hash.Bytes(), sentBlock.Block.Hash().Bytes()) + return sentBlock + case <-time.After(expectTimeout): + assert.FailNow(t, "BDN did not receive block", "hash=%v", hash) + } + return blockchain.BlockFromNode{} +} + +func assertNoBlockSentToBDN(t *testing.T, bridge blockchain.Bridge) { + select { + case sentBlock := <-bridge.ReceiveBlockFromNode(): + assert.FailNow(t, "BDN received unexpected block", "hash=%v", sentBlock.Block.Hash()) + case <-time.After(expectTimeout): + } +} + +func assertBlockSentToBlockchain(t *testing.T, rw *test.MsgReadWriter, hash common.Hash) eth.NewBlockPacket { + assert.True(t, rw.ExpectWrite(time.Millisecond)) + assert.Equal(t, 1, len(rw.WriteMessages)) + msg := rw.PopWrittenMessage() + assert.Equal(t, uint64(eth.NewBlockMsg), msg.Code) + + var newBlocks eth.NewBlockPacket + err := msg.Decode(&newBlocks) + assert.Nil(t, err) + + assert.Equal(t, hash, newBlocks.Block.Hash()) + return newBlocks +} + +func assertNoBlockSentToBlockchain(t *testing.T, rw *test.MsgReadWriter) { + assert.False(t, rw.ExpectWrite(expectTimeout)) +} diff --git a/blockchain/eth/chain.go b/blockchain/eth/chain.go new file mode 100644 index 0000000..07497f5 --- /dev/null +++ b/blockchain/eth/chain.go @@ -0,0 +1,708 @@ +package eth + +import ( + "bytes" + "context" + "errors" + "fmt" + ethcommon "github.com/ethereum/go-ethereum/common" + ethtypes "github.com/ethereum/go-ethereum/core/types" + "github.com/ethereum/go-ethereum/eth/protocols/eth" + cmap "github.com/orcaman/concurrent-map" + log "github.com/sirupsen/logrus" + "math/big" + "strconv" + "sync" + "time" +) + +const ( + maxReorgLength = 20 + minValidChainLength = 10 + defaultMaxSize = 500 + defaultCleanInterval = 30 * time.Minute +) + +// Chain represents and stores blockchain state info in memory +type Chain struct { + chainLock sync.RWMutex // lock for updating chainstate + headerLock sync.RWMutex // lock for block headers/heights + + // if reconciling a fork takes longer than this value, then trim the chain to this length + maxReorg int + + // if a missing block is preventing updating the chain head, once a valid chain of this length is possible, discard the old chain + minValidChain int + + heightToBlockHeaders cmap.ConcurrentMap + blockHashMetadata cmap.ConcurrentMap + blockHashToBody cmap.ConcurrentMap + blockHashToDifficulty cmap.ConcurrentMap + + chainState blockRefChain +} + +// BlockSource indicates the origin of a block message in the blockchain +type BlockSource string + +// enumerate types of BlockSource +const ( + BSBDN BlockSource = "BDN" + BSBlockchain = "Blockchain" +) + +// BlockInfo wraps an Ethereum block with its total difficulty. +type BlockInfo struct { + Block *ethtypes.Block + totalDifficulty *big.Int +} + +// NewBlockInfo composes a new BlockInfo. nil is considered a valid total difficulty for constructing this struct +func NewBlockInfo(block *ethtypes.Block, totalDifficulty *big.Int) *BlockInfo { + info := &BlockInfo{ + Block: block, + } + info.SetTotalDifficulty(totalDifficulty) + return info +} + +// SetTotalDifficulty sets the total difficulty, filtering nil arguments +func (e *BlockInfo) SetTotalDifficulty(td *big.Int) { + if td == nil { + e.totalDifficulty = big.NewInt(0) + } else { + e.totalDifficulty = td + } +} + +// TotalDifficulty validates and returns the block's total difficulty. Any value <=0 may be encoded in types.BxBlock, and are considered invalid difficulties that should be treated as "unknown diffiuclty" +func (e BlockInfo) TotalDifficulty() *big.Int { + if e.totalDifficulty.Int64() <= 0 { + return nil + } + return e.totalDifficulty +} + +type blockMetadata struct { + height uint64 + sentToBDN bool + confirmed bool +} + +type ethHeader struct { + *ethtypes.Header + hash ethcommon.Hash +} + +// NewChain returns a new chainstate struct for usage +func NewChain(ctx context.Context) *Chain { + return newChain(ctx, maxReorgLength, minValidChainLength, defaultCleanInterval, defaultMaxSize) +} + +func newChain(ctx context.Context, maxReorg, minValidChain int, cleanInterval time.Duration, maxSize int) *Chain { + c := &Chain{ + chainLock: sync.RWMutex{}, + headerLock: sync.RWMutex{}, + heightToBlockHeaders: cmap.New(), + blockHashMetadata: cmap.New(), + blockHashToBody: cmap.New(), + blockHashToDifficulty: cmap.New(), + chainState: make([]blockRef, 0), + maxReorg: maxReorg, + minValidChain: minValidChain, + } + go c.cleanBlockStorage(ctx, cleanInterval, maxSize) + return c +} + +func (c *Chain) cleanBlockStorage(ctx context.Context, cleanInterval time.Duration, maxSize int) { + ticker := time.NewTicker(cleanInterval) + for { + select { + case <-ticker.C: + c.clean(maxSize) + case <-ctx.Done(): + return + } + } +} + +// AddBlock adds the provided block from the source into storage, updating the chainstate if the block comes from a reliable source. AddBlock returns the number of new canonical hashes added to the head if a reorganization happened. TODO: consider computing difficulty in here? +func (c *Chain) AddBlock(b *BlockInfo, source BlockSource) int { + c.chainLock.Lock() + defer c.chainLock.Unlock() + + height := b.Block.NumberU64() + hash := b.Block.Hash() + parentHash := b.Block.ParentHash() + + // update metadata if block already stored, otherwise update all block info + if c.HasBlock(hash) { + c.storeBlockMetadata(hash, height, source == BSBlockchain) + } else { + c.storeBlock(b.Block, b.TotalDifficulty(), source) + } + + // if source is BDN, then no authority to update chainstate and indicate no new heads to return + if source == BSBDN { + return 0 + } + + return c.updateChainState(height, hash, parentHash) +} + +// ConfirmBlock marks a block as confirmed by a trustworthy source, updating the chain state if possible and returning the number of new canonical hashes added to the head if an update happened. +func (c *Chain) ConfirmBlock(hash ethcommon.Hash) int { + c.chainLock.Lock() + defer c.chainLock.Unlock() + + // update metadata + bm, ok := c.getBlockMetadata(hash) + if !ok { + return 0 + } + + bm.confirmed = true + c.blockHashMetadata.Set(hash.String(), bm) + + header, ok := c.getBlockHeader(bm.height, hash) + if !ok { + return 0 + } + + return c.updateChainState(bm.height, hash, header.ParentHash) +} + +// GetNewHeadsForBDN fetches the newest blocks on the chainstate that have not previously been sent to the BDN. In cases of error, as many entries are still returned along with the error. Entries are returned in descending order. +func (c *Chain) GetNewHeadsForBDN(count int) ([]*BlockInfo, error) { + c.chainLock.RLock() + defer c.chainLock.RUnlock() + + heads := make([]*BlockInfo, 0, count) + + for i := 0; i < count; i++ { + if len(c.chainState) <= i { + return heads, errors.New("chain state insufficient length") + } + + head := c.chainState[i] + + // !ok blocks should never be triggered, as any state cleanup should also cleanup the chain state + bm, ok := c.getBlockMetadata(head.hash) + if !ok { + return heads, fmt.Errorf("inconsistent chainstate: no metadata stored for %v", head.hash) + } + + // blocks have previously been sent to BDN, ok to stop here + if bm.sentToBDN { + break + } + + header, ok := c.getBlockHeader(head.height, head.hash) + if !ok { + return heads, fmt.Errorf("inconsistent chainstate: no header stored for %v", head.hash) + } + + body, ok := c.getBlockBody(head.hash) + if !ok { + return heads, fmt.Errorf("inconsistent chainstate: no body stored for %v", head.hash) + } + + block := ethtypes.NewBlockWithHeader(header).WithBody(body.Transactions, body.Uncles) + // ok for difficulty to not be found since not always available + td, _ := c.getBlockDifficulty(head.hash) + + heads = append(heads, NewBlockInfo(block, td)) + } + + return heads, nil +} + +// ValidateBlock determines if block can potentially be added to the chain +func (c *Chain) ValidateBlock(hash ethcommon.Hash, height int64) error { + if c.HasBlock(hash) && c.HasSentToBDN(hash) && c.HasConfirmedBlock(hash) { + return ErrAlreadySeen + } + + chainHead := c.chainState.head() + maxBlockNumber := chainHead.height + maxFutureBlockNumber + if chainHead.height != 0 && height > int64(maxBlockNumber) { + return fmt.Errorf("too far in future (best height: %v, max allowed height: %v)", chainHead.height, maxBlockNumber) + } + + return nil +} + +// HasBlock indicates if block has been stored locally +func (c *Chain) HasBlock(hash ethcommon.Hash) bool { + return c.hasHeader(hash) && c.hasBody(hash) +} + +// HasSentToBDN indicates if the block has been sent to the BDN +func (c *Chain) HasSentToBDN(hash ethcommon.Hash) bool { + bm, ok := c.getBlockMetadata(hash) + if !ok { + return false + } + return bm.sentToBDN +} + +// HasConfirmedBlock indicates if the block has been confirmed by a reliable source +func (c *Chain) HasConfirmedBlock(hash ethcommon.Hash) bool { + bm, ok := c.getBlockMetadata(hash) + if !ok { + return false + } + return bm.confirmed +} + +// MarkSentToBDN marks a block as having been sent to the BDN, so it does not need to be sent again in the future +func (c *Chain) MarkSentToBDN(hash ethcommon.Hash) { + c.chainLock.Lock() + defer c.chainLock.Unlock() + + bm, ok := c.getBlockMetadata(hash) + if !ok { + return + } + + bm.sentToBDN = true + c.blockHashMetadata.Set(hash.String(), bm) +} + +// InitializeDifficulty stores an initial difficulty if needed to start calculating total difficulties. Only 1 difficulty is stored, since these difficulties are never GC'ed. +func (c *Chain) InitializeDifficulty(hash ethcommon.Hash, td *big.Int) { + c.storeBlockDifficulty(hash, td) + c.storeEthHeaderAtHeight(0, ethHeader{hash: hash}) + c.storeBlockMetadata(hash, 0, true) +} + +// SetTotalDifficulty computes, sets, and stores the difficulty for a provided block +func (c *Chain) SetTotalDifficulty(info *BlockInfo) error { + totalDifficulty := info.TotalDifficulty() + if totalDifficulty != nil { + return nil + } + + parentHash := info.Block.ParentHash() + parentDifficulty, ok := c.getBlockDifficulty(parentHash) + if ok { + info.SetTotalDifficulty(new(big.Int).Add(parentDifficulty, info.Block.Difficulty())) + c.storeBlockDifficulty(info.Block.Hash(), info.TotalDifficulty()) + return nil + } + return errors.New("could not calculate difficulty") +} + +// GetBodies assembles and returns a set of block bodies +func (c *Chain) GetBodies(hashes []ethcommon.Hash) ([]*ethtypes.Body, error) { + bodies := make([]*ethtypes.Body, 0, len(hashes)) + for _, hash := range hashes { + body, ok := c.getBlockBody(hash) + if !ok { + return nil, ErrBodyNotFound + } + bodies = append(bodies, body) + } + return bodies, nil +} + +// GetHeaders assembles and returns a set of headers +func (c *Chain) GetHeaders(start eth.HashOrNumber, count int, skip int, reverse bool) ([]*ethtypes.Header, error) { + c.chainLock.RLock() + defer c.chainLock.RUnlock() + + requestedHeaders := make([]*ethtypes.Header, 0, count) + + var ( + originHash ethcommon.Hash + originHeight uint64 + ) + + // figure out query scheme, then initialize requested headers with the first entry + if start.Number > 0 { + originHeight = start.Number + + if originHeight > c.chainState.head().height { + return nil, ErrFutureHeaders + } + + tail := c.chainState.tail() + if tail != nil && originHeight < tail.height { + return nil, ErrAncientHeaders + } + + originHeader, err := c.getHeaderAtHeight(originHeight) + if err != nil { + return nil, err + } + + // originHeader may be nil if a block in the future is requested, return empty headers in that case + if originHeader != nil { + requestedHeaders = append(requestedHeaders, originHeader) + } + } else if start.Hash != (ethcommon.Hash{}) { + originHash = start.Hash + bm, ok := c.getBlockMetadata(originHash) + originHeight = bm.height + + if !ok { + return nil, fmt.Errorf("could not retrieve a corresponding height for block: %v", originHash) + } + originHeader, ok := c.getBlockHeader(originHeight, originHash) + if !ok { + return nil, fmt.Errorf("no header was with height %v and hash %v", originHeight, originHash) + } + requestedHeaders = append(requestedHeaders, originHeader) + } else { + return nil, ErrInvalidRequest + } + + // if only 1 header was requested, return result + if count == 1 { + return requestedHeaders, nil + } + + directionalMultiplier := 1 + increment := skip + 1 + if reverse { + directionalMultiplier = -1 + } + increment *= directionalMultiplier + + nextHeight := int(originHeight) + increment + if len(c.chainState) == 0 { + return nil, fmt.Errorf("no entries stored at height: %v", nextHeight) + } + + // iterate through all requested headers and fetch results + for height := nextHeight; len(requestedHeaders) < count; height += increment { + header, err := c.getHeaderAtHeight(uint64(height)) + if err != nil { + return nil, err + } + + if header == nil { + log.Tracef("requested height %v is beyond best height: ok", height) + break + } + + requestedHeaders = append(requestedHeaders, header) + } + + return requestedHeaders, nil +} + +// should be called with c.chainLock held +func (c *Chain) updateChainState(height uint64, hash ethcommon.Hash, parentHash ethcommon.Hash) int { + if len(c.chainState) == 0 { + c.chainState = append(c.chainState, blockRef{ + height: height, + hash: hash, + }) + return 1 + } + + chainHead := c.chainState[0] + + // canonical block, append immediately + if chainHead.height+1 == height && chainHead.hash == parentHash { + c.chainState = append([]blockRef{{height, hash}}, c.chainState...) + return 1 + } + + // non-canonical block in the past, ignore for now + if height <= chainHead.height { + return 0 + } + + // better block than current head, try reorganizing with new best block + missingEntries := make([]blockRef, 0, height-chainHead.height) + + headHeight := height + headHash := hash + + // build chainstate from previous head to the latest block + // e.g. suppose we had 10 on our head, and we just received block 14; we try to fill in entries 11-13 and check if that's all ok + for ; headHeight > chainHead.height; headHeight-- { + headHeader, ok := c.getBlockHeader(headHeight, headHash) + if !ok { + // TODO: log anything? chainstate can't be reconciled (maybe should just prune to head) + return 0 + } + + missingEntries = append(missingEntries, blockRef{height: headHeight, hash: headHash}) + headHash = headHeader.ParentHash + + // suppose our head is 10, and we receive block 15-100 (for some reason 11-14 are never received), then we'll switch over the chain to be 15-100 as soon as the valid chain is >= c.minValidChain length + if len(missingEntries) >= c.minValidChain { + c.chainState = missingEntries + return len(missingEntries) + } + } + + // chainstate was successfully reconciled (some entries were just missing), nothing else needed + if headHeight == chainHead.height && headHash == chainHead.hash { + c.chainState = append(missingEntries, c.chainState...) + return len(missingEntries) + } + + // reorganization is required, look back until c.maxReorg + // e.g. suppose we had 10 on our head, and we just received block 14 + // - we filled 11-13, but 10b is 11's parent, so we iterate backward until we find a common ancestor + // - for example, suppose 9 doesn't match 10b's parent, but 8 matches 9b's parent, then we stop there + // - if this takes too long (> c.maxReorg needed), then we just trim the chainstate to c.maxReorg + + i := 0 + for ; i < len(c.chainState); i++ { + chainRef := c.chainState[i] + if headHash == chainRef.hash { + // common ancestor found, break and recombine chains + break + } + + headHeader, ok := c.getBlockHeader(headHeight, headHash) + if !ok { + // TODO: log anything? chainstate can't be reconciled + return 0 + } + + missingEntries = append(missingEntries, blockRef{height: headHeight, hash: headHash}) + headHash = headHeader.ParentHash + headHeight-- + + // exceeded c.maxReorg, trim the chainstate + if i+1 >= c.maxReorg { + c.chainState = missingEntries + return len(missingEntries) + } + } + + c.chainState = append(missingEntries, c.chainState[i:]...) + return len(missingEntries) +} + +// fetches correct header from chain, not store (require lock?) +func (c *Chain) getHeaderAtHeight(height uint64) (*ethtypes.Header, error) { + if len(c.chainState) == 0 { + return nil, fmt.Errorf("%v: no header at height %v", c.chainState, height) + } + + head := c.chainState[0] + requestedIndex := int(head.height - height) + + // requested block in the future, ok to break with no header + if requestedIndex < 0 { + return nil, nil + } + + // requested block too far in the past, fail out + if requestedIndex >= len(c.chainState) { + return nil, fmt.Errorf("%v: no header at height %v", c.chainState, height) + } + + header, ok := c.getBlockHeader(height, c.chainState[requestedIndex].hash) + + // block in chainstate seems to no longer be in storage, error out + if !ok { + return nil, fmt.Errorf("%v: no header at height %v", c.chainState, height) + } + return header, nil +} + +func (c *Chain) storeBlock(block *ethtypes.Block, difficulty *big.Int, source BlockSource) { + c.storeBlockHeader(block.Header(), source) + c.storeBlockBody(block.Hash(), block.Body()) + c.storeBlockDifficulty(block.Hash(), difficulty) +} + +func (c *Chain) getBlockHeader(height uint64, hash ethcommon.Hash) (*ethtypes.Header, bool) { + headers, ok := c.getHeadersAtHeight(height) + if !ok { + return nil, ok + } + for _, header := range headers { + if bytes.Equal(header.Hash().Bytes(), hash.Bytes()) { + return header, true + } + } + return nil, false +} + +func (c *Chain) storeBlockHeader(header *ethtypes.Header, source BlockSource) { + blockHash := header.Hash() + height := header.Number.Uint64() + c.storeHeaderAtHeight(height, header) + c.storeBlockMetadata(blockHash, height, source == BSBlockchain) +} + +func (c *Chain) getBlockMetadata(hash ethcommon.Hash) (blockMetadata, bool) { + bm, ok := c.blockHashMetadata.Get(hash.String()) + if !ok { + return blockMetadata{}, ok + } + return bm.(blockMetadata), ok +} + +func (c *Chain) storeBlockMetadata(hash ethcommon.Hash, height uint64, confirmed bool) { + set := c.blockHashMetadata.SetIfAbsent(hash.String(), blockMetadata{height, false, confirmed}) + if !set { + bm, _ := c.getBlockMetadata(hash) + bm.confirmed = bm.confirmed || confirmed + c.blockHashMetadata.Set(hash.String(), bm) + } +} + +func (c *Chain) removeBlockMetadata(hash ethcommon.Hash) { + c.blockHashMetadata.Remove(hash.String()) +} + +func (c *Chain) hasHeader(hash ethcommon.Hash) bool { + // always corresponds to a header stored at c.heightToBlockHeaders + return c.blockHashMetadata.Has(hash.String()) +} + +func (c *Chain) getHeadersAtHeight(height uint64) ([]*ethtypes.Header, bool) { + rawHeaders, ok := c.heightToBlockHeaders.Get(strconv.FormatUint(height, 10)) + if !ok { + return nil, ok + } + + ethHeaders := rawHeaders.([]ethHeader) + headers := make([]*ethtypes.Header, 0, len(ethHeaders)) + + for _, eh := range ethHeaders { + headers = append(headers, eh.Header) + } + return headers, ok +} + +func (c *Chain) storeHeaderAtHeight(height uint64, header *ethtypes.Header) { + eh := ethHeader{ + Header: header, + hash: header.Hash(), + } + c.storeEthHeaderAtHeight(height, eh) +} + +// generally avoid calling this function directly +func (c *Chain) storeEthHeaderAtHeight(height uint64, eh ethHeader) { + // concurrent calls to this function are ok, only needs to be exclusionary with clean + c.headerLock.RLock() + defer c.headerLock.RUnlock() + + heightStr := strconv.FormatUint(height, 10) + + ok := c.heightToBlockHeaders.SetIfAbsent(heightStr, []ethHeader{eh}) + if !ok { + rawHeaders, _ := c.heightToBlockHeaders.Get(heightStr) + ethHeaders := rawHeaders.([]ethHeader) + ethHeaders = append(ethHeaders, eh) + c.heightToBlockHeaders.Set(heightStr, ethHeaders) + } +} + +func (c *Chain) storeBlockDifficulty(hash ethcommon.Hash, difficulty *big.Int) { + if difficulty != nil { + c.blockHashToDifficulty.Set(hash.String(), difficulty) + } +} + +func (c *Chain) getBlockDifficulty(hash ethcommon.Hash) (*big.Int, bool) { + difficulty, ok := c.blockHashToDifficulty.Get(hash.String()) + if !ok { + return nil, ok + } + return difficulty.(*big.Int), ok +} + +func (c *Chain) removeBlockDifficulty(hash ethcommon.Hash) { + c.blockHashToDifficulty.Remove(hash.String()) +} + +func (c *Chain) storeBlockBody(hash ethcommon.Hash, body *ethtypes.Body) { + c.blockHashToBody.Set(hash.String(), body) +} + +func (c *Chain) getBlockBody(hash ethcommon.Hash) (*ethtypes.Body, bool) { + body, ok := c.blockHashToBody.Get(hash.String()) + if !ok { + return nil, ok + } + return body.(*ethtypes.Body), ok +} + +func (c *Chain) hasBody(hash ethcommon.Hash) bool { + return c.blockHashToBody.Has(hash.String()) +} + +func (c *Chain) removeBlockBody(hash ethcommon.Hash) { + c.blockHashToBody.Remove(hash.String()) +} + +// removes all info corresponding to a given block in storage +func (c *Chain) pruneHash(hash ethcommon.Hash) { + c.removeBlockMetadata(hash) + c.removeBlockBody(hash) + c.removeBlockDifficulty(hash) +} + +func (c *Chain) clean(maxSize int) (lowestCleaned int, highestCleaned int, numCleaned int) { + c.headerLock.Lock() + defer c.headerLock.Unlock() + + c.chainLock.Lock() + defer c.chainLock.Unlock() + + if len(c.chainState) == 0 { + return + } + + head := c.chainState[0] + + numCleaned = 0 + lowestCleaned = int(head.height) + highestCleaned = 0 + + // minimum height to not be cleaned + minHeight := lowestCleaned - maxSize + 1 + numHeadersStored := c.heightToBlockHeaders.Count() + + if numHeadersStored >= maxSize { + for elem := range c.heightToBlockHeaders.IterBuffered() { + heightStr := elem.Key + height, err := strconv.Atoi(heightStr) + if err != nil { + log.Errorf("failed to convert height %v from string to integer: %v", heightStr, err) + continue + } + if height < minHeight { + headers := elem.Val.([]ethHeader) + c.heightToBlockHeaders.Remove(heightStr) + for _, header := range headers { + hash := header.hash + c.pruneHash(hash) + + numCleaned++ + if height < lowestCleaned { + lowestCleaned = height + } + if height > highestCleaned { + highestCleaned = height + } + } + } + } + + chainStatePruned := 0 + if len(c.chainState) > maxSize { + chainStatePruned = len(c.chainState) - maxSize + c.chainState = c.chainState[:maxSize] + } + + log.Debugf("cleaned block storage (previous size %v out of max %v): %v block headers from %v to %v, pruning %v elements off of chainstate", numHeadersStored, maxSize, numCleaned, lowestCleaned, highestCleaned, chainStatePruned) + } else { + log.Debugf("skipping block storage cleanup, only had %v block headers out of a limit of %v", numHeadersStored, maxSize) + } + return +} diff --git a/blockchain/eth/chain_test.go b/blockchain/eth/chain_test.go new file mode 100644 index 0000000..ff9beaa --- /dev/null +++ b/blockchain/eth/chain_test.go @@ -0,0 +1,396 @@ +package eth + +import ( + "context" + "github.com/bloXroute-Labs/gateway/test/bxmock" + "github.com/ethereum/go-ethereum/common" + ethtypes "github.com/ethereum/go-ethereum/core/types" + "github.com/ethereum/go-ethereum/eth/protocols/eth" + "github.com/stretchr/testify/assert" + "math/big" + "testing" + "time" +) + +func TestChain_AddBlock(t *testing.T) { + c := newChain(context.Background(), 5, 5, time.Hour, 1000) + + block1 := bxmock.NewEthBlock(1, common.Hash{}) + block2 := bxmock.NewEthBlock(2, block1.Hash()) + block3a := bxmock.NewEthBlock(3, block2.Hash()) + block3b := bxmock.NewEthBlock(3, block2.Hash()) + block4a := bxmock.NewEthBlock(4, block3a.Hash()) + block4b := bxmock.NewEthBlock(4, block3b.Hash()) + block5a := bxmock.NewEthBlock(5, block4a.Hash()) + block5b := bxmock.NewEthBlock(5, block4b.Hash()) + block6 := bxmock.NewEthBlock(6, block5b.Hash()) + + newHeads := addBlockWithTD(c, block1, block1.Difficulty()) + assert.Equal(t, 1, newHeads) + assertChainState(t, c, block1, 0, 1) + + newHeads = addBlock(c, block2) + assert.Equal(t, 1, newHeads) + assertChainState(t, c, block2, 0, 2) + assertChainState(t, c, block1, 1, 2) + + newHeads = addBlock(c, block3a) + assert.Equal(t, 1, newHeads) + assertChainState(t, c, block3a, 0, 3) + assertChainState(t, c, block2, 1, 3) + assertChainState(t, c, block1, 2, 3) + + newHeads = addBlock(c, block3b) + assert.Equal(t, 0, newHeads) + assertChainState(t, c, block3a, 0, 3) + assertChainState(t, c, block2, 1, 3) + assertChainState(t, c, block1, 2, 3) + + newHeads = addBlock(c, block4a) + assert.Equal(t, 1, newHeads) + assertChainState(t, c, block4a, 0, 4) + assertChainState(t, c, block3a, 1, 4) + assertChainState(t, c, block2, 2, 4) + assertChainState(t, c, block1, 3, 4) + + newHeads = addBlock(c, block4b) + assert.Equal(t, 0, newHeads) + assertChainState(t, c, block4a, 0, 4) + assertChainState(t, c, block3a, 1, 4) + assertChainState(t, c, block2, 2, 4) + assertChainState(t, c, block1, 3, 4) + + newHeads = addBlock(c, block5a) + assert.Equal(t, 1, newHeads) + assertChainState(t, c, block5a, 0, 5) + assertChainState(t, c, block4a, 1, 5) + assertChainState(t, c, block3a, 2, 5) + assertChainState(t, c, block2, 3, 5) + assertChainState(t, c, block1, 4, 5) + + newHeads = addBlock(c, block5b) + assert.Equal(t, 0, newHeads) + assertChainState(t, c, block5a, 0, 5) + assertChainState(t, c, block4a, 1, 5) + assertChainState(t, c, block3a, 2, 5) + assertChainState(t, c, block2, 3, 5) + assertChainState(t, c, block1, 4, 5) + + newHeads = addBlock(c, block6) + assert.Equal(t, 4, newHeads) + assertChainState(t, c, block6, 0, 6) + assertChainState(t, c, block5b, 1, 6) + assertChainState(t, c, block4b, 2, 6) + assertChainState(t, c, block3b, 3, 6) + assertChainState(t, c, block2, 4, 6) + assertChainState(t, c, block1, 5, 6) +} + +func TestChain_AddBlock_MissingBlocks(t *testing.T) { + c := newChain(context.Background(), 5, 3, time.Hour, 1000) + + block1 := bxmock.NewEthBlock(1, common.Hash{}) + block2 := bxmock.NewEthBlock(2, block1.Hash()) + block3 := bxmock.NewEthBlock(3, block2.Hash()) + block4a := bxmock.NewEthBlock(4, block3.Hash()) + block4b := bxmock.NewEthBlock(4, block3.Hash()) + block5 := bxmock.NewEthBlock(5, block4b.Hash()) + block6 := bxmock.NewEthBlock(6, block5.Hash()) + block7 := bxmock.NewEthBlock(7, block6.Hash()) + + addBlockWithTD(c, block1, block1.Difficulty()) + + newHeads := addBlock(c, block3) + assert.Equal(t, 0, newHeads) + assertChainState(t, c, block1, 0, 1) + + newHeads = addBlock(c, block2) + assert.Equal(t, 1, newHeads) + assertChainState(t, c, block2, 0, 2) + assertChainState(t, c, block1, 1, 2) + + // found block 3 and filled it in + newHeads = addBlock(c, block4a) + assert.Equal(t, 2, newHeads) + assertChainState(t, c, block4a, 0, 4) + + // add a bunch of entries that can't be added to chain (for some reason 4b is missing) + newHeads = addBlock(c, block5) + assert.Equal(t, 0, newHeads) + newHeads = addBlock(c, block6) + assert.Equal(t, 0, newHeads) + + // chain is long enough, don't care about 4b anymore + newHeads = addBlock(c, block7) + assert.Equal(t, 3, newHeads) + assertChainState(t, c, block7, 0, 3) + assertChainState(t, c, block6, 1, 3) + assertChainState(t, c, block5, 2, 3) +} + +func TestChain_AddBlock_LongFork(t *testing.T) { + c := newChain(context.Background(), 2, 5, time.Hour, 1000) + + block1 := bxmock.NewEthBlock(1, common.Hash{}) + block2a := bxmock.NewEthBlock(2, block1.Hash()) + block2b := bxmock.NewEthBlock(2, block1.Hash()) + block3a := bxmock.NewEthBlock(3, block2a.Hash()) + block3b := bxmock.NewEthBlock(3, block2b.Hash()) + block4a := bxmock.NewEthBlock(4, block3a.Hash()) + block4b := bxmock.NewEthBlock(4, block3b.Hash()) + block5a := bxmock.NewEthBlock(5, block4a.Hash()) + block5b := bxmock.NewEthBlock(5, block4b.Hash()) + block6 := bxmock.NewEthBlock(6, block5b.Hash()) + + addBlockWithTD(c, block1, block1.Difficulty()) + addBlock(c, block2a) + addBlock(c, block2b) + addBlock(c, block3a) + addBlock(c, block3b) + addBlock(c, block4a) + addBlock(c, block4b) + addBlock(c, block5a) + addBlock(c, block5b) + + assertChainState(t, c, block5a, 0, 5) + assertChainState(t, c, block4a, 1, 5) + assertChainState(t, c, block3a, 2, 5) + assertChainState(t, c, block2a, 3, 5) + assertChainState(t, c, block1, 4, 5) + + newHeads := addBlock(c, block6) + assert.Equal(t, 3, newHeads) + + assertChainState(t, c, block6, 0, 3) + assertChainState(t, c, block5b, 1, 3) + assertChainState(t, c, block4b, 2, 3) +} + +func TestChain_GetHeaders_ByNumber(t *testing.T) { + c := NewChain(context.Background()) + + // true chain: 1, 2, 3b, 4 + block1 := bxmock.NewEthBlock(1, common.Hash{}) + block2 := bxmock.NewEthBlock(2, block1.Hash()) + block3a := bxmock.NewEthBlock(3, block2.Hash()) + block3b := bxmock.NewEthBlock(3, block2.Hash()) + block4 := bxmock.NewEthBlock(4, block3b.Hash()) + + addBlockWithTD(c, block1, block1.Difficulty()) + addBlock(c, block2) + addBlock(c, block3a) + addBlock(c, block3b) + addBlock(c, block4) + + var ( + headers []*ethtypes.Header + err error + ) + + // expected: err (neither header or hash provided) + headers, err = c.GetHeaders(eth.HashOrNumber{}, 1, 0, false) + assert.Equal(t, ErrInvalidRequest, err) + + // expected: err (headers in future) + headers, err = c.GetHeaders(eth.HashOrNumber{Number: 10}, 1, 0, false) + assert.Equal(t, ErrFutureHeaders, err) + + // expected: 1 + headers, err = c.GetHeaders(eth.HashOrNumber{Number: 1}, 1, 0, false) + assert.Nil(t, err) + assert.Equal(t, 1, len(headers)) + assert.Equal(t, block1.Header(), headers[0]) + + // fork point, expected: 3b + headers, err = c.GetHeaders(eth.HashOrNumber{Number: 3}, 1, 0, false) + assert.Nil(t, err) + assert.Equal(t, 1, len(headers)) + assert.Equal(t, block3b.Header(), headers[0]) + + // expected: 1, 2, 3b, 4 + headers, err = c.GetHeaders(eth.HashOrNumber{Number: 1}, 4, 0, false) + assert.Nil(t, err) + assert.Equal(t, 4, len(headers)) + assert.Equal(t, block1.Header(), headers[0]) + assert.Equal(t, block2.Header(), headers[1]) + assert.Equal(t, block3b.Header(), headers[2]) + assert.Equal(t, block4.Header(), headers[3]) + + // expected: 1, 3b + headers, err = c.GetHeaders(eth.HashOrNumber{Number: 1}, 2, 1, false) + assert.Nil(t, err) + assert.Equal(t, 2, len(headers)) + assert.Equal(t, block1.Header(), headers[0]) + assert.Equal(t, block3b.Header(), headers[1]) + + // expected: 4, 2 + headers, err = c.GetHeaders(eth.HashOrNumber{Number: 4}, 2, 1, true) + assert.Nil(t, err) + assert.Equal(t, 2, len(headers)) + assert.Equal(t, block4.Header(), headers[0]) + assert.Equal(t, block2.Header(), headers[1]) + + // expected: 1, 2, 3b, 4 (found all that was possible) + headers, err = c.GetHeaders(eth.HashOrNumber{Number: 1}, 100, 0, false) + assert.Nil(t, err) + assert.Equal(t, 4, len(headers)) + assert.Equal(t, block1.Header(), headers[0]) + assert.Equal(t, block2.Header(), headers[1]) + assert.Equal(t, block3b.Header(), headers[2]) + assert.Equal(t, block4.Header(), headers[3]) + + // expected: err (header couldn't be located at the requested height in the past, so most create error) + headers, err = c.GetHeaders(eth.HashOrNumber{Number: 1}, 100, 0, true) + assert.NotNil(t, err) +} + +func TestChain_GetHeaders_ByHash(t *testing.T) { + c := NewChain(context.Background()) + + // true chain: 1, 2, 3b, 4 + block1 := bxmock.NewEthBlock(1, common.Hash{}) + block2 := bxmock.NewEthBlock(2, block1.Hash()) + block3a := bxmock.NewEthBlock(3, block2.Hash()) + block3b := bxmock.NewEthBlock(3, block2.Hash()) + block4 := bxmock.NewEthBlock(4, block3b.Hash()) + + addBlockWithTD(c, block1, block1.Difficulty()) + addBlock(c, block2) + addBlock(c, block3a) + addBlock(c, block3b) + addBlock(c, block4) + + var ( + headers []*ethtypes.Header + err error + ) + + // expected: 1 + headers, err = c.GetHeaders(eth.HashOrNumber{Hash: block1.Hash()}, 1, 0, false) + assert.Nil(t, err) + assert.Equal(t, 1, len(headers)) + assert.Equal(t, block1.Header(), headers[0]) + + // fork point, expected: 3a + headers, err = c.GetHeaders(eth.HashOrNumber{Hash: block3a.Hash()}, 1, 0, false) + assert.Nil(t, err) + assert.Equal(t, 1, len(headers)) + assert.Equal(t, block3a.Header(), headers[0]) + + // fork point, expected: 3b (even though it's not part of chain, still return it if requested) + headers, err = c.GetHeaders(eth.HashOrNumber{Hash: block3b.Hash()}, 1, 0, false) + assert.Nil(t, err) + assert.Equal(t, 1, len(headers)) + assert.Equal(t, block3b.Header(), headers[0]) + + // expected: 1, 2, 3b, 4 + headers, err = c.GetHeaders(eth.HashOrNumber{Hash: block1.Hash()}, 4, 0, false) + assert.Nil(t, err) + assert.Equal(t, 4, len(headers)) + assert.Equal(t, block1.Header(), headers[0]) + assert.Equal(t, block2.Header(), headers[1]) + assert.Equal(t, block3b.Header(), headers[2]) + assert.Equal(t, block4.Header(), headers[3]) + + // expected: 1, 3b + headers, err = c.GetHeaders(eth.HashOrNumber{Hash: block1.Hash()}, 2, 1, false) + assert.Nil(t, err) + assert.Equal(t, 2, len(headers)) + assert.Equal(t, block1.Header(), headers[0]) + assert.Equal(t, block3b.Header(), headers[1]) + + // expected: 4, 2 + headers, err = c.GetHeaders(eth.HashOrNumber{Hash: block4.Hash()}, 2, 1, true) + assert.Nil(t, err) + assert.Equal(t, 2, len(headers)) + assert.Equal(t, block4.Header(), headers[0]) + assert.Equal(t, block2.Header(), headers[1]) + + // expected: 1, 2, 3b, 4 (found all that was possible) + headers, err = c.GetHeaders(eth.HashOrNumber{Hash: block1.Hash()}, 100, 0, false) + assert.Nil(t, err) + assert.Equal(t, 4, len(headers)) + assert.Equal(t, block1.Header(), headers[0]) + assert.Equal(t, block2.Header(), headers[1]) + assert.Equal(t, block3b.Header(), headers[2]) + assert.Equal(t, block4.Header(), headers[3]) +} + +func TestChain_InitializeStatus(t *testing.T) { + var ok bool + c := NewChain(context.Background()) + + initialHash1 := common.Hash{1, 2, 3} + initialHash2 := common.Hash{2, 3, 4} + initialDifficulty := big.NewInt(100) + + // initialize difficulty creates entries, but doesn't modify any other state + c.InitializeDifficulty(initialHash1, initialDifficulty) + assert.Equal(t, 1, c.heightToBlockHeaders.Count()) + assert.Equal(t, 0, len(c.chainState)) + + addBlock(c, bxmock.NewEthBlock(100, common.Hash{})) + assert.Equal(t, 2, c.heightToBlockHeaders.Count()) + assert.Equal(t, 1, len(c.chainState)) + + c.InitializeDifficulty(initialHash2, initialDifficulty) + assert.Equal(t, 2, c.heightToBlockHeaders.Count()) + assert.Equal(t, 1, len(c.chainState)) + + _, ok = c.getBlockDifficulty(initialHash1) + assert.True(t, ok) + _, ok = c.getBlockDifficulty(initialHash2) + assert.True(t, ok) + + // after any cleanup call, initialized entries status will be ejected + c.clean(1) + assert.Equal(t, 1, c.heightToBlockHeaders.Count()) + assert.Equal(t, 1, len(c.chainState)) + + _, ok = c.getBlockDifficulty(initialHash1) + assert.False(t, ok) + _, ok = c.getBlockDifficulty(initialHash2) + assert.False(t, ok) +} + +func TestChain_GetNewHeadsForBDN(t *testing.T) { + c := newChain(context.Background(), 5, 5, time.Hour, 1000) + + block1 := bxmock.NewEthBlock(1, common.Hash{}) + block2 := bxmock.NewEthBlock(2, block1.Hash()) + block3 := bxmock.NewEthBlock(3, block2.Hash()) + + addBlockWithTD(c, block1, block1.Difficulty()) + addBlock(c, block2) + + blocks, err := c.GetNewHeadsForBDN(2) + assert.Nil(t, err) + assert.Equal(t, block2.Hash(), blocks[0].Block.Hash()) + assert.Equal(t, block1.Hash(), blocks[1].Block.Hash()) + + _, err = c.GetNewHeadsForBDN(3) + assert.NotNil(t, err) + + c.MarkSentToBDN(block2.Hash()) + addBlock(c, block3) + + blocks, err = c.GetNewHeadsForBDN(2) + assert.Nil(t, err) + assert.Equal(t, block3.Hash(), blocks[0].Block.Hash()) +} + +func addBlock(c *Chain, block *ethtypes.Block) int { + return addBlockWithTD(c, block, nil) +} + +func addBlockWithTD(c *Chain, block *ethtypes.Block, td *big.Int) int { + bi := NewBlockInfo(block, td) + _ = c.SetTotalDifficulty(bi) + return c.AddBlock(bi, BSBlockchain) +} + +func assertChainState(t *testing.T, c *Chain, block *ethtypes.Block, index int, length int) { + assert.Equal(t, length, len(c.chainState)) + assert.Equal(t, block.NumberU64(), c.chainState[index].height) + assert.Equal(t, block.Hash(), c.chainState[index].hash) +} diff --git a/blockchain/eth/converter.go b/blockchain/eth/converter.go new file mode 100644 index 0000000..656113d --- /dev/null +++ b/blockchain/eth/converter.go @@ -0,0 +1,128 @@ +package eth + +import ( + "fmt" + "github.com/bloXroute-Labs/gateway/types" + ethtypes "github.com/ethereum/go-ethereum/core/types" + "github.com/ethereum/go-ethereum/rlp" + "math/big" +) + +type bxBlockRLP struct { + Header rlp.RawValue + Txs []rlp.RawValue + Trailer rlp.RawValue +} + +// Converter is an Ethereum-BDN converter struct +type Converter struct{} + +// TransactionBDNToBlockchain convert a BDN transaction to an Ethereum one +func (c Converter) TransactionBDNToBlockchain(transaction *types.BxTransaction) (interface{}, error) { + var ethTransaction ethtypes.Transaction + err := rlp.DecodeBytes(transaction.Content(), ðTransaction) + return ðTransaction, err +} + +// TransactionBlockchainToBDN converts an Ethereum transaction to a BDN transaction +func (c Converter) TransactionBlockchainToBDN(i interface{}) (*types.BxTransaction, error) { + transaction := i.(*ethtypes.Transaction) + hash := NewSHA256Hash(transaction.Hash()) + + content, err := rlp.EncodeToBytes(transaction) + if err != nil { + return nil, err + } + + return types.NewRawBxTransaction(hash, content), nil +} + +// BlockBlockchainToBDN converts an Ethereum block to a BDN block +func (c Converter) BlockBlockchainToBDN(i interface{}) (*types.BxBlock, error) { + blockInfo := i.(*BlockInfo) + block := blockInfo.Block + hash := NewSHA256Hash(block.Hash()) + + encodedHeader, err := rlp.EncodeToBytes(block.Header()) + if err != nil { + return nil, fmt.Errorf("could not encode block header: %v: %v", block.Header(), err) + } + + encodedTrailer, err := rlp.EncodeToBytes(block.Uncles()) + if err != nil { + return nil, fmt.Errorf("could not encode block trailer: %v: %v", block.Uncles(), err) + } + + var txs []*types.BxBlockTransaction + for _, tx := range block.Transactions() { + txBytes, err := rlp.EncodeToBytes(tx) + if err != nil { + return nil, fmt.Errorf("could not encode transaction %v", tx) + } + + txHash := NewSHA256Hash(tx.Hash()) + compressedTx := types.NewBxBlockTransaction(txHash, txBytes) + txs = append(txs, compressedTx) + } + + difficulty := blockInfo.TotalDifficulty() + if difficulty == nil { + difficulty = big.NewInt(0) + } + return types.NewBxBlock(hash, encodedHeader, txs, encodedTrailer, difficulty, block.Number()) +} + +// BlockBDNtoBlockchain converts a BDN block to an Ethereum block +func (c Converter) BlockBDNtoBlockchain(block *types.BxBlock) (interface{}, error) { + txs := make([]rlp.RawValue, 0, len(block.Txs)) + for _, tx := range block.Txs { + txs = append(txs, tx.Content()) + } + + b, err := rlp.EncodeToBytes(bxBlockRLP{ + Header: block.Header, + Txs: txs, + Trailer: block.Trailer, + }) + if err != nil { + return nil, fmt.Errorf("could not convert block %v to blockchain format: %v", block.Hash(), err) + } + + var ethBlock ethtypes.Block + if err = rlp.DecodeBytes(b, ðBlock); err != nil { + return nil, fmt.Errorf("could not convert block %v to blockchain format: %v", block.Hash(), err) + } + return NewBlockInfo(ðBlock, block.TotalDifficulty), nil +} + +// BxBlockToCanonicFormat converts a block from BDN format to BlockNotification format +func (c Converter) BxBlockToCanonicFormat(bxBlock *types.BxBlock) (*types.BlockNotification, error) { + result, err := c.BlockBDNtoBlockchain(bxBlock) + if err != nil { + return nil, err + } + ethBlock := result.(*BlockInfo).Block + + ethTxs := make([]types.EthTransaction, 0, len(ethBlock.Transactions())) + for _, tx := range ethBlock.Transactions() { + var ethTx *types.EthTransaction + txHash := NewSHA256Hash(tx.Hash()) + ethTx, err = types.NewEthTransaction(txHash, tx, true) + if err != nil { + return nil, err + } + ethTxs = append(ethTxs, *ethTx) + } + ethUncles := make([]types.Header, 0, len(ethBlock.Uncles())) + for _, uncle := range ethBlock.Uncles() { + ethUncle := types.ConvertEthHeaderToBlockNotificationHeader(uncle) + ethUncles = append(ethUncles, *ethUncle) + } + blockNotification := types.BlockNotification{ + BlockHash: ethBlock.Hash(), + Header: types.ConvertEthHeaderToBlockNotificationHeader(ethBlock.Header()), + Transactions: ethTxs, + Uncles: ethUncles, + } + return &blockNotification, nil +} diff --git a/blockchain/eth/converter_test.go b/blockchain/eth/converter_test.go new file mode 100644 index 0000000..c3c5b81 --- /dev/null +++ b/blockchain/eth/converter_test.go @@ -0,0 +1,66 @@ +package eth + +import ( + "github.com/bloXroute-Labs/gateway/test/bxmock" + "github.com/ethereum/go-ethereum/common" + ethtypes "github.com/ethereum/go-ethereum/core/types" + "github.com/ethereum/go-ethereum/rlp" + "github.com/stretchr/testify/assert" + "math/big" + "testing" +) + +func TestConverter_Transactions(t *testing.T) { + testTransactionType(t, ethtypes.LegacyTxType) + testTransactionType(t, ethtypes.AccessListTxType) + testTransactionType(t, ethtypes.DynamicFeeTxType) +} + +func testTransactionType(t *testing.T, txType uint8) { + c := Converter{} + tx := bxmock.NewSignedEthTx(txType, 1, nil) + + bdnTx, err := c.TransactionBlockchainToBDN(tx) + assert.Nil(t, err) + + blockchainTx, err := c.TransactionBDNToBlockchain(bdnTx) + assert.Nil(t, err) + + originalEncodedBytes, err := rlp.EncodeToBytes(tx) + assert.Nil(t, err) + + processedEncodedBytes, err := rlp.EncodeToBytes(blockchainTx.(*ethtypes.Transaction)) + assert.Nil(t, err) + + assert.Equal(t, originalEncodedBytes, processedEncodedBytes) +} + +func TestConverter_Block(t *testing.T) { + c := Converter{} + block := bxmock.NewEthBlock(10, common.Hash{}) + td := big.NewInt(100) + + bxBlock, err := c.BlockBlockchainToBDN(NewBlockInfo(block, td)) + assert.Nil(t, err) + assert.Equal(t, block.Hash().Bytes(), bxBlock.Hash().Bytes()) + + blockchainBlock, err := c.BlockBDNtoBlockchain(bxBlock) + assert.Nil(t, err) + + blockInfo := blockchainBlock.(*BlockInfo) + + ethBlock := blockInfo.Block + assert.Equal(t, block.Header(), ethBlock.Header()) + for i, tx := range block.Transactions() { + assert.Equal(t, tx.Hash(), ethBlock.Transactions()[i].Hash()) + } + assert.Equal(t, block.Uncles(), ethBlock.Uncles()) + + assert.Equal(t, td, blockInfo.TotalDifficulty()) + + canonicFormat, err := c.BxBlockToCanonicFormat(bxBlock) + assert.Nil(t, err) + for i, tx := range canonicFormat.Transactions { + assert.Equal(t, tx.Hash.Bytes(), ethBlock.Transactions()[i].Hash().Bytes()) + } +} diff --git a/blockchain/eth/handlers.go b/blockchain/eth/handlers.go new file mode 100644 index 0000000..67bb48c --- /dev/null +++ b/blockchain/eth/handlers.go @@ -0,0 +1,303 @@ +package eth + +import ( + "fmt" + ethtypes "github.com/ethereum/go-ethereum/core/types" + "github.com/ethereum/go-ethereum/eth/protocols/eth" + log "github.com/sirupsen/logrus" +) + +func handleGetBlockHeaders(backend Backend, msg Decoder, peer *Peer) error { + var query eth.GetBlockHeadersPacket + if err := msg.Decode(&query); err != nil { + return fmt.Errorf("could not decode message: %v: %v", msg, err) + } + + headers, err := answerGetBlockHeaders(backend, &query, peer) + if err == ErrAncientHeaders { + go func() { + peer.Log().Debugf("requested ancient headers, fetching result from blockchain node: %v", query) + headerCh := make(chan eth.Packet) + err := peer.RequestBlockHeaderRaw(query.Origin, query.Amount, query.Skip, query.Reverse, headerCh) + if err != nil { + peer.Log().Errorf("could not request headers from peer: %v", err) + return + } + headersResponse := (<-headerCh).(*eth.BlockHeadersPacket) + peer.Log().Debugf("successfully fetched %v ancient headers from blockchain node", len(*headersResponse)) + + err = peer.SendBlockHeaders(*headersResponse) + if err != nil { + peer.Log().Errorf("could not send headers to peer: %v", err) + } + }() + return nil + } + + if err != nil { + return nil + } + return peer.SendBlockHeaders(headers) +} + +func handleGetBlockHeaders66(backend Backend, msg Decoder, peer *Peer) error { + var query eth.GetBlockHeadersPacket66 + if err := msg.Decode(&query); err != nil { + return fmt.Errorf("could not decode message: %v: %v", msg, err) + } + + headers, err := answerGetBlockHeaders(backend, query.GetBlockHeadersPacket, peer) + if err == ErrAncientHeaders { + go func() { + peer.Log().Debugf("requested (id: %v) ancient headers, fetching result from blockchain node: %v", query.RequestId, query.GetBlockHeadersPacket) + headerCh := make(chan eth.Packet) + + err := peer.RequestBlockHeaderRaw(query.Origin, query.Amount, query.Skip, query.Reverse, headerCh) + if err != nil { + peer.Log().Errorf("could not request headers from peer: %v", err) + return + } + + headersResponse := (<-headerCh).(*eth.BlockHeadersPacket) + peer.Log().Debugf("successfully fetched %v ancient headers from blockchain node (id: %v)", len(*headersResponse), query.RequestId) + + err = peer.ReplyBlockHeaders(query.RequestId, *headersResponse) + if err != nil { + peer.Log().Errorf("could not send headers to peer: %v", err) + } + }() + return nil + } + if err != nil { + return nil + } + return peer.ReplyBlockHeaders(query.RequestId, headers) +} + +func answerGetBlockHeaders(backend Backend, query *eth.GetBlockHeadersPacket, peer *Peer) ([]*ethtypes.Header, error) { + if !peer.checkpointPassed { + peer.checkpointPassed = true + return []*ethtypes.Header{}, nil + } + headers, err := backend.GetHeaders(query.Origin, int(query.Amount), int(query.Skip), query.Reverse) + + switch { + case err == ErrInvalidRequest || err == ErrAncientHeaders: + return nil, err + case err == ErrFutureHeaders: + return []*ethtypes.Header{}, nil + case err != nil: + peer.Log().Warnf("could not retrieve all %v headers starting at %v, err: %v", int(query.Amount), query.Origin, err) + return []*ethtypes.Header{}, nil + default: + return headers, nil + } +} + +func handleGetBlockBodies(backend Backend, msg Decoder, peer *Peer) error { + var query eth.GetBlockBodiesPacket + if err := msg.Decode(&query); err != nil { + return fmt.Errorf("could not decode message: %v: %v", msg, err) + } + + bodies, err := answerGetBlockBodies(backend, query) + if err != nil { + log.Errorf("error retrieving block bodies hashes %v: %v", query, err) + return err + } + return peer.SendBlockBodies(bodies) +} + +func handleGetBlockBodies66(backend Backend, msg Decoder, peer *Peer) error { + var query eth.GetBlockBodiesPacket66 + if err := msg.Decode(&query); err != nil { + return fmt.Errorf("could not decode message: %v: %v", msg, err) + } + + bodies, err := answerGetBlockBodies(backend, query.GetBlockBodiesPacket) + if err != nil { + log.Errorf("error retrieving block bodies for request ID %v, hashes %v: %v", query.RequestId, query.GetBlockBodiesPacket, err) + return err + } + return peer.ReplyBlockBodies(query.RequestId, bodies) +} + +func answerGetBlockBodies(backend Backend, query eth.GetBlockBodiesPacket) ([]*eth.BlockBody, error) { + bodies, err := backend.GetBodies(query) + if err == ErrBodyNotFound { + log.Debugf("could not find all block bodies: %v", query) + return []*eth.BlockBody{}, nil + } else if err != nil { + return nil, err + } + + blockBodies := make([]*eth.BlockBody, 0, len(bodies)) + for _, body := range bodies { + blockBody := ð.BlockBody{ + Transactions: body.Transactions, + Uncles: body.Uncles, + } + blockBodies = append(blockBodies, blockBody) + } + + return blockBodies, nil +} + +func handleNewBlockMsg(backend Backend, msg Decoder, peer *Peer) error { + var blockPacket eth.NewBlockPacket + if err := msg.Decode(&blockPacket); err != nil { + return fmt.Errorf("could not decode message %v: %v", msg, err) + } + peer.UpdateHead(blockPacket.Block.NumberU64(), blockPacket.Block.Hash()) + return backend.Handle(peer, &blockPacket) +} + +func handleTransactions(backend Backend, msg Decoder, peer *Peer) error { + var txs eth.TransactionsPacket + if err := msg.Decode(&txs); err != nil { + return fmt.Errorf("could not decode message: %v: %v", msg, err) + } + for _, tx := range txs { + log.Tracef("%v: receive tx %v", peer, tx.Hash()) + } + + return backend.Handle(peer, &txs) +} + +func handlePooledTransactions(backend Backend, msg Decoder, peer *Peer) error { + var txs eth.PooledTransactionsPacket + if err := msg.Decode(&txs); err != nil { + return fmt.Errorf("could not decode message: %v: %v", msg, err) + } + + for _, tx := range txs { + log.Tracef("%v: received pooled tx %v", peer, tx.Hash()) + } + + return backend.Handle(peer, &txs) +} + +func handlePooledTransactions66(backend Backend, msg Decoder, peer *Peer) error { + var pooledTxsResponse eth.PooledTransactionsPacket66 + if err := msg.Decode(&pooledTxsResponse); err != nil { + return fmt.Errorf("could not decode message: %v: %v", msg, err) + } + + for _, tx := range pooledTxsResponse.PooledTransactionsPacket { + log.Tracef("%v: received pooled tx %v", peer, tx.Hash()) + } + + return backend.Handle(peer, &pooledTxsResponse.PooledTransactionsPacket) +} + +func handleNewPooledTransactionHashes(backend Backend, msg Decoder, peer *Peer) error { + var txHashes eth.NewPooledTransactionHashesPacket + if err := msg.Decode(&txHashes); err != nil { + return fmt.Errorf("could not decode message: %v: %v", msg, err) + } + + for _, txHash := range txHashes { + log.Tracef("%v: received tx announcement %v", peer, txHash) + } + + return backend.Handle(peer, &txHashes) +} + +func handleNewBlockHashes(backend Backend, msg Decoder, peer *Peer) error { + var blockHashes eth.NewBlockHashesPacket + if err := msg.Decode(&blockHashes); err != nil { + return fmt.Errorf("could not decode message: %v: %v", msg, err) + } + + updatePeerHeadFromNewHashes(blockHashes, peer) + return backend.Handle(peer, &blockHashes) +} + +func handleBlockHeaders(backend Backend, msg Decoder, peer *Peer) error { + var blockHeaders eth.BlockHeadersPacket + if err := msg.Decode(&blockHeaders); err != nil { + return fmt.Errorf("could not decode message: %v: %v", msg, err) + } + + updatePeerHeadFromHeaders(blockHeaders, peer) + handled := peer.NotifyResponse(&blockHeaders) + + if handled { + return nil + } + return backend.Handle(peer, &blockHeaders) +} + +func handleBlockHeaders66(backend Backend, msg Decoder, peer *Peer) error { + var blockHeaders eth.BlockHeadersPacket66 + if err := msg.Decode(&blockHeaders); err != nil { + return fmt.Errorf("could not decode message: %v: %v", msg, err) + } + + updatePeerHeadFromHeaders(blockHeaders.BlockHeadersPacket, peer) + handled, err := peer.NotifyResponse66(blockHeaders.RequestId, &blockHeaders.BlockHeadersPacket) + + if err != nil { + return err + } + + if handled { + return nil + } + + return backend.Handle(peer, &blockHeaders.BlockHeadersPacket) +} + +func handleBlockBodies(backend Backend, msg Decoder, peer *Peer) error { + var blockBodies eth.BlockBodiesPacket + if err := msg.Decode(&blockBodies); err != nil { + return fmt.Errorf("could not decode message: %v: %v", msg, err) + } + + peer.NotifyResponse(&blockBodies) + return nil +} + +func handleBlockBodies66(backend Backend, msg Decoder, peer *Peer) error { + var blockBodies eth.BlockBodiesPacket66 + if err := msg.Decode(&blockBodies); err != nil { + return fmt.Errorf("could not decode message: %v: %v", msg, err) + } + + _, err := peer.NotifyResponse66(blockBodies.RequestId, &blockBodies.BlockBodiesPacket) + return err +} + +func updatePeerHeadFromHeaders(headers eth.BlockHeadersPacket, peer *Peer) { + if len(headers) > 0 { + maxHeight := headers[0].Number + hash := headers[0].Hash() + for _, header := range headers[1:] { + number := header.Number + if number.Cmp(maxHeight) == 1 { + maxHeight = number + hash = header.Hash() + } + } + peer.UpdateHead(maxHeight.Uint64(), hash) + } +} + +func updatePeerHeadFromNewHashes(newBlocks eth.NewBlockHashesPacket, peer *Peer) { + if len(newBlocks) > 0 { + maxHeight := newBlocks[0].Number + hash := newBlocks[0].Hash + for _, newBlock := range newBlocks[1:] { + number := newBlock.Number + if number > maxHeight { + maxHeight = number + hash = newBlock.Hash + } + } + peer.UpdateHead(maxHeight, hash) + } +} + +func handleUnimplemented(backend Backend, msg Decoder, peer *Peer) error { + return nil +} diff --git a/blockchain/eth/peer.go b/blockchain/eth/peer.go new file mode 100644 index 0000000..e70da04 --- /dev/null +++ b/blockchain/eth/peer.go @@ -0,0 +1,575 @@ +package eth + +import ( + "context" + "errors" + "fmt" + "github.com/bloXroute-Labs/gateway/types" + "github.com/bloXroute-Labs/gateway/utils" + "github.com/ethereum/go-ethereum/common" + ethtypes "github.com/ethereum/go-ethereum/core/types" + "github.com/ethereum/go-ethereum/eth/protocols/eth" + "github.com/ethereum/go-ethereum/p2p" + cmap "github.com/orcaman/concurrent-map" + log "github.com/sirupsen/logrus" + "math/big" + "math/rand" + "strconv" + "time" +) + +const ( + maxMessageSize = 10 * 1024 * 1024 + responseQueueSize = 10 + responseTimeout = 5 * time.Minute + fastBlockConfirmationInterval = 200 * time.Millisecond + fastBlockConfirmationAttempts = 5 + slowBlockConfirmationInterval = 1 * time.Second + headChannelBacklog = 10 + blockChannelBacklog = 10 + blockConfirmationChannelBacklog = 10 + blockQueueMaxSize = 50 +) + +// special error constants during peer message processing +var ( + ErrResponseTimeout = errors.New("response timed out") + ErrUnknownRequestID = errors.New("unknown request ID on message") +) + +// Peer wraps an Ethereum peer structure +type Peer struct { + p *p2p.Peer + rw p2p.MsgReadWriter + version uint + endpoint types.NodeEndpoint + clock utils.Clock + ctx context.Context + cancel context.CancelFunc + log *log.Entry + + disconnected bool + checkpointPassed bool + + responseQueue chan chan eth.Packet // chan is used as a concurrency safe queue + responseQueue66 cmap.ConcurrentMap + + newHeadCh chan blockRef + newBlockCh chan *eth.NewBlockPacket + blockConfirmationCh chan common.Hash + confirmedHead blockRef + sentHead blockRef + queuedBlocks []*eth.NewBlockPacket + + RequestConfirmations bool +} + +// NewPeer returns a wrapped Ethereum peer +func NewPeer(ctx context.Context, p *p2p.Peer, rw p2p.MsgReadWriter, version uint) *Peer { + return newPeer(ctx, p, rw, version, utils.RealClock{}) +} + +func newPeer(parent context.Context, p *p2p.Peer, rw p2p.MsgReadWriter, version uint, clock utils.Clock) *Peer { + ctx, cancel := context.WithCancel(parent) + peer := &Peer{ + p: p, + rw: rw, + version: version, + ctx: ctx, + cancel: cancel, + endpoint: types.NodeEndpoint{IP: p.Node().IP().String(), Port: p.Node().TCP(), PublicKey: p.Info().Enode}, + clock: clock, + newHeadCh: make(chan blockRef, headChannelBacklog), + newBlockCh: make(chan *eth.NewBlockPacket, blockChannelBacklog), + blockConfirmationCh: make(chan common.Hash, blockConfirmationChannelBacklog), + queuedBlocks: make([]*eth.NewBlockPacket, 0), + responseQueue: make(chan chan eth.Packet, responseQueueSize), + responseQueue66: cmap.New(), + RequestConfirmations: true, + } + peerID := p.ID() + peer.log = log.WithFields(log.Fields{ + "connType": "ETH", + "remoteAddr": p.RemoteAddr(), + "id": fmt.Sprintf("%x", peerID[:8]), + }) + return peer +} + +// ID provides a unique identifier for each Ethereum peer +func (ep *Peer) ID() string { + return ep.p.ID().String() +} + +// String formats the peer for display +func (ep *Peer) String() string { + id := ep.p.ID() + return fmt.Sprintf("ETH/%x@%v", id[:8], ep.p.RemoteAddr()) +} + +// IPEndpoint provides the peer IP endpoint +func (ep *Peer) IPEndpoint() types.NodeEndpoint { + return ep.endpoint +} + +// Log returns the context logger for the peer connection +func (ep *Peer) Log() *log.Entry { + return ep.log.WithField("head", ep.confirmedHead) +} + +// Disconnect closes the running peer with a protocol error +func (ep *Peer) Disconnect(reason p2p.DiscReason) { + ep.p.Disconnect(reason) + ep.disconnected = true +} + +// Start launches the block sending loop that queued blocks get sent in order to the peer +func (ep *Peer) Start() { + go ep.blockLoop() +} + +// Stop shuts down the running goroutines +func (ep *Peer) Stop() { + ep.cancel() +} + +func (ep *Peer) isVersion66() bool { + return ep.version >= eth.ETH66 +} + +func (ep *Peer) blockLoop() { + for { + select { + case newHead := <-ep.newHeadCh: + if newHead.height < ep.confirmedHead.height { + break + } + + // update most recent blocks + ep.confirmedHead = newHead + if newHead.height >= ep.sentHead.height { + ep.sentHead = newHead + } + + // check if any blocks need to be released and determine number of blocks to prune + var ( + breakpoint = -1 + releaseBlock = false + ) + for i, qb := range ep.queuedBlocks { + queuedBlock := qb.Block + + nextHeight := queuedBlock.NumberU64() == ep.confirmedHead.height+1 + isNextBlock := nextHeight && queuedBlock.ParentHash() == ep.confirmedHead.hash + exceedsHeight := queuedBlock.NumberU64() > ep.confirmedHead.height+1 + + // next block: stop here and release + if isNextBlock { + breakpoint = i + releaseBlock = true + break + } + // next height: could be another block at same height, set breakpoint and continue + if nextHeight { + breakpoint = i + } + // exceeds height: set breakpoint if not set previously from block at "nextHeight" and break + if exceedsHeight { + if breakpoint == -1 { + breakpoint = i + } + break + } + } + + // no blocks match, so all must be stale + if breakpoint == -1 { + breakpoint = len(ep.queuedBlocks) + } + + // prune blocks + for _, prunedBlock := range ep.queuedBlocks[:breakpoint] { + ep.Log().Debugf("pruning now stale block %v at height %v after confirming %v", prunedBlock.Block.Hash(), prunedBlock.Block.NumberU64(), newHead) + } + ep.queuedBlocks = ep.queuedBlocks[breakpoint:] + + // release block if possible + if releaseBlock { + ep.sendTopBlock() + } + case newBlock := <-ep.newBlockCh: + newBlockNumber := newBlock.Block.NumberU64() + newBlockHash := newBlock.Block.Hash() + + // always ignore stales blocks + if newBlockNumber <= ep.sentHead.height && ep.sentHead.height != 0 { + ep.Log().Debugf("skipping queued stale block %v at height %v, already sent block of height %v", newBlockHash, newBlockNumber, ep.sentHead) + break + } + + // release next block if ready + nextBlock := newBlockNumber == ep.confirmedHead.height+1 && newBlock.Block.ParentHash() == ep.confirmedHead.hash + initialBlock := ep.confirmedHead.height == 0 && ep.sentHead.height == 0 && newBlock.Block.ParentHash() == ep.confirmedHead.hash + if nextBlock || initialBlock { + ep.Log().Debugf("queued block %v at height %v is next expected (%v), sending immediately", newBlockHash, newBlockNumber, ep.confirmedHead.height+1) + err := ep.sendNewBlock(newBlock) + if err != nil { + ep.Log().Errorf("could not send block %v to peer %v: %v", newBlockHash, ep, err) + } + ep.sentHead = blockRef{ + height: newBlockNumber, + hash: newBlockHash, + } + } else { + // find position in block queue that block should be inserted at + insertionPoint := len(ep.queuedBlocks) + for i, block := range ep.queuedBlocks { + if block.Block.NumberU64() > newBlock.Block.NumberU64() { + insertionPoint = i + } + } + + ep.Log().Debugf("queuing block %v at height %v behind %v blocks", newBlockHash, newBlockNumber, insertionPoint) + + // insert block at insertion point + ep.queuedBlocks = append(ep.queuedBlocks, ð.NewBlockPacket{}) + copy(ep.queuedBlocks[insertionPoint+1:], ep.queuedBlocks[insertionPoint:]) + ep.queuedBlocks[insertionPoint] = newBlock + + if len(ep.queuedBlocks) > blockQueueMaxSize { + ep.queuedBlocks = ep.queuedBlocks[len(ep.queuedBlocks)-blockQueueMaxSize:] + } + } + case <-ep.ctx.Done(): + return + } + } +} + +// should only be called by blockLoop when then length of the queue has already been checked +func (ep *Peer) sendTopBlock() { + block := ep.queuedBlocks[0] + ep.queuedBlocks = ep.queuedBlocks[1:] + ep.sentHead = blockRef{ + height: block.Block.NumberU64(), + hash: block.Block.Hash(), + } + + ep.Log().Debugf("after confirming height %v, immediately sending next block %v at height %v", ep.confirmedHead.height, block.Block.Hash(), block.Block.NumberU64()) + err := ep.sendNewBlock(block) + if err != nil { + ep.Log().Errorf("could not send block %v: %v", block.Block.Hash(), err) + } +} + +// Handshake executes the Ethereum protocol Handshake. Unlike Geth, the gateway waits for the peer status message before sending its own, in order to replicate some peer status fields. +func (ep *Peer) Handshake(version uint32, network uint64, td *big.Int, head common.Hash, genesis common.Hash) (*eth.StatusPacket, error) { + peerStatus, err := ep.readStatus() + if err != nil { + return peerStatus, err + } + + ep.confirmedHead = blockRef{hash: head} + + // used same fork ID as received from peer; gateway is expected to usually be compatible with Ethereum peer + err = ep.send(eth.StatusMsg, ð.StatusPacket{ + ProtocolVersion: version, + NetworkID: network, + TD: td, + Head: head, + Genesis: genesis, + ForkID: peerStatus.ForkID, + }) + + if peerStatus.NetworkID != network { + return peerStatus, fmt.Errorf("network ID does not match: expected %v, but got %v", network, peerStatus.NetworkID) + } + + if peerStatus.ProtocolVersion != version { + return peerStatus, fmt.Errorf("protocol version does not match: expected %v, but got %v", version, peerStatus.ProtocolVersion) + } + + if peerStatus.Genesis != genesis { + return peerStatus, fmt.Errorf("genesis block does not match: expected %v, but got %v", genesis, peerStatus.Genesis) + } + + return peerStatus, nil +} + +func (ep *Peer) readStatus() (*eth.StatusPacket, error) { + var status eth.StatusPacket + + msg, err := ep.rw.ReadMsg() + if err != nil { + return nil, err + } + + defer func() { + _ = msg.Discard() + }() + + if msg.Code != eth.StatusMsg { + return &status, fmt.Errorf("unexpected first message: %v, should have been %v", msg.Code, eth.StatusMsg) + } + + if msg.Size > maxMessageSize { + return &status, fmt.Errorf("message is too big: %v > %v", msg.Size, maxMessageSize) + } + + if err = msg.Decode(&status); err != nil { + return &status, fmt.Errorf("could not decode status message: %v", err) + } + + return &status, nil +} + +// NotifyResponse informs any listeners dependent on a request/response call to this peer, indicating if any channels were waiting for the message +func (ep *Peer) NotifyResponse(packet eth.Packet) bool { + responseCh := <-ep.responseQueue + if responseCh != nil { + responseCh <- packet + } + return responseCh != nil +} + +// NotifyResponse66 informs any listeners dependent on a request/response call to this ETH66 peer, indicating if any channels were waiting for the message +func (ep *Peer) NotifyResponse66(requestID uint64, packet eth.Packet) (bool, error) { + rawResponseCh, ok := ep.responseQueue66.Pop(convertRequestIDKey(requestID)) + if !ok { + return false, ErrUnknownRequestID + } + + responseCh := rawResponseCh.(chan eth.Packet) + if responseCh != nil { + responseCh <- packet + } + return responseCh != nil, nil +} + +// UpdateHead sets the latest confirmed block on the peer. This may release or prune queued blocks on the peer connection. +func (ep *Peer) UpdateHead(height uint64, hash common.Hash) { + ep.Log().Debugf("confirming new head (height=%v, hash=%v)", height, hash) + ep.newHeadCh <- blockRef{ + height: height, + hash: hash, + } +} + +// QueueNewBlock adds a new block to the queue to be sent to the peer in the order the peer is ready for. +func (ep *Peer) QueueNewBlock(block *ethtypes.Block, td *big.Int) { + packet := eth.NewBlockPacket{ + Block: block, + TD: td, + } + ep.newBlockCh <- &packet +} + +// AnnounceBlock pushes a new block announcement to the peer. This is used when the total difficult is unknown, and so a new block message would be invalid. +func (ep *Peer) AnnounceBlock(hash common.Hash, number uint64) error { + packet := eth.NewBlockHashesPacket{ + {Hash: hash, Number: number}, + } + return ep.send(eth.NewBlockHashesMsg, packet) +} + +// SendBlockBodies sends a batch of block bodies to the peer +func (ep *Peer) SendBlockBodies(bodies []*eth.BlockBody) error { + return ep.send(eth.BlockBodiesMsg, eth.BlockBodiesPacket(bodies)) +} + +// ReplyBlockBodies sends a batch of requested block bodies to the peer (ETH66) +func (ep *Peer) ReplyBlockBodies(id uint64, bodies []*eth.BlockBody) error { + return ep.send(eth.BlockBodiesMsg, eth.BlockBodiesPacket66{ + RequestId: id, + BlockBodiesPacket: bodies, + }) +} + +// SendBlockHeaders sends a batch of block headers to the peer +func (ep *Peer) SendBlockHeaders(headers []*ethtypes.Header) error { + return ep.send(eth.BlockHeadersMsg, eth.BlockHeadersPacket(headers)) +} + +// ReplyBlockHeaders sends batch of requested block headers to the peer (ETH66) +func (ep *Peer) ReplyBlockHeaders(id uint64, headers []*ethtypes.Header) error { + return ep.send(eth.BlockHeadersMsg, eth.BlockHeadersPacket66{ + RequestId: id, + BlockHeadersPacket: headers, + }) +} + +// SendTransactions sends pushes a batch of transactions to the peer +func (ep *Peer) SendTransactions(txs ethtypes.Transactions) error { + return ep.send(eth.TransactionsMsg, txs) +} + +// RequestTransactions requests a batch of announced transactions from the peer +func (ep *Peer) RequestTransactions(txHashes []common.Hash) error { + packet := eth.GetPooledTransactionsPacket(txHashes) + if ep.isVersion66() { + return ep.send(eth.GetPooledTransactionsMsg, eth.GetPooledTransactionsPacket66{ + RequestId: rand.Uint64(), + GetPooledTransactionsPacket: packet, + }) + } + return ep.send(eth.GetPooledTransactionsMsg, packet) +} + +// RequestBlock fetches the specified block from the peer, pushing the components to the channels upon request/response completion. +func (ep *Peer) RequestBlock(blockHash common.Hash, headersCh chan eth.Packet, bodiesCh chan eth.Packet) error { + if ep.isVersion66() { + panic("unexpected call to request block for a ETH66 peer") + } + + getHeadersPacket := ð.GetBlockHeadersPacket{ + Origin: eth.HashOrNumber{Hash: blockHash}, + Amount: 1, + Skip: 0, + Reverse: false, + } + + getBodiesPacket := eth.GetBlockBodiesPacket{blockHash} + + ep.registerForResponse(headersCh) + ep.registerForResponse(bodiesCh) + + if err := ep.send(eth.GetBlockHeadersMsg, getHeadersPacket); err != nil { + return err + } + + return ep.send(eth.GetBlockBodiesMsg, getBodiesPacket) +} + +// RequestBlock66 fetches the specified block from the ETH66 peer, pushing the components to the channels upon request/response completion. +func (ep *Peer) RequestBlock66(blockHash common.Hash, headersCh chan eth.Packet, bodiesCh chan eth.Packet) error { + if !ep.isVersion66() { + panic("unexpected call to request block 66 for a = blockNumber { + return true + } + + err := ep.RequestBlockHeader(blockHash) + if err != nil { + return true + } + + // TODO: implement ticker for clock + return false + } + + // check for fast confirmation + for count < fastBlockConfirmationAttempts { + if wait(fastBlockConfirmationInterval) { + return + } + count++ + } + + // check for slower confirmation + for { + if wait(slowBlockConfirmationInterval) { + return + } + } + }() + } + return nil +} + +func (ep *Peer) registerForResponse(responseCh chan eth.Packet) { + ep.responseQueue <- responseCh +} + +func (ep *Peer) registerForResponse66(requestID uint64, responseCh chan eth.Packet) { + ep.responseQueue66.Set(convertRequestIDKey(requestID), responseCh) +} + +func (ep *Peer) send(msgCode uint64, data interface{}) error { + if ep.disconnected { + return nil + } + return p2p.Send(ep.rw, msgCode, data) +} + +func convertRequestIDKey(requestID uint64) string { + return strconv.FormatUint(requestID, 10) +} diff --git a/blockchain/eth/peer_test.go b/blockchain/eth/peer_test.go new file mode 100644 index 0000000..9513234 --- /dev/null +++ b/blockchain/eth/peer_test.go @@ -0,0 +1,237 @@ +package eth + +import ( + "context" + "github.com/bloXroute-Labs/gateway/blockchain/eth/test" + "github.com/bloXroute-Labs/gateway/test/bxmock" + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/core/forkid" + "github.com/ethereum/go-ethereum/eth/protocols/eth" + "github.com/ethereum/go-ethereum/p2p" + "github.com/stretchr/testify/assert" + "math/big" + "testing" + "time" +) + +func testPeer(writeChannelSize int) (*Peer, *test.MsgReadWriter) { + rw := test.NewMsgReadWriter(100, writeChannelSize) + peer := newPeer(context.Background(), p2p.NewPeerPipe(test.GenerateEnodeID(), "test peer", []p2p.Cap{}, nil), rw, 0, &bxmock.MockClock{}) + return peer, rw +} + +func TestPeer_Handshake(t *testing.T) { + peer, rw := testPeer(-1) + + peerStatus := eth.StatusPacket{ + ProtocolVersion: 1, + NetworkID: 1, + TD: big.NewInt(10), + Head: common.Hash{1, 2, 3}, + Genesis: common.Hash{2, 3, 4}, + ForkID: forkid.ID{}, + } + + // matching parameters + rw.QueueIncomingMessage(eth.StatusMsg, peerStatus) + ps, err := peer.Handshake(1, 1, new(big.Int), common.Hash{1, 2, 3}, common.Hash{2, 3, 4}) + assert.Nil(t, err) + assert.Equal(t, peerStatus, *ps) + + // outgoing status message enqueued + assert.Equal(t, 1, len(rw.WriteMessages)) + assert.Equal(t, uint64(eth.StatusMsg), rw.WriteMessages[0].Code) + + // version mismatch + rw.QueueIncomingMessage(eth.StatusMsg, peerStatus) + _, err = peer.Handshake(0, 1, new(big.Int), common.Hash{1, 2, 3}, common.Hash{2, 3, 4}) + assert.NotNil(t, err) + + // network mismatch + rw.QueueIncomingMessage(eth.StatusMsg, peerStatus) + _, err = peer.Handshake(1, 0, new(big.Int), common.Hash{1, 2, 3}, common.Hash{2, 3, 4}) + assert.NotNil(t, err) + + // head mismatch is ok + rw.QueueIncomingMessage(eth.StatusMsg, peerStatus) + _, err = peer.Handshake(1, 1, new(big.Int), common.Hash{3, 4, 5}, common.Hash{2, 3, 4}) + assert.Nil(t, err) + + // genesis mismatch + rw.QueueIncomingMessage(eth.StatusMsg, peerStatus) + _, err = peer.Handshake(1, 1, new(big.Int), common.Hash{1, 2, 3}, common.Hash{3, 3, 4}) + assert.NotNil(t, err) +} + +func TestPeer_SendNewBlock(t *testing.T) { + var ( + msg p2p.Msg + err error + ) + + peer, rw := testPeer(1) + maxWriteTimeout := time.Millisecond // to allow for blockLoop goroutine to write to buffer + clock := peer.clock.(*bxmock.MockClock) + go peer.Start() + + block1a := bxmock.NewEthBlock(1, common.Hash{}) + block1b := bxmock.NewEthBlock(1, common.Hash{}) + block2a := bxmock.NewEthBlock(2, block1a.Hash()) + block2b := bxmock.NewEthBlock(2, block1b.Hash()) + block3 := bxmock.NewEthBlock(3, block2b.Hash()) + block4 := bxmock.NewEthBlock(4, block3.Hash()) + block5a := bxmock.NewEthBlock(5, common.Hash{}) + block5b := bxmock.NewEthBlock(5, block4.Hash()) + + peer.confirmedHead = blockRef{0, block1a.ParentHash()} + peer.QueueNewBlock(block1a, big.NewInt(10)) + + // block 1a instantly sent (first block) + assert.True(t, rw.ExpectWrite(maxWriteTimeout)) + assert.Equal(t, 1, len(rw.WriteMessages)) + + msg = rw.WriteMessages[0] + var blockPacket1 eth.NewBlockPacket + assert.Equal(t, uint64(eth.NewBlockMsg), msg.Code) + err = msg.Decode(&blockPacket1) + assert.Nil(t, err) + assert.Equal(t, block1a.Hash(), blockPacket1.Block.Hash()) + + // confirm block 1b + peer.UpdateHead(1, block1b.Hash()) + + // block 1b ignored since stale + peer.QueueNewBlock(block1b, big.NewInt(10)) + assert.False(t, rw.ExpectWrite(maxWriteTimeout)) + assert.Equal(t, 1, len(rw.WriteMessages)) + + // block 3 queued for a while + peer.QueueNewBlock(block3, big.NewInt(10)) + assert.False(t, rw.ExpectWrite(maxWriteTimeout)) + assert.Equal(t, 1, len(rw.WriteMessages)) + + // block 2b will be instantly sent, 2a will never be sent (1b was the confirmation) + peer.QueueNewBlock(block2a, big.NewInt(10)) + peer.QueueNewBlock(block2b, big.NewInt(10)) + + // block 2b instantly sent + assert.True(t, rw.ExpectWrite(maxWriteTimeout)) + assert.Equal(t, 2, len(rw.WriteMessages)) + + var blockPacket2 eth.NewBlockPacket + msg = rw.WriteMessages[1] + assert.Equal(t, uint64(eth.NewBlockMsg), msg.Code) + err = msg.Decode(&blockPacket2) + assert.Nil(t, err) + assert.Equal(t, block2b.Hash(), blockPacket2.Block.Hash()) + + peer.QueueNewBlock(block4, big.NewInt(10)) + peer.QueueNewBlock(block5a, big.NewInt(10)) + peer.QueueNewBlock(block5b, big.NewInt(10)) + + // confirm block 3, so skip block 3 and go directly to 4 + peer.UpdateHead(3, block3.Hash()) + assert.True(t, rw.ExpectWrite(maxWriteTimeout)) + assert.Equal(t, 3, len(rw.WriteMessages)) + + var blockPacket4 eth.NewBlockPacket + msg = rw.WriteMessages[2] + assert.Equal(t, uint64(eth.NewBlockMsg), msg.Code) + err = msg.Decode(&blockPacket4) + assert.Nil(t, err) + assert.Equal(t, block4.Hash(), blockPacket4.Block.Hash()) + + // next block will never be released without confirmation + clock.IncTime(100 * time.Second) + assert.False(t, rw.ExpectWrite(maxWriteTimeout)) + + // confirm block 4, 5b should be released (even though it's queued second at height 5) + peer.UpdateHead(4, block4.Hash()) + + assert.True(t, rw.ExpectWrite(maxWriteTimeout)) + assert.Equal(t, 4, len(rw.WriteMessages)) + + var blockPacket5 eth.NewBlockPacket + msg = rw.WriteMessages[3] + assert.Equal(t, uint64(eth.NewBlockMsg), msg.Code) + err = msg.Decode(&blockPacket5) + assert.Nil(t, err) + assert.Equal(t, block5b.Hash(), blockPacket5.Block.Hash()) +} + +func TestPeer_SendNewBlock_HeadUpdates(t *testing.T) { + var ( + msg p2p.Msg + err error + ) + + peer, rw := testPeer(1) + maxWriteTimeout := time.Millisecond // to allow for blockLoop goroutine to write to buffer + go peer.Start() + + block1 := bxmock.NewEthBlock(1, common.Hash{}) + block2 := bxmock.NewEthBlock(2, block1.Hash()) + block3 := bxmock.NewEthBlock(3, block2.Hash()) + block4a := bxmock.NewEthBlock(4, block3.Hash()) + block4b := bxmock.NewEthBlock(4, block3.Hash()) + block5b := bxmock.NewEthBlock(5, block4b.Hash()) + + peer.confirmedHead = blockRef{1, block1.Hash()} + + peer.QueueNewBlock(block3, big.NewInt(30)) + peer.QueueNewBlock(block4a, big.NewInt(41)) + peer.QueueNewBlock(block4b, big.NewInt(42)) + peer.QueueNewBlock(block5b, big.NewInt(52)) + + // process all queue entries first + time.Sleep(time.Millisecond) + + // queue before 5b should be cleared out + peer.UpdateHead(4, block4a.Hash()) + assert.False(t, rw.ExpectWrite(maxWriteTimeout)) + assert.Equal(t, 1, len(peer.queuedBlocks)) + + // 5b queued since wrong parent, but once 4b confirmed should be released + peer.UpdateHead(4, block4b.Hash()) + + assert.True(t, rw.ExpectWrite(maxWriteTimeout)) + assert.Equal(t, 1, len(rw.WriteMessages)) + + var blockPacket eth.NewBlockPacket + msg = rw.WriteMessages[0] + assert.Equal(t, uint64(eth.NewBlockMsg), msg.Code) + err = msg.Decode(&blockPacket) + assert.Nil(t, err) + assert.Equal(t, block5b.Hash(), blockPacket.Block.Hash()) +} + +func TestPeer_RequestBlockHeaderNonBlocking(t *testing.T) { + var ( + msg p2p.Msg + err error + ) + + peer, rw := testPeer(-1) + peer.version = eth.ETH66 + + err = peer.RequestBlockHeader(common.Hash{}) + assert.Nil(t, err) + + assert.Equal(t, 1, len(rw.WriteMessages)) + + var getHeaders eth.GetBlockHeadersPacket66 + msg = rw.WriteMessages[0] + err = msg.Decode(&getHeaders) + assert.Nil(t, err) + + requestID := getHeaders.RequestId + rw.QueueIncomingMessage(eth.BlockHeadersMsg, eth.BlockHeadersPacket66{ + RequestId: requestID, + BlockHeadersPacket: nil, + }) + + // should not block, since no response needed + handled, err := peer.NotifyResponse66(requestID, nil) + assert.False(t, handled) + assert.Nil(t, err) +} diff --git a/blockchain/eth/peerset.go b/blockchain/eth/peerset.go new file mode 100644 index 0000000..eb505ad --- /dev/null +++ b/blockchain/eth/peerset.go @@ -0,0 +1,60 @@ +package eth + +import ( + "errors" + "sync" +) + +type peerSet struct { + peers map[string]*Peer + lock sync.RWMutex +} + +func newPeerSet() *peerSet { + return &peerSet{ + peers: make(map[string]*Peer), + } +} + +func (ps *peerSet) register(ep *Peer) error { + ps.lock.Lock() + defer ps.lock.Unlock() + + id := ep.ID() + if _, ok := ps.peers[id]; ok { + return errors.New("peer already registered") + } + ps.peers[ep.ID()] = ep + return nil +} + +func (ps *peerSet) unregister(id string) error { + ps.lock.Lock() + defer ps.lock.Unlock() + + if _, ok := ps.peers[id]; !ok { + return errors.New("peer does not exist") + } + + delete(ps.peers, id) + return nil +} + +func (ps *peerSet) get(id string) (*Peer, bool) { + ps.lock.RLock() + defer ps.lock.RUnlock() + + p, ok := ps.peers[id] + return p, ok +} + +func (ps *peerSet) getAll() []*Peer { + ps.lock.RLock() + defer ps.lock.RUnlock() + + peers := make([]*Peer, 0, len(ps.peers)) + for _, peer := range ps.peers { + peers = append(peers, peer) + } + return peers +} diff --git a/blockchain/eth/protocol.go b/blockchain/eth/protocol.go new file mode 100644 index 0000000..f9ba7c6 --- /dev/null +++ b/blockchain/eth/protocol.go @@ -0,0 +1,126 @@ +package eth + +import ( + "context" + "github.com/ethereum/go-ethereum/eth/protocols/eth" + "github.com/ethereum/go-ethereum/p2p" + "github.com/ethereum/go-ethereum/p2p/enode" + log "github.com/sirupsen/logrus" +) + +// SupportedProtocols is the list of all Ethereum devp2p protocols supported by this client +var SupportedProtocols = []uint{ + eth.ETH65, eth.ETH66, +} + +// ProtocolLengths is a mapping of each supported devp2p protocol to its message version length +var ProtocolLengths = map[uint]uint64{eth.ETH65: 17, eth.ETH66: 17} + +// MakeProtocols generates the set of supported protocols structs for the p2p server +func MakeProtocols(ctx context.Context, backend Backend) []p2p.Protocol { + protocols := make([]p2p.Protocol, 0, len(SupportedProtocols)) + for _, version := range SupportedProtocols { + protocols = append(protocols, makeProtocol(ctx, backend, version, ProtocolLengths[version])) + } + return protocols +} + +func makeProtocol(ctx context.Context, backend Backend, version uint, versionLength uint64) p2p.Protocol { + return p2p.Protocol{ + Name: "eth", + Version: version, + Length: versionLength, + Run: func(p *p2p.Peer, rw p2p.MsgReadWriter) error { + ep := NewPeer(ctx, p, rw, version) + + config := backend.NetworkConfig() + peerStatus, err := ep.Handshake(uint32(version), config.Network, config.TotalDifficulty, config.Head, config.Genesis) + if err != nil { + return err + } + + // process status message on backend to set initial total difficulty + _ = backend.Handle(ep, peerStatus) + + return backend.RunPeer(ep, func(peer *Peer) error { + for { + if err = handleMessage(backend, ep); err != nil { + return err + } + } + }) + }, + NodeInfo: func() interface{} { + return nil + }, + PeerInfo: func(id enode.ID) interface{} { + return nil + }, + } +} + +type msgHandler func(backend Backend, msg Decoder, peer *Peer) error + +// Decoder represents any struct that can be decoded into an Ethereum message type +type Decoder interface { + Decode(val interface{}) error +} + +var eth65 = map[uint64]msgHandler{ + eth.GetBlockHeadersMsg: handleGetBlockHeaders, + eth.BlockHeadersMsg: handleBlockHeaders, + eth.GetBlockBodiesMsg: handleGetBlockBodies, + eth.BlockBodiesMsg: handleBlockBodies, + eth.GetNodeDataMsg: handleUnimplemented, + eth.NodeDataMsg: handleUnimplemented, + eth.GetReceiptsMsg: handleUnimplemented, + eth.ReceiptsMsg: handleUnimplemented, + eth.NewBlockHashesMsg: handleNewBlockHashes, + eth.NewBlockMsg: handleNewBlockMsg, + eth.TransactionsMsg: handleTransactions, + eth.NewPooledTransactionHashesMsg: handleNewPooledTransactionHashes, + eth.GetPooledTransactionsMsg: handleUnimplemented, + eth.PooledTransactionsMsg: handlePooledTransactions, +} + +var eth66 = map[uint64]msgHandler{ + eth.NewBlockHashesMsg: handleNewBlockHashes, + eth.NewBlockMsg: handleNewBlockMsg, + eth.TransactionsMsg: handleTransactions, + eth.NewPooledTransactionHashesMsg: handleNewPooledTransactionHashes, + // eth66 messages have request-id + eth.GetBlockHeadersMsg: handleGetBlockHeaders66, + eth.BlockHeadersMsg: handleBlockHeaders66, + eth.GetBlockBodiesMsg: handleGetBlockBodies66, + eth.BlockBodiesMsg: handleBlockBodies66, + eth.GetNodeDataMsg: handleUnimplemented, + eth.NodeDataMsg: handleUnimplemented, + eth.GetReceiptsMsg: handleUnimplemented, + eth.ReceiptsMsg: handleUnimplemented, + eth.GetPooledTransactionsMsg: handleUnimplemented, + eth.PooledTransactionsMsg: handlePooledTransactions66, +} + +func handleMessage(backend Backend, peer *Peer) error { + msg, err := peer.rw.ReadMsg() + if err != nil { + return err + } + + defer func() { + _ = msg.Discard() + }() + + handlers := eth65 + if peer.version >= eth.ETH66 { + handlers = eth66 + } + handler, ok := handlers[msg.Code] + + log.Tracef("%v: handling message with code: %v", peer, msg.Code) + + if ok { + return handler(backend, msg, peer) + } + return nil +} diff --git a/blockchain/eth/server.go b/blockchain/eth/server.go new file mode 100644 index 0000000..c030fe2 --- /dev/null +++ b/blockchain/eth/server.go @@ -0,0 +1,105 @@ +package eth + +import ( + "context" + "crypto/ecdsa" + "fmt" + "github.com/bloXroute-Labs/gateway/blockchain" + "github.com/bloXroute-Labs/gateway/blockchain/network" + "github.com/bloXroute-Labs/gateway/version" + "github.com/ethereum/go-ethereum/log" + "github.com/ethereum/go-ethereum/p2p" + "os" + "path" +) + +// Server wraps the Ethereum p2p server, for use with the BDN +type Server struct { + *p2p.Server + cancel context.CancelFunc +} + +// NewServer return an Ethereum p2p server, configured with BDN friendly defaults +func NewServer(parent context.Context, config *network.EthConfig, bridge blockchain.Bridge, dataDir string, logger log.Logger, ws blockchain.WSProvider) (*Server, error) { + var privateKey *ecdsa.PrivateKey + + if config.PrivateKey != nil { + privateKey = config.PrivateKey + } else { + privateKeyPath := path.Join(dataDir, ".gatewaykey") + privateKeyFromFile, generated, err := LoadOrGeneratePrivateKey(privateKeyPath) + + if err != nil { + keyWriteErr, ok := err.(keyWriteError) + if ok { + logger.Warn("could not write private key", "err", keyWriteErr) + } else { + return nil, err + } + } + + if generated { + logger.Warn("no private key found, generating new one", "path", privateKeyPath) + } + privateKey = privateKeyFromFile + } + + ctx, cancel := context.WithCancel(parent) + backend := NewHandler(ctx, bridge, config, ws) + + server := p2p.Server{ + Config: p2p.Config{ + PrivateKey: privateKey, + MaxPeers: 1, + MaxPendingPeers: 1, + DialRatio: 1, + NoDiscovery: true, + DiscoveryV5: false, + Name: fmt.Sprintf("bloXroute Gateway Go v%v", version.BuildVersion), + BootstrapNodes: nil, + BootstrapNodesV5: nil, + StaticNodes: config.StaticPeers, + TrustedNodes: nil, + NetRestrict: nil, + NodeDatabase: "", + Protocols: MakeProtocols(ctx, backend), + ListenAddr: "", + NAT: nil, + Dialer: nil, + NoDial: false, + EnableMsgEvents: false, + Logger: logger, + }, + DiscV5: nil, + } + + s := &Server{ + Server: &server, + cancel: cancel, + } + return s, nil +} + +// NewServerWithEthLogger returns the p2p server preconfigured with the default Ethereum logger +func NewServerWithEthLogger(ctx context.Context, config *network.EthConfig, bridge blockchain.Bridge, dataDir string, ws blockchain.WSProvider) (*Server, error) { + l := log.New() + l.SetHandler(log.StreamHandler(os.Stdout, log.TerminalFormat(true))) + + return NewServer(ctx, config, bridge, dataDir, l, ws) +} + +// Stop shutdowns the p2p server and any additional context relevant goroutines +func (s *Server) Stop() { + s.cancel() + s.Server.Stop() +} + +// AddEthLoggerFileHandler registers additional file handler by file path +func (s *Server) AddEthLoggerFileHandler(path string) error { + fileHandler, err := log.FileHandler(path, log.TerminalFormat(false)) + if err != nil { + return err + } + s.Server.Logger.SetHandler(log.MultiHandler(fileHandler, s.Server.Logger.GetHandler())) + return nil +} diff --git a/blockchain/eth/test/message.go b/blockchain/eth/test/message.go new file mode 100644 index 0000000..e113bd4 --- /dev/null +++ b/blockchain/eth/test/message.go @@ -0,0 +1,80 @@ +package test + +import ( + "github.com/ethereum/go-ethereum/p2p" + "github.com/ethereum/go-ethereum/rlp" + "time" +) + +// MsgReadWriter is a test implementation of the RW connection interface on RLP peers +type MsgReadWriter struct { + ReadMessages chan p2p.Msg + WriteMessages []p2p.Msg + + // provide an alerting mechanism if write doesn't happen on main goroutine (writeChannelSize = -1 if this is not needed) + writeAlertCh chan bool + writeToChannel bool +} + +// NewMsgReadWriter returns a test read writer with the provided channel buffer size +func NewMsgReadWriter(readChannelSize, writeChannelSize int) *MsgReadWriter { + rw := &MsgReadWriter{ + ReadMessages: make(chan p2p.Msg, readChannelSize), + WriteMessages: make([]p2p.Msg, 0), + writeToChannel: writeChannelSize != -1, + } + if rw.writeToChannel { + rw.writeAlertCh = make(chan bool, writeChannelSize) + } + return rw +} + +// ReadMsg pulls an encoded message off the read queue +func (t *MsgReadWriter) ReadMsg() (p2p.Msg, error) { + msg := <-t.ReadMessages + return msg, nil +} + +// WriteMsg tracks all messages that are supposedly written to the RW peer +func (t *MsgReadWriter) WriteMsg(msg p2p.Msg) error { + t.WriteMessages = append(t.WriteMessages, msg) + if t.writeToChannel { + t.writeAlertCh <- true + } + return nil +} + +// QueueIncomingMessage simulates the peer sending a message to be read +func (t *MsgReadWriter) QueueIncomingMessage(code uint64, payload interface{}) { + size, reader, err := rlp.EncodeToReader(payload) + if err != nil { + panic(err) + } + msg := p2p.Msg{ + Code: code, + Size: uint32(size), + Payload: reader, + ReceivedAt: time.Time{}, + } + t.ReadMessages <- msg +} + +// ExpectWrite waits for up to the provided duration for writes to the channel. This method is useful if the message writing happens on a goroutine. +func (t *MsgReadWriter) ExpectWrite(d time.Duration) bool { + if !t.writeToChannel { + panic("cannot expect writes when write channel is not being used") + } + select { + case <-t.writeAlertCh: + return true + case <-time.After(d): + return false + } +} + +// PopWrittenMessage pops the first sent message from off the mesage queue and returns it +func (t *MsgReadWriter) PopWrittenMessage() p2p.Msg { + msg := t.WriteMessages[0] + t.WriteMessages = t.WriteMessages[1:] + return msg +} diff --git a/blockchain/eth/test/util.go b/blockchain/eth/test/util.go new file mode 100644 index 0000000..4626092 --- /dev/null +++ b/blockchain/eth/test/util.go @@ -0,0 +1,16 @@ +package test + +import ( + "github.com/ethereum/go-ethereum/p2p/enode" + "math/rand" +) + +// GenerateEnodeID randomly creates an enode for testing purposes +func GenerateEnodeID() enode.ID { + var enodeID enode.ID + id := make([]byte, 32) + _, _ = rand.Read(id) + + copy(enodeID[:], id) + return enodeID +} diff --git a/blockchain/eth/util.go b/blockchain/eth/util.go new file mode 100644 index 0000000..55baa2e --- /dev/null +++ b/blockchain/eth/util.go @@ -0,0 +1,86 @@ +package eth + +import ( + "crypto/ecdsa" + "errors" + "fmt" + "github.com/bloXroute-Labs/gateway/types" + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/crypto" + "os" +) + +// special error constant types +var ( + ErrInvalidRequest = errors.New("invalid request") + ErrInvalidPacketType = errors.New("invalid packet type") + ErrBodyNotFound = errors.New("block body not stored") + ErrAlreadySeen = errors.New("already seen") + ErrAncientHeaders = errors.New("headers requested are ancient") + ErrFutureHeaders = errors.New("headers requested are in the future") +) + +// blockRef represents block info used for storing best block +type blockRef struct { + height uint64 + hash common.Hash +} + +// String formats blockRef for concise printing +func (b blockRef) String() string { + return fmt.Sprintf("%v[%v]", b.height, b.hash.TerminalString()) +} + +type blockRefChain []blockRef + +func (bc blockRefChain) head() *blockRef { + if len(bc) == 0 { + return &blockRef{} + } + return &bc[0] +} + +func (bc blockRefChain) tail() *blockRef { + if len(bc) == 0 { + return nil + } + return &bc[len(bc)-1] +} + +// String formats blockRefChain for concise printing +func (bc blockRefChain) String() string { + return fmt.Sprintf("chainstate(best: %v, oldest: %v)", bc.head(), bc.tail()) +} + +type keyWriteError struct { + error +} + +// NewSHA256Hash is a utility function for converting between Ethereum common hashes and bloxroute hashes +func NewSHA256Hash(hash common.Hash) types.SHA256Hash { + var sha256Hash types.SHA256Hash + copy(sha256Hash[:], hash.Bytes()) + return sha256Hash +} + +// LoadOrGeneratePrivateKey tries to load an ECDSA private key from the provided path. If this file does not exist, a new key is generated in its place. +func LoadOrGeneratePrivateKey(keyPath string) (privateKey *ecdsa.PrivateKey, generated bool, err error) { + privateKey, err = crypto.LoadECDSA(keyPath) + if err != nil { + if os.IsNotExist(err) { + if privateKey, err = crypto.GenerateKey(); err != nil { + return + } + + if err = crypto.SaveECDSA(keyPath, privateKey); err != nil { + err = keyWriteError{err} + return + } + + generated = true + } else { + return + } + } + return +} diff --git a/blockchain/eth/wsprovider.go b/blockchain/eth/wsprovider.go new file mode 100644 index 0000000..12b88d0 --- /dev/null +++ b/blockchain/eth/wsprovider.go @@ -0,0 +1,185 @@ +package eth + +import ( + "context" + "fmt" + "github.com/bloXroute-Labs/gateway/blockchain" + "github.com/ethereum/go-ethereum/rpc" + log "github.com/sirupsen/logrus" + "strings" + "time" +) + +// WSProvider implements the blockchain.WSProvider interface for Ethereum +type WSProvider struct { + addr string + client *rpc.Client + log *log.Entry + ctx context.Context + timeout time.Duration + subscriptions []blockchain.Subscription + syncStatus blockchain.NodeSyncStatus + syncStatusCh chan blockchain.NodeSyncStatus +} + +// RPCResponse represents the Ethereum RPC response +type RPCResponse struct { + JSONRPC string `json:"jsonrpc"` + ID interface{} `json:"id"` + Method string `json:"method"` + Params interface{} `json:"params"` +} + +var validRPCCallPayloadFields = []string{"data", "from", "to", "gasPrice", "gas", "address", "pos"} + +var validRPCCallMethods = []string{"eth_call", "eth_getBalance", "eth_getTransactionCount", "eth_getCode", "eth_getStorageAt", "eth_blockNumber"} + +var commandMethodsToRequiredPayloadFields = map[string][]string{ + "eth_call": {"data"}, + "eth_getBalance": {"address"}, + "eth_getTransactionCount": {"address"}, + "eth_getCode": {"address"}, + "eth_getStorageAt": {"address", "pos"}, + "eth_blockNumber": {}, +} + +// NewEthWSProvider - returns a new instance of WSProvider +func NewEthWSProvider(ethWSUri string, timeout time.Duration) blockchain.WSProvider { + var ws WSProvider + + ws.log = log.WithFields(log.Fields{ + "connType": "WS", + "remoteAddr": ethWSUri, + }) + ws.timeout = timeout + ws.syncStatus = blockchain.Synced + ws.syncStatusCh = make(chan blockchain.NodeSyncStatus, 1) + ws.addr = ethWSUri + ws.Connect() + + ws.log.Infof("connection was successfully established with %v", ethWSUri) + return &ws +} + +// Connect - dials websocket address and returns client with established connection +func (ws *WSProvider) Connect() { + // gateway should retry connecting to the ws url until it's successfully connected + ws.log.Debugf("dialing %v...", ws.addr) + for { + client, err := rpc.Dial(ws.addr) + if err == nil { + ws.client = client + return + } + + time.Sleep(5 * time.Second) + ws.log.Warnf("Failed to dial %v, retrying..", ws.addr) + continue + } +} + +// Subscribe - subscribes to Ethereum feeds and returns subscription +func (ws *WSProvider) Subscribe(responseChannel interface{}, feedName string) (*blockchain.Subscription, error) { + ctx, cancel := context.WithTimeout(context.Background(), ws.timeout) + defer cancel() + + sub, err := ws.client.EthSubscribe(ctx, responseChannel, feedName) + if err != nil { + return nil, fmt.Errorf("failed to subscribe to feed %v: %v", feedName, err) + } + ws.subscriptions = append(ws.subscriptions, blockchain.Subscription{Sub: sub}) + return &blockchain.Subscription{Sub: sub}, nil +} + +// Close - unsubscribes all active subscriptions and closes client connection +func (ws *WSProvider) Close() { + for _, s := range ws.subscriptions { + s.Sub.(*rpc.ClientSubscription).Unsubscribe() + } + ws.subscriptions = ws.subscriptions[:0] + ws.client.Close() +} + +// CallRPC - executes Ethereum RPC calls +func (ws *WSProvider) CallRPC(method string, payload []interface{}, options blockchain.RPCOptions) (interface{}, error) { + var ( + response interface{} + err error + ) + for retries := 0; retries < options.RetryAttempts; retries++ { + err = ws.client.Call(&response, method, payload...) + if (err != nil && strings.Contains(err.Error(), "header not found")) || response == nil { + time.Sleep(options.RetryInterval) + continue + } + break + } + return response, err +} + +// FetchTransactionReceipt fetches transaction receipt via CallRPC +func (ws *WSProvider) FetchTransactionReceipt(payload []interface{}, options blockchain.RPCOptions) (interface{}, error) { + return ws.CallRPC("eth_getTransactionReceipt", payload, options) +} + +// Log - returns WSProvider log entry +func (ws *WSProvider) Log() *log.Entry { + return ws.log +} + +//GetValidRPCCallMethods returns valid Ethereum RPC command methods +func (ws *WSProvider) GetValidRPCCallMethods() []string { + return validRPCCallMethods +} + +// GetValidRPCCallPayloadFields returns valid Ethereum RPC method payload fields +func (ws *WSProvider) GetValidRPCCallPayloadFields() []string { + return validRPCCallPayloadFields +} + +// GetRequiredPayloadFieldsForRPCMethod returns the valid payload fields for the provided Ethereum RPC command method +func (ws *WSProvider) GetRequiredPayloadFieldsForRPCMethod(method string) ([]string, bool) { + requiredFields, ok := commandMethodsToRequiredPayloadFields[method] + return requiredFields, ok +} + +// ConstructRPCCallPayload returns payload used in RPC call +func (ws *WSProvider) ConstructRPCCallPayload(method string, callParams map[string]string, tag string) ([]interface{}, error) { + switch method { + case "eth_call": + payload := []interface{}{callParams, tag} + return payload, nil + case "eth_blockNumber": + return []interface{}{}, nil + case "eth_getStorageAt": + payload := []interface{}{callParams["address"], callParams["pos"], tag} + return payload, nil + case "eth_getBalance": + fallthrough + case "eth_getCode": + fallthrough + case "eth_getTransactionCount": + payload := []interface{}{callParams["address"], tag} + return payload, nil + default: + return nil, fmt.Errorf("unexpectedly failed to match method %v", method) + } +} + +// UpdateNodeSyncStatus sends update on NodeSyncStatus channel if status has changed +func (ws *WSProvider) UpdateNodeSyncStatus(syncStatus blockchain.NodeSyncStatus) { + if syncStatus == ws.syncStatus { + return + } + select { + case ws.syncStatusCh <- syncStatus: + ws.syncStatus = syncStatus + default: + // enables non-blocking + } +} + +// ReceiveNodeSyncStatusUpdate provides a channel to receive NodeSyncStatus updates +func (ws *WSProvider) ReceiveNodeSyncStatusUpdate() chan blockchain.NodeSyncStatus { + return ws.syncStatusCh +} diff --git a/blockchain/network/config.go b/blockchain/network/config.go new file mode 100644 index 0000000..da188bb --- /dev/null +++ b/blockchain/network/config.go @@ -0,0 +1,81 @@ +package network + +import ( + "crypto/ecdsa" + "errors" + "fmt" + "github.com/bloXroute-Labs/gateway/utils" + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/crypto" + "github.com/ethereum/go-ethereum/p2p/enode" + "github.com/urfave/cli/v2" + "math/big" + "strings" + "time" +) + +// EthConfig represents Ethereum network configuration settings (e.g. indicate Mainnet, Rinkeby, BSC, etc.). Most of this information will be exchanged in the status messages. +type EthConfig struct { + StaticPeers []*enode.Node + PrivateKey *ecdsa.PrivateKey + + Network uint64 + TotalDifficulty *big.Int + Head common.Hash + Genesis common.Hash + + IgnoreBlockTimeout time.Duration +} + +const privateKeyLen = 64 + +// NewPresetEthConfigFromCLI builds a new EthConfig from the command line context. Selects a specific network configuration based on the provided startup flag. +func NewPresetEthConfigFromCLI(ctx *cli.Context) (*EthConfig, error) { + preset, err := NewEthereumPreset(ctx.String(utils.BlockchainNetworkFlag.Name)) + if err != nil { + return nil, err + } + + var peers []*enode.Node + + if ctx.IsSet(utils.EnodesFlag.Name) { + enodeStrings := strings.Split(ctx.String(utils.EnodesFlag.Name), ",") + peers = make([]*enode.Node, 0, len(enodeStrings)) + + for _, enodeString := range enodeStrings { + enode, err := enode.Parse(enode.ValidSchemes, enodeString) + if err != nil { + return nil, err + } + peers = append(peers, enode) + } + + if len(enodeStrings) > 1 { + return nil, errors.New("blockchain client does not currently support more than a single connection") + } + } + + preset.StaticPeers = peers + + if ctx.IsSet(utils.PrivateKeyFlag.Name) { + privateKeyHexString := ctx.String(utils.PrivateKeyFlag.Name) + if len(privateKeyHexString) != privateKeyLen { + return nil, fmt.Errorf("incorrect private key length: expected length %v, actual length %v", privateKeyLen, len(privateKeyHexString)) + } + privateKey, err := crypto.HexToECDSA(privateKeyHexString) + if err != nil { + return nil, err + } + preset.PrivateKey = privateKey + } + + return &preset, nil +} + +// Update updates properties of the EthConfig pushed down from server configuration +func (ec *EthConfig) Update(otherConfig EthConfig) { + ec.Network = otherConfig.Network + ec.TotalDifficulty = otherConfig.TotalDifficulty + ec.Head = otherConfig.Head + ec.Genesis = otherConfig.Genesis +} diff --git a/blockchain/network/preset.go b/blockchain/network/preset.go new file mode 100644 index 0000000..871747e --- /dev/null +++ b/blockchain/network/preset.go @@ -0,0 +1,72 @@ +package network + +import ( + "fmt" + "github.com/ethereum/go-ethereum/common" + "math/big" + "time" +) + +var networkMapping = map[string]EthConfig{ + "Mainnet": newEthereumMainnetConfig(), + "BSC-Mainnet": newBSCMainnetConfig(), + "Polygon-Mainnet": newPolygonMainnetConfig(), +} + +// NewEthereumPreset returns an Ethereum configuration for the given network name. For most of these presets, the client will present itself as only having the genesis block, but that shouldn't matter too much. +func NewEthereumPreset(network string) (EthConfig, error) { + config, ok := networkMapping[network] + if !ok { + return unknownConfig(), fmt.Errorf("network %v did not have an available configuration", network) + } + return config, nil +} + +func newEthereumMainnetConfig() EthConfig { + td, ok := new(big.Int).SetString("0400000000", 16) + if !ok { + panic("could not load Ethereum Mainnet configuration") + } + + return EthConfig{ + Network: 1, + TotalDifficulty: td, + Head: common.HexToHash("0xd4e56740f876aef8c010b86a40d5f56745a118d0906a34e69aec8c0db1cb8fa3"), + Genesis: common.HexToHash("0xd4e56740f876aef8c010b86a40d5f56745a118d0906a34e69aec8c0db1cb8fa3"), + IgnoreBlockTimeout: 150 * time.Second, + } +} + +func newBSCMainnetConfig() EthConfig { + td, ok := new(big.Int).SetString("40000", 16) + if !ok { + panic("could not load BSC Mainnet configuration") + } + + return EthConfig{ + Network: 56, + TotalDifficulty: td, + Head: common.HexToHash("0x0d21840abff46b96c84b2ac9e10e4f5cdaeb5693cb665db62a2f3b02d2d57b5b"), + Genesis: common.HexToHash("0x0d21840abff46b96c84b2ac9e10e4f5cdaeb5693cb665db62a2f3b02d2d57b5b"), + IgnoreBlockTimeout: 30 * time.Second, + } +} + +func newPolygonMainnetConfig() EthConfig { + td, ok := new(big.Int).SetString("40000", 16) + if !ok { + panic("could not load BSC Mainnet configuration") + } + + return EthConfig{ + Network: 137, + TotalDifficulty: td, + Head: common.HexToHash("0xa9c28ce2141b56c474f1dc504bee9b01eb1bd7d1a507580d5519d4437a97de1b"), + Genesis: common.HexToHash("0xa9c28ce2141b56c474f1dc504bee9b01eb1bd7d1a507580d5519d4437a97de1b"), + IgnoreBlockTimeout: 30 * time.Second, + } +} + +func unknownConfig() EthConfig { + return EthConfig{} +} diff --git a/blockchain/noopbridge.go b/blockchain/noopbridge.go new file mode 100644 index 0000000..2a73fc2 --- /dev/null +++ b/blockchain/noopbridge.go @@ -0,0 +1,118 @@ +package blockchain + +import ( + "github.com/bloXroute-Labs/gateway/blockchain/network" + "github.com/bloXroute-Labs/gateway/types" +) + +// NoOpBxBridge is a placeholder bridge that still operates as a Converter +type NoOpBxBridge struct { + Converter +} + +// NewNoOpBridge is a placeholder bridge implementation for starting the node without any blockchain connections, so that there's no blocking on channels +func NewNoOpBridge(converter Converter) Bridge { + return &NoOpBxBridge{ + Converter: converter, + } +} + +// TransactionBlockchainToBDN is a no-op +func (n NoOpBxBridge) TransactionBlockchainToBDN(i interface{}) (*types.BxTransaction, error) { + return nil, nil +} + +// TransactionBDNToBlockchain is a no-op +func (n NoOpBxBridge) TransactionBDNToBlockchain(transaction *types.BxTransaction) (interface{}, error) { + return nil, nil +} + +// BlockBlockchainToBDN is a no-op +func (n NoOpBxBridge) BlockBlockchainToBDN(i interface{}) (*types.BxBlock, error) { + return nil, nil +} + +// BlockBDNtoBlockchain is a no-op +func (n NoOpBxBridge) BlockBDNtoBlockchain(block *types.BxBlock) (interface{}, error) { + return nil, nil +} + +// ReceiveNetworkConfigUpdates is a no-op +func (n NoOpBxBridge) ReceiveNetworkConfigUpdates() <-chan network.EthConfig { + return make(chan network.EthConfig) +} + +// UpdateNetworkConfig is a no-op +func (n NoOpBxBridge) UpdateNetworkConfig(config network.EthConfig) error { + return nil +} + +// AnnounceTransactionHashes is a no-op +func (n NoOpBxBridge) AnnounceTransactionHashes(s string, list types.SHA256HashList) error { + return nil +} + +// SendTransactionsFromBDN is a no-op +func (n NoOpBxBridge) SendTransactionsFromBDN(transactions []*types.BxTransaction) error { + return nil +} + +// SendTransactionsToBDN is a no-op +func (n NoOpBxBridge) SendTransactionsToBDN(txs []*types.BxTransaction, peerEndpoint types.NodeEndpoint) error { + return nil +} + +// RequestTransactionsFromNode is a no-op +func (n NoOpBxBridge) RequestTransactionsFromNode(s string, list types.SHA256HashList) error { + return nil +} + +// ReceiveNodeTransactions is a no-op +func (n NoOpBxBridge) ReceiveNodeTransactions() <-chan TransactionsFromNode { + return make(chan TransactionsFromNode) +} + +// ReceiveBDNTransactions is a no-op +func (n NoOpBxBridge) ReceiveBDNTransactions() <-chan []*types.BxTransaction { + return make(chan []*types.BxTransaction) +} + +// ReceiveTransactionHashesAnnouncement is a no-op +func (n NoOpBxBridge) ReceiveTransactionHashesAnnouncement() <-chan TransactionAnnouncement { + return make(chan TransactionAnnouncement) +} + +// ReceiveTransactionHashesRequest is a no-op +func (n NoOpBxBridge) ReceiveTransactionHashesRequest() <-chan TransactionAnnouncement { + return make(chan TransactionAnnouncement) +} + +// SendBlockToBDN is a no-op +func (n NoOpBxBridge) SendBlockToBDN(block *types.BxBlock, endpoint types.NodeEndpoint) error { + return nil +} + +// SendBlockToNode is a no-op +func (n NoOpBxBridge) SendBlockToNode(block *types.BxBlock) error { + return nil +} + +// ReceiveBlockFromBDN is a no-op +func (n NoOpBxBridge) ReceiveBlockFromBDN() <-chan *types.BxBlock { + return make(chan *types.BxBlock) +} + +// ReceiveBlockFromNode is a no-op +func (n NoOpBxBridge) ReceiveBlockFromNode() <-chan BlockFromNode { + return make(chan BlockFromNode) +} + +// ReceiveNoActiveBlockchainPeersAlert is a no-op +func (n NoOpBxBridge) ReceiveNoActiveBlockchainPeersAlert() <-chan NoActiveBlockchainPeersAlert { + return make(chan NoActiveBlockchainPeersAlert) +} + +// SendNoActiveBlockchainPeersAlert is a no-op +func (n NoOpBxBridge) SendNoActiveBlockchainPeersAlert() error { + return nil +} diff --git a/blockchain/wsprovider.go b/blockchain/wsprovider.go new file mode 100644 index 0000000..2b631a9 --- /dev/null +++ b/blockchain/wsprovider.go @@ -0,0 +1,42 @@ +package blockchain + +import ( + log "github.com/sirupsen/logrus" + "time" +) + +// RPCOptions provides options to customize RPC call using WSProvider.CallRPC +type RPCOptions struct { + RetryAttempts int + RetryInterval time.Duration +} + +// NodeSyncStatus indicates if blockchain node is synced or unsynced +type NodeSyncStatus int + +// enumeration for NodeSyncStatus +const ( + Synced NodeSyncStatus = iota + Unsynced +) + +// Subscription represents a client RPC subscription +type Subscription struct { + Sub interface{} +} + +// WSProvider provides an interface to interact with blockchain client via websocket RPC +type WSProvider interface { + Connect() + Close() + Subscribe(responseChannel interface{}, feedName string) (*Subscription, error) + CallRPC(method string, payload []interface{}, options RPCOptions) (interface{}, error) + FetchTransactionReceipt(payload []interface{}, options RPCOptions) (interface{}, error) + Log() *log.Entry + GetValidRPCCallMethods() []string + GetValidRPCCallPayloadFields() []string + GetRequiredPayloadFieldsForRPCMethod(method string) ([]string, bool) + ConstructRPCCallPayload(method string, callParams map[string]string, tag string) ([]interface{}, error) + UpdateNodeSyncStatus(NodeSyncStatus) + ReceiveNodeSyncStatusUpdate() chan NodeSyncStatus +} diff --git a/build.sh b/build.sh new file mode 100755 index 0000000..f13dbc5 --- /dev/null +++ b/build.sh @@ -0,0 +1,4 @@ +#!/usr/bin/env bash +IMAGE=bloxroute/gateway:${1:-latest} +echo "Building container... $IMAGE" +docker build . -f Dockerfile --rm=true -t $IMAGE diff --git a/bxmessage/abstactcleanup.go b/bxmessage/abstactcleanup.go new file mode 100644 index 0000000..8320be6 --- /dev/null +++ b/bxmessage/abstactcleanup.go @@ -0,0 +1,87 @@ +package bxmessage + +import ( + "encoding/binary" + "github.com/bloXroute-Labs/gateway/bxmessage/utils" + "github.com/bloXroute-Labs/gateway/types" +) + +// abstractCleanup represents a transactions that can be cleaned from tx-service +type abstractCleanup struct { + BroadcastHeader + Hashes types.SHA256HashList + ShortIDs types.ShortIDList +} + +// SetHash sets hash +func (m *abstractCleanup) SetHash() { + bufLen := (len(m.ShortIDs) * types.UInt32Len) + (len(m.Hashes) * types.SHA256HashLen) + buf := make([]byte, bufLen) + + offset := 0 + for i := 0; i < len(m.ShortIDs); i++ { + binary.LittleEndian.PutUint32(buf[offset:], uint32(m.ShortIDs[i])) + offset += types.UInt32Len + } + + for i := 0; i < len(m.Hashes); i++ { + copy(buf[offset:], m.Hashes[i][:]) + offset += types.SHA256HashLen + } + + m.hash = utils.DoubleSHA256(buf[:]) +} + +// Pack serializes a cleanup message into a buffer for sending +func (m abstractCleanup) Pack(_ Protocol, msgType string) ([]byte, error) { + bufLen := m.size() + buf := make([]byte, bufLen) + + m.BroadcastHeader.Pack(&buf, msgType) + offset := BroadcastHeaderLen + binary.LittleEndian.PutUint32(buf[offset:], uint32(len(m.ShortIDs))) + offset += types.UInt32Len + for i := 0; i < len(m.ShortIDs); i++ { + binary.LittleEndian.PutUint32(buf[offset:], uint32(m.ShortIDs[i])) + offset += types.UInt32Len + } + binary.LittleEndian.PutUint32(buf[offset:], uint32(len(m.Hashes))) + offset += types.UInt32Len + for i := 0; i < len(m.Hashes); i++ { + copy(buf[offset:], m.Hashes[i][:]) + offset += types.SHA256HashLen + } + + return buf, nil +} + +// Unpack deserializes a cleanup message from a buffer +func (m *abstractCleanup) Unpack(buf []byte, protocol Protocol) error { + offset := HeaderLen + copy(m.hash[:], buf[offset:]) + offset += types.SHA256HashLen + m.networkNumber = types.NetworkNum(binary.LittleEndian.Uint32(buf[offset:])) + offset += types.NetworkNumLen + copy(m.sourceID[:], buf[offset:]) + offset += SourceIDLen + sidCount := binary.LittleEndian.Uint32(buf[offset:]) + offset += types.UInt32Len + for i := 0; i < int(sidCount); i++ { + sid := types.ShortID(binary.LittleEndian.Uint32(buf[offset:])) + offset += types.UInt32Len + m.ShortIDs = append(m.ShortIDs, sid) + } + hashCount := binary.LittleEndian.Uint32(buf[offset:]) + offset += types.UInt32Len + for i := 0; i < int(hashCount); i++ { + var hash types.SHA256Hash + copy(hash[:], buf[offset:]) + offset += types.SHA256HashLen + m.Hashes = append(m.Hashes, hash) + } + return m.BroadcastHeader.Unpack(buf, protocol) +} + +func (m *abstractCleanup) size() uint32 { + return m.BroadcastHeader.Size() + uint32(2*types.UInt32Len+len(m.Hashes)*types.SHA256HashLen+len(m.ShortIDs)*types.ShortIDLen) +} diff --git a/bxmessage/ack.go b/bxmessage/ack.go new file mode 100644 index 0000000..2a9b04b --- /dev/null +++ b/bxmessage/ack.go @@ -0,0 +1,14 @@ +package bxmessage + +// Ack acknowledges a received header message +type Ack struct { + Header +} + +// Pack serializes an Ack into a buffer for sending on the wire +func (m Ack) Pack(protocol Protocol) ([]byte, error) { + bufLen := m.Header.Size() + buf := make([]byte, bufLen) + m.Header.Pack(&buf, AckType) + return buf, nil +} diff --git a/bxmessage/bdnperformancestats.go b/bxmessage/bdnperformancestats.go new file mode 100644 index 0000000..a582afa --- /dev/null +++ b/bxmessage/bdnperformancestats.go @@ -0,0 +1,303 @@ +package bxmessage + +import ( + "encoding/binary" + "github.com/bloXroute-Labs/gateway" + "github.com/bloXroute-Labs/gateway/bxmessage/utils" + "github.com/bloXroute-Labs/gateway/types" + cmap "github.com/orcaman/concurrent-map" + log "github.com/sirupsen/logrus" + "math" + "strconv" + "strings" + "time" +) + +// BdnPerformanceStatsData - represent the bdn stat data struct sent in BdnPerformanceStats +type BdnPerformanceStatsData struct { + BlockchainNodeIPEndpoint string + NewBlocksReceivedFromBlockchainNode uint16 + NewBlocksReceivedFromBdn uint16 + NewBlocksSeen uint32 + + // block_messages vs. block_announcements might not be a distinction that + // exists in all blockchains. For example, in Ethereum this is the + // distinction between NewBlock and NewBlockHashes messages + NewBlockMessagesFromBlockchainNode uint32 + NewBlockAnnouncementsFromBlockchainNode uint32 + + NewTxReceivedFromBlockchainNode uint32 + NewTxReceivedFromBdn uint32 + TxSentToNode uint32 + DuplicateTxFromNode uint32 +} + +// BdnPerformanceStats - represent the "bdnstats" message +type BdnPerformanceStats struct { + Header + intervalStartTime time.Time + intervalEndTime time.Time + memoryUtilizationMb uint16 + nodeStats cmap.ConcurrentMap +} + +// NewBDNStats returns a new instance of BDNPerformanceStats +func NewBDNStats() *BdnPerformanceStats { + bdnStats := BdnPerformanceStats{ + intervalStartTime: time.Now(), + nodeStats: cmap.New(), + } + return &bdnStats +} + +// CloseInterval sets the closing interval end time, starts new interval with cleared stats, and returns BdnPerformanceStats of closed interval +func (bs *BdnPerformanceStats) CloseInterval() BdnPerformanceStats { + // close interval + bs.intervalEndTime = time.Now() + + // create BDNStats from closed interval for logging and sending + prevBDNStats := BdnPerformanceStats{ + intervalStartTime: bs.intervalStartTime, + intervalEndTime: bs.intervalEndTime, + memoryUtilizationMb: bs.memoryUtilizationMb, + nodeStats: bs.nodeStats, + } + + // create fresh map with existing nodes for new interval + bs.nodeStats = cmap.New() + for elem := range prevBDNStats.nodeStats.IterBuffered() { + stats := elem.Val.(*BdnPerformanceStatsData) + newStatsData := BdnPerformanceStatsData{BlockchainNodeIPEndpoint: stats.BlockchainNodeIPEndpoint} + bs.nodeStats.Set(stats.BlockchainNodeIPEndpoint, &newStatsData) + } + + // start new interval + bs.intervalStartTime = time.Now() + bs.memoryUtilizationMb = 0 + + return prevBDNStats +} + +// SetMemoryUtilization sets the memory utilization field of message +func (bs *BdnPerformanceStats) SetMemoryUtilization(mb int) { + bs.memoryUtilizationMb = uint16(mb) +} + +// LogNewBlockFromNode logs new block from blockchain for specified node; new block seen; from BDN for other nodes +func (bs *BdnPerformanceStats) LogNewBlockFromNode(node types.NodeEndpoint) { + nodeStats := bs.getNodeStats(node) + nodeStats.NewBlocksReceivedFromBlockchainNode++ + for elem := range bs.nodeStats.IterBuffered() { + stats := elem.Val.(*BdnPerformanceStatsData) + stats.NewBlocksSeen++ + if stats.BlockchainNodeIPEndpoint == node.IPPort() { + continue + } + stats.NewBlocksReceivedFromBdn++ + } +} + +// LogNewBlockFromBDN logs a new block from the BDN and new block seen in the stats for all nodes +func (bs *BdnPerformanceStats) LogNewBlockFromBDN() { + for elem := range bs.nodeStats.IterBuffered() { + stats := elem.Val.(*BdnPerformanceStatsData) + stats.NewBlocksSeen++ + stats.NewBlocksReceivedFromBdn++ + } +} + +// LogNewBlockMessageFromNode logs a new block message from blockchain node in the stats for specified node +func (bs *BdnPerformanceStats) LogNewBlockMessageFromNode(node types.NodeEndpoint) { + nodeStats := bs.getNodeStats(node) + nodeStats.NewBlockMessagesFromBlockchainNode++ +} + +// LogNewBlockAnnouncementFromNode logs a new block announcement from blockchain node in the stats for specified node +func (bs *BdnPerformanceStats) LogNewBlockAnnouncementFromNode(node types.NodeEndpoint) { + nodeStats := bs.getNodeStats(node) + nodeStats.NewBlockAnnouncementsFromBlockchainNode++ +} + +// LogNewTxFromNode logs new tx from blockchain in stats for specified node, from BDN for other nodes +func (bs *BdnPerformanceStats) LogNewTxFromNode(node types.NodeEndpoint) { + nodeStats := bs.getNodeStats(node) + nodeStats.NewTxReceivedFromBlockchainNode++ + for elem := range bs.nodeStats.IterBuffered() { + stats := elem.Val.(*BdnPerformanceStatsData) + if stats.BlockchainNodeIPEndpoint == node.IPPort() { + continue + } + stats.NewTxReceivedFromBdn++ + } +} + +// LogNewTxFromBDN logs a new tx from BDN in the stats for all nodes +func (bs *BdnPerformanceStats) LogNewTxFromBDN() { + for elem := range bs.nodeStats.IterBuffered() { + stats := elem.Val.(*BdnPerformanceStatsData) + stats.NewTxReceivedFromBdn++ + } +} + +// LogTxSentToNode logs a tx sent to all blockchain nodes +func (bs *BdnPerformanceStats) LogTxSentToNode() { + for elem := range bs.nodeStats.IterBuffered() { + stats := elem.Val.(*BdnPerformanceStatsData) + stats.TxSentToNode++ + } +} + +// LogDuplicateTxFromNode logs a duplicate tx from blockchain node in the stats for specified node +func (bs *BdnPerformanceStats) LogDuplicateTxFromNode(node types.NodeEndpoint) { + nodeStats := bs.getNodeStats(node) + nodeStats.DuplicateTxFromNode++ +} + +// StartTime returns the start time of the current stat interval +func (bs *BdnPerformanceStats) StartTime() time.Time { + return bs.intervalStartTime +} + +// EndTime returns the start time of the current stat interval +func (bs *BdnPerformanceStats) EndTime() time.Time { + return bs.intervalEndTime +} + +// Memory returns memory utilization stat +func (bs *BdnPerformanceStats) Memory() uint16 { + return bs.memoryUtilizationMb +} + +// NodeStats returns the bdn stats data for all nodes +func (bs *BdnPerformanceStats) NodeStats() cmap.ConcurrentMap { + return bs.nodeStats +} + +func (bs *BdnPerformanceStats) getNodeStats(node types.NodeEndpoint) *BdnPerformanceStatsData { + val, ok := bs.nodeStats.Get(node.IPPort()) + if ok { + stats := val.(*BdnPerformanceStatsData) + return stats + } + + newStatsData := BdnPerformanceStatsData{BlockchainNodeIPEndpoint: node.IPPort()} + bs.nodeStats.Set(node.IPPort(), &newStatsData) + return &newStatsData +} + +// Pack serializes a BdnPerformanceStats into a buffer for sending +func (bs *BdnPerformanceStats) Pack(_ Protocol) ([]byte, error) { + bufLen := bs.size() + buf := make([]byte, bufLen) + offset := uint32(HeaderLen) + binary.LittleEndian.PutUint64(buf[offset:], math.Float64bits(float64(bs.intervalStartTime.UnixNano())/float64(1e9))) + offset += TimestampLen + binary.LittleEndian.PutUint64(buf[offset:], math.Float64bits(float64(bs.intervalEndTime.UnixNano())/float64(1e9))) + offset += TimestampLen + binary.LittleEndian.PutUint16(buf[offset:], bs.memoryUtilizationMb) + offset += types.UInt16Len + nodeStatsLen := bs.nodeStats.Count() + binary.LittleEndian.PutUint16(buf[offset:], uint16(nodeStatsLen)) + offset += types.UInt16Len + + for elem := range bs.nodeStats.IterBuffered() { + nodeStats := elem.Val.(*BdnPerformanceStatsData) + ipPort := strings.Split(nodeStats.BlockchainNodeIPEndpoint, " ") + port, err := strconv.Atoi(ipPort[1]) + if err != nil { + return nil, err + } + utils.PackIPPort(buf[offset:], ipPort[0], uint16(port)) + offset += utils.IPAddrSizeInBytes + types.UInt16Len + binary.LittleEndian.PutUint16(buf[offset:], nodeStats.NewBlocksReceivedFromBlockchainNode) + offset += types.UInt16Len + binary.LittleEndian.PutUint16(buf[offset:], nodeStats.NewBlocksReceivedFromBdn) + offset += types.UInt16Len + binary.LittleEndian.PutUint32(buf[offset:], nodeStats.NewTxReceivedFromBlockchainNode) + offset += types.UInt32Len + binary.LittleEndian.PutUint32(buf[offset:], nodeStats.NewTxReceivedFromBdn) + offset += types.UInt32Len + binary.LittleEndian.PutUint32(buf[offset:], nodeStats.NewBlocksSeen) + offset += types.UInt32Len + binary.LittleEndian.PutUint32(buf[offset:], nodeStats.NewBlockMessagesFromBlockchainNode) + offset += types.UInt32Len + binary.LittleEndian.PutUint32(buf[offset:], nodeStats.NewBlockAnnouncementsFromBlockchainNode) + offset += types.UInt32Len + binary.LittleEndian.PutUint32(buf[offset:], nodeStats.TxSentToNode) + offset += types.UInt32Len + binary.LittleEndian.PutUint32(buf[offset:], nodeStats.DuplicateTxFromNode) + offset += types.UInt32Len + } + bs.Header.Pack(&buf, BDNPerformanceStatsType) + return buf, nil +} + +// Unpack deserializes a BdnPerformanceStats from a buffer +func (bs *BdnPerformanceStats) Unpack(buf []byte, protocol Protocol) error { + bs.nodeStats = cmap.New() + nodeStatsOffset := (types.UInt64Len * 2) + (types.UInt16Len * 2) + if err := checkBufSize(&buf, HeaderLen, nodeStatsOffset); err != nil { + return err + } + offset := uint32(HeaderLen) + startTimestamp := math.Float64frombits(binary.LittleEndian.Uint64(buf[offset:])) + startNanoseconds := int64(float64(startTimestamp) * float64(1e9)) + bs.intervalStartTime = time.Unix(0, startNanoseconds) + offset += TimestampLen + endTimestamp := math.Float64frombits(binary.LittleEndian.Uint64(buf[offset:])) + endNanoseconds := int64(float64(endTimestamp) * float64(1e9)) + bs.intervalEndTime = time.Unix(0, endNanoseconds) + offset += TimestampLen + bs.memoryUtilizationMb = binary.LittleEndian.Uint16(buf[offset:]) + offset += types.UInt16Len + nodesStatsLen := binary.LittleEndian.Uint16(buf[offset:]) + offset += types.UInt16Len + + nodeStatsSize := int(nodesStatsLen) * ((utils.IPAddrSizeInBytes) + (types.UInt32Len * 7) + (types.UInt16Len * 3)) + if err := checkBufSize(&buf, int(offset), nodeStatsSize); err != nil { + return err + } + for i := 0; i < int(nodesStatsLen); i++ { + var singleNodeStats BdnPerformanceStatsData + ip, port, err := utils.UnpackIPPort(buf[offset:]) + if err != nil { + log.Errorf("unable to parse ip and port from BDNPerformanceStats message: %v", err) + } + endpoint := types.NodeEndpoint{IP: ip, Port: int(port)} + singleNodeStats.BlockchainNodeIPEndpoint = endpoint.IPPort() + offset += utils.IPAddrSizeInBytes + types.UInt16Len + singleNodeStats.NewBlocksReceivedFromBlockchainNode = binary.LittleEndian.Uint16(buf[offset:]) + offset += types.UInt16Len + singleNodeStats.NewBlocksReceivedFromBdn = binary.LittleEndian.Uint16(buf[offset:]) + offset += types.UInt16Len + singleNodeStats.NewTxReceivedFromBlockchainNode = binary.LittleEndian.Uint32(buf[offset:]) + offset += types.UInt32Len + singleNodeStats.NewTxReceivedFromBdn = binary.LittleEndian.Uint32(buf[offset:]) + offset += types.UInt32Len + singleNodeStats.NewBlocksSeen = binary.LittleEndian.Uint32(buf[offset:]) + offset += types.UInt32Len + singleNodeStats.NewBlockMessagesFromBlockchainNode = binary.LittleEndian.Uint32(buf[offset:]) + offset += types.UInt32Len + singleNodeStats.NewBlockAnnouncementsFromBlockchainNode = binary.LittleEndian.Uint32(buf[offset:]) + offset += types.UInt32Len + singleNodeStats.TxSentToNode = binary.LittleEndian.Uint32(buf[offset:]) + offset += types.UInt32Len + singleNodeStats.DuplicateTxFromNode = binary.LittleEndian.Uint32(buf[offset:]) + offset += types.UInt32Len + bs.nodeStats.Set(singleNodeStats.BlockchainNodeIPEndpoint, &singleNodeStats) + } + return bs.Header.Unpack(buf, protocol) +} + +// Log logs stats +func (bs *BdnPerformanceStats) Log() { + for elem := range bs.NodeStats().IterBuffered() { + nodeStats := elem.Val.(*BdnPerformanceStatsData) + log.Infof("[%v - %v]: Processed %v blocks and %v transactions from the BDN", bs.StartTime().Format(bxgateway.TimeLayoutISO), bs.EndTime().Format(bxgateway.TimeLayoutISO), nodeStats.NewBlocksReceivedFromBdn, nodeStats.NewTxReceivedFromBdn) + } +} + +func (bs *BdnPerformanceStats) size() uint32 { + nodeStatsSize := utils.IPAddrSizeInBytes + (types.UInt32Len * 7) + (types.UInt16Len * 3) + return bs.Header.Size() + uint32((types.UInt64Len*2)+(types.UInt16Len*2)+(bs.nodeStats.Count()*nodeStatsSize)) +} diff --git a/bxmessage/bdnperformancestats_test.go b/bxmessage/bdnperformancestats_test.go new file mode 100644 index 0000000..28eaca7 --- /dev/null +++ b/bxmessage/bdnperformancestats_test.go @@ -0,0 +1,103 @@ +package bxmessage + +import ( + "github.com/bloXroute-Labs/gateway/types" + "github.com/stretchr/testify/assert" + "testing" +) + +// msg bytes generated by python gateway BDNPerformanceStatsMessage serialization +var BDNStatsMsgBytes = []byte("\xff\xfe\xfd\xfcbdnstats\x00\x00\x00\x00y\x00\x00\x00W\xe8\xf6\xde\x005\xd8A`\xea\xf6\xde\x005\xd8Ad\x00\x02\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xff\xff\x7f\x00\x00\x01A\x1f\x14\x00\x1e\x00(\x00\x00\x002\x00\x00\x00\n\x00\x00\x00\n\x00\x00\x00\x14\x00\x00\x00d\x00\x00\x002\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xff\xff\xac\x11\x00\x01B\x1f\x15\x00\x1f\x00)\x00\x00\x003\x00\x00\x00\x0b\x00\x00\x00\x0b\x00\x00\x00\x15\x00\x00\x00e\x00\x00\x003\x00\x00\x00\x01") + +func TestBdnPerformanceStats_Unpack(t *testing.T) { + bdnStats := BdnPerformanceStats{} + err := bdnStats.Unpack(BDNStatsMsgBytes, 0) + assert.Nil(t, err) + assert.Equal(t, uint16(100), bdnStats.memoryUtilizationMb) + assert.Equal(t, 2, bdnStats.nodeStats.Count()) + ipEndpoint := types.NodeEndpoint{ + IP: "127.0.0.1", + Port: 8001, + } + + val, _ := bdnStats.nodeStats.Get(ipEndpoint.IPPort()) + nodeStats := val.(*BdnPerformanceStatsData) + assert.Equal(t, ipEndpoint.IPPort(), nodeStats.BlockchainNodeIPEndpoint) + assert.Equal(t, uint16(20), nodeStats.NewBlocksReceivedFromBlockchainNode) + assert.Equal(t, uint16(30), nodeStats.NewBlocksReceivedFromBdn) + assert.Equal(t, uint32(40), nodeStats.NewTxReceivedFromBlockchainNode) + assert.Equal(t, uint32(50), nodeStats.NewTxReceivedFromBdn) + assert.Equal(t, uint32(10), nodeStats.NewBlocksSeen) + assert.Equal(t, uint32(10), nodeStats.NewBlockMessagesFromBlockchainNode) + assert.Equal(t, uint32(20), nodeStats.NewBlockAnnouncementsFromBlockchainNode) + assert.Equal(t, uint32(100), nodeStats.TxSentToNode) + assert.Equal(t, uint32(50), nodeStats.DuplicateTxFromNode) + + ipEndpoint2 := types.NodeEndpoint{ + IP: "172.17.0.1", + Port: 8002, + } + val2, _ := bdnStats.nodeStats.Get(ipEndpoint2.IPPort()) + nodeStats2 := val2.(*BdnPerformanceStatsData) + assert.Equal(t, ipEndpoint2.IPPort(), nodeStats2.BlockchainNodeIPEndpoint) + assert.Equal(t, uint16(21), nodeStats2.NewBlocksReceivedFromBlockchainNode) + assert.Equal(t, uint16(31), nodeStats2.NewBlocksReceivedFromBdn) + assert.Equal(t, uint32(41), nodeStats2.NewTxReceivedFromBlockchainNode) + assert.Equal(t, uint32(51), nodeStats2.NewTxReceivedFromBdn) + assert.Equal(t, uint32(11), nodeStats2.NewBlocksSeen) + assert.Equal(t, uint32(11), nodeStats2.NewBlockMessagesFromBlockchainNode) + assert.Equal(t, uint32(21), nodeStats2.NewBlockAnnouncementsFromBlockchainNode) + assert.Equal(t, uint32(101), nodeStats2.TxSentToNode) + assert.Equal(t, uint32(51), nodeStats2.DuplicateTxFromNode) +} + +func TestBdnPerformanceStats_Pack(t *testing.T) { + bdnStatsFromBytes := BdnPerformanceStats{} + err := bdnStatsFromBytes.Unpack(BDNStatsMsgBytes, 0) + + bdnStats := NewBDNStats() + bdnStats.intervalStartTime = bdnStatsFromBytes.intervalStartTime + bdnStats.intervalEndTime = bdnStatsFromBytes.intervalEndTime + bdnStats.memoryUtilizationMb = 100 + endpoint1 := types.NodeEndpoint{IP: "127.0.0.1", Port: 8001} + nodeStats1 := BdnPerformanceStatsData{ + BlockchainNodeIPEndpoint: endpoint1.IPPort(), + NewBlocksReceivedFromBlockchainNode: 20, + NewBlocksReceivedFromBdn: 30, + NewBlocksSeen: 10, + NewBlockMessagesFromBlockchainNode: 10, + NewBlockAnnouncementsFromBlockchainNode: 20, + NewTxReceivedFromBlockchainNode: 40, + NewTxReceivedFromBdn: 50, + TxSentToNode: 100, + DuplicateTxFromNode: 50, + } + bdnStats.nodeStats.Set(nodeStats1.BlockchainNodeIPEndpoint, &nodeStats1) + + endpoint2 := types.NodeEndpoint{IP: "172.17.0.1", Port: 8002} + nodeStats2 := BdnPerformanceStatsData{ + BlockchainNodeIPEndpoint: endpoint2.IPPort(), + NewBlocksReceivedFromBlockchainNode: 21, + NewBlocksReceivedFromBdn: 31, + NewBlocksSeen: 11, + NewBlockMessagesFromBlockchainNode: 11, + NewBlockAnnouncementsFromBlockchainNode: 21, + NewTxReceivedFromBlockchainNode: 41, + NewTxReceivedFromBdn: 51, + TxSentToNode: 101, + DuplicateTxFromNode: 51, + } + bdnStats.nodeStats.Set(nodeStats2.BlockchainNodeIPEndpoint, &nodeStats2) + packed, err := bdnStats.Pack(0) + + assert.Equal(t, BDNStatsMsgBytes, packed) + assert.Nil(t, err) +} + +func TestBdnPerformanceStats_UnpackBadBuffer(t *testing.T) { + bdnStats := BdnPerformanceStats{} + // truncated msg bytes + var bdnStatsMsgBytes = []byte("\xff\xfe\xfd\xfcbdnstats\x00\x00\x00\x00y\x00\x00\x00W\xe8\xf6\xde\x005\xd8A`\xea\xf6\xde\x005\xd8Ad\x00\x02\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xff\xff\x7f\x00\x00\x01A\x1f\x14\x00\x1e\x00(\x00\x00\x002\x00\x00\x00\n\x00\x00\x00\n\x00\x00\x00\x14\x00\x00\x00d\x00\x00\x002\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xff\xff\xac\x11\x00\x01B\x1f\x15\x00\x1f\x00)\x00\x00\x003\x00\x00\x00\x0b\x00\x00\x00\x0b\x00\x00\x00\x15\x00\x00\x00e\x00") + err := bdnStats.Unpack(bdnStatsMsgBytes, 0) + assert.NotNil(t, err) +} diff --git a/bxmessage/blockconfirmation.go b/bxmessage/blockconfirmation.go new file mode 100644 index 0000000..bcce464 --- /dev/null +++ b/bxmessage/blockconfirmation.go @@ -0,0 +1,27 @@ +package bxmessage + +import ( + log "github.com/sirupsen/logrus" +) + +// BlockConfirmation represents a transactions that can be cleaned from tx-service due to block confirmation +type BlockConfirmation struct { + abstractCleanup +} + +func (m *BlockConfirmation) size() uint32 { + return m.abstractCleanup.size() +} + +// Pack serializes a BlockConfirmation into a buffer for sending +func (m BlockConfirmation) Pack(protocol Protocol) ([]byte, error) { + buf, err := m.abstractCleanup.Pack(protocol, BlockConfirmationType) + return buf, err +} + +// Unpack deserializes a BlockConfirmation from a buffer +func (m *BlockConfirmation) Unpack(buf []byte, protocol Protocol) error { + err := m.abstractCleanup.Unpack(buf, protocol) + log.Tracef("%v: network %v, sids %v, hashes %v", BlockConfirmationType, m.networkNumber, len(m.ShortIDs), len(m.Hashes)) + return err +} diff --git a/bxmessage/blockconfirmation_test.go b/bxmessage/blockconfirmation_test.go new file mode 100644 index 0000000..b9d4784 --- /dev/null +++ b/bxmessage/blockconfirmation_test.go @@ -0,0 +1,13 @@ +package bxmessage + +import ( + "github.com/stretchr/testify/assert" + "testing" +) + +func TestEmptyPackSize(t *testing.T) { + bc := BlockConfirmation{} + buf, err := bc.Pack(22) + assert.Equal(t, err, nil) + assert.Equal(t, len(buf), 81) +} diff --git a/bxmessage/broadcast.go b/bxmessage/broadcast.go new file mode 100644 index 0000000..754078b --- /dev/null +++ b/bxmessage/broadcast.go @@ -0,0 +1,153 @@ +package bxmessage + +import ( + "encoding/binary" + "github.com/bloXroute-Labs/gateway/types" +) + +// Broadcast - represent the "broadcast" message +type Broadcast struct { + BroadcastHeader + broadcastType [BroadcastTypeLen]byte + encrypted bool + block []byte + sids types.ShortIDList +} + +// NewBlockBroadcast creates a new broadcast message containing block message bytes +func NewBlockBroadcast(hash types.SHA256Hash, block []byte, shortIDs types.ShortIDList, networkNum types.NetworkNum) *Broadcast { + var broadcastType [BroadcastTypeLen]byte + copy(broadcastType[:], "blck") + + b := &Broadcast{ + broadcastType: broadcastType, + encrypted: false, + block: block, + sids: shortIDs, + } + b.SetHash(hash) + b.SetNetworkNum(networkNum) + return b +} + +// BroadcastType returns the broadcast type +func (b Broadcast) BroadcastType() [BroadcastTypeLen]byte { + return b.broadcastType +} + +// Encrypted returns the encrypted byte +func (b Broadcast) Encrypted() bool { + return b.encrypted +} + +// Block returns the block +func (b Broadcast) Block() []byte { + return b.block +} + +// ShortIDs return sids +func (b Broadcast) ShortIDs() types.ShortIDList { + return b.sids +} + +// SetBroadcastType sets the broadcast type +func (b *Broadcast) SetBroadcastType(broadcastType [BroadcastTypeLen]byte) { + b.broadcastType = broadcastType +} + +// SetEncrypted sets the encrypted byte +func (b *Broadcast) SetEncrypted(encrypted bool) { + b.encrypted = encrypted +} + +// SetBlock sets the block +func (b *Broadcast) SetBlock(block []byte) { + b.block = block +} + +// SetSids sets the sids +func (b *Broadcast) SetSids(sids types.ShortIDList) { + b.sids = sids +} + +// Pack serializes a Broadcast into a buffer for sending +func (b *Broadcast) Pack(protocol Protocol) ([]byte, error) { + bufLen := b.Size() + buf := make([]byte, bufLen) + b.BroadcastHeader.Pack(&buf, BroadcastType) + offset := BroadcastHeaderLen + copy(buf[offset:], b.broadcastType[:]) + offset += BroadcastTypeLen + if b.encrypted { + copy(buf[offset:], []uint8{1}) + } else { + copy(buf[offset:], []uint8{0}) + } + offset += EncryptedTypeLen + binary.LittleEndian.PutUint64(buf[offset:], uint64(len(b.block)+types.UInt64Len)) + offset += types.UInt64Len + copy(buf[offset:], b.block) + offset += len(b.block) + binary.LittleEndian.PutUint32(buf[offset:], uint32(len(b.sids))) + offset += types.UInt32Len + for _, sid := range b.sids { + binary.LittleEndian.PutUint32(buf[offset:], uint32(sid)) + offset += types.UInt32Len + } + return buf, nil +} + +// Unpack deserializes a Broadcast from a buffer +func (b *Broadcast) Unpack(buf []byte, protocol Protocol) error { + if err := b.BroadcastHeader.Unpack(buf, protocol); err != nil { + return err + } + + offset := BroadcastHeaderLen + copy(b.broadcastType[:], buf[offset:]) + offset += BroadcastTypeLen + b.encrypted = int(buf[offset : offset+EncryptedTypeLen][0]) != 0 + offset += EncryptedTypeLen + + if err := checkBufSize(&buf, offset, types.UInt64Len); err != nil { + return err + } + sidsOffset := int(binary.LittleEndian.Uint64(buf[offset:])) + + // sidsOffset includes its types.UInt64Len + if err := checkBufSize(&buf, offset, sidsOffset); err != nil { + return err + } + b.block = buf[offset+types.UInt64Len : offset+sidsOffset] + offset += sidsOffset + + if err := checkBufSize(&buf, offset, types.UInt32Len); err != nil { + return err + } + sidsLen := int(binary.LittleEndian.Uint32(buf[offset:])) + offset += types.UInt32Len + + if err := checkBufSize(&buf, offset, types.UInt32Len*sidsLen); err != nil { + return err + } + for i := 0; i < sidsLen; i++ { + sid := types.ShortID(binary.LittleEndian.Uint32(buf[offset:])) + offset += types.UInt32Len + b.sids = append(b.sids, sid) + } + + return nil +} + +// Size calculate msg size +func (b *Broadcast) Size() uint32 { + return b.fixedSize() + + types.UInt64Len + // sids offset + uint32(len(b.block)) + + types.UInt32Len + // sids len + (uint32(len(b.sids)) * types.UInt32Len) +} + +func (b *Broadcast) fixedSize() uint32 { + return b.BroadcastHeader.Size() + BroadcastTypeLen + EncryptedTypeLen +} diff --git a/bxmessage/broadcast_test.go b/bxmessage/broadcast_test.go new file mode 100644 index 0000000..97bdced --- /dev/null +++ b/bxmessage/broadcast_test.go @@ -0,0 +1,62 @@ +package bxmessage + +import ( + "encoding/hex" + "github.com/bloXroute-Labs/gateway/test" + "github.com/bloXroute-Labs/gateway/test/fixtures" + "github.com/bloXroute-Labs/gateway/types" + "github.com/stretchr/testify/assert" + "testing" +) + +const ( + networkNum = types.NetworkNum(5) +) + +func TestBroadcastPackUnpack(t *testing.T) { + blockHash := types.GenerateSHA256Hash() + blockBody := test.GenerateBytes(500) + broadcast := NewBlockBroadcast(blockHash, blockBody, types.ShortIDList{}, networkNum) + + b, err := broadcast.Pack(0) + assert.Nil(t, err) + + var decodedBroadcast Broadcast + err = decodedBroadcast.Unpack(b, 0) + assert.Nil(t, err) + + assert.Equal(t, blockHash, decodedBroadcast.Hash()) + assert.Equal(t, blockBody, decodedBroadcast.Block()) + assert.Equal(t, networkNum, decodedBroadcast.GetNetworkNum()) +} + +func TestBroadcastUnpackFixtureWithShortIDs(t *testing.T) { + b, _ := hex.DecodeString(fixtures.BroadcastMessageWithShortIDs) + h, _ := types.NewSHA256HashFromString(fixtures.BroadcastShortIDsMessageHash) + + var broadcast Broadcast + err := broadcast.Unpack(b, 0) + assert.Nil(t, err) + assert.Equal(t, networkNum, broadcast.networkNumber) + assert.Equal(t, h, broadcast.Hash()) + assert.Equal(t, 2, len(broadcast.ShortIDs())) + + encodedBroadcast, err := broadcast.Pack(0) + assert.Nil(t, err) + assert.Equal(t, b, encodedBroadcast) +} + +func TestBroadcastUnpackFixtureEmptyBlock(t *testing.T) { + b, _ := hex.DecodeString(fixtures.BroadcastEmptyBlock) + h, _ := types.NewSHA256HashFromString(fixtures.BroadcastEmptyBlockHash) + + var broadcast Broadcast + err := broadcast.Unpack(b, 0) + assert.Nil(t, err) + assert.Equal(t, networkNum, broadcast.networkNumber) + assert.Equal(t, h, broadcast.Hash()) + + encodedBroadcast, err := broadcast.Pack(0) + assert.Nil(t, err) + assert.Equal(t, b, encodedBroadcast) +} diff --git a/bxmessage/broadcastheader.go b/bxmessage/broadcastheader.go new file mode 100644 index 0000000..cadb8e2 --- /dev/null +++ b/bxmessage/broadcastheader.go @@ -0,0 +1,94 @@ +package bxmessage + +import ( + "encoding/binary" + "fmt" + "github.com/bloXroute-Labs/gateway/types" + uuid "github.com/satori/go.uuid" + log "github.com/sirupsen/logrus" +) + +// BroadcastHeader represents the shared header of a bloxroute broadcast message +type BroadcastHeader struct { + Header + hash types.SHA256Hash + networkNumber types.NetworkNum + sourceID [SourceIDLen]byte +} + +// GetNetworkNum gets the message network number +func (b *BroadcastHeader) GetNetworkNum() types.NetworkNum { + return b.networkNumber +} + +// SetNetworkNum sets the message network number +func (b *BroadcastHeader) SetNetworkNum(networkNum types.NetworkNum) { + b.networkNumber = networkNum +} + +// Hash returns the message hash +func (b *BroadcastHeader) Hash() (hash types.SHA256Hash) { + return b.hash +} + +// SetHash sets the block hash +func (b *BroadcastHeader) SetHash(hash types.SHA256Hash) { + b.hash = hash +} + +// SourceID returns the source ID of the broadcast in string format +func (b *BroadcastHeader) SourceID() (sourceID types.NodeID) { + u, err := uuid.FromBytes(b.sourceID[:]) + if err != nil { + log.Errorf("Failed to parse source id from broadcast message, raw bytes: %v", b.sourceID) + return + } + return types.NodeID(u.String()) +} + +// SetSourceID sets the source id of the tx +func (b *BroadcastHeader) SetSourceID(sourceID types.NodeID) error { + sourceIDBytes, err := uuid.FromString(string(sourceID)) + if err != nil { + return fmt.Errorf("Failed to set source id, source id: %v", sourceIDBytes) + } + + copy(b.sourceID[:], sourceIDBytes[:]) + return nil +} + +// Pack serializes a BroadcastHeader into a buffer for sending on the wire +func (b *BroadcastHeader) Pack(buf *[]byte, msgType string) { + offset := HeaderLen + + copy((*buf)[offset:], b.hash[:]) + offset += types.SHA256HashLen + binary.LittleEndian.PutUint32((*buf)[offset:], uint32(b.networkNumber)) + offset += types.NetworkNumLen + copy((*buf)[offset:], b.sourceID[:]) + offset += SourceIDLen + b.Header.Pack(buf, msgType) +} + +// Unpack deserializes a BroadcastHeader from a buffer +func (b *BroadcastHeader) Unpack(buf []byte, protocol Protocol) error { + if err := b.Header.Unpack(buf, protocol); err != nil { + return err + } + if err := checkBufSize(&buf, 0, int(b.Size())); err != nil { + return err + } + + offset := HeaderLen + copy(b.hash[:], buf[HeaderLen:]) + offset += types.SHA256HashLen + b.networkNumber = types.NetworkNum(binary.LittleEndian.Uint32(buf[offset:])) + offset += types.NetworkNumLen + copy(b.sourceID[:], buf[offset:]) + return nil +} + +// Size returns the byte length of BroadcastHeader +func (b *BroadcastHeader) Size() uint32 { + return b.Header.Size() + uint32(types.SHA256HashLen+types.NetworkNumLen+SourceIDLen) +} diff --git a/bxmessage/constants.go b/bxmessage/constants.go new file mode 100644 index 0000000..95678fa --- /dev/null +++ b/bxmessage/constants.go @@ -0,0 +1,104 @@ +package bxmessage + +import ( + "bytes" +) + +// StartingBytesLen is the byte length of the starting bytes of bloxroute messages +const StartingBytesLen = 4 + +// ControlByteLen is the byte length of the control byte +const ControlByteLen = 1 + +// ValidControlByte is the final byte of all bloxroute messages, indicating a fully packed message +const ValidControlByte = 0x01 + +// HeaderLen is the byte length of the common bloxroute message headers +const HeaderLen = 20 + +// BroadcastHeaderLen is the byte length of the common bloxroute broadcast message headers +const BroadcastHeaderLen = 72 + +// PayloadSizeOffset is the byte offset of the packed message size +const PayloadSizeOffset = 16 + +// TypeOffset is the byte offset of the packed message type +const TypeOffset = 4 + +// TypeLength is the byte length of the packed message type +const TypeLength = 12 + +// EncryptedTypeLen is the byte length of the encrypted byte +const EncryptedTypeLen = 1 + +// BroadcastTypeLen is the byte length of the broadcastType byte +const BroadcastTypeLen = 4 + +// Message type constants +const ( + HelloType = "hello" + AckType = "ack" + TxType = "tx" + PingType = "ping" + PongType = "pong" + BroadcastType = "broadcast" + BlockTxsType = "blocktxs" + TxCleanupType = "txclnup" + SyncTxsType = "txtxs" + SyncReqType = "txstart" + SyncDoneType = "txdone" + DropRelayType = "droprelay" + RefreshBlockchainNetworkType = "blkntwrk" + BlockConfirmationType = "blkcnfrm" + GetTransactionsType = "gettxs" + TransactionsType = "txs" + BDNPerformanceStatsType = "bdnstats" + MEVBundleType = "mevbundle" + MEVSearcherType = "mevsearcher" + ErrorNotificationType = "errnotify" +) + +// TimestampLen is the byte length of timestamps +const TimestampLen = 8 + +// SourceIDLen is the byte length of message source IDs +const SourceIDLen = 16 + +// ProtocolLen is the byte length of the packed message protocol version +const ProtocolLen = 4 + +// EmptyProtocol can be used for connections that do not track versions +const EmptyProtocol = 0 + +// MinProtocol provides the minimal supported protocol version +const MinProtocol = 19 + +// CurrentProtocol tracks the most recent version of the bloxroute wire protocol +const CurrentProtocol = 24 + +// MinFastSyncProtocol is the minimum protocol version that supports fast sync +const MinFastSyncProtocol = 24 + +// MevProtocol add to hello msg indication for the mev service +const MevProtocol = 24 + +// UnifiedRelayProtocol is the version of gateways that expects broadcast messages from relay proxy and discontinues split relays +const UnifiedRelayProtocol = 23 + +// AccountProtocol is the version that the account ID was introduced in tx messages +const AccountProtocol = 22 + +// NullByte is a character that is packed at the end of strings in buffers +const NullByte = "\x00" + +// AccountIDLen is the byte length of AccountID +const AccountIDLen = 36 + +// CapabilitiesLen is the byte length of the Capabilities +const CapabilitiesLen = 2 + +// ClientVersionLen is the byte length of the client version +const ClientVersionLen = 100 + +// NullByteAccountID is a null byte packed series, which represents an empty accountID +var NullByteAccountID = bytes.Repeat([]byte("\x00"), AccountIDLen) diff --git a/bxmessage/droprelay.go b/bxmessage/droprelay.go new file mode 100644 index 0000000..a3d3c58 --- /dev/null +++ b/bxmessage/droprelay.go @@ -0,0 +1,14 @@ +package bxmessage + +// DropRelay represents a message from relay to gateway requesting gateway disconnect and get new peers +type DropRelay struct { + Header +} + +// Pack serializes an DropRelay into a buffer for sending on the wire +func (m DropRelay) Pack(protocol Protocol) ([]byte, error) { + bufLen := m.Header.Size() + buf := make([]byte, bufLen) + m.Header.Pack(&buf, DropRelayType) + return buf, nil +} diff --git a/bxmessage/errornotification.go b/bxmessage/errornotification.go new file mode 100644 index 0000000..8dd5cec --- /dev/null +++ b/bxmessage/errornotification.go @@ -0,0 +1,44 @@ +package bxmessage + +import ( + "encoding/binary" + "github.com/bloXroute-Labs/gateway/types" +) + +// ErrorNotification represents an error msg the relay sends to the gateway +type ErrorNotification struct { + Header + ErrorType types.ErrorType + Code types.ErrorNotificationCode + Reason string +} + +func (m *ErrorNotification) size() uint32 { + return m.Header.Size() + uint32(types.ErrorTypeLen+types.ErrorNotificationCodeLen+len(m.Reason)) +} + +// Pack serializes an ErrorNotification into the buffer for sending +func (m *ErrorNotification) Pack(protocol Protocol) ([]byte, error) { + bufLen := m.size() + buf := make([]byte, bufLen) + m.Header.Pack(&buf, ErrorNotificationType) + offset := HeaderLen + binary.LittleEndian.PutUint16(buf[offset:], uint16(m.ErrorType)) + offset += types.ErrorTypeLen + binary.LittleEndian.PutUint32(buf[offset:], uint32(m.Code)) + offset += types.ErrorNotificationCodeLen + copy(buf[offset:], m.Reason) + return buf, nil +} + +// Unpack deserializes an ErrorNotification from a buffer +func (m *ErrorNotification) Unpack(buf []byte, protocol Protocol) error { + offset := HeaderLen + m.ErrorType = types.ErrorType(binary.LittleEndian.Uint16(buf[offset:])) + offset += types.ErrorTypeLen + m.Code = types.ErrorNotificationCode(binary.LittleEndian.Uint32(buf[offset:])) + offset += types.ErrorNotificationCodeLen + m.Reason = string(buf[offset : len(buf)-ControlByteLen]) + offset += len(m.Reason) + return m.Header.Unpack(buf, protocol) +} diff --git a/bxmessage/errornotification_test.go b/bxmessage/errornotification_test.go new file mode 100644 index 0000000..f6b187a --- /dev/null +++ b/bxmessage/errornotification_test.go @@ -0,0 +1,24 @@ +package bxmessage + +import ( + "github.com/bloXroute-Labs/gateway/types" + "github.com/stretchr/testify/assert" + "testing" +) + +func TestErrorNotificationPackUnpack(t *testing.T) { + errorNotification := ErrorNotification{} + errorNotification.ErrorType = types.ErrorTypeTemporary + errorNotification.Code = 12 + errorNotification.Reason = "failed for error" + e, err := errorNotification.Pack(0) + assert.Nil(t, err) + + var decodedErrorNotification ErrorNotification + err = decodedErrorNotification.Unpack(e, 0) + assert.Nil(t, err) + + assert.Equal(t, types.ErrorTypeTemporary, decodedErrorNotification.ErrorType) + assert.Equal(t, 12, int(decodedErrorNotification.Code)) + assert.Equal(t, "failed for error", decodedErrorNotification.Reason) +} diff --git a/bxmessage/gettxs.go b/bxmessage/gettxs.go new file mode 100644 index 0000000..e0476f8 --- /dev/null +++ b/bxmessage/gettxs.go @@ -0,0 +1,47 @@ +package bxmessage + +import ( + "encoding/binary" + utils2 "github.com/bloXroute-Labs/gateway/bxmessage/utils" + "github.com/bloXroute-Labs/gateway/types" +) + +// GetTxs - represent the "gettxs" message +type GetTxs struct { + Header + ShortIDs types.ShortIDList + Hash types.SHA256Hash // Hash is not a part of the buffer +} + +// Pack serializes a GetTxs into a buffer for sending +func (getTxs *GetTxs) Pack(protocol Protocol) ([]byte, error) { + bufLen := getTxs.size() + buf := make([]byte, bufLen) + offset := uint32(HeaderLen) + binary.LittleEndian.PutUint32(buf[offset:], uint32(len(getTxs.ShortIDs))) + offset += types.UInt32Len + for _, shortID := range getTxs.ShortIDs { + binary.LittleEndian.PutUint32(buf[offset:], uint32(shortID)) + offset += types.UInt32Len + } + getTxs.Header.Pack(&buf, GetTransactionsType) + return buf, nil +} + +// Unpack deserializes a GetTxs from a buffer +func (getTxs *GetTxs) Unpack(buf []byte, protocol Protocol) error { + getTxs.Hash = utils2.DoubleSHA256(buf[:]) + var shortIDs uint32 + shortIDs = binary.LittleEndian.Uint32(buf[HeaderLen:]) + offset := HeaderLen + types.UInt32Len + for i := 0; i < int(shortIDs); i++ { + shortID := binary.LittleEndian.Uint32(buf[offset:]) + getTxs.ShortIDs = append(getTxs.ShortIDs, types.ShortID(shortID)) + offset += types.UInt32Len + } + return getTxs.Header.Unpack(buf, protocol) +} + +func (getTxs *GetTxs) size() uint32 { + return getTxs.Header.Size() + uint32(types.UInt32Len+len(getTxs.ShortIDs)*types.UInt32Len) +} diff --git a/bxmessage/gettxs_test.go b/bxmessage/gettxs_test.go new file mode 100644 index 0000000..0960a8c --- /dev/null +++ b/bxmessage/gettxs_test.go @@ -0,0 +1,21 @@ +package bxmessage + +import ( + "encoding/hex" + "github.com/bloXroute-Labs/gateway/types" + "github.com/stretchr/testify/assert" + "testing" +) + +func TestGetTxsPack(t *testing.T) { + getTxs := GetTxs{} + getTxs.ShortIDs = []types.ShortID{1, 2, 3, 4, 5} + x, _ := getTxs.Pack(0) + assert.Equal(t, "fffefdfc6765747478730000000000001900000005000000010000000200000003000000040000000500000001", hex.EncodeToString(x)) +} +func TestGetTxsUnpack(t *testing.T) { + getTxs := GetTxs{} + x, _ := hex.DecodeString("fffefdfc6765747478730000000000001900000005000000010000000200000003000000040000000500000001") + _ = getTxs.Unpack(x, 0) + assert.Equal(t, types.ShortIDList{1, 2, 3, 4, 5}, getTxs.ShortIDs) +} diff --git a/bxmessage/header.go b/bxmessage/header.go new file mode 100644 index 0000000..31862ae --- /dev/null +++ b/bxmessage/header.go @@ -0,0 +1,81 @@ +package bxmessage + +import ( + "bytes" + "encoding/binary" + "encoding/hex" + "fmt" +) + +// Protocol represents the Protocol version number +type Protocol uint32 + +// SendPriority controls the priority send queue +type SendPriority int + +// message sending priorities +const ( + HighestPriority SendPriority = iota + HighPriority + NormalPriority + LowPriority + LowestPriority + OnPongPriority +) + +func (p SendPriority) String() string { + return [...]string{"HighestPriority", "HighPriority", "NormalPriority", "LowPriority", "LowestPriority"}[p] +} + +// Header represents the shared header of a bloxroute message +type Header struct { + priority *SendPriority + msgType string +} + +// Pack serializes a Header into a buffer for sending on the wire +func (h *Header) Pack(buf *[]byte, msgType string) { + h.msgType = msgType + + binary.BigEndian.PutUint32(*buf, 0xfffefdfc) + copy((*buf)[TypeOffset:], msgType) + binary.LittleEndian.PutUint32((*buf)[PayloadSizeOffset:], uint32(len(*buf))-HeaderLen) + + (*buf)[len(*buf)-ControlByteLen] = ValidControlByte +} + +// Unpack deserializes a Header from a buffer +func (h *Header) Unpack(buf []byte, _ Protocol) error { + h.msgType = string(bytes.Trim(buf[TypeOffset:TypeOffset+TypeLength], NullByte)) + return nil +} + +// Size returns the byte length of header plus ControlByteLen +func (h *Header) Size() uint32 { + return HeaderLen + ControlByteLen +} + +// GetPriority extracts the message send priority +func (h *Header) GetPriority() SendPriority { + if h.priority == nil { + return NormalPriority + } + return *h.priority +} + +// SetPriority sets the message send priority +func (h *Header) SetPriority(priority SendPriority) { + h.priority = &priority +} + +func (h *Header) String() string { + return fmt.Sprintf("Message", h.msgType) +} + +func checkBufSize(buf *[]byte, offset int, size int) error { + if len(*buf) < offset+size { + return fmt.Errorf("Invalid message format. %v bytes needed at offset %v but buff size is %v. buffer: %v", + size, offset, len(*buf), hex.EncodeToString(*buf)) + } + return nil +} diff --git a/bxmessage/hello.go b/bxmessage/hello.go new file mode 100644 index 0000000..db72cd4 --- /dev/null +++ b/bxmessage/hello.go @@ -0,0 +1,81 @@ +package bxmessage + +import ( + "encoding/binary" + "encoding/hex" + "github.com/bloXroute-Labs/gateway/types" + "strings" +) + +// Hello exchanges node and protocol info when two bloxroute nodes initially connect +type Hello struct { + Header + Protocol Protocol + networkNumber types.NetworkNum + NodeID types.NodeID + Capabilities types.CapabilityFlags + ClientVersion string +} + +// GetNetworkNum gets the message network number +func (m *Hello) GetNetworkNum() types.NetworkNum { + return m.networkNumber +} + +// SetNetworkNum sets the message network number +func (m *Hello) SetNetworkNum(networkNum types.NetworkNum) { + m.networkNumber = networkNum +} + +func (m *Hello) size() uint32 { + size := m.Header.Size() + ProtocolLen + types.NetworkNumLen + types.NodeIDLen + if m.Protocol >= MevProtocol { + size += ClientVersionLen + CapabilitiesLen + } + + return size +} + +// Pack serializes a Hello into the buffer for sending +func (m *Hello) Pack(protocol Protocol) ([]byte, error) { + bufLen := m.size() + buf := make([]byte, bufLen) + offset := HeaderLen + binary.LittleEndian.PutUint32(buf[offset:], uint32(m.Protocol)) + offset += ProtocolLen + binary.LittleEndian.PutUint32(buf[offset:], uint32(m.networkNumber)) + offset += types.NetworkNumLen + nodeID, err := hex.DecodeString(strings.ReplaceAll(string(m.NodeID), "-", "")) + if err != nil { + return nil, err + } + copy(buf[offset:], nodeID) + offset += types.NodeIDLen + if m.Protocol >= MevProtocol { + binary.LittleEndian.PutUint16(buf[offset:], uint16(m.Capabilities)) + offset += CapabilitiesLen + copy(buf[offset:], m.ClientVersion) + offset += ClientVersionLen + } + m.Header.Pack(&buf, "hello") + return buf, nil + +} + +// Unpack deserializes a Hello from a buffer +func (m *Hello) Unpack(buf []byte, protocol Protocol) error { + offset := HeaderLen + m.Protocol = Protocol(binary.LittleEndian.Uint32(buf[offset:])) + offset += ProtocolLen + m.networkNumber = types.NetworkNum(binary.LittleEndian.Uint32(buf[offset:])) + offset += types.NetworkNumLen + m.NodeID = types.NodeID(buf[offset:]) + offset += types.NodeIDLen + if m.Protocol >= MevProtocol { + m.Capabilities = types.CapabilityFlags(binary.LittleEndian.Uint16(buf[offset:])) + offset += CapabilitiesLen + m.ClientVersion = string(buf[offset:]) + offset += ClientVersionLen + } + return m.Header.Unpack(buf, protocol) +} diff --git a/bxmessage/message.go b/bxmessage/message.go new file mode 100644 index 0000000..feb0503 --- /dev/null +++ b/bxmessage/message.go @@ -0,0 +1,36 @@ +package bxmessage + +import ( + "bytes" + "fmt" + "github.com/bloXroute-Labs/gateway/types" + "github.com/ethereum/go-ethereum/common/hexutil" +) + +// Message is the base interface of all connection message sent on the wire +type Message interface { + Pack(protocol Protocol) ([]byte, error) + Unpack(buf []byte, protocol Protocol) error + GetPriority() SendPriority + SetPriority(priority SendPriority) + String() string +} + +// BroadcastMessage is the base interface of all broadcast message sent on the wire +type BroadcastMessage interface { + Message + GetNetworkNum() types.NetworkNum +} + +// MessageBytes type def for message byte sets +type MessageBytes []byte + +// BxType parses the message type from a bx message +func (mb MessageBytes) BxType() string { + return string(bytes.Trim(mb[TypeOffset:TypeOffset+TypeLength], NullByte)) +} + +// String formats the message bytes for hex output +func (mb MessageBytes) String() string { + return fmt.Sprintf("%v[%v]", mb.BxType(), hexutil.Encode(mb)) +} diff --git a/bxmessage/mevbundle.go b/bxmessage/mevbundle.go new file mode 100644 index 0000000..1f546c3 --- /dev/null +++ b/bxmessage/mevbundle.go @@ -0,0 +1,160 @@ +package bxmessage + +import ( + "encoding/binary" + "fmt" + "github.com/bloXroute-Labs/gateway/bxmessage/utils" + "github.com/bloXroute-Labs/gateway/types" +) + +const mevBundleNameMaxSize = 255 + +// MEVMinerNames alias for []string +type MEVMinerNames = []string + +// MEVBundleParams alias for []byte +type MEVBundleParams = []byte + +// MEVBundle represents data that we receive from MEV-miners and send to BDN +type MEVBundle struct { + BroadcastHeader + + ID string `json:"id"` + JSONRPC string `json:"jsonrpc"` + Method string `json:"method"` + + names MEVMinerNames + Params MEVBundleParams `json:"params"` +} + +// NewMEVBundle create MEVBundle +func NewMEVBundle(mevMinerMethod string, mevMinerNames MEVMinerNames, mevBundleParams MEVBundleParams) (MEVBundle, error) { + if err := checkMEVBundleNameSize(len(mevMinerNames)); err != nil { + return MEVBundle{}, err + } + return MEVBundle{ + Method: mevMinerMethod, + names: mevMinerNames, + Params: mevBundleParams, + }, nil +} + +// Names gets the message MEVMinerNames +func (m MEVBundle) Names() MEVMinerNames { + return m.names +} + +// SetHash set hash based in params +func (m *MEVBundle) SetHash() { + buf := []byte{} + for _, name := range m.names { + buf = append(buf, []byte(name)...) + } + buf = append(buf, m.Params...) + m.hash = utils.DoubleSHA256(buf[:]) +} + +func (m MEVBundle) size() uint32 { + var size uint32 + for _, name := range m.names { + size += uint32(types.UInt16Len + len(name)) + } + return size + types.UInt16Len + uint32(len(m.Method)) + types.UInt8Len + uint32(len(m.Params)) + m.BroadcastHeader.Size() +} + +// Pack serializes a MEVBundle into a buffer for sending +func (m MEVBundle) Pack(_ Protocol) ([]byte, error) { + bufLen := m.size() + buf := make([]byte, bufLen) + m.BroadcastHeader.Pack(&buf, MEVBundleType) + offset := BroadcastHeaderLen + + binary.LittleEndian.PutUint16(buf[offset:], uint16(len(m.Method))) + offset += types.UInt16Len + + copy(buf[offset:], m.Method) + offset += len(m.Method) + + if err := checkMEVBundleNameSize(len(m.names)); err != nil { + return nil, err + } + mevMiners := make([]byte, 1) + mevMiners[0] = byte(len(m.names)) + copy(buf[offset:], mevMiners) + offset++ + + for _, name := range m.names { + nameLength := len(name) + binary.LittleEndian.PutUint16(buf[offset:], uint16(nameLength)) + offset += types.UInt16Len + copy(buf[offset:], name) + offset += nameLength + } + + copy(buf[offset:], m.Params) + + return buf, nil +} + +// Unpack deserializes a MEVBundle from a buffer +func (m *MEVBundle) Unpack(buf []byte, _ Protocol) error { + err := m.BroadcastHeader.Unpack(buf, 0) + if err != nil { + return err + } + offset := BroadcastHeaderLen + + if err := checkBufSize(&buf, offset, types.UInt16Len); err != nil { + return err + } + mevMinerMethodLen := binary.LittleEndian.Uint16(buf[offset:]) + offset += types.UInt16Len + + if err := checkBufSize(&buf, offset, int(mevMinerMethodLen)); err != nil { + return err + } + m.Method = string(buf[offset : offset+int(mevMinerMethodLen)]) + offset += len(m.Method) + + if err := checkBufSize(&buf, offset, types.UInt8Len); err != nil { + return err + } + mevMinersLength := buf[offset : offset+types.UInt8Len][0] + offset++ + + m.names = MEVMinerNames{} + + for i := 0; i < int(mevMinersLength); i++ { + if err := checkBufSize(&buf, offset, types.UInt16Len); err != nil { + return err + } + mevMinerNameLength := binary.LittleEndian.Uint16(buf[offset:]) + offset += types.UInt16Len + + if err := checkBufSize(&buf, offset, int(mevMinerNameLength)); err != nil { + return err + } + mevMinerName := string(buf[offset : offset+int(mevMinerNameLength)]) + offset += int(mevMinerNameLength) + m.names = append(m.names, mevMinerName) + } + + payloadOffsetEnd := len(buf) - ControlByteLen + if err := checkBufSize(&buf, offset, payloadOffsetEnd-offset); err != nil { + return err + } + m.Params = buf[offset:payloadOffsetEnd] + + return nil +} + +func checkMEVBundleNameSize(namesSize int) error { + if namesSize > mevBundleNameMaxSize { + return fmt.Errorf("number of mev builders names %v exceeded the limit (%v)", namesSize, mevBundleNameMaxSize) + } + if namesSize == 0 { + return fmt.Errorf("at least 1 mev miner must be present") + } + + return nil +} diff --git a/bxmessage/mevbundle_test.go b/bxmessage/mevbundle_test.go new file mode 100644 index 0000000..f7f8a87 --- /dev/null +++ b/bxmessage/mevbundle_test.go @@ -0,0 +1,63 @@ +package bxmessage + +import ( + "encoding/hex" + "fmt" + "github.com/bloXroute-Labs/gateway/test/fixtures" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "strconv" + "testing" +) + +func TestMEVBundlePackSuccess(t *testing.T) { + mevMinerNames := MEVMinerNames{"test miner1", "test miner2"} + params := []byte("test params") + + mevMinerBundle, err := NewMEVBundle("eth_sendMegabundle", mevMinerNames, params) + assert.NoError(t, err) + assert.Equal(t, mevMinerNames, mevMinerBundle.Names()) + + packPayload, _ := mevMinerBundle.Pack(0) + + err = mevMinerBundle.Unpack(packPayload, 0) + assert.NoError(t, err) + assert.Equal(t, string(params), string(mevMinerBundle.Params)) + assert.Equal(t, len(mevMinerNames), len(mevMinerBundle.Names())) + assert.Equal(t, "eth_sendMegabundle", mevMinerBundle.Method) + assert.Equal(t, 131, len(packPayload)) +} + +func TestMEVBundleNewFailedMinerNamesToLong(t *testing.T) { + mevSearchersAuthorization := MEVMinerNames{} + for i := 0; i < 258; i++ { + mevSearchersAuthorization = append(mevSearchersAuthorization, strconv.Itoa(i)) + } + params := []byte("content test") + _, err := NewMEVBundle("eth_sendMegabundle", mevSearchersAuthorization, params) + require.Error(t, err) + assert.Equal(t, fmt.Sprintf("number of mev builders names %v exceeded the limit (%v)", len(mevSearchersAuthorization), mevBundleNameMaxSize), err.Error()) +} + +func TestMEVSearcherPackFailedMinerNamesLengthNotEnough(t *testing.T) { + mevSearchersAuthorization := MEVMinerNames{} + params := []byte("content test") + _, err := NewMEVBundle("eth_sendMegabundle", mevSearchersAuthorization, params) + require.Error(t, err) + assert.Equal(t, "at least 1 mev miner must be present", err.Error()) +} + +func TestMEVBundleUnpackSuccess(t *testing.T) { + mevSearcher := MEVBundle{} + buf, err := hex.DecodeString(fixtures.MEVBundlePayload) + assert.NoError(t, err) + params, err := hex.DecodeString("7465737420706172616d73") + assert.NoError(t, err) + + assert.NoError(t, err) + err = mevSearcher.Unpack(buf, 0) + require.NoError(t, err) + assert.Equal(t, params, mevSearcher.Params) + assert.Equal(t, "eth_sendBundle", mevSearcher.Method) + assert.Equal(t, MEVMinerNames{"test miner1", "test miner2"}, mevSearcher.Names()) +} diff --git a/bxmessage/mevsearcher.go b/bxmessage/mevsearcher.go new file mode 100644 index 0000000..bc0420a --- /dev/null +++ b/bxmessage/mevsearcher.go @@ -0,0 +1,175 @@ +package bxmessage + +import ( + "encoding/binary" + "fmt" + "github.com/bloXroute-Labs/gateway/bxmessage/utils" + "github.com/bloXroute-Labs/gateway/types" +) + +const maxAuthNames = 255 + +// MEVSearcherAuth alias for map[string]string +type MEVSearcherAuth map[string]string + +// MEVSearcherParams alias for []byte +type MEVSearcherParams = []byte + +// MEVSearcher represents data that we receive from searcher and send to BDN +type MEVSearcher struct { + BroadcastHeader + + ID string `json:"id"` + JSONRPC string `json:"jsonrpc"` + Method string `json:"method"` + + auth MEVSearcherAuth + Params MEVSearcherParams `json:"params"` +} + +// NewMEVSearcher create MEVSearcher +func NewMEVSearcher(mevMinerMethod string, auth MEVSearcherAuth, params MEVSearcherParams) (MEVSearcher, error) { + if err := checkAuthSize(len(auth)); err != nil { + return MEVSearcher{}, err + } + return MEVSearcher{ + Method: mevMinerMethod, + auth: auth, + Params: params, + }, nil +} + +// SetHash set hash based on MEVSearcher params and auth +func (m *MEVSearcher) SetHash() { + buf := []byte{} + for name, auth := range m.auth { + buf = append(buf, []byte(name+auth)...) + } + buf = append(buf, m.Params...) + m.hash = utils.DoubleSHA256(buf[:]) +} + +// Auth gets the message MEVSearcherAuth +func (m MEVSearcher) Auth() MEVSearcherAuth { + return m.auth +} + +func (m MEVSearcher) size() uint32 { + var size uint32 + for name, authorization := range m.auth { + size += uint32(types.UInt16Len + len(name) + types.UInt16Len + len(authorization)) + } + + return size + types.UInt16Len + uint32(len(m.Method)) + types.UInt8Len + uint32(len(m.Params)) + m.BroadcastHeader.Size() +} + +// Pack serializes a MevBundle into a buffer for sending +func (m MEVSearcher) Pack(_ Protocol) ([]byte, error) { + bufLen := m.size() + buf := make([]byte, bufLen) + m.BroadcastHeader.Pack(&buf, MEVSearcherType) + offset := BroadcastHeaderLen + + binary.LittleEndian.PutUint16(buf[offset:], uint16(len(m.Method))) + offset += types.UInt16Len + + copy(buf[offset:], m.Method) + offset += len(m.Method) + + if err := checkAuthSize(len(m.auth)); err != nil { + return nil, err + } + mevMiners := make([]uint8, 1) + mevMiners[0] = byte(len(m.auth)) + copy(buf[offset:], mevMiners) + offset++ + + for name, auth := range m.auth { + nameLength := len(name) + authorizationLength := len(auth) + binary.LittleEndian.PutUint16(buf[offset:], uint16(nameLength)) + offset += types.UInt16Len + copy(buf[offset:], name) + offset += nameLength + + binary.LittleEndian.PutUint16(buf[offset:], uint16(authorizationLength)) + offset += types.UInt16Len + copy(buf[offset:], auth) + offset += authorizationLength + } + + copy(buf[offset:], m.Params) + + return buf, nil +} + +// Unpack deserializes a MevBundle from a buffer +func (m *MEVSearcher) Unpack(buf []byte, _ Protocol) error { + err := m.BroadcastHeader.Unpack(buf, 0) + if err != nil { + return err + } + offset := BroadcastHeaderLen + + if err := checkBufSize(&buf, offset, types.UInt16Len); err != nil { + return err + } + mevMinerMethodLen := binary.LittleEndian.Uint16(buf[offset:]) + offset += types.UInt16Len + + if err := checkBufSize(&buf, offset, int(mevMinerMethodLen)); err != nil { + return err + } + m.Method = string(buf[offset : offset+int(mevMinerMethodLen)]) + offset += len(m.Method) + + if err := checkBufSize(&buf, offset, types.UInt8Len); err != nil { + return err + } + mevSearchers := buf[offset] + offset++ + + m.auth = MEVSearcherAuth{} + for i := 0; i < int(mevSearchers); i++ { + if err := checkBufSize(&buf, offset, types.UInt16Len); err != nil { + return err + } + nameLength := binary.LittleEndian.Uint16(buf[offset:]) + offset += types.UInt16Len + + if err := checkBufSize(&buf, offset, int(nameLength)); err != nil { + return err + } + name := string(buf[offset : offset+int(nameLength)]) + offset += int(nameLength) + + if err := checkBufSize(&buf, offset, types.UInt16Len); err != nil { + return err + } + authLength := binary.LittleEndian.Uint16(buf[offset:]) + offset += types.UInt16Len + + if err := checkBufSize(&buf, offset, int(authLength)); err != nil { + return err + } + auth := string(buf[offset : offset+int(authLength)]) + offset += int(authLength) + + m.auth[name] = auth + } + + m.Params = buf[offset : len(buf)-ControlByteLen] + + return nil +} + +func checkAuthSize(authSize int) error { + if authSize > maxAuthNames { + return fmt.Errorf("number of mev builders names %v exceeded the limit (%v)", authSize, maxAuthNames) + } + if authSize == 0 { + return fmt.Errorf("at least 1 mev builder must be present") + } + + return nil +} diff --git a/bxmessage/mevsearcher_test.go b/bxmessage/mevsearcher_test.go new file mode 100644 index 0000000..c41f8f6 --- /dev/null +++ b/bxmessage/mevsearcher_test.go @@ -0,0 +1,64 @@ +package bxmessage + +import ( + "encoding/hex" + "fmt" + "github.com/bloXroute-Labs/gateway/test/fixtures" + "github.com/stretchr/testify/require" + "strconv" + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestMEVSearcherPackSuccess(t *testing.T) { + mevSearchersAuthorization := MEVSearcherAuth{"name test": "auth test"} + params := []byte("content test") + + mevSearcher, err := NewMEVSearcher("eth_sendMegabundle", mevSearchersAuthorization, params) + assert.NoError(t, err) + assert.Equal(t, len(mevSearchersAuthorization), len(mevSearcher.Auth())) + + packPayload, _ := mevSearcher.Pack(0) + + err = mevSearcher.Unpack(packPayload, 0) + assert.NoError(t, err) + assert.Equal(t, string(params), string(mevSearcher.Params)) + assert.Equal(t, "eth_sendMegabundle", mevSearcher.Method) + assert.Equal(t, len(mevSearchersAuthorization), len(mevSearcher.Auth())) +} + +func TestMEVSearcherPackFailedAuthToLong(t *testing.T) { + mevSearchersAuthorization := MEVSearcherAuth{} + for i := 0; i < 258; i++ { + mevSearchersAuthorization[strconv.Itoa(i)] = strconv.Itoa(i) + } + params := []byte("content test") + _, err := NewMEVSearcher("eth_sendMegabundle", mevSearchersAuthorization, params) + require.Error(t, err) + assert.Equal(t, fmt.Sprintf("number of mev builders names %v exceeded the limit (%v)", len(mevSearchersAuthorization), maxAuthNames), err.Error()) +} + +func TestMEVSearcherPackFailedAuthLengthNotEnough(t *testing.T) { + mevSearchersAuthorization := MEVSearcherAuth{} + params := []byte("content test") + _, err := NewMEVSearcher("eth_sendMegabundle", mevSearchersAuthorization, params) + require.Error(t, err) + assert.Equal(t, "at least 1 mev builder must be present", err.Error()) +} + +func TestMEVSearcherUnpackSuccess(t *testing.T) { + mevSearcher := MEVSearcher{} + + buf, err := hex.DecodeString(fixtures.MEVSearcherPayload) + assert.NoError(t, err) + params, err := hex.DecodeString("636f6e74656e742074657374") + assert.NoError(t, err) + + assert.NoError(t, err) + err = mevSearcher.Unpack(buf, 0) + assert.NoError(t, err) + assert.Equal(t, params, mevSearcher.Params) + assert.Equal(t, "eth_sendMegabundle", mevSearcher.Method) + assert.Equal(t, MEVSearcherAuth{"name test": "auth test"}, mevSearcher.Auth()) +} diff --git a/bxmessage/ping.go b/bxmessage/ping.go new file mode 100644 index 0000000..56c4616 --- /dev/null +++ b/bxmessage/ping.go @@ -0,0 +1,35 @@ +package bxmessage + +import ( + "encoding/binary" + "github.com/bloXroute-Labs/gateway/types" + "time" +) + +// Ping initiates a ping for connection liveliness +type Ping struct { + Header + Nonce uint64 +} + +func (pm *Ping) size() uint32 { + return pm.Header.Size() + types.UInt64Len +} + +// Pack serializes a Ping into a buffer for sending +func (pm Ping) Pack(protocol Protocol) ([]byte, error) { + if pm.Nonce == 0 { + pm.Nonce = uint64(time.Now().UnixNano() / 1000) + } + bufLen := pm.size() + buf := make([]byte, bufLen) + binary.LittleEndian.PutUint64(buf[HeaderLen:], pm.Nonce) + pm.Header.Pack(&buf, "ping") + return buf, nil +} + +// Unpack deserializes a Ping from a buffer +func (pm *Ping) Unpack(buf []byte, protocol Protocol) error { + pm.Nonce = binary.LittleEndian.Uint64(buf[HeaderLen:]) + return pm.Header.Unpack(buf, protocol) +} diff --git a/bxmessage/pong.go b/bxmessage/pong.go new file mode 100644 index 0000000..bfd6898 --- /dev/null +++ b/bxmessage/pong.go @@ -0,0 +1,40 @@ +package bxmessage + +import ( + "encoding/binary" + "github.com/bloXroute-Labs/gateway/types" + "time" +) + +// Pong is a response to Ping messages +type Pong struct { + Header + Nonce uint64 + TimeStamp uint64 +} + +func (pm *Pong) size() uint32 { + return pm.Header.Size() + (2 * types.UInt64Len) +} + +// Pack serializes a Pong into a buffer for sending +func (pm Pong) Pack(protocol Protocol) ([]byte, error) { + bufLen := pm.size() + buf := make([]byte, bufLen) + offset := HeaderLen + binary.LittleEndian.PutUint64(buf[offset:], pm.Nonce) + offset += types.UInt64Len + pm.TimeStamp = uint64(time.Now().UnixNano() / 1000) + binary.LittleEndian.PutUint64(buf[offset:], pm.TimeStamp) + pm.Header.Pack(&buf, "pong") + return buf, nil +} + +// Unpack deserializes a Pong from a buffer +func (pm *Pong) Unpack(buf []byte, protocol Protocol) error { + offset := HeaderLen + pm.Nonce = binary.LittleEndian.Uint64(buf[offset:]) + offset += types.UInt64Len + pm.TimeStamp = binary.LittleEndian.Uint64(buf[offset:]) + return pm.Header.Unpack(buf, protocol) +} diff --git a/bxmessage/refreshblockchainnetwork.go b/bxmessage/refreshblockchainnetwork.go new file mode 100644 index 0000000..ac7617f --- /dev/null +++ b/bxmessage/refreshblockchainnetwork.go @@ -0,0 +1,25 @@ +package bxmessage + +import "github.com/bloXroute-Labs/gateway/types" + +// RefreshBlockchainNetwork acknowledges a received header message +type RefreshBlockchainNetwork struct { + Header +} + +func (m *RefreshBlockchainNetwork) size() uint32 { + return m.Header.Size() +} + +// GetNetworkNum gets the message network number +func (m *RefreshBlockchainNetwork) GetNetworkNum() types.NetworkNum { + return types.AllNetworkNum +} + +// Pack serializes an RefreshBlockchainNetwork into a buffer for sending on the wire +func (m RefreshBlockchainNetwork) Pack(protocol Protocol) ([]byte, error) { + bufLen := m.size() + buf := make([]byte, bufLen) + m.Header.Pack(&buf, RefreshBlockchainNetworkType) + return buf, nil +} diff --git a/bxmessage/syncdone.go b/bxmessage/syncdone.go new file mode 100644 index 0000000..6a63299 --- /dev/null +++ b/bxmessage/syncdone.go @@ -0,0 +1,41 @@ +package bxmessage + +import ( + "encoding/binary" + "github.com/bloXroute-Labs/gateway/types" +) + +// SyncDone indicates that transaction service sync is completed +type SyncDone struct { + Header + networkNumber types.NetworkNum +} + +// GetNetworkNum gets the message network number +func (m *SyncDone) GetNetworkNum() types.NetworkNum { + return m.networkNumber +} + +// SetNetworkNum sets the message network number +func (m *SyncDone) SetNetworkNum(networkNum types.NetworkNum) { + m.networkNumber = networkNum +} + +func (m *SyncDone) size() uint32 { + return m.Header.Size() + types.NetworkNumLen +} + +// Pack serializes a SyncDone into a buffer for sending +func (m *SyncDone) Pack(protocol Protocol) ([]byte, error) { + bufLen := m.size() + buf := make([]byte, bufLen) + binary.LittleEndian.PutUint32(buf[HeaderLen:], uint32(m.networkNumber)) + m.Header.Pack(&buf, SyncDoneType) + return buf, nil +} + +// Unpack deserializes a SyncDone from a buffer +func (m *SyncDone) Unpack(buf []byte, protocol Protocol) error { + m.networkNumber = types.NetworkNum(binary.LittleEndian.Uint32(buf[HeaderLen:])) + return m.Header.Unpack(buf, protocol) +} diff --git a/bxmessage/syncreq.go b/bxmessage/syncreq.go new file mode 100644 index 0000000..3467d48 --- /dev/null +++ b/bxmessage/syncreq.go @@ -0,0 +1,41 @@ +package bxmessage + +import ( + "encoding/binary" + "github.com/bloXroute-Labs/gateway/types" +) + +// SyncReq requests to start transaction state sync on a network +type SyncReq struct { + Header + networkNumber types.NetworkNum +} + +// GetNetworkNum gets the message network number +func (m *SyncReq) GetNetworkNum() types.NetworkNum { + return m.networkNumber +} + +// SetNetworkNum sets the message network number +func (m *SyncReq) SetNetworkNum(networkNum types.NetworkNum) { + m.networkNumber = networkNum +} + +func (m *SyncReq) size() uint32 { + return m.Header.Size() + types.NetworkNumLen +} + +// Pack serializes a SyncReq into a buffer for sending +func (m SyncReq) Pack(protocol Protocol) ([]byte, error) { + bufLen := m.size() + buf := make([]byte, bufLen) + binary.LittleEndian.PutUint32(buf[HeaderLen:], uint32(m.networkNumber)) + m.Header.Pack(&buf, SyncReqType) + return buf, nil +} + +// Unpack deserializes a SyncReq from a buffer +func (m *SyncReq) Unpack(buf []byte, protocol Protocol) error { + m.networkNumber = types.NetworkNum(binary.LittleEndian.Uint32(buf[HeaderLen:])) + return m.Header.Unpack(buf, protocol) +} diff --git a/bxmessage/synctxs.go b/bxmessage/synctxs.go new file mode 100644 index 0000000..341cbe1 --- /dev/null +++ b/bxmessage/synctxs.go @@ -0,0 +1,186 @@ +package bxmessage + +import ( + "encoding/binary" + "github.com/bloXroute-Labs/gateway/types" + log "github.com/sirupsen/logrus" + "time" +) + +// SyncTxContentsShortIDs represents information about a sync transaction received over sync +type SyncTxContentsShortIDs struct { + Hash types.SHA256Hash + Content types.TxContent + timestamp time.Time + ShortIDs types.ShortIDList + ShortIDFlags []types.TxFlags +} + +// Timestamp return the tx creation time +func (m *SyncTxContentsShortIDs) Timestamp() time.Time { + return m.timestamp +} + +// SetTimestamp sets the message creation timestamp +func (m *SyncTxContentsShortIDs) SetTimestamp(timestamp time.Time) { + m.timestamp = timestamp +} + +// SyncTxsMessage represents a chunk of transactions received during sync +type SyncTxsMessage struct { + Header + networkNumber types.NetworkNum + ContentShortIds []SyncTxContentsShortIDs + _size int + + //ShortIDs int +} + +// GetNetworkNum gets the message network number +func (m *SyncTxsMessage) GetNetworkNum() types.NetworkNum { + return m.networkNumber +} + +// SetNetworkNum sets the message network number +func (m *SyncTxsMessage) SetNetworkNum(networkNum types.NetworkNum) { + m.networkNumber = networkNum +} + +// Add adds another transaction into the message +func (m *SyncTxsMessage) Add(txInfo *types.BxTransaction) uint32 { + // don't sync transactions without short ID + if len(txInfo.ShortIDs()) == 0 { + return 0 + } + shortIDs := txInfo.ShortIDs() + csi := SyncTxContentsShortIDs{ + Hash: txInfo.Hash(), + Content: txInfo.Content(), + timestamp: txInfo.AddTime(), + ShortIDs: make(types.ShortIDList, len(shortIDs)), + ShortIDFlags: make([]types.TxFlags, len(shortIDs)), + } + for i, shortID := range shortIDs { + csi.ShortIDs[i] = shortID + csi.ShortIDFlags[i] = txInfo.Flags() + } + m.ContentShortIds = append(m.ContentShortIds, csi) + m._size += + types.SHA256HashLen + //hash + types.UInt32Len + // content size + len(csi.Content) + // content itself + types.UInt16Len + // shortIds count + types.UInt32Len + // transaction creation timestamp + len(csi.ShortIDs)*types.UInt32Len + // shortIds + len(csi.ShortIDFlags)*types.TxFlagsLen // flags + return m.size() +} + +func (m *SyncTxsMessage) size() uint32 { + return m.Header.Size() + uint32(types.NetworkNumLen+types.UInt32Len+m._size) +} + +// Count provides the number of tx included in the SyncTxsMessage +func (m *SyncTxsMessage) Count() int { + return len(m.ContentShortIds) +} + +// Pack serializes a SyncTxsMessage into a buffer for sending +func (m SyncTxsMessage) Pack(_ Protocol) ([]byte, error) { + bufLen := m.size() + buf := make([]byte, bufLen) + binary.LittleEndian.PutUint32(buf[HeaderLen:], uint32(m.networkNumber)) + binary.LittleEndian.PutUint32(buf[HeaderLen+types.NetworkNumLen:], uint32(len(m.ContentShortIds))) + m.packContentShortIds(&buf, HeaderLen+types.NetworkNumLen+types.UInt32Len) + m.Header.Pack(&buf, SyncTxsType) + return buf, nil +} + +// Unpack deserializes a SyncTxsMessage from a buffer +func (m *SyncTxsMessage) Unpack(buf []byte, protocol Protocol) error { + if err := checkBufSize(&buf, HeaderLen, types.NetworkNumLen+types.UInt32Len); err != nil { + return err + } + m.networkNumber = types.NetworkNum(binary.LittleEndian.Uint32(buf[HeaderLen:])) + txCount := binary.LittleEndian.Uint32(buf[HeaderLen+types.NetworkNumLen:]) + log.Tracef("%v: size %v, network %v, txcount %v", SyncTxsType, len(buf), m.networkNumber, txCount) + err := m.unpackContentShortIds(&buf, HeaderLen+types.NetworkNumLen+types.UInt32Len, txCount) + if err != nil { + return err + } + return m.Header.Unpack(buf, protocol) + +} + +func (m *SyncTxsMessage) packContentShortIds(buf *[]byte, offset int) int { + for _, csi := range m.ContentShortIds { + copy((*buf)[offset:], csi.Hash[:]) + offset += types.SHA256HashLen + binary.LittleEndian.PutUint32((*buf)[offset:], uint32(len(csi.Content))) + offset += types.UInt32Len + copy((*buf)[offset:], csi.Content[:]) + offset += len(csi.Content) + timestamp := uint32(csi.timestamp.UnixNano() / int64(1e9)) + binary.LittleEndian.PutUint32((*buf)[offset:], timestamp) + offset += types.UInt32Len + binary.LittleEndian.PutUint16((*buf)[offset:], uint16(len(csi.ShortIDs))) + offset += types.UInt16Len + for _, shortID := range csi.ShortIDs { + // is there a need to read it or just cast the memory? + binary.LittleEndian.PutUint32((*buf)[offset:], uint32(shortID)) + offset += types.UInt32Len + } + for _, flags := range csi.ShortIDFlags { + binary.LittleEndian.PutUint16((*buf)[offset:], uint16(flags)) + offset += types.UInt16Len + } + + } + return offset +} + +func (m *SyncTxsMessage) unpackContentShortIds(buf *[]byte, offset int, txCount uint32) error { + m.ContentShortIds = make([]SyncTxContentsShortIDs, txCount) + for i := 0; i < int(txCount); i++ { + if err := checkBufSize(buf, offset, types.SHA256HashLen+types.UInt32Len); err != nil { + return err + } + copy(m.ContentShortIds[i].Hash[:], (*buf)[offset:]) + offset += types.SHA256HashLen + ContentSize := int(binary.LittleEndian.Uint32((*buf)[offset:])) + offset += types.UInt32Len + if err := checkBufSize(buf, offset, ContentSize); err != nil { + return err + } + m.ContentShortIds[i].Content = (*buf)[offset : offset+ContentSize] + offset += ContentSize + if err := checkBufSize(buf, offset, types.UInt32Len+types.UInt16Len); err != nil { + return err + } + timestamp := int64(binary.LittleEndian.Uint32((*buf)[offset:])) * int64(1e9) + m.ContentShortIds[i].timestamp = time.Unix(0, timestamp) + offset += types.UInt32Len + ShortIdsCount := int(binary.LittleEndian.Uint16((*buf)[offset:])) + offset += types.UInt16Len + m.ContentShortIds[i].ShortIDs = make(types.ShortIDList, ShortIdsCount) + m.ContentShortIds[i].ShortIDFlags = make([]types.TxFlags, ShortIdsCount) + + if err := checkBufSize(buf, offset, ShortIdsCount*types.UInt32Len); err != nil { + return err + } + for j := 0; j < ShortIdsCount; j++ { + // is there a need to read it or just cast the memory? + m.ContentShortIds[i].ShortIDs[j] = types.ShortID(binary.LittleEndian.Uint32((*buf)[offset:])) + offset += types.UInt32Len + } + if err := checkBufSize(buf, offset, ShortIdsCount*types.UInt16Len); err != nil { + return err + } + for j := 0; j < ShortIdsCount; j++ { + // read short ID flags + m.ContentShortIds[i].ShortIDFlags[j] = types.TxFlags(binary.LittleEndian.Uint16((*buf)[offset:])) + offset += types.UInt16Len + } + } + return nil +} diff --git a/bxmessage/synctxs_test.go b/bxmessage/synctxs_test.go new file mode 100644 index 0000000..9d933e3 --- /dev/null +++ b/bxmessage/synctxs_test.go @@ -0,0 +1,79 @@ +package bxmessage + +import ( + "github.com/bloXroute-Labs/gateway/types" + "github.com/stretchr/testify/assert" + "testing" + "time" +) + +func TestPackUnpackTimeStamp(t *testing.T) { + tx1 := &types.BxTransaction{} + tx1.AddShortID(1) + tx1.SetAddTime(time.Now()) + + tx2 := &types.BxTransaction{} + tx2.AddShortID(2) + tx2.SetAddTime(time.Unix(0, 0)) + + syncTxs1 := SyncTxsMessage{} + syncTxs1.Add(tx1) + syncTxs1.Add(tx2) + buf, err := syncTxs1.Pack(CurrentProtocol) + assert.Nil(t, err) + + syncTxs2 := SyncTxsMessage{} + err = syncTxs2.Unpack(buf, CurrentProtocol) + assert.Nil(t, err) + assert.Equal(t, 2, len(syncTxs2.ContentShortIds)) + //log.Info(syncTxs2.ContentShortIds[0].Timestamp().Second()) + assert.Equal(t, tx1.AddTime().Second(), syncTxs2.ContentShortIds[0].Timestamp().Second()) + assert.Equal(t, time.Unix(0, 0).Second(), syncTxs2.ContentShortIds[1].Timestamp().Second()) + +} + +func TestPackUnpackBadBuffer(t *testing.T) { + tx1 := &types.BxTransaction{} + tx1.AddShortID(1) + tx1.SetContent([]byte{1}) + tx1.SetAddTime(time.Now()) + + tx2 := &types.BxTransaction{} + tx2.AddShortID(2) + tx2.SetContent([]byte{1, 2, 3, 4, 5}) + tx2.SetAddTime(time.Unix(0, 0)) + + syncTxs1 := SyncTxsMessage{} + syncTxs1.Add(tx1) + syncTxs1.Add(tx2) + buf, err := syncTxs1.Pack(CurrentProtocol) + assert.Nil(t, err) + + syncTxs2 := SyncTxsMessage{} + err = syncTxs2.Unpack(buf[0:0], CurrentProtocol) + assert.NotNil(t, err) + err = syncTxs2.Unpack(buf[0:1], CurrentProtocol) + assert.NotNil(t, err) + err = syncTxs2.Unpack(buf[0:2], CurrentProtocol) + assert.NotNil(t, err) + err = syncTxs2.Unpack(buf[0:20], CurrentProtocol) + assert.NotNil(t, err) + err = syncTxs2.Unpack(buf[0:30], CurrentProtocol) + assert.NotNil(t, err) + + err = syncTxs2.Unpack(buf[0:65+5], CurrentProtocol) + assert.NotNil(t, err) + + err = syncTxs2.Unpack(buf[0:77+35], CurrentProtocol) + assert.NotNil(t, err) + + err = syncTxs2.Unpack(buf[0:124+3], CurrentProtocol) + assert.NotNil(t, err) + + err = syncTxs2.Unpack(buf[0:len(buf)-2], CurrentProtocol) + assert.NotNil(t, err) + + err = syncTxs2.Unpack(buf[0:], CurrentProtocol) + assert.Nil(t, err) + +} diff --git a/bxmessage/tx.go b/bxmessage/tx.go new file mode 100644 index 0000000..abf170c --- /dev/null +++ b/bxmessage/tx.go @@ -0,0 +1,260 @@ +package bxmessage + +import ( + "bytes" + "encoding/binary" + "fmt" + "github.com/bloXroute-Labs/gateway/types" + "math" + "time" +) + +// Tx is the bloxroute message struct that carries a transaction from a specific blockchain +type Tx struct { + BroadcastHeader + shortID types.ShortID + flags types.TxFlags + timestamp time.Time + accountID [AccountIDLen]byte + content []byte + quota byte +} + +// NewTx constructs a new transaction message, using the provided hash function on the transaction contents to determine the hash +func NewTx(hash types.SHA256Hash, content []byte, networkNum types.NetworkNum, flags types.TxFlags, accountID types.AccountID) *Tx { + tx := &Tx{} + + if accountID != types.EmptyAccountID { + if !flags.IsPaid() { + panic("invalid usage: account ID cannot be set on unpaid transaction") + } + tx.SetAccountID(accountID) + } + + if accountID == types.EmptyAccountID && flags.IsPaid() { + panic("invalid usage: account ID must be set on paid transaction") + } + + tx.SetFlags(flags) + tx.SetNetworkNum(networkNum) + tx.SetContent(content) + tx.SetHash(hash) + tx.SetTimestamp(time.Now()) + return tx +} + +// HashString returns the hex encoded Hash, optionally with 0x prefix +func (m *Tx) HashString(prefix bool) string { + return m.hash.Format(prefix) +} + +// Content returns the blockchain transaction bytes +func (m *Tx) Content() (content []byte) { + return m.content +} + +// ShortID returns the assigned short ID of the transaction +func (m *Tx) ShortID() (sid types.ShortID) { + return m.shortID +} + +// Timestamp indicates when the TxMessage was created. This attribute is only set by relays. +func (m *Tx) Timestamp() time.Time { + return m.timestamp +} + +// AccountID indicates the account ID the TxMessage originated from +func (m *Tx) AccountID() types.AccountID { + if bytes.Equal(m.accountID[:], NullByteAccountID) { + return "" + } + return types.NewAccountID(m.accountID[:]) +} + +// SetAccountID sets the account ID +func (m *Tx) SetAccountID(accountID types.AccountID) { + copy(m.accountID[:], accountID) +} + +// SetContent sets the transaction content +func (m *Tx) SetContent(content []byte) { + m.content = content +} + +// SetShortID sets the assigned short ID +func (m *Tx) SetShortID(sid types.ShortID) { + m.shortID = sid +} + +// SetTimestamp sets the message creation timestamp +func (m *Tx) SetTimestamp(timestamp time.Time) { + m.timestamp = timestamp +} + +// ClearProtectedAttributes unsets and validates fields that are restricted for gateways +func (m *Tx) ClearProtectedAttributes() { + m.shortID = types.ShortIDEmpty + m.timestamp = time.Unix(0, 0) + m.sourceID = [SourceIDLen]byte{} + + m.flags &= ^types.TFDeliverToNode + m.flags &= ^types.TFEnterpriseSender + m.flags &= ^types.TFEliteSender + + // account ID should only be set on paid txs + if !m.Flags().IsPaid() { + m.accountID = [AccountIDLen]byte{} + } +} + +// ClearInternalAttributes unsets fields that should not be seen by gateways +func (m *Tx) ClearInternalAttributes() { + // not reset timestamp. Gateways use it to see if transaction is old or not + m.sourceID = [SourceIDLen]byte{} + m.accountID = [AccountIDLen]byte{} + + m.flags &= ^types.TFEnterpriseSender + m.flags &= ^types.TFEliteSender + m.flags &= ^types.TFPaidTx +} + +// Flags returns the transaction flags +func (m *Tx) Flags() (flags types.TxFlags) { + return m.flags +} + +// AddFlags adds the provided flag to the transaction flag set +func (m *Tx) AddFlags(flags types.TxFlags) { + m.flags |= flags +} + +// SetFlags sets the message flags +func (m *Tx) SetFlags(flags types.TxFlags) { + m.flags = flags +} + +// RemoveFlags sets off txFlag +func (m *Tx) RemoveFlags(flags types.TxFlags) { + m.SetFlags(m.Flags() &^ flags) +} + +// CompactClone returns a shallow clone of the current transaction, with the content omitted +func (m *Tx) CompactClone() Tx { + tx := Tx{ + BroadcastHeader: m.BroadcastHeader, + shortID: m.shortID, + timestamp: m.timestamp, + flags: m.flags, + accountID: m.accountID, + content: nil, + quota: m.quota, + } + tx.ClearInternalAttributes() + return tx +} + +// CleanClone returns a shallow clone of the current transaction, with the internal attributes removed +func (m *Tx) CleanClone() Tx { + tx := Tx{ + BroadcastHeader: m.BroadcastHeader, + shortID: m.shortID, + timestamp: m.timestamp, + flags: m.flags, + accountID: m.accountID, + content: m.content, + quota: m.quota, + } + tx.ClearInternalAttributes() + return tx +} + +// Pack serializes a Tx into a buffer for sending +func (m Tx) Pack(protocol Protocol) ([]byte, error) { + bufLen := m.size(protocol) + buf := make([]byte, bufLen) + m.BroadcastHeader.Pack(&buf, TxType) + offset := BroadcastHeaderLen + + binary.LittleEndian.PutUint32(buf[offset:], uint32(m.shortID)) + offset += types.ShortIDLen + flags := m.flags + switch { + case protocol < 20: + flags &= types.TFStatusMonitoring | types.TFPaidTx | types.TFNonceMonitoring | types.TFRePropagate | types.TFEnterpriseSender + default: + } + binary.LittleEndian.PutUint16(buf[offset:], uint16(flags)) + + offset += types.TxFlagsLen + timestamp := float64(m.timestamp.UnixNano()) / 1e9 + switch { + case protocol < 21: + binary.LittleEndian.PutUint32(buf[offset:], uint32(timestamp+1.0)) + offset += types.UInt32Len + default: + binary.LittleEndian.PutUint64(buf[offset:], math.Float64bits(timestamp)) + offset += TimestampLen + } + switch { + case protocol < 22: + // do nothing. accountID added in protocol 22 + default: + copy(buf[offset:], m.accountID[:]) + offset += AccountIDLen + } + copy(buf[offset:], m.content) + offset += len(m.content) + + return buf, nil +} + +// Unpack deserializes a Tx from a buffer +func (m *Tx) Unpack(buf []byte, protocol Protocol) error { + if err := m.BroadcastHeader.Unpack(buf, protocol); err != nil { + return err + } + offset := BroadcastHeaderLen + m.shortID = types.ShortID(binary.LittleEndian.Uint32(buf[offset:])) + offset += types.ShortIDLen + m.flags = types.TxFlags(binary.LittleEndian.Uint16(buf[offset:])) + offset += types.TxFlagsLen + timestamp := float64(0) + switch { + case protocol < 21: + timestamp = float64(binary.LittleEndian.Uint32(buf[offset:])) + offset += types.UInt32Len + default: + timestamp = math.Float64frombits(binary.LittleEndian.Uint64(buf[offset:])) + offset += TimestampLen + } + nanoseconds := int64(timestamp) * int64(1e9) + m.timestamp = time.Unix(0, nanoseconds) + switch { + case protocol < 22: + // do nothing. accountID added in protocol 22 + default: + copy(m.accountID[:], buf[offset:]) + offset += AccountIDLen + } + + m.content = buf[offset : len(buf)-ControlByteLen] + offset += len(m.content) + return nil +} + +func (m *Tx) size(protocol Protocol) uint32 { + switch { + case protocol < 19: + return 0 + case protocol < 21: + return m.BroadcastHeader.Size() + types.ShortIDLen + types.TxFlagsLen + types.UInt32Len + uint32(len(m.content)) + case protocol < 22: + return m.BroadcastHeader.Size() + types.ShortIDLen + types.TxFlagsLen + TimestampLen + uint32(len(m.content)) + } + return m.BroadcastHeader.Size() + types.ShortIDLen + types.TxFlagsLen + TimestampLen + AccountIDLen + uint32(len(m.content)) +} + +// String serializes a Tx as a string for pretty printing +func (m Tx) String() string { + return fmt.Sprintf("Tx", m.HashString(false), m.priority, m.flags) +} diff --git a/bxmessage/tx_test.go b/bxmessage/tx_test.go new file mode 100644 index 0000000..5fc166a --- /dev/null +++ b/bxmessage/tx_test.go @@ -0,0 +1,47 @@ +package bxmessage + +import ( + "bytes" + "github.com/bloXroute-Labs/gateway/types" + "github.com/stretchr/testify/assert" + "testing" +) + +var nullByteAccountID = bytes.Repeat([]byte("\x00"), 36) + +func TestTx_AccountIDEmpty(t *testing.T) { + tx := Tx{} + assert.Equal(t, types.AccountID(""), tx.AccountID()) +} + +func TestTx_AccountIDNullBytes(t *testing.T) { + accountID := [AccountIDLen]byte{} + copy(accountID[:], nullByteAccountID) + + tx := Tx{ + accountID: accountID, + } + assert.Equal(t, types.AccountID(""), tx.AccountID()) + + b, _ := tx.Pack(AccountProtocol) + tx2 := Tx{} + _ = tx2.Unpack(b, AccountProtocol) + assert.Equal(t, types.AccountID(""), tx2.AccountID()) +} + +func TestTx_SourceIDValid(t *testing.T) { + sourceID := "4c5df5f8-2fd9-4739-a319-8beeba554a88" + tx := Tx{} + err := tx.SetSourceID(types.NodeID(sourceID)) + assert.Nil(t, err) + assert.Equal(t, types.NodeID(sourceID), tx.SourceID()) + + newSourceID := "9ee4ec57-d189-428e-92e6-d496670b5022" + err = tx.SetSourceID(types.NodeID(newSourceID)) + assert.Nil(t, err) + assert.Equal(t, types.NodeID(newSourceID), tx.SourceID()) + + newInvalidSourceID := "invalid-source-id" + err = tx.SetSourceID(types.NodeID(newInvalidSourceID)) + assert.NotNil(t, err) +} diff --git a/bxmessage/txcleanup.go b/bxmessage/txcleanup.go new file mode 100644 index 0000000..6737831 --- /dev/null +++ b/bxmessage/txcleanup.go @@ -0,0 +1,27 @@ +package bxmessage + +import ( + log "github.com/sirupsen/logrus" +) + +// TxCleanup represents a transactions that can be cleaned from tx-service +type TxCleanup struct { + abstractCleanup +} + +func (m *TxCleanup) size() uint32 { + return m.abstractCleanup.size() +} + +// Pack serializes a SyncTxsMessage into a buffer for sending +func (m TxCleanup) Pack(protocol Protocol) ([]byte, error) { + buf, err := m.abstractCleanup.Pack(protocol, TxCleanupType) + return buf, err +} + +// Unpack deserializes a SyncTxsMessage from a buffer +func (m *TxCleanup) Unpack(buf []byte, protocol Protocol) error { + err := m.abstractCleanup.Unpack(buf, protocol) + log.Tracef("%v: network %v, sids %v, hashes %v", TxCleanupType, m.networkNumber, len(m.ShortIDs), len(m.Hashes)) + return err +} diff --git a/bxmessage/txs.go b/bxmessage/txs.go new file mode 100644 index 0000000..02288e2 --- /dev/null +++ b/bxmessage/txs.go @@ -0,0 +1,102 @@ +package bxmessage + +import ( + "encoding/binary" + "github.com/bloXroute-Labs/gateway/types" +) + +// TxsItem represents simplified data about a given requested transaction +type TxsItem struct { + Hash types.SHA256Hash + Content types.TxContent + ShortID types.ShortID +} + +// Txs represents the response to GetTxs from relay proxies +type Txs struct { + Header + items []TxsItem +} + +// NewTxs returns a new Txs message for packing +func NewTxs(items []TxsItem) *Txs { + return &Txs{ + items: items, + } +} + +// Items returns all the requested transaction info +func (m *Txs) Items() []TxsItem { + return m.items +} + +func (m *Txs) size() uint32 { + itemsSize := 0 + for _, item := range m.items { + itemsSize += types.SHA256HashLen + types.ShortIDLen + types.UInt32Len + len(item.Content) + } + return m.Header.Size() + uint32(types.UInt32Len+itemsSize) +} + +// Pack returns the txs encoded into the byte protocol +func (m Txs) Pack(protocol Protocol) ([]byte, error) { + buf := make([]byte, m.size()) + + offset := HeaderLen + binary.LittleEndian.PutUint32(buf[offset:], uint32(len(m.items))) + offset += types.UInt32Len + + for _, item := range m.items { + binary.LittleEndian.PutUint32(buf[offset:], uint32(item.ShortID)) + offset += types.ShortIDLen + + copy(buf[offset:], item.Hash[:]) + offset += types.SHA256HashLen + + binary.LittleEndian.PutUint32(buf[offset:], uint32(len(item.Content))) + offset += types.UInt32Len + + copy(buf[offset:], item.Content[:]) + offset += len(item.Content) + } + + m.Header.Pack(&buf, TransactionsType) + return buf, nil +} + +// Unpack decodes a Txs message from the serialized byte protoool +func (m *Txs) Unpack(buf []byte, protocol Protocol) error { + offset := HeaderLen + + itemCount := int(binary.LittleEndian.Uint32(buf[offset:])) + offset += types.UInt32Len + + items := make([]TxsItem, 0) + for i := 0; i < itemCount; i++ { + if err := checkBufSize(&buf, offset, types.ShortIDLen+types.SHA256HashLen+types.UInt32Len); err != nil { + return err + } + + item := TxsItem{} + + item.ShortID = types.ShortID(binary.LittleEndian.Uint32(buf[offset:])) + offset += types.ShortIDLen + + item.Hash, _ = types.NewSHA256Hash(buf[offset : offset+types.SHA256HashLen]) + offset += types.SHA256HashLen + + contentLength := int(binary.LittleEndian.Uint32(buf[offset:])) + offset += types.UInt32Len + + if err := checkBufSize(&buf, offset, contentLength); err != nil { + return err + } + item.Content = buf[offset : offset+contentLength] + offset += contentLength + + items = append(items, item) + } + m.items = items + + return m.Header.Unpack(buf, protocol) +} diff --git a/bxmessage/txs_test.go b/bxmessage/txs_test.go new file mode 100644 index 0000000..042309a --- /dev/null +++ b/bxmessage/txs_test.go @@ -0,0 +1,64 @@ +package bxmessage + +import ( + "encoding/hex" + "github.com/bloXroute-Labs/gateway/test" + "github.com/bloXroute-Labs/gateway/test/fixtures" + "github.com/bloXroute-Labs/gateway/types" + "github.com/stretchr/testify/assert" + "testing" +) + +func TestTxs_PackUnpack(t *testing.T) { + txs := Txs{ + Header: Header{msgType: TransactionsType}, + items: []TxsItem{ + { + Hash: types.GenerateSHA256Hash(), + Content: test.GenerateBytes(100), + ShortID: 1, + }, + { + Hash: types.GenerateSHA256Hash(), + Content: test.GenerateBytes(150), + ShortID: 2, + }, + }, + } + + b, err := txs.Pack(0) + assert.Nil(t, err) + + var unpackedTxs Txs + err = unpackedTxs.Unpack(b, 0) + assert.Nil(t, err) + + assert.Equal(t, txs, unpackedTxs) +} + +func TestTxs_Fixture(t *testing.T) { + expectedHash1, _ := types.NewSHA256HashFromString(fixtures.TxsHash1) + expectedContent1, _ := hex.DecodeString(fixtures.TxsContent1) + expectedHash2, _ := types.NewSHA256HashFromString(fixtures.TxsHash2) + expectedContent2, _ := hex.DecodeString(fixtures.TxsContent2) + + b, _ := hex.DecodeString(fixtures.TxsMessage) + + var txs Txs + err := txs.Unpack(b, 0) + assert.Nil(t, err) + + items := txs.Items() + assert.Equal(t, 2, len(items)) + + assert.Equal(t, expectedHash1, items[0].Hash) + assert.Equal(t, types.TxContent(expectedContent1), items[0].Content) + assert.Equal(t, types.ShortID(fixtures.TxsShortID1), items[0].ShortID) + assert.Equal(t, expectedHash2, items[1].Hash) + assert.Equal(t, types.TxContent(expectedContent2), items[1].Content) + assert.Equal(t, types.ShortID(fixtures.TxsShortID2), items[1].ShortID) + + output, err := txs.Pack(0) + assert.Nil(t, err) + assert.Equal(t, b, output) +} diff --git a/bxmessage/utils/crypto.go b/bxmessage/utils/crypto.go new file mode 100644 index 0000000..fec6a37 --- /dev/null +++ b/bxmessage/utils/crypto.go @@ -0,0 +1,9 @@ +package utils + +import "crypto/sha256" + +// DoubleSHA256 - returns the SHA256 checksum of the data after two checksums +func DoubleSHA256(buf []byte) [32]byte { + hash := sha256.Sum256(buf[:]) + return sha256.Sum256(hash[:]) +} diff --git a/bxmessage/utils/messageutils.go b/bxmessage/utils/messageutils.go new file mode 100644 index 0000000..260f328 --- /dev/null +++ b/bxmessage/utils/messageutils.go @@ -0,0 +1,52 @@ +package utils + +import ( + "bytes" + "encoding/binary" + "fmt" + "math/big" + "net" +) + +// IPV4Prefix is the IPV4 address prefix in bytes +var IPV4Prefix = []byte("\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xff\xff") + +// IPAddrSizeInBytes is byte length of an IP Address +const IPAddrSizeInBytes = 16 + +// IPV4PrefixLength is the byte length of the IPV4 prefix +const IPV4PrefixLength = 12 + +// UnpackIPPort unpacks IPV4 address bytes into string format +func UnpackIPPort(buf []byte) (string, uint16, error) { + port := binary.LittleEndian.Uint16(buf[IPAddrSizeInBytes:]) + if bytes.Compare(buf[:+IPV4PrefixLength], IPV4Prefix[:]) == 0 { + ipBytes := buf[IPV4PrefixLength:IPAddrSizeInBytes] + return ipv4ToStringFormat(ipBytes), port, nil + } + return "", 0, fmt.Errorf("IP address is not in IPV4 format") +} + +// PackIPPort packs an IP address and port in IPV4 format +func PackIPPort(buf []byte, ip string, port uint16) { + copy(buf[:], IPV4Prefix) + offset := IPV4PrefixLength + ipv4 := net.ParseIP(ip) + ipv4Decimal := ipv4toInt(ipv4) + binary.BigEndian.PutUint32(buf[offset:], uint32(ipv4Decimal)) + offset = IPAddrSizeInBytes + binary.LittleEndian.PutUint16(buf[offset:], port) +} + +func ipv4ToStringFormat(buf []byte) string { + val := binary.LittleEndian.Uint32(buf) + ip := make(net.IP, 4) + binary.LittleEndian.PutUint32(ip, val) + return ip.String() +} + +func ipv4toInt(ipv4Address net.IP) int64 { + ipv4Int := big.NewInt(0) + ipv4Int.SetBytes(ipv4Address.To4()) + return ipv4Int.Int64() +} diff --git a/bxmessage/utils/messageutils_test.go b/bxmessage/utils/messageutils_test.go new file mode 100644 index 0000000..c7f6287 --- /dev/null +++ b/bxmessage/utils/messageutils_test.go @@ -0,0 +1,27 @@ +package utils + +import ( + "github.com/bloXroute-Labs/gateway/types" + "github.com/stretchr/testify/assert" + "testing" +) + +var ipPortBytes = []byte("\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xff\xff\x7f6\x03\x01@\x1f") + +func TestUnpackIPPort(t *testing.T) { + testIPV4 := "127.54.3.1" + testPort := uint16(8000) + ip, port, err := UnpackIPPort(ipPortBytes) + assert.Equal(t, ip, testIPV4) + assert.Equal(t, port, testPort) + assert.Nil(t, err) +} + +func TestPackIPPort(t *testing.T) { + testIPV4 := "127.54.3.1" + testPort := uint16(8000) + + buf := make([]byte, IPAddrSizeInBytes+types.UInt16Len) + PackIPPort(buf, testIPV4, testPort) + assert.Equal(t, ipPortBytes, buf) +} diff --git a/cmd/bxcli/main.go b/cmd/bxcli/main.go new file mode 100644 index 0000000..14a1643 --- /dev/null +++ b/cmd/bxcli/main.go @@ -0,0 +1,170 @@ +package main + +import ( + "context" + "fmt" + "github.com/bloXroute-Labs/gateway/config" + pb "github.com/bloXroute-Labs/gateway/protobuf" + "github.com/bloXroute-Labs/gateway/rpc" + "github.com/bloXroute-Labs/gateway/utils" + log "github.com/sirupsen/logrus" + "github.com/urfave/cli/v2" + "os" +) + +func main() { + app := &cli.App{ + UseShortOptionHandling: true, + Name: "bxcli", + Usage: "interact with bloxroute gateway", + Commands: []*cli.Command{ + { + Name: "newtxs", + Usage: "provides a stream of new txs", + Flags: []cli.Flag{}, + Action: cmdNewTXs, + }, + { + Name: "pendingtxs", + Usage: "provides a stream of pending txs", + Flags: []cli.Flag{}, + Action: cmdPendingTXs, + }, + { + Name: "blxrtx", + Usage: "send paid transaction", + Flags: []cli.Flag{ + &cli.StringFlag{ + Name: "transaction", + Required: true, + }, + }, + Action: cmdBlxrTX, + }, + { + Name: "blxrtxs", + Usage: "send multiple paid transaction", + Flags: []cli.Flag{}, + Action: cmdBlxrTXs, + }, + { + Name: "getinfo", + Usage: "query information on running instance", + Flags: []cli.Flag{}, + Action: cmdGetInfo, + }, + { + Name: "listpeers", + Usage: "list current connected peers", + Flags: []cli.Flag{}, + Action: cmdListPeers, + }, + { + Name: "txservice", + Usage: "query information related to the TxStore", + Flags: []cli.Flag{}, + Action: cmdTxService, + }, + { + Name: "stop", + Flags: []cli.Flag{}, + Action: cmdStop, + }, + { + Name: "version", + Usage: "query information related to the TxService", + Flags: []cli.Flag{}, + Action: cmdVersion, + }, + }, + Flags: []cli.Flag{ + utils.GRPCHostFlag, + utils.GRPCPortFlag, + utils.GRPCUserFlag, + utils.GRPCPasswordFlag, + utils.GRPCAuthFlag, + }, + } + + err := app.Run(os.Args) + if err != nil { + log.Fatal(err) + } +} + +func cmdStop(ctx *cli.Context) error { + err := rpc.GatewayConsoleCall( + config.NewGRPCFromCLI(ctx), + func(callCtx context.Context, client pb.GatewayClient) (interface{}, error) { + return client.Stop(callCtx, &pb.StopRequest{}) + }, + ) + if err != nil { + return fmt.Errorf("could not run stop: %v", err) + } + return nil +} + +func cmdVersion(ctx *cli.Context) error { + err := rpc.GatewayConsoleCall( + config.NewGRPCFromCLI(ctx), + func(callCtx context.Context, client pb.GatewayClient) (interface{}, error) { + return client.Version(callCtx, &pb.VersionRequest{}) + }, + ) + if err != nil { + return fmt.Errorf("could not fetch version: %v", err) + } + return nil +} + +func cmdNewTXs(*cli.Context) error { + fmt.Printf("left to do:") + return nil +} + +func cmdPendingTXs(*cli.Context) error { + fmt.Printf("left to do:") + return nil +} + +func cmdBlxrTX(ctx *cli.Context) error { + err := rpc.GatewayConsoleCall( + config.NewGRPCFromCLI(ctx), + func(callCtx context.Context, client pb.GatewayClient) (interface{}, error) { + return client.BlxrTx(callCtx, &pb.BlxrTxRequest{Transaction: ctx.String("transaction")}) + }, + ) + if err != nil { + return fmt.Errorf("could not process blxr tx: %v", err) + } + return nil +} + +func cmdBlxrTXs(*cli.Context) error { + fmt.Printf("left to do:") + return nil +} + +func cmdGetInfo(*cli.Context) error { + fmt.Printf("left to do:") + return nil +} + +func cmdTxService(*cli.Context) error { + fmt.Printf("left to do:") + return nil +} + +func cmdListPeers(ctx *cli.Context) error { + err := rpc.GatewayConsoleCall( + config.NewGRPCFromCLI(ctx), + func(callCtx context.Context, client pb.GatewayClient) (interface{}, error) { + return client.Peers(callCtx, &pb.PeersRequest{}) + }, + ) + if err != nil { + return fmt.Errorf("could not fetch peers: %v", err) + } + return nil +} diff --git a/cmd/gateway/main.go b/cmd/gateway/main.go new file mode 100644 index 0000000..128afba --- /dev/null +++ b/cmd/gateway/main.go @@ -0,0 +1,170 @@ +package main + +import ( + "context" + "fmt" + "github.com/bloXroute-Labs/gateway/blockchain" + "github.com/bloXroute-Labs/gateway/blockchain/eth" + "github.com/bloXroute-Labs/gateway/blockchain/network" + "github.com/bloXroute-Labs/gateway/config" + "github.com/bloXroute-Labs/gateway/nodes" + "github.com/bloXroute-Labs/gateway/types" + "github.com/bloXroute-Labs/gateway/utils" + "github.com/bloXroute-Labs/gateway/version" + "github.com/ethereum/go-ethereum/crypto" + log "github.com/sirupsen/logrus" + "github.com/urfave/cli/v2" + "net/http" + _ "net/http/pprof" + "os" + "os/signal" + "syscall" + "time" +) + +func main() { + app := &cli.App{ + Name: "gateway", + Usage: "run a NG gateway", + Flags: []cli.Flag{ + utils.ExternalIPFlag, + utils.PortFlag, + utils.SDNURLFlag, + utils.CACertURLFlag, + utils.RegistrationCertDirFlag, + utils.WSFlag, + utils.WSPortFlag, + utils.EnvFlag, + utils.LogLevelFlag, + utils.LogFileLevelFlag, + utils.LogMaxSizeFlag, + utils.LogMaxAgeFlag, + utils.LogMaxBackupsFlag, + utils.TxTraceEnabledFlag, + utils.TxTraceMaxFileSizeFlag, + utils.TxTraceMaxBackupFilesFlag, + utils.AvoidPrioritySendingFlag, + utils.RelayHostFlag, + utils.DataDirFlag, + utils.GRPCFlag, + utils.GRPCHostFlag, + utils.GRPCPortFlag, + utils.GRPCUserFlag, + utils.GRPCPasswordFlag, + utils.BlockchainNetworkFlag, + utils.EnodesFlag, + utils.BlocksOnlyFlag, + utils.AllTransactionsFlag, + utils.PrivateKeyFlag, + utils.EthWSUriFlag, + utils.NodeTypeFlag, + utils.DisableProfilingFlag, + utils.FluentDFlag, + utils.FluentdHostFlag, + utils.ManageWSServer, + utils.LogNetworkContentFlag, + utils.WSTLSFlag, + utils.MevBuilderURIFlag, + utils.MevMinerURIFlag, + }, + Action: runGateway, + } + + err := app.Run(os.Args) + if err != nil { + log.Fatal(err) + } +} + +func runGateway(c *cli.Context) error { + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + if !c.Bool(utils.DisableProfilingFlag.Name) { + go func() { + log.Infof("pprof http server is running on 0.0.0.0:6060 - %v", "http://localhost:6060/debug/pprof") + log.Error(http.ListenAndServe("0.0.0.0:6060", nil)) + }() + } + + sigc := make(chan os.Signal, 1) + signal.Notify(sigc, syscall.SIGINT, syscall.SIGTERM) + + bxConfig, err := config.NewBxFromCLI(c) + if err != nil { + return err + } + + ethConfig, err := network.NewPresetEthConfigFromCLI(c) + if err != nil { + return err + } + + var blockchainPeers []types.NodeEndpoint + for _, blockchainPeer := range ethConfig.StaticPeers { + enodePublicKey := fmt.Sprintf("%x", crypto.FromECDSAPub(blockchainPeer.Pubkey())[1:]) + blockchainPeers = append(blockchainPeers, types.NodeEndpoint{IP: blockchainPeer.IP().String(), Port: blockchainPeer.TCP(), PublicKey: enodePublicKey}) + } + startupBlockchainClient := len(blockchainPeers) > 0 + + err = nodes.InitLogs(bxConfig.Log, version.BuildVersion) + if err != nil { + return err + } + + var bridge blockchain.Bridge + if startupBlockchainClient { + bridge = blockchain.NewBxBridge(eth.Converter{}) + } else { + bridge = blockchain.NewNoOpBridge(eth.Converter{}) + } + + var wsProvider blockchain.WSProvider + ethWSURI := c.String(utils.EthWSUriFlag.Name) + if ethWSURI != "" { + // TODO range over blockchain peers when supporting multiple nodes connections + wsProvider = eth.NewEthWSProvider(ethWSURI, 10*time.Second) + } else if bxConfig.ManageWSServer { + return fmt.Errorf("--eth-ws-uri must be provided if --manage-ws-server is enabled") + } else if bxConfig.WebsocketEnabled || bxConfig.WebsocketTLSEnabled { + log.Warnf("websocket server enabled but --eth-ws-uri startup parameter not provided: only newTxs and bdnBlocks feeds are available") + } + + gateway, err := nodes.NewGateway(ctx, bxConfig, bridge, wsProvider, blockchainPeers) + if err != nil { + return err + } + go func() { + err = gateway.Run() + if err != nil { + panic(err) + } + }() + + var blockchainServer *eth.Server + if startupBlockchainClient { + log.Infof("starting blockchain client with config for network ID: %v", ethConfig.Network) + + blockchainServer, err = eth.NewServerWithEthLogger(ctx, ethConfig, bridge, c.String(utils.DataDirFlag.Name), wsProvider) + if err != nil { + return nil + } + + if err = blockchainServer.AddEthLoggerFileHandler(bxConfig.Log.FileName); err != nil { + log.Warnf("skipping reconfiguration of eth p2p server logger due to error: %v", err) + } + + if err = blockchainServer.Start(); err != nil { + return nil + } + } else { + log.Infof("skipping starting blockchain client as no enodes have been provided") + } + + <-sigc + + if blockchainServer != nil { + blockchainServer.Stop() + } + return nil +} diff --git a/config/bx.go b/config/bx.go new file mode 100644 index 0000000..0ac0581 --- /dev/null +++ b/config/bx.go @@ -0,0 +1,144 @@ +package config + +import ( + "errors" + "github.com/bloXroute-Labs/gateway/utils" + "github.com/urfave/cli/v2" + "time" +) + +const ( + defaultRPCTimeout = time.Second +) + +// Todo: separate GW and relay config + +// Bx represents generic node configuration +type Bx struct { + Host string + OverrideExternalIP bool + ExternalIP string + ExternalPort int64 + BlockchainNetwork string + PrioritySending bool + NodeType utils.NodeType + LogNetworkContent bool + FluentDEnabled bool + FluentDHost string + + OverrideRelay bool + OverrideRelayHost string + + WebsocketEnabled bool + WebsocketTLSEnabled bool + WebsocketHost string + WebsocketPort int + ManageWSServer bool + + BlocksOnly bool + AllTransactions bool + MevBuilderURI string + MevMinerURI string + + *GRPC + *Env + *Log +} + +// NewBxFromCLI builds bx node configuration from the CLI context +func NewBxFromCLI(ctx *cli.Context) (*Bx, error) { + env, err := NewEnvFromCLI(ctx.String(utils.EnvFlag.Name), ctx) + if err != nil { + return nil, err + } + + log, err := NewLogFromCLI(ctx) + if err != nil { + return nil, err + } + + grpcConfig := NewGRPCFromCLI(ctx) + + nodeType, err := utils.FromStringToNodeType(ctx.String(utils.NodeTypeFlag.Name)) + bxConfig := &Bx{ + Host: ctx.String(utils.HostFlag.Name), + OverrideExternalIP: ctx.IsSet(utils.ExternalIPFlag.Name), + ExternalIP: ctx.String(utils.ExternalIPFlag.Name), + ExternalPort: ctx.Int64(utils.PortFlag.Name), + BlockchainNetwork: ctx.String(utils.BlockchainNetworkFlag.Name), + PrioritySending: !ctx.Bool(utils.AvoidPrioritySendingFlag.Name), + OverrideRelay: ctx.IsSet(utils.RelayHostFlag.Name), + OverrideRelayHost: ctx.String(utils.RelayHostFlag.Name), + NodeType: nodeType, + LogNetworkContent: ctx.Bool(utils.LogNetworkContentFlag.Name), + FluentDEnabled: ctx.Bool(utils.FluentDFlag.Name), + FluentDHost: ctx.String(utils.FluentdHostFlag.Name), + + WebsocketEnabled: ctx.Bool(utils.WSFlag.Name), + WebsocketTLSEnabled: ctx.Bool(utils.WSTLSFlag.Name), + WebsocketHost: ctx.String(utils.WSHostFlag.Name), + WebsocketPort: ctx.Int(utils.WSPortFlag.Name), + ManageWSServer: ctx.Bool(utils.ManageWSServer.Name), + + BlocksOnly: ctx.Bool(utils.BlocksOnlyFlag.Name), + AllTransactions: ctx.Bool(utils.AllTransactionsFlag.Name), + + MevBuilderURI: ctx.String(utils.MevBuilderURIFlag.Name), + MevMinerURI: ctx.String(utils.MevMinerURIFlag.Name), + + GRPC: grpcConfig, + Env: env, + Log: log, + } + + if bxConfig.BlocksOnly && bxConfig.AllTransactions { + return bxConfig, errors.New("cannot set both --blocks-only and --all-txs") + } + + return bxConfig, nil +} + +// GRPC represents Go RPC configuration details +type GRPC struct { + Enabled bool + Host string + Port int + User string + Password string + EncodedAuth string + + AuthEnabled bool + EncodedAuthSet bool + + Timeout time.Duration +} + +// NewGRPCFromCLI builds GRPC configuration from the CLI context +func NewGRPCFromCLI(ctx *cli.Context) *GRPC { + grpcConfig := GRPC{ + Enabled: ctx.Bool(utils.GRPCFlag.Name), + Host: ctx.String(utils.GRPCHostFlag.Name), + Port: ctx.Int(utils.GRPCPortFlag.Name), + User: ctx.String(utils.GRPCUserFlag.Name), + Password: ctx.String(utils.GRPCPasswordFlag.Name), + EncodedAuth: ctx.String(utils.GRPCAuthFlag.Name), + EncodedAuthSet: ctx.IsSet(utils.GRPCAuthFlag.Name), + AuthEnabled: ctx.IsSet(utils.GRPCAuthFlag.Name) || (ctx.IsSet(utils.GRPCUserFlag.Name) && ctx.IsSet(utils.GRPCPasswordFlag.Name)), + Timeout: defaultRPCTimeout, + } + return &grpcConfig +} + +// NewGRPC builds a simple GRPC configuration from parameters +func NewGRPC(host string, port int, user string, password string) *GRPC { + authEnabled := user != "" && password != "" + return &GRPC{ + Enabled: true, + Host: host, + Port: port, + User: user, + Password: password, + AuthEnabled: authEnabled, + Timeout: defaultRPCTimeout, + } +} diff --git a/config/env.go b/config/env.go new file mode 100644 index 0000000..374ca83 --- /dev/null +++ b/config/env.go @@ -0,0 +1,68 @@ +package config + +import ( + "fmt" + "github.com/bloXroute-Labs/gateway/utils" + "github.com/urfave/cli/v2" + "path" + "strconv" +) + +// Env represents configuration pertaining to a specific development environment +type Env struct { + SDNURL string + RegistrationCertDir string + CACertURL string + DataDir string + Environment string +} + +// constants for identifying environment configurations +const ( + Local = "local" + LocalTunnel = "localtunnel" + Testnet = "testnet" + Mainnet = "mainnet" +) + +// NewEnvFromCLI parses an environment from a CLI provided string +// provided arguments override defaults from the --env argument +func NewEnvFromCLI(env string, ctx *cli.Context) (*Env, error) { + gatewayEnv, err := NewEnv(env) + if err != nil { + return gatewayEnv, err + } + + if ctx.IsSet(utils.RegistrationCertDirFlag.Name) { + gatewayEnv.RegistrationCertDir = ctx.String(utils.RegistrationCertDirFlag.Name) + } + if ctx.IsSet(utils.SDNURLFlag.Name) { + gatewayEnv.SDNURL = ctx.String(utils.SDNURLFlag.Name) + } + if ctx.IsSet(utils.CACertURLFlag.Name) { + gatewayEnv.CACertURL = ctx.String(utils.CACertURLFlag.Name) + } + if ctx.IsSet(utils.DataDirFlag.Name) { + gatewayEnv.DataDir = ctx.String(utils.DataDirFlag.Name) + } + gatewayEnv.DataDir = path.Join(gatewayEnv.DataDir, env, strconv.Itoa(ctx.Int(utils.PortFlag.Name))) + return gatewayEnv, nil +} + +// NewEnv returns the preconfigured environment without the need for CLI overrides +func NewEnv(env string) (*Env, error) { + var gatewayEnv Env + switch env { + case Local: + gatewayEnv = LocalEnv + case LocalTunnel: + gatewayEnv = LocalTunnelEnv + case Testnet: + gatewayEnv = TestnetEnv + case Mainnet: + gatewayEnv = MainnetEnv + default: + return nil, fmt.Errorf("could not parse unrecognized env: %v", env) + } + return &gatewayEnv, nil +} diff --git a/config/local.go b/config/local.go new file mode 100644 index 0000000..02aed3e --- /dev/null +++ b/config/local.go @@ -0,0 +1,11 @@ +package config + +// LocalEnv is configuration for running in a local dev environment +// (i.e. running your own bxapi and relay instances) +var LocalEnv = Env{ + SDNURL: "https://localhost:8080", + RegistrationCertDir: "ssl/local", + CACertURL: "ssl/local/ca", + DataDir: "datadir", + Environment: "local", +} diff --git a/config/localtunnel.go b/config/localtunnel.go new file mode 100644 index 0000000..409914a --- /dev/null +++ b/config/localtunnel.go @@ -0,0 +1,12 @@ +package config + +// LocalTunnelEnv is configuration for running in a local dev environment, +// but connecting to the testnet SDN socket broker over a SSH tunnel, e.g. with +// ssh 54.174.28.236 -fNL 1800:bxapi-s.testnet-v176-16.testnet.blxrbdn.com:1800 -M -S sdn-socket +var LocalTunnelEnv = Env{ + SDNURL: "https://bdn-api.testnet.blxrbdn.com", + RegistrationCertDir: "ssl/", + CACertURL: "ssl/ca", + DataDir: "datadir", + Environment: "localTunnel", +} diff --git a/config/log.go b/config/log.go new file mode 100644 index 0000000..c837e12 --- /dev/null +++ b/config/log.go @@ -0,0 +1,56 @@ +package config + +import ( + "fmt" + "github.com/bloXroute-Labs/gateway/utils" + log "github.com/sirupsen/logrus" + "github.com/urfave/cli/v2" +) + +// TxTraceLog represents tx trace log config +type TxTraceLog struct { + Enabled bool + MaxFileSize int + MaxBackupFiles int +} + +// Log represents logger options for where to write data and what data to write +type Log struct { + AppName string + FileName string + FileLevel log.Level + ConsoleLevel log.Level + MaxSize int + MaxBackups int + MaxAge int + TxTrace TxTraceLog +} + +// NewLogFromCLI builds new log configuration from the CLI context +func NewLogFromCLI(ctx *cli.Context) (*Log, error) { + consoleLevel, err := log.ParseLevel(ctx.String(utils.LogLevelFlag.Name)) + if err != nil { + return nil, err + } + + fileLevel, err := log.ParseLevel(ctx.String(utils.LogFileLevelFlag.Name)) + if err != nil { + return nil, err + } + + logConfig := Log{ + AppName: ctx.App.Name, + FileName: fmt.Sprintf("logs/%v-%v.log", ctx.App.Name, ctx.Int(utils.PortFlag.Name)), + FileLevel: fileLevel, + ConsoleLevel: consoleLevel, + MaxSize: ctx.Int(utils.LogMaxSizeFlag.Name), + MaxBackups: ctx.Int(utils.LogMaxBackupsFlag.Name), + MaxAge: ctx.Int(utils.LogMaxAgeFlag.Name), + TxTrace: TxTraceLog{ + Enabled: ctx.Bool(utils.TxTraceEnabledFlag.Name), + MaxFileSize: ctx.Int(utils.TxTraceMaxFileSizeFlag.Name), + MaxBackupFiles: ctx.Int(utils.TxTraceMaxBackupFilesFlag.Name), + }, + } + return &logConfig, nil +} diff --git a/config/mainnet.go b/config/mainnet.go new file mode 100644 index 0000000..6ce4d36 --- /dev/null +++ b/config/mainnet.go @@ -0,0 +1,10 @@ +package config + +// MainnetEnv is configuration for a instance running on a relay instance in testnet +var MainnetEnv = Env{ + SDNURL: "https://bdn-api.blxrbdn.com", + RegistrationCertDir: "ssl/", + CACertURL: "ssl/ca", + DataDir: "datadir", + Environment: "mainnet", +} diff --git a/config/testnet.go b/config/testnet.go new file mode 100644 index 0000000..56b7d2e --- /dev/null +++ b/config/testnet.go @@ -0,0 +1,10 @@ +package config + +// TestnetEnv is configuration for a instance running on a relay instance in testnet +var TestnetEnv = Env{ + SDNURL: "https://bdn-api.testnet.blxrbdn.com", + RegistrationCertDir: "ssl/", + CACertURL: "ssl/ca", + DataDir: "datadir", + Environment: "testnet", +} diff --git a/connections/blockchain.go b/connections/blockchain.go new file mode 100644 index 0000000..3ebe791 --- /dev/null +++ b/connections/blockchain.go @@ -0,0 +1,94 @@ +package connections + +import ( + "fmt" + "github.com/bloXroute-Labs/gateway/bxmessage" + "github.com/bloXroute-Labs/gateway/types" + "github.com/bloXroute-Labs/gateway/utils" + log "github.com/sirupsen/logrus" + "time" +) + +// Blockchain is a placeholder struct to represent a connection for blockchain nodes +type Blockchain struct { + endpoint types.NodeEndpoint + log *log.Entry +} + +var blockchainTLSPlaceholder = TLS{} + +// NewBlockchainConn return a new instance of the Blockchain placeholder connection +func NewBlockchainConn(ipEndpoint types.NodeEndpoint) Blockchain { + return Blockchain{ + endpoint: ipEndpoint, + log: log.WithFields(log.Fields{ + "connType": utils.Blockchain.String(), + "remoteAddr": fmt.Sprintf("%v:%v", ipEndpoint.IP, ipEndpoint.Port), + }), + } +} + +// Info returns connection metadata +func (b Blockchain) Info() Info { + return Info{ + ConnectionType: utils.Blockchain, + NetworkNum: types.AllNetworkNum, + PeerIP: b.endpoint.IP, + PeerPort: int64(b.endpoint.Port), + PeerEnode: b.endpoint.PublicKey, + } +} + +// ID returns placeholder +func (b Blockchain) ID() Socket { + return blockchainTLSPlaceholder +} + +// IsOpen is never true, since the Blockchain is not writable +func (b Blockchain) IsOpen() bool { + return false +} + +// Protocol indicates that the Blockchain does not have a protocol +func (b Blockchain) Protocol() bxmessage.Protocol { + return bxmessage.EmptyProtocol +} + +// SetProtocol is a no-op +func (b Blockchain) SetProtocol(protocol bxmessage.Protocol) { +} + +// Log returns the blockchain connection logger +func (b Blockchain) Log() *log.Entry { + return b.log +} + +// Connect is a no-op +func (b Blockchain) Connect() error { + return nil +} + +// ReadMessages is a no-op +func (b Blockchain) ReadMessages(callBack func(bxmessage.MessageBytes), readDeadline time.Duration, headerLen int, readPayloadLen func([]byte) int) (int, error) { + return 0, nil +} + +// Send is a no-op +func (b Blockchain) Send(msg bxmessage.Message) error { + return nil +} + +// SendWithDelay is a no-op +func (b Blockchain) SendWithDelay(msg bxmessage.Message, delay time.Duration) error { + return nil +} + +// Close is a no-op +func (b Blockchain) Close(reason string) error { + return nil +} + +// String returns the formatted representation of this placeholder connection +func (b Blockchain) String() string { + return fmt.Sprintf("Blockchain %v", b.endpoint.String()) +} diff --git a/connections/conn.go b/connections/conn.go new file mode 100644 index 0000000..71347fc --- /dev/null +++ b/connections/conn.go @@ -0,0 +1,63 @@ +package connections + +import ( + "github.com/bloXroute-Labs/gateway/bxmessage" + "github.com/bloXroute-Labs/gateway/types" + log "github.com/sirupsen/logrus" + "time" +) + +// ConnHandler defines the methods needed to handle bloxroute connections +type ConnHandler interface { + ProcessMessage(msg bxmessage.MessageBytes) +} + +// Conn defines a network interface that sends and receives messages +type Conn interface { + ID() Socket + Info() Info + IsOpen() bool + + Protocol() bxmessage.Protocol + SetProtocol(bxmessage.Protocol) + + Log() *log.Entry + + Connect() error + ReadMessages(callBack func(bxmessage.MessageBytes), readDeadline time.Duration, headerLen int, readPayloadLen func([]byte) int) (int, error) + Send(msg bxmessage.Message) error + SendWithDelay(msg bxmessage.Message, delay time.Duration) error + Close(reason string) error +} + +// NodeStatus defines any metadata a connection may need to know about the running node. This is expected to be rarely necessary. +type NodeStatus struct { + // temporary property for sending sync status with pings + TransactionServiceSynced bool + Capabilities types.CapabilityFlags + Version string +} + +// MsgHandlingOptions represents background/foreground options for message handling +type MsgHandlingOptions bool + +// MsgHandlingOptions enumeration +const ( + RunBackground MsgHandlingOptions = true + RunForeground MsgHandlingOptions = false +) + +// BxListener defines a struct that is capable of processing bloxroute messages +type BxListener interface { + NodeStatus() NodeStatus + HandleMsg(msg bxmessage.Message, conn Conn, background MsgHandlingOptions) error + + // OnConnEstablished is a callback for when a connection has been connected and finished its handshake + OnConnEstablished(conn Conn) error + + // OnConnClosed is a callback for when a connection is closed with no expectation of retrying + OnConnClosed(conn Conn) error +} + +// ConnList represents the set of connections a node is maintaining +type ConnList []Conn diff --git a/connections/handler/bxconn.go b/connections/handler/bxconn.go new file mode 100644 index 0000000..e33ef7a --- /dev/null +++ b/connections/handler/bxconn.go @@ -0,0 +1,413 @@ +package handler + +import ( + "container/list" + "encoding/binary" + "fmt" + "github.com/bloXroute-Labs/gateway" + "github.com/bloXroute-Labs/gateway/bxmessage" + "github.com/bloXroute-Labs/gateway/connections" + "github.com/bloXroute-Labs/gateway/types" + "github.com/bloXroute-Labs/gateway/utils" + log "github.com/sirupsen/logrus" + "math" + "sync" + "time" +) + +const ( + connTimeout = 5 * time.Second +) + +// BxConn is a connection to any other bloxroute Node. BxConn implements connections.ConnHandler. +type BxConn struct { + connections.Conn + + Node connections.BxListener + Handler connections.ConnHandler + + lock *sync.Mutex + connectionEstablished bool + closed bool + nodeID types.NodeID + peerID types.NodeID + accountID types.AccountID + minToRelay int64 // time in microseconds + minFromRelay int64 // time in microseconds + minRoundTrip int64 // time in microseconds + slowCount int64 + connectionType utils.NodeType + stringRepresentation string + onPongMsgs *list.List + networkNum types.NetworkNum + localGEO bool + privateNetwork bool + localPort int64 + log *log.Entry + clock utils.Clock + capabilities types.CapabilityFlags + clientVersion string + sameRegion bool +} + +// NewBxConn constructs a connection to a bloxroute node. +func NewBxConn(node connections.BxListener, connect func() (connections.Socket, error), handler connections.ConnHandler, + sslCerts *utils.SSLCerts, ip string, port int64, nodeID types.NodeID, connectionType utils.NodeType, + usePQ bool, logMessages bool, localGEO bool, privateNetwork bool, localPort int64, clock utils.Clock, + sameRegion bool) *BxConn { + bc := &BxConn{ + Conn: connections.NewSSLConnection(connect, sslCerts, ip, port, bxmessage.CurrentProtocol, usePQ, logMessages, bxgateway.MaxConnectionBacklog, clock), + Node: node, + Handler: handler, + nodeID: nodeID, + minFromRelay: math.MaxInt64, + minToRelay: math.MaxInt64, + minRoundTrip: math.MaxInt64, + connectionType: connectionType, + lock: &sync.Mutex{}, + onPongMsgs: list.New(), + localGEO: localGEO, + privateNetwork: privateNetwork, + localPort: localPort, + log: log.WithFields(log.Fields{ + "connType": connectionType, + "remoteAddr": "", + }), + clock: clock, + sameRegion: sameRegion, + } + bc.stringRepresentation = fmt.Sprintf("%v/%v@", connectionType, bc.Conn) + return bc +} + +// Start kicks off main goroutine of the connection +func (b *BxConn) Start() error { + go b.readLoop() + return nil +} + +// Send sends a message to the peer. +func (b *BxConn) Send(msg bxmessage.Message) error { + if msg.GetPriority() != bxmessage.OnPongPriority { + return b.Conn.Send(msg) + } + b.lock.Lock() + defer b.lock.Unlock() + b.onPongMsgs.PushBack(msg) + // if we added the first OnPongPriority message, request a pong from the peer + if b.onPongMsgs.Len() == 1 { + // send ping to handle the next message + ping := &bxmessage.Ping{} + _ = b.Conn.Send(ping) + } + return nil +} + +// Info returns connection metadata +func (b *BxConn) Info() connections.Info { + meta := b.Conn.Info() + return connections.Info{ + NodeID: b.peerID, + AccountID: b.accountID, + PeerIP: meta.PeerIP, + PeerPort: meta.PeerPort, + LocalPort: b.localPort, + ConnectionState: "todo", + ConnectionType: b.connectionType, + FromMe: meta.FromMe, + NetworkNum: b.networkNum, + LocalGEO: b.localGEO, + PrivateNetwork: b.privateNetwork, + Capabilities: b.capabilities, + Version: b.clientVersion, + SameRegion: b.sameRegion, + } +} + +// IsOpen returns when the connection is ready for broadcasting +func (b *BxConn) IsOpen() bool { + return b.Conn.IsOpen() && b.connectionEstablished +} + +// Log returns the connection context logger +func (b *BxConn) Log() *log.Entry { + return b.log +} + +// Connect establishes the connections.SSLConn if necessary, then sets attributes based on +// the provided SSL certificates +func (b *BxConn) Connect() error { + err := b.Conn.Connect() + if err != nil { + return err + } + + connInfo := b.Conn.Info() + b.peerID = connInfo.NodeID + b.accountID = connInfo.AccountID + + b.stringRepresentation = fmt.Sprintf("%v/%v@%v{%v}", b.connectionType, b.Conn, b.accountID, b.peerID) + b.log = log.WithFields(log.Fields{ + "connType": b.connectionType, + "remoteAddr": fmt.Sprint(b.Conn), + "accountID": b.accountID, + "peerID": b.peerID, + }) + return nil +} + +// Close marks the connection for termination from this Node's side. Close will not allow this connection to be retried. Close will not actually stop this connection's event loop, only trigger the readLoop to exit, which will then close the event loop. +func (b *BxConn) Close(reason string) error { + b.lock.Lock() + defer b.lock.Unlock() + + if b.closed { + return nil + } + + err := b.closeWithRetry(reason) + b.closed = true + + return err +} + +// ProcessMessage constructs a message from the buffer and handles it +// This method only handles messages that do not require querying the BxListener interface +func (b *BxConn) ProcessMessage(msg bxmessage.MessageBytes) { + msgType := msg.BxType() + switch msgType { + case bxmessage.HelloType: + helloMsg := &bxmessage.Hello{} + _ = helloMsg.Unpack(msg, 0) + if helloMsg.Protocol < bxmessage.MinProtocol { + b.Log().Warnf("can't establish connection - proposed protocol %v is below the minimum supported %v", + helloMsg.Protocol, bxmessage.MinProtocol) + _ = b.Close("protocol version not supported") + return + } + if b.Protocol() > helloMsg.Protocol { + b.SetProtocol(helloMsg.Protocol) + } + b.networkNum = helloMsg.GetNetworkNum() + + b.capabilities = helloMsg.Capabilities + b.clientVersion = helloMsg.ClientVersion + + b.Log().Debugf("completed handshake: network %v, protocol %v, peer id %v ", b.networkNum, b.Protocol(), b.peerID) + ack := bxmessage.Ack{} + _ = b.Send(&ack) + if !b.Info().FromMe { + hello := bxmessage.Hello{NodeID: b.nodeID, Protocol: b.Protocol()} + hello.SetNetworkNum(b.networkNum) + _ = b.Send(&hello) + } else { + b.setConnectionEstablished() + } + case bxmessage.AckType: + b.lock.Lock() + // avoid racing with Close + if !b.Info().FromMe { + b.setConnectionEstablished() + } + b.lock.Unlock() + + case bxmessage.PingType: + ping := &bxmessage.Ping{} + _ = ping.Unpack(msg, b.Protocol()) + b.msgPing(ping) + + case bxmessage.PongType: + pong := &bxmessage.Pong{} + _ = pong.Unpack(msg, b.Protocol()) + b.msgPong(pong) + // now, check if there are queued message that should be delivered on pong message + b.lock.Lock() + defer b.lock.Unlock() + + if b.onPongMsgs.Len() == 0 { + break + } + + onPongMsg := b.onPongMsgs.Front() + msg := onPongMsg.Value.(bxmessage.Message) + msg.SetPriority(bxmessage.HighestPriority) + _ = b.Conn.Send(msg) + b.onPongMsgs.Remove(onPongMsg) + + if b.onPongMsgs.Len() > 0 { + // send ping to handle the next message + ping := &bxmessage.Ping{} + _ = b.Conn.Send(ping) + } + case bxmessage.BroadcastType: + block := &bxmessage.Broadcast{} + err := block.Unpack(msg, b.Protocol()) + if err != nil { + b.Log().Errorf("could not unpack broadcast message: %v. Failed bytes: %v", err, msg) + return + } + _ = b.Node.HandleMsg(block, b, connections.RunBackground) + + case bxmessage.TxCleanupType: + txcleanup := &bxmessage.TxCleanup{} + _ = txcleanup.Unpack(msg, b.Protocol()) + _ = b.Node.HandleMsg(txcleanup, b, connections.RunBackground) + case bxmessage.BlockConfirmationType: + blockConfirmation := &bxmessage.BlockConfirmation{} + _ = blockConfirmation.Unpack(msg, b.Protocol()) + _ = b.Node.HandleMsg(blockConfirmation, b, connections.RunBackground) + case bxmessage.SyncReqType: + syncReq := &bxmessage.SyncReq{} + _ = syncReq.Unpack(msg, b.Protocol()) + b.Log().Debugf("TxStore sync: got a request for network %v", syncReq.GetNetworkNum()) + _ = b.Node.HandleMsg(syncReq, b, connections.RunBackground) + case bxmessage.MEVBundleType: + mevBundle := &bxmessage.MEVBundle{} + _ = mevBundle.Unpack(msg, b.Protocol()) + _ = b.Node.HandleMsg(mevBundle, b, connections.RunBackground) + case bxmessage.MEVSearcherType: + mevSearcher := &bxmessage.MEVSearcher{} + _ = mevSearcher.Unpack(msg, b.Protocol()) + _ = b.Node.HandleMsg(mevSearcher, b, connections.RunBackground) + default: + b.Log().Debugf("read %v (%d bytes)", msgType, len(msg)) + } +} + +// SetNetworkNum is used to specify the connection network number +func (b *BxConn) SetNetworkNum(networkNum types.NetworkNum) { + b.networkNum = networkNum +} + +func (b *BxConn) setConnectionEstablished() { + b.connectionEstablished = true + _ = b.Node.OnConnEstablished(b) +} + +func (b *BxConn) msgPing(ping *bxmessage.Ping) { + pong := &bxmessage.Pong{Nonce: ping.Nonce} + _ = b.Send(pong) + b.handleNonces(0, ping.Nonce) +} + +func (b *BxConn) msgPong(pong *bxmessage.Pong) { + b.handleNonces(pong.Nonce, pong.TimeStamp) +} + +func (b *BxConn) handleNonces(nodeNonce, peerNonce uint64) { + nonceNow := b.clock.Now().UnixNano() / 1000 + timeFromPeer := nonceNow - int64(peerNonce) + if timeFromPeer < b.minFromRelay { + b.minFromRelay = timeFromPeer + } + if timeFromPeer > b.minFromRelay+bxgateway.SlowPingPong && + (!utils.IsGateway || log.GetLevel() > log.InfoLevel) { + b.slowCount++ + if utils.IsGateway || b.privateNetwork || b.localGEO { + b.Log().Debugf("slow message: took %v ms vs minimum %v ms", timeFromPeer/1000, b.minFromRelay/1000) + } + } + // if this is a ping message we are done. + if nodeNonce == 0 { + return + } + + // pong message. + timeToPeer := int64(peerNonce) - int64(nodeNonce) + if timeToPeer < b.minToRelay { + b.minToRelay = timeToPeer + } + if timeToPeer > b.minToRelay+bxgateway.SlowPingPong && + (!utils.IsGateway || log.GetLevel() > log.InfoLevel) { + b.slowCount++ + if utils.IsGateway || b.privateNetwork || b.localGEO { + b.Log().Debugf("slow message: took %v ms vs minimum %v ms", timeToPeer/1000, b.minToRelay/1000) + } + } + roundTrip := nonceNow - int64(nodeNonce) + if roundTrip < b.minRoundTrip { + b.minRoundTrip = roundTrip + } + +} + +// readLoop connects and reads messages from the socket. +// If we are the initiator of the connection we auto-recover on disconnect. +func (b *BxConn) readLoop() { + isInitiator := b.Info().FromMe + for { + err := b.Connect() + if err != nil { + b.Log().Errorf("encountered connection error while connecting: %v", err) + + reason := "could not connect to remote" + if !isInitiator { + _ = b.Close(reason) + break + } + + _ = b.closeWithRetry(reason) + // sleep before next connection attempt + b.clock.Sleep(connTimeout) + continue + } + + if isInitiator { + hello := bxmessage.Hello{NodeID: b.nodeID, Protocol: b.Protocol()} + hello.SetNetworkNum(b.networkNum) + nodeStatus := b.Node.NodeStatus() + hello.ClientVersion = nodeStatus.Version + hello.Capabilities = nodeStatus.Capabilities + _ = b.Send(&hello) + } + + closeReason := "read loop closed" + for b.Conn.IsOpen() { + _, err := b.ReadMessages(b.Handler.ProcessMessage, 30*time.Second, bxmessage.HeaderLen, + func(b []byte) int { + return int(binary.LittleEndian.Uint32(b[bxmessage.PayloadSizeOffset:])) + }) + + if err != nil { + closeReason = err.Error() + b.Log().Tracef("connection closed: %v", err) + break + } + } + if !isInitiator { + _ = b.Close(closeReason) + break + } + + _ = b.closeWithRetry(closeReason) + if b.closed { + break + } + // sleep before next connection attempt + // note - in docker environment the docker-proxy may keep the port open after the docker was stopped. we + // need this sleep to avoid fast connect/disconnect loop + b.clock.Sleep(connTimeout) + } +} + +// IsBloxroute detect if the peer belongs to bloxroute +func (b BxConn) IsBloxroute() bool { + return b.accountID == types.BloxrouteAccountID +} + +// String represents a string conversion of this connection +func (b BxConn) String() string { + return b.stringRepresentation +} + +// closeWithRetry does not shutdown the main go routines present in BxConn, only the ones in the ssl connection, which can be restarted on the next Connect +func (b *BxConn) closeWithRetry(reason string) error { + b.connectionEstablished = false + _ = b.Node.OnConnClosed(b) + return b.Conn.Close(reason) +} + +// GetMinLatencies exposes the best latencies in ms form and to peer +func (b BxConn) GetMinLatencies() (int64, int64, int64, int64) { + return b.minFromRelay / 1000, b.minToRelay / 1000, b.slowCount, b.minRoundTrip / 1000 +} diff --git a/connections/handler/bxconn_test.go b/connections/handler/bxconn_test.go new file mode 100644 index 0000000..87ebda2 --- /dev/null +++ b/connections/handler/bxconn_test.go @@ -0,0 +1,70 @@ +package handler + +import ( + "github.com/bloXroute-Labs/gateway/bxmessage" + "github.com/bloXroute-Labs/gateway/connections" + "github.com/bloXroute-Labs/gateway/test/bxmock" + "github.com/bloXroute-Labs/gateway/utils" + "github.com/stretchr/testify/assert" + "runtime" + "testing" + "time" +) + +type testHandler struct { + *BxConn +} + +// testHandler immediately closes the connection when a message is received +func (th *testHandler) ProcessMessage(msg bxmessage.MessageBytes) { + _ = th.BxConn.Close("message handler test") +} + +func (th *testHandler) setConn(b *BxConn) { + th.BxConn = b +} + +// semi integration test: in general, sleep should be avoided, but these closing tests cases are checking that we are closing goroutines correctly +func TestBxConn_ClosingFromHandler(t *testing.T) { + startCount := runtime.NumGoroutine() + + th := testHandler{} + tls, bx := bxConn(&th) + th.setConn(bx) + + err := bx.Start() + assert.Nil(t, err) + + // wait for hello message to be sent on connection so all goroutines are started + _, err = tls.MockAdvanceSent() + assert.Nil(t, err) + + // expect 2 additional goroutines: read loop, send loop + assert.Equal(t, startCount+2, runtime.NumGoroutine()) + + // queue message, which should trigger a close + helloMessage := bxmessage.Hello{} + b, err := helloMessage.Pack(bxmessage.CurrentProtocol) + tls.MockQueue(b) + + // allow small delta for goroutines to finish + time.Sleep(1 * time.Millisecond) + + endCount := runtime.NumGoroutine() + assert.Equal(t, startCount, endCount) +} + +func bxConn(handler connections.ConnHandler) (bxmock.MockTLS, *BxConn) { + ip := "127.0.0.1" + port := int64(3000) + + tls := bxmock.NewMockTLS(ip, port, "", utils.ExternalGateway, "") + certs := utils.TestCerts() + b := NewBxConn(bxmock.MockBxListener{}, + func() (connections.Socket, error) { + return tls, nil + }, + handler, &certs, ip, port, "", utils.RelayTransaction, true, false, true, false, connections.LocalInitiatedPort, utils.RealClock{}, + false) + return tls, b +} diff --git a/connections/handler/relay.go b/connections/handler/relay.go new file mode 100644 index 0000000..caf5123 --- /dev/null +++ b/connections/handler/relay.go @@ -0,0 +1,127 @@ +package handler + +import ( + "github.com/bloXroute-Labs/gateway/bxmessage" + "github.com/bloXroute-Labs/gateway/connections" + "github.com/bloXroute-Labs/gateway/sdnmessage" + "github.com/bloXroute-Labs/gateway/types" + "github.com/bloXroute-Labs/gateway/utils" + log "github.com/sirupsen/logrus" + "sync/atomic" +) + +// Relay represents a connection to a relay Node +type Relay struct { + *BxConn + networks *sdnmessage.BlockchainNetworks + syncDoneCount uint32 +} + +// NewOutboundRelay builds a new connection to a relay Node +func NewOutboundRelay(node connections.BxListener, + sslCerts *utils.SSLCerts, relayIP string, relayPort int64, nodeID types.NodeID, relayType utils.NodeType, + usePQ bool, networks *sdnmessage.BlockchainNetworks, localGEO bool, privateNetwork bool, clock utils.Clock, + sameRegion bool) *Relay { + return NewRelay(node, + func() (connections.Socket, error) { + return connections.NewTLS(relayIP, int(relayPort), sslCerts) + }, + sslCerts, relayIP, relayPort, nodeID, relayType, usePQ, networks, localGEO, privateNetwork, connections.LocalInitiatedPort, clock, + sameRegion) +} + +// NewInboundRelay builds a relay connection from a socket event initiated by a remote relay node +func NewInboundRelay(node connections.BxListener, + socket connections.Socket, sslCerts *utils.SSLCerts, relayIP string, nodeID types.NodeID, + relayType utils.NodeType, usePQ bool, networks *sdnmessage.BlockchainNetworks, + localGEO bool, privateNetwork bool, localPort int64, clock utils.Clock, + sameRegion bool) *Relay { + return NewRelay(node, + func() (connections.Socket, error) { + return socket, nil + }, + sslCerts, relayIP, connections.RemoteInitiatedPort, nodeID, relayType, usePQ, networks, localGEO, privateNetwork, localPort, clock, + sameRegion) +} + +// NewRelay should only be called from test cases or NewOutboundRelay. It allows specifying a particular connect function for the SSL socket. However, in essentially all usages this should not be necessary as any node will initiate a connection to the relay, and as such should just use the default connect function to open a new socket. +func NewRelay(node connections.BxListener, + connect func() (connections.Socket, error), sslCerts *utils.SSLCerts, relayIP string, relayPort int64, + nodeID types.NodeID, relayType utils.NodeType, usePQ bool, networks *sdnmessage.BlockchainNetworks, + localGEO bool, privateNetwork bool, localPort int64, clock utils.Clock, + sameRegion bool) *Relay { + if networks == nil { + log.Panicf("TxStore sync: networks not provided. Please provide empty list of networks") + } + r := &Relay{ + networks: networks, + } + r.BxConn = NewBxConn(node, connect, r, sslCerts, relayIP, relayPort, nodeID, relayType, + usePQ, true, localGEO, privateNetwork, localPort, clock, sameRegion) + return r +} + +// ProcessMessage handles messages received on the relay connection, delegating to the BxListener when appropriate +func (r *Relay) ProcessMessage(msg bxmessage.MessageBytes) { + var err error + + msgType := msg.BxType() + if msgType != bxmessage.TxType { + r.Log().Tracef("processing message %v", msgType) + } + + switch msgType { + + case bxmessage.TxType: + tx := &bxmessage.Tx{} + _ = tx.Unpack(msg, r.Protocol()) + _ = r.Node.HandleMsg(tx, r, connections.RunForeground) + case bxmessage.HelloType: + r.BxConn.ProcessMessage(msg) + r.syncDoneCount = 0 + for _, network := range *r.networks { + r.Log().Debugf("TxStore sync: requesting network %v", network.NetworkNum) + syncReq := bxmessage.SyncReq{} + syncReq.SetNetworkNum(network.NetworkNum) + _ = r.Send(&syncReq) + } + + case bxmessage.SyncTxsType: + txs := &bxmessage.SyncTxsMessage{} + err := txs.Unpack(msg, r.Protocol()) + if err != nil { + r.Log().Errorf("unable to unpack SyncTxsMessage: %v. Closing connetion", err) + r.Close(err.Error()) + } + _ = r.Node.HandleMsg(txs, r, connections.RunBackground) + // TODO: add txs to txservice + case bxmessage.SyncDoneType: + syncDone := &bxmessage.SyncDone{} + _ = syncDone.Unpack(msg, r.Protocol()) + r.Log().Debugf("TxStore sync: done for network %v", syncDone.GetNetworkNum()) + atomic.AddUint32(&r.syncDoneCount, 1) + if atomic.CompareAndSwapUint32(&r.syncDoneCount, uint32(len(*r.networks)), 0) { + r.Log().Debugf("TxStore sync: done for %v networks", len(*r.networks)) + _ = r.Node.HandleMsg(syncDone, r, connections.RunBackground) + } + case bxmessage.RefreshBlockchainNetworkType: + refresh := &bxmessage.RefreshBlockchainNetwork{} + _ = refresh.Unpack(msg, r.Protocol()) + _ = r.Node.HandleMsg(refresh, r, connections.RunForeground) + case bxmessage.TransactionsType: + txs := &bxmessage.Txs{} + err = txs.Unpack(msg, r.Protocol()) + if err != nil { + break + } + + err = r.Node.HandleMsg(txs, r, connections.RunForeground) + case bxmessage.BlockTxsType: + default: + r.BxConn.ProcessMessage(msg) + } + + if err != nil { + r.Log().Errorf("encountered error processing message %v: %v", msgType, err) + } +} diff --git a/connections/handler/relay_test.go b/connections/handler/relay_test.go new file mode 100644 index 0000000..55b2214 --- /dev/null +++ b/connections/handler/relay_test.go @@ -0,0 +1,79 @@ +package handler + +import ( + "github.com/bloXroute-Labs/gateway/connections" + "github.com/bloXroute-Labs/gateway/sdnmessage" + "github.com/bloXroute-Labs/gateway/test/bxmock" + "github.com/bloXroute-Labs/gateway/utils" + "github.com/stretchr/testify/assert" + "runtime" + "testing" + "time" +) + +// semi integration test: in general, sleep should be avoided, but these closing tests cases are checking that we are closing goroutines correctly +// relay tests differ from gateway tests in that relay connections are initiated by the node +func TestRelay_ClosingFromLocal(t *testing.T) { + startCount := runtime.NumGoroutine() + + tls, r := relayConn() + err := r.Start() + assert.Nil(t, err) + + // wait for hello message to be sent on connection so all goroutines are started + _, err = tls.MockAdvanceSent() + assert.Nil(t, err) + + // expect 2 new goroutines: read loop, send loop + assert.Equal(t, startCount+2, runtime.NumGoroutine()) + + err = r.Close("test close") + assert.Nil(t, err) + + // allow small delta for goroutines to finish + time.Sleep(1 * time.Millisecond) + + endCount := runtime.NumGoroutine() + assert.Equal(t, startCount, endCount) +} + +func TestRelay_ClosingFromRemote(t *testing.T) { + startCount := runtime.NumGoroutine() + + tls, r := relayConn() + err := r.Start() + assert.Nil(t, err) + + // allow small wait for goroutines to start, returns when connection is ready and hello message sent out + _, err = tls.MockAdvanceSent() + assert.Nil(t, err) + + // expect 2 new goroutines: read loop, send loop + startedCount := runtime.NumGoroutine() + assert.Equal(t, startCount+2, startedCount) + + err = tls.Close("test close") + assert.Nil(t, err) + + // allow small delta for goroutines to finish + time.Sleep(1 * time.Millisecond) + + // only readloop go routines should be closed, since connection is expecting retry + + assert.Equal(t, startCount+1, runtime.NumGoroutine()) +} + +func relayConn() (bxmock.MockTLS, *Relay) { + ip := "127.0.0.1" + port := int64(3000) + + tls := bxmock.NewMockTLS(ip, port, "", utils.ExternalGateway, "") + certs := utils.TestCerts() + r := NewRelay(bxmock.MockBxListener{}, + func() (connections.Socket, error) { + return tls, nil + }, + &certs, ip, port, "", utils.RelayTransaction, true, &sdnmessage.BlockchainNetworks{}, true, false, 0, utils.RealClock{}, + false) + return tls, r +} diff --git a/connections/info.go b/connections/info.go new file mode 100644 index 0000000..6e0e6fd --- /dev/null +++ b/connections/info.go @@ -0,0 +1,80 @@ +package connections + +import ( + "github.com/bloXroute-Labs/gateway/types" + "github.com/bloXroute-Labs/gateway/utils" +) + +// Info represents various information fields about the connection. +type Info struct { + NodeID types.NodeID + AccountID types.AccountID + PeerIP string + PeerPort int64 + PeerEnode string + LocalPort int64 // either the local listening server port, or 0 for outbound connections + ConnectionType utils.NodeType + ConnectionState string // TODO: flag? + NetworkNum types.NetworkNum + FromMe bool + LocalGEO bool + PrivateNetwork bool + Capabilities types.CapabilityFlags + Version string + SameRegion bool +} + +// IsCustomerGateway indicates whether the connected gateway belongs to a customer +func (ci Info) IsCustomerGateway() bool { + return ci.ConnectionType&(utils.ExternalGateway|utils.GatewayGo) != 0 && ci.AccountID != types.BloxrouteAccountID +} + +// IsBloxrouteGateway indicates if the connected gateway belongs to bloxroute +func (ci Info) IsBloxrouteGateway() bool { + return ci.ConnectionType&utils.Gateway != 0 && ci.AccountID == types.BloxrouteAccountID +} + +// IsGateway indicates if the connection is a gateway +func (ci Info) IsGateway() bool { + return ci.ConnectionType&utils.Gateway != 0 +} + +// IsCloudAPI indicates if the connection is a cloud-api +func (ci Info) IsCloudAPI() bool { + return ci.ConnectionType&utils.CloudAPI != 0 +} + +// IsLocalRegion indicates if the connection is a GW or a cloud-api +func (ci Info) IsLocalRegion() bool { + return ci.IsCloudAPI() || ci.IsGateway() +} + +// IsRelayTransaction indicates if the connection is a transaction relay +func (ci Info) IsRelayTransaction() bool { + return ci.ConnectionType&utils.RelayTransaction != 0 +} + +// IsRelayProxy indicates if the connection is a relay proxy +func (ci Info) IsRelayProxy() bool { + return ci.ConnectionType&utils.RelayProxy != 0 +} + +// IsRelay indicates if the connection is a relay type +func (ci Info) IsRelay() bool { + return ci.ConnectionType&utils.RelayProxy != 0 || ci.ConnectionType&utils.RelayTransaction != 0 || ci.ConnectionType&utils.RelayBlock != 0 +} + +// IsPrivateNetwork indicates of the peer connection is over a private network (CEN) +func (ci Info) IsPrivateNetwork() bool { + return ci.PrivateNetwork +} + +// IsLocalGEO indicates if the peer is form the same GEO as we (China vs non-China) +func (ci Info) IsLocalGEO() bool { + return ci.LocalGEO +} + +// IsSameRegion indicates if the peer is from the same region as we (us-east1, eu-west1, ...) +func (ci Info) IsSameRegion() bool { + return ci.SameRegion +} diff --git a/connections/rpc.go b/connections/rpc.go new file mode 100644 index 0000000..f781422 --- /dev/null +++ b/connections/rpc.go @@ -0,0 +1,100 @@ +package connections + +import ( + "fmt" + "github.com/bloXroute-Labs/gateway/bxmessage" + "github.com/bloXroute-Labs/gateway/types" + "github.com/bloXroute-Labs/gateway/utils" + log "github.com/sirupsen/logrus" + "time" +) + +var rpcTLSConn = TLS{} + +// RPCConn is a placeholder struct to represent connection requests from RPC transaction requests +type RPCConn struct { + AccountID types.AccountID + RemoteAddress string + networkNum types.NetworkNum + connectionType utils.NodeType + log *log.Entry +} + +// NewRPCConn return a new instance of RPCConn +func NewRPCConn(accountID types.AccountID, remoteAddr string, networkNum types.NetworkNum, connType utils.NodeType) RPCConn { + return RPCConn{ + AccountID: accountID, + RemoteAddress: remoteAddr, + networkNum: networkNum, + connectionType: connType, + log: log.WithFields(log.Fields{ + "connType": "RPC", + "remoteAddr": remoteAddr, + "accountID": accountID, + }), + } +} + +// ID identifies the underlying socket +func (r RPCConn) ID() Socket { + return rpcTLSConn +} + +// Info returns connection metadata +func (r RPCConn) Info() Info { + return Info{ + AccountID: r.AccountID, + ConnectionType: r.connectionType, + NetworkNum: r.networkNum, + } +} + +// IsOpen is never true, since the RPCConn is not writable +func (r RPCConn) IsOpen() bool { + return false +} + +// Protocol indicates that the RPCConn does not have a protocol +func (r RPCConn) Protocol() bxmessage.Protocol { + return bxmessage.EmptyProtocol +} + +// SetProtocol is a no-op +func (r RPCConn) SetProtocol(protocol bxmessage.Protocol) { +} + +// Connect is a no-op +func (r RPCConn) Connect() error { + return nil +} + +// Log returns the context logger for the RPC connection +func (r RPCConn) Log() *log.Entry { + return r.log +} + +// ReadMessages is a no-op +func (r RPCConn) ReadMessages(callBack func(bxmessage.MessageBytes), readDeadline time.Duration, headerLen int, readPayloadLen func([]byte) int) (int, error) { + return 0, nil +} + +// Send is a no-op +func (r RPCConn) Send(msg bxmessage.Message) error { + return nil +} + +// SendWithDelay is a no-op +func (r RPCConn) SendWithDelay(msg bxmessage.Message, delay time.Duration) error { + return nil +} + +// Close is a no-op +func (r RPCConn) Close(reason string) error { + return nil +} + +// String returns the formatted representation of this placeholder connection +func (r RPCConn) String() string { + accountID := string(r.AccountID) + return fmt.Sprintf("%v connection with address: %v, account id: %v", r.connectionType, r.RemoteAddress, accountID) +} diff --git a/connections/sdnhttp.go b/connections/sdnhttp.go new file mode 100644 index 0000000..d0d7339 --- /dev/null +++ b/connections/sdnhttp.go @@ -0,0 +1,418 @@ +package connections + +import ( + "bytes" + "crypto/tls" + "encoding/json" + "errors" + "fmt" + "github.com/bloXroute-Labs/gateway" + "github.com/bloXroute-Labs/gateway/config" + "github.com/bloXroute-Labs/gateway/sdnmessage" + "github.com/bloXroute-Labs/gateway/types" + "github.com/bloXroute-Labs/gateway/utils" + log "github.com/sirupsen/logrus" + "io" + "io/ioutil" + "net/http" + "os/exec" + "regexp" + "runtime/debug" + "sort" + "strconv" + "sync" + "time" +) + +// SDN Http type constants +const ( + PingTimeout = 2000.0 + TimeRegEx = "= ([^/]*)" + blockchainNetworksCacheFileName = "blockchainNetworks.json" + blockchainNetworkCacheFileName = "blockchainNetwork.json" + nodeModelCacheFileName = "nodemodel.json" + potentialRelaysFileName = "potentialrelays.json" + accountModelsFileName = "accountmodel.json" +) + +// SDNHTTP is a connection to the bloxroute API +type SDNHTTP struct { + sslCerts *utils.SSLCerts + SdnURL string + NodeID types.NodeID + nodeModel sdnmessage.NodeModel + relays sdnmessage.Peers + Networks sdnmessage.BlockchainNetworks + accountModel sdnmessage.Account + getPingLatencies func(peers sdnmessage.Peers) []nodeLatencyInfo + dataDir string +} + +// nodeLatencyInfo contains ping results with host and latency info +type nodeLatencyInfo struct { + IP string + Port int64 + Latency float64 +} + +// NewSDNHTTP creates a new connection to the bloxroute API +func NewSDNHTTP(sslCerts *utils.SSLCerts, sdnURL string, nodeModel sdnmessage.NodeModel, dataDir string) *SDNHTTP { + if nodeModel.ExternalIP == "" { + var err error + nodeModel.ExternalIP, err = utils.GetPublicIP() + if err != nil { + panic(fmt.Errorf("could not determine node's public ip: %v. consider specifying an --external-ip address", err)) + } + if nodeModel.ExternalIP == "" { + panic(fmt.Errorf("could not determine node's public ip. consider specifying an --external-ip address")) + } + log.Infof("no external ip address was provided, using autodiscovered ip address %v", nodeModel.ExternalIP) + } + sdn := &SDNHTTP{ + sslCerts: sslCerts, + SdnURL: sdnURL, + nodeModel: nodeModel, + getPingLatencies: getPingLatencies, + dataDir: dataDir, + } + return sdn +} + +// GetBlockchainNetworks fetches list of blockchain networks from the sdn +func (s *SDNHTTP) GetBlockchainNetworks() error { + err := s.getBlockchainNetworks() + if err != nil { + return err + } + return nil +} + +// GetBlockchainNetwork fetches a blockchain network given the blockchain number of the model registered with SDN +func (s *SDNHTTP) GetBlockchainNetwork() error { + networkNum := s.NetworkNum() + url := fmt.Sprintf("%v/blockchain-networks/%v", s.SdnURL, networkNum) + resp, err := s.httpWithCache(url, bxgateway.GetMethod, blockchainNetworkCacheFileName, nil) + if err != nil { + return err + } + prev, ok := s.Networks[networkNum] + if !ok { + s.Networks[networkNum] = new(sdnmessage.BlockchainNetwork) + } + if err = json.Unmarshal(resp, s.Networks[networkNum]); err != nil { + return fmt.Errorf("could not deserialize blockchain network (previously cached as: %v) for networkNum %v, because : %v", prev, networkNum, err) + } + if prev != nil && s.Networks[networkNum].MinTxAgeSeconds != prev.MinTxAgeSeconds { + log.Debugf("The MinTxAgeSeconds changed from %v seconds to %v seconds after the update", prev.MinTxAgeSeconds, s.Networks[networkNum].MinTxAgeSeconds) + } + return nil +} + +// InitGateway fetches all necessary information over HTTP from the SDN +func (s *SDNHTTP) InitGateway(protocol string, network string) error { + var err error + s.nodeModel.Network = network + s.nodeModel.Protocol = protocol + s.Networks = make(sdnmessage.BlockchainNetworks) + + if err = s.Register(); err != nil { + return err + } + if err = s.GetBlockchainNetwork(); err != nil { + return err + } + if err = s.getRelays(s.nodeModel.NodeID, s.nodeModel.BlockchainNetworkNum); err != nil { + return err + } + err = s.getAccountModel(s.nodeModel.AccountID) + if err != nil { + return err + } + return nil +} + +// BestRelay finds the recommended relay from the SDN +func (s SDNHTTP) BestRelay(bxConfig *config.Bx) (ip string, port int64, err error) { + if bxConfig.OverrideRelay { + return bxConfig.OverrideRelayHost, 1809, nil + } + + if len(s.relays) == 0 { + return "", 0, errors.New("no relays are available") + } + + relayLatencies := s.getPingLatencies(s.relays) + if len(relayLatencies) == 0 { + return "", 0, errors.New("no latencies were acquired for the relays") + } + + lowestLatencyRelay := relayLatencies[0] + if lowestLatencyRelay.Latency > 40 { + log.Warnf("ping latency of the fastest relay %v:%v is %v ms, which is more than 40 ms", + lowestLatencyRelay.IP, lowestLatencyRelay.Port, lowestLatencyRelay.Latency) + } + + log.Infof("selected relay %v:%v with latency %v ms", lowestLatencyRelay.IP, lowestLatencyRelay.Port, lowestLatencyRelay.Latency) + return lowestLatencyRelay.IP, lowestLatencyRelay.Port, nil +} + +// NodeModel returns the node model returned by the SDN +func (s SDNHTTP) NodeModel() *sdnmessage.NodeModel { + return &s.nodeModel +} + +// AccountTier returns the account tier name +func (s SDNHTTP) AccountTier() sdnmessage.AccountTier { + return s.accountModel.TierName +} + +// AccountModel returns the account model +func (s SDNHTTP) AccountModel() sdnmessage.Account { + return s.accountModel +} + +// NetworkNum returns the registered network number of the node model +func (s SDNHTTP) NetworkNum() types.NetworkNum { + return s.nodeModel.BlockchainNetworkNum +} + +func (s SDNHTTP) httpClient() (*http.Client, error) { + var tlsConfig *tls.Config + var err error + if s.sslCerts.NeedsPrivateCert() { + tlsConfig, err = s.sslCerts.LoadRegistrationConfig() + } else { + tlsConfig, err = s.sslCerts.LoadPrivateConfig() + } + if err != nil { + return nil, err + } + + client := &http.Client{ + Transport: &http.Transport{ + TLSClientConfig: tlsConfig, + }, + } + return client, nil +} + +// Register submits a registration request to bxapi. This will return private certificates for the node +// and assign a node ID. +func (s *SDNHTTP) Register() error { + if s.sslCerts.NeedsPrivateCert() { + log.Debug("new private certificate needed, appending csr to node registration") + csr, err := s.sslCerts.CreateCSR() + if err != nil { + return err + } + s.nodeModel.Csr = string(csr) + } else { + nodeID, err := s.sslCerts.GetNodeID() + if err != nil { + return err + } + s.NodeID = nodeID + } + + resp, err := s.httpWithCache(s.SdnURL+"/nodes", bxgateway.PostMethod, nodeModelCacheFileName, bytes.NewBuffer(s.nodeModel.Pack())) + if err != nil { + return err + } + if err = json.Unmarshal(resp, &s.nodeModel); err != nil { + return fmt.Errorf("could not deserialize node model: %v", err) + } + + s.NodeID = s.nodeModel.NodeID + + if s.sslCerts.NeedsPrivateCert() { + err := s.sslCerts.SavePrivateCert(s.nodeModel.Cert) + // should pretty much never happen unless there are SDN problems, in which + // case just abort on startup + if err != nil { + debug.PrintStack() + panic(err) + } + } + return nil +} + +// NeedsRegistration indicates whether proxy must register with the SDN to run +func (s *SDNHTTP) NeedsRegistration() bool { + return s.NodeID == "" || s.sslCerts.NeedsPrivateCert() +} + +func (s *SDNHTTP) close(resp *http.Response) { + err := resp.Body.Close() + if err != nil { + log.Error(fmt.Errorf("unable to close response body %v error %v", resp.Body, err)) + } +} + +func (s *SDNHTTP) getAccountModelWithEndpoint(accountID types.AccountID, endpoint string) (sdnmessage.Account, error) { + url := fmt.Sprintf("%v/%v/%v", s.SdnURL, endpoint, accountID) + accountModel := sdnmessage.Account{} + // for accounts endpoint we do no want to use the cache file. + // in case of SDN error, we set default enterprise account for the customer + var resp []byte + var err error + switch endpoint { + case "accounts": + resp, err = s.http(url, bxgateway.GetMethod, nil) + case "account": + resp, err = s.httpWithCache(url, bxgateway.GetMethod, accountModelsFileName, nil) + default: + log.Panicf("getAccountModelWithEndpoint called with unsuppored endpoint %v", endpoint) + } + + if err != nil { + return accountModel, err + } + + if err = json.Unmarshal(resp, &accountModel); err != nil { + return accountModel, fmt.Errorf("could not deserialize account model: %v", err) + } + return accountModel, err +} + +func (s *SDNHTTP) getAccountModel(accountID types.AccountID) error { + accountModel, err := s.getAccountModelWithEndpoint(accountID, "account") + s.accountModel = accountModel + return err +} + +// GetCustomerAccountModel get customer account model +func (s *SDNHTTP) GetCustomerAccountModel(accountID types.AccountID) (sdnmessage.Account, error) { + return s.getAccountModelWithEndpoint(accountID, "accounts") +} + +func (s *SDNHTTP) getRelays(nodeID types.NodeID, networkNum types.NetworkNum) error { + url := fmt.Sprintf("%v/nodes/%v/%v/potential-relays", s.SdnURL, nodeID, networkNum) + resp, err := s.httpWithCache(url, bxgateway.GetMethod, potentialRelaysFileName, nil) + if err != nil { + return err + } + if err = json.Unmarshal(resp, &s.relays); err != nil { + return fmt.Errorf("could not deserialize potential relays: %v", err) + } + return nil +} + +func (s *SDNHTTP) httpWithCache(uri string, method string, fileName string, body io.Reader) ([]byte, error) { + data, httpErr := s.http(uri, method, body) + if httpErr == nil { + err := utils.UpdateCacheFile(s.dataDir, fileName, data) + if err != nil { + log.Warnf("can not update cache file %v with data %s. error %v", fileName, data, err) + } + return data, nil + } + // we can't get the data from http - try to read from cache file + data, err := utils.LoadCacheFile(s.dataDir, fileName) + if err != nil { + return nil, fmt.Errorf("got error from http request: %v and can't load cache file %v: %v", httpErr, fileName, err) + } + + // we managed to read the data from cache file - issue a warning + log.Warnf("got error from http request: %v but loaded cache file %v", httpErr, fileName) + return data, nil +} + +func (s *SDNHTTP) http(uri string, method string, body io.Reader) ([]byte, error) { + client, err := s.httpClient() + if err != nil { + return nil, err + } + var resp *http.Response + defer func() { + if resp != nil { + s.close(resp) + } + }() + switch method { + case bxgateway.GetMethod: + resp, err = client.Get(uri) + case bxgateway.PostMethod: + resp, err = client.Post(uri, "application/json", body) + } + if err == nil && resp != nil && resp.StatusCode != 200 { + if body != nil { + err = fmt.Errorf("doing %v on %v recv and error %v, payload %v", method, uri, resp.Status, s.nodeModel) + } else { + err = fmt.Errorf("doing %v on %v recv and error %v", method, uri, resp.Status) + } + } + if err != nil { + return nil, err + } + return ioutil.ReadAll(resp.Body) +} + +func (s *SDNHTTP) getBlockchainNetworks() error { + url := fmt.Sprintf("%v/blockchain-networks", s.SdnURL) + resp, err := s.httpWithCache(url, bxgateway.GetMethod, blockchainNetworksCacheFileName, nil) + if err != nil { + return err + } + var networks []*sdnmessage.BlockchainNetwork + if err = json.Unmarshal(resp, &networks); err != nil { + return fmt.Errorf("could not deserialize blockchain networks: %v", err) + } + s.Networks = sdnmessage.BlockchainNetworks{} + for _, network := range networks { + s.Networks[network.NetworkNum] = network + } + return nil +} + +// FindNetwork finds a BlockchainNetwork instance by its number and allow update +func (s *SDNHTTP) FindNetwork(networkNum types.NetworkNum) (*sdnmessage.BlockchainNetwork, error) { + return s.Networks.FindNetwork(networkNum) +} + +// GetMinTxAge returns MinTxAge for the current blockchain number the node model registered +func (s *SDNHTTP) GetMinTxAge() time.Duration { + blockchainNetwork, err := s.FindNetwork(s.NetworkNum()) + if err != nil { + log.Debugf("could not get blockchainNetwork: %v, returning default 2 seconds for MinTxAgeSecond", err) + return 2 * time.Second + } + return time.Duration(float64(time.Second) * blockchainNetwork.MinTxAgeSeconds) +} + +// getPingLatencies pings list of SDN peers and returns sorted list of nodeLatencyInfo for each successful peer ping +func getPingLatencies(peers sdnmessage.Peers) []nodeLatencyInfo { + potentialRelaysCount := len(peers) + pingResults := make([]nodeLatencyInfo, potentialRelaysCount) + var wg sync.WaitGroup + wg.Add(potentialRelaysCount) + + for peerCount, peer := range peers { + pingResults[peerCount] = nodeLatencyInfo{peer.IP, peer.Port, PingTimeout} + go func(pingResult *nodeLatencyInfo) { + defer wg.Done() + cmd := exec.Command("ping", (*pingResult).IP, "-c1", "-W2") + var out bytes.Buffer + var stderr bytes.Buffer + cmd.Stdout = &out + cmd.Stderr = &stderr + if err := cmd.Run(); err != nil { + log.Errorf("error executing (%v) %v: %v", cmd, err, stderr) + return + } + log.Tracef("ping results from %v : %v", (*pingResult).IP, out) + re := regexp.MustCompile(TimeRegEx) + latencyTimeList := re.FindStringSubmatch(out.String()) + if len(latencyTimeList) > 0 { + latencyTime, _ := strconv.ParseFloat(latencyTimeList[1], 64) + if latencyTime > 0 { + (*pingResult).Latency = latencyTime + } + } + }(&pingResults[peerCount]) + } + wg.Wait() + + sort.Slice(pingResults, func(i int, j int) bool { return pingResults[i].Latency < pingResults[j].Latency }) + log.Infof("latency results for potential relays: %v", pingResults) + return pingResults +} diff --git a/connections/sdnhttp_integration_test.go b/connections/sdnhttp_integration_test.go new file mode 100644 index 0000000..2ea8bdc --- /dev/null +++ b/connections/sdnhttp_integration_test.go @@ -0,0 +1,132 @@ +//go:build integration +// +build integration + +package connections + +import ( + "fmt" + "github.com/bloXroute-Labs/gateway/sdnmessage" + "github.com/bloXroute-Labs/gateway/types" + "github.com/bloXroute-Labs/gateway/utils" + "github.com/stretchr/testify/assert" + "os" + "testing" +) + +//Requires setting these environment variables: +//REGISTRATION_ONLY_CERT_PATH +//REGISTRATION_ONLY_KEY_PATH +//API_CERT_PATH (for testnet) +//API_KEY_PATH (for testnet) + +func getSSLCerts(t *testing.T) utils.SSLCerts { + apiCertPath := os.Getenv("API_CERT_PATH") + apiKeyPath := os.Getenv("API_KEY_PATH") + registrationOnlyCertPath := os.Getenv("REGISTRATION_ONLY_CERT_PATH") + registrationOnlyKeyPath := os.Getenv("REGISTRATION_ONLY_KEY_PATH") + + if apiCertPath == "" || apiKeyPath == "" || registrationOnlyCertPath == "" || registrationOnlyKeyPath == "" { + t.FailNow() + } + + sslCerts := utils.NewSSLCertsFromFiles(apiCertPath, apiKeyPath, registrationOnlyCertPath, registrationOnlyKeyPath) + + return sslCerts +} + +func tearDown() { + os.Remove(blockchainNetworkCacheFileName) + os.Remove(nodeModelCacheFileName) +} + +func TestInitGateway_GetsCorrectBlockchainNetworkFromProtocolAndNetwork(t *testing.T) { + testTable := []struct { + protocol string + network string + networkNumber types.NetworkNum + genesisHash string + blockInterval int64 + maxBlockSizeBytes int64 + maxTxSizeBytes int64 + blockConfirmationsCount int64 + txSyncIntervalS float64 + }{ + {"Ethereum", "Mainnet", 5, "d4e56740f876aef8c010b86a40d5f56745a118d0906a34e69aec8c0db1cb8fa3", 15, 1048576, 1048576, 12, 1800}, + {"Ethereum", "Polygon-Mainnet", 36, "a9c28ce2141b56c474f1dc504bee9b01eb1bd7d1a507580d5519d4437a97de1b", 600, 2097152, 100000, 6, 1800}, + {"Ethereum", "BSC-Mainnet", 10, "0d21840abff46b96c84b2ac9e10e4f5cdaeb5693cb665db62a2f3b02d2d57b5b", 15, 1048576, 1048576, 12, 1800}, + {"BitcoinCash", "Mainnet", 3, "", 600, 33554432, 1048576, 6, 1800}, + } + + for _, testCase := range testTable { + t.Run(fmt.Sprint(testCase), func(t *testing.T) { + sslCerts := getSSLCerts(t) + sdnURL := "https://bdn-api.testnet.blxrbdn.com" + s := SDNHTTP{ + sslCerts: &sslCerts, + SdnURL: sdnURL, + nodeModel: sdnmessage.NodeModel{ + NodeType: "EXTERNAL_GATEWAY", + }, + } + err := s.InitGateway(testCase.protocol, testCase.network) + assert.Nil(t, err) + + //check Network Number correct + bcn, found := s.Networks[testCase.networkNumber] + if !found || bcn == nil { + t.FailNow() + } + assert.Len(t, s.Networks, 1) + + //check Network correct + assert.Equal(t, testCase.protocol, bcn.Protocol) + assert.Equal(t, testCase.network, bcn.Network) + assert.Equal(t, testCase.networkNumber, bcn.NetworkNum) + if testCase.protocol == "Ethereum" { + assert.Equal(t, testCase.genesisHash, bcn.DefaultAttributes.GenesisHash) + } + assert.Equal(t, testCase.blockInterval, bcn.BlockInterval) + assert.Equal(t, testCase.maxBlockSizeBytes, bcn.MaxBlockSizeBytes) + assert.Equal(t, testCase.maxTxSizeBytes, bcn.MaxTxSizeBytes) + assert.Equal(t, testCase.blockConfirmationsCount, bcn.BlockConfirmationsCount) + assert.Equal(t, testCase.txSyncIntervalS, bcn.TxSyncIntervalS) + + tearDown() + }) + } +} + +func TestInitGateway_ReturnsErrorIfIncorrectNetworkOrProtocol(t *testing.T) { + testTable := []struct { + protocol string + network string + errMsg string + }{ + {"Ethereumm", "Mainnet", "got error from http request: doing POST on https://bdn-api.testnet.blxrbdn.com/nodes recv and error 400 Bad Request, payload {INTERNAL_GATEWAY 0 0 false false Mainnet Ethereumm"}, + {"Ethereum", "Minnet", "got error from http request: doing POST on https://bdn-api.testnet.blxrbdn.com/nodes recv and error 400 Bad Request, payload {INTERNAL_GATEWAY 0 0 false false Minnet Ethereum"}, + {"Ethereumm", "Minnet", "got error from http request: doing POST on https://bdn-api.testnet.blxrbdn.com/nodes recv and error 400 Bad Request, payload {INTERNAL_GATEWAY 0 0 false false Minnet Ethereumm"}, + {"Ethereumm", "BSCMainnet", "got error from http request: doing POST on https://bdn-api.testnet.blxrbdn.com/nodes recv and error 400 Bad Request, payload {INTERNAL_GATEWAY 0 0 false false BSCMainnet Ethereumm"}, + } + + for _, testCase := range testTable { + t.Run(fmt.Sprint(testCase), func(t *testing.T) { + sslCerts := getSSLCerts(t) + sdnURL := "https://bdn-api.testnet.blxrbdn.com" + s := SDNHTTP{ + sslCerts: &sslCerts, + SdnURL: sdnURL, + nodeModel: sdnmessage.NodeModel{ + NodeType: "INTERNAL_GATEWAY", + }, + } + + err := s.InitGateway(testCase.protocol, testCase.network) + + if err == nil { + t.FailNow() + } + assert.Contains(t, err.Error(), testCase.errMsg) + assert.Contains(t, err.Error(), "and can't load cache file nodemodel.json: open nodemodel.json: no such file or directory") + }) + } +} diff --git a/connections/sdnhttp_test.go b/connections/sdnhttp_test.go new file mode 100644 index 0000000..886e696 --- /dev/null +++ b/connections/sdnhttp_test.go @@ -0,0 +1,278 @@ +package connections + +import ( + "bytes" + "encoding/json" + "fmt" + "github.com/bloXroute-Labs/gateway" + "github.com/bloXroute-Labs/gateway/config" + "github.com/bloXroute-Labs/gateway/sdnmessage" + "github.com/bloXroute-Labs/gateway/test" + "github.com/bloXroute-Labs/gateway/types" + "github.com/bloXroute-Labs/gateway/utils" + "github.com/gorilla/mux" + logrusTest "github.com/sirupsen/logrus/hooks/test" + "github.com/stretchr/testify/assert" + "io/ioutil" + "net/http" + "net/http/httptest" + "os" + "testing" +) + +func TestRegister_BlockchainNetworkNumberUpdated(t *testing.T) { + testTable := []struct { + protocol string + network string + networkNumber types.NetworkNum + }{ + {"Ethereum", "Mainnet", 5}, + {"Ethereum", "Testnet", 23}, + } + + for _, testCase := range testTable { + t.Run(fmt.Sprint(testCase), func(t *testing.T) { + server := createNodesServer(t, testCase.protocol, testCase.network, testCase.networkNumber) + defer func() { + server.Close() + }() + testCerts := utils.TestCerts() + s := SDNHTTP{ + SdnURL: server.URL, + sslCerts: &testCerts, + nodeModel: sdnmessage.NodeModel{ + Protocol: testCase.protocol, + Network: testCase.network, + }, + } + + err := s.Register() + + assert.Nil(t, err) + assert.Equal(t, testCase.network, s.nodeModel.Network) + assert.Equal(t, testCase.protocol, s.nodeModel.Protocol) + assert.Equal(t, testCase.networkNumber, s.nodeModel.BlockchainNetworkNum) + }) + } +} + +func createNodesServer(t *testing.T, protocol string, network string, blockchainNetworkNum types.NetworkNum) *httptest.Server { + router := mux.NewRouter() + handler := func(w http.ResponseWriter, r *http.Request) { + requestBytes, err := ioutil.ReadAll(r.Body) + if err != nil { + t.FailNow() + } + + var requestNodeModel sdnmessage.NodeModel + err = json.Unmarshal(requestBytes, &requestNodeModel) + if err != nil || requestNodeModel.Protocol != protocol || requestNodeModel.Network != network { + t.FailNow() + } + + if requestNodeModel.BlockchainNetworkNum == 0 { + requestNodeModel.BlockchainNetworkNum = blockchainNetworkNum + } + responseNodeModel := sdnmessage.NodeModel{ + Protocol: protocol, + Network: network, + BlockchainNetworkNum: requestNodeModel.BlockchainNetworkNum, + } + + responseBytes, err := json.Marshal(responseNodeModel) + if err != nil { + t.FailNow() + } + + _, err = w.Write(responseBytes) + if err != nil { + t.FailNow() + } + } + pattern := "/nodes" + router.HandleFunc(pattern, handler).Methods("POST") + server := httptest.NewServer(router) + return server +} + +func TestBestRelay_IfPingOver40MSLogsWarning(t *testing.T) { + testTable := []struct { + name string + relayCount int + latencies []nodeLatencyInfo + log string + }{ + {"Latency 5", 1, []nodeLatencyInfo{{Latency: 5, IP: "1.1.1.0", Port: 40}}, "selected relay 1.1.1.0:40 with latency 5 ms"}, + {"Latency 20", 1, []nodeLatencyInfo{{Latency: 20, IP: "1.1.1.1", Port: 41}}, "selected relay 1.1.1.1:41 with latency 20 ms"}, + {"Latency 5, 41", 2, []nodeLatencyInfo{{Latency: 5, IP: "1.1.1.2", Port: 42}, {Latency: 41, IP: "1.1.1.3", Port: 43}}, "selected relay 1.1.1.2:42 with latency 5 ms"}, + {"Latency 41", 1, []nodeLatencyInfo{{Latency: 41, IP: "1.1.1.3", Port: 43}}, + "ping latency of the fastest relay 1.1.1.3:43 is 41 ms, which is more than 40 ms"}, + {"Latency 1000, 2000", 2, []nodeLatencyInfo{{Latency: 1000, IP: "1.1.1.4", Port: 44}, {Latency: 2000, IP: "1.1.1.5", Port: 45}}, + "ping latency of the fastest relay 1.1.1.4:44 is 1000 ms, which is more than 40 ms"}, + } + + for _, testCase := range testTable { + t.Run(testCase.name, func(t *testing.T) { + globalHook := logrusTest.NewGlobal() + getPingLatenciesFunction := func(peers sdnmessage.Peers) []nodeLatencyInfo { + return testCase.latencies + } + s := SDNHTTP{relays: make([]sdnmessage.Peer, testCase.relayCount), getPingLatencies: getPingLatenciesFunction} + b := config.Bx{} + + b.OverrideRelay = false + _, _, err := s.BestRelay(&b) + assert.Nil(t, err) + + logs := globalHook.Entries + if testCase.log == "" { + assert.Nil(t, logs) + } else { + if len(logs) == 0 { + t.Fail() + } + firstLog := logs[0] + assert.Equal(t, testCase.log, firstLog.Message) + } + }) + } +} + +func TestSDNHTTP_CacheFiles(t *testing.T) { + defer func() { + os.Remove(blockchainNetworksCacheFileName) + os.Remove(nodeModelCacheFileName) + os.Remove(potentialRelaysFileName) + os.Remove(accountModelsFileName) + }() + // using bad certificate so get/post to bxapi will fail + sslCerts := utils.SSLCerts{} + + // using bad sdn url so get/post to bxapi will fail + sdn := NewSDNHTTP(&sslCerts, "https://xxxxxxxxx.com", sdnmessage.NodeModel{}, "") + url := fmt.Sprintf("%v/blockchain-networks", sdn.SdnURL) + + networks := generateNetworks() + // generate blockchainNetworks.json file which contains networks using UpdateCacheFile method + writeToFile(t, networks, blockchainNetworksCacheFileName) + + // calling to httpWithCache -> tying to get blockchain networks from bxapi + // bxapi is not responsive + // -> trying to load the blockchain networks from cache file + resp, err := sdn.httpWithCache(url, bxgateway.GetMethod, blockchainNetworksCacheFileName, nil) + assert.Nil(t, err) + assert.NotNil(t, resp) + cachedNetwork := []*sdnmessage.BlockchainNetwork{} + assert.Nil(t, json.Unmarshal(resp, &cachedNetwork)) + assert.Equal(t, networks, cachedNetwork) + + nodeModel := generateNodeModel() + // generate nodemodel.json file which contains nodeModel using UpdateCacheFile method + writeToFile(t, nodeModel, nodeModelCacheFileName) + + // calling to httpWithCache -> tying to get node model from bxapi + // bxapi is not responsive + // -> trying to load the node model from cache file + resp, err = sdn.httpWithCache(sdn.SdnURL+"/nodes", bxgateway.PostMethod, nodeModelCacheFileName, bytes.NewBuffer(sdn.NodeModel().Pack())) + assert.Nil(t, err) + assert.NotNil(t, resp) + cachedNodeModel := &sdnmessage.NodeModel{} + assert.Nil(t, json.Unmarshal(resp, &cachedNodeModel)) + assert.Equal(t, nodeModel, cachedNodeModel) + + url = fmt.Sprintf("%v/nodes/%v/%v/potential-relays", sdn.SdnURL, sdn.NodeModel().NodeID, sdn.NodeModel().BlockchainNetworkNum) + peers := generatePeers() + // generate potentialrelays.json file which contains peers using UpdateCacheFile method + writeToFile(t, peers, potentialRelaysFileName) + + // calling to httpWithCache -> tying to get peers from bxapi + // bxapi is not responsive + // -> trying to load the peers from cache file + resp, err = sdn.httpWithCache(url, bxgateway.GetMethod, potentialRelaysFileName, nil) + assert.Nil(t, err) + assert.NotNil(t, resp) + cachedPeers := sdnmessage.Peers{} + assert.Nil(t, json.Unmarshal(resp, &cachedPeers)) + assert.Equal(t, peers, cachedPeers) + + accountModel := generateAccountModel() + // generate accountmodel.json file which contains accountModel using UpdateCacheFile method + writeToFile(t, accountModel, accountModelsFileName) + url = fmt.Sprintf("%v/%v/%v", sdn.SdnURL, "account", sdn.NodeModel().AccountID) + + // calling to httpWithCache -> tying to get account model from bxapi + // bxapi is not responsive + // -> trying to load the account model from cache file + resp, err = sdn.httpWithCache(url, bxgateway.GetMethod, accountModelsFileName, nil) + assert.Nil(t, err) + assert.NotNil(t, resp) + cachedAccountModel := sdnmessage.Account{} + assert.Nil(t, json.Unmarshal(resp, &cachedAccountModel)) + assert.Equal(t, accountModel, cachedAccountModel) +} + +func generateAccountModel() sdnmessage.Account { + accountModel := sdnmessage.Account{SecretHash: "1234"} + return accountModel +} + +func generatePeers() sdnmessage.Peers { + peers := sdnmessage.Peers{} + peers = append(peers, sdnmessage.Peer{IP: "8.208.101.30", Port: 1809}) + peers = append(peers, sdnmessage.Peer{IP: "47.90.133.153", Port: 1809}) + return peers +} + +func generateNodeModel() *sdnmessage.NodeModel { + nodeModel := &sdnmessage.NodeModel{NodeType: "EXTERNAL_GATEWAY", ExternalPort: 1809, IsDocker: true} + return nodeModel +} + +func generateNetworks() []*sdnmessage.BlockchainNetwork { + var networks []*sdnmessage.BlockchainNetwork + network1 := &sdnmessage.BlockchainNetwork{AllowGasPriceChangeReuseSenderNonce: 1.1, AllowedFromTier: "Developer", SendCrossGeo: true, Network: "Mainnet", Protocol: "Ethereum", NetworkNum: 5} + network2 := &sdnmessage.BlockchainNetwork{AllowGasPriceChangeReuseSenderNonce: 1.1, AllowedFromTier: "Enterprise", SendCrossGeo: true, Network: "BSC-Mainnet", Protocol: "Ethereum", NetworkNum: 10} + networks = append(networks, network1) + networks = append(networks, network2) + return networks +} + +func generateTestNetwork() *sdnmessage.BlockchainNetwork { + return &sdnmessage.BlockchainNetwork{AllowGasPriceChangeReuseSenderNonce: 1.1, AllowedFromTier: "Developer", SendCrossGeo: true, Network: "TestNetwork", Protocol: "TestProtocol", NetworkNum: 0} +} + +func TestSDNHTTP_InitGateway(t *testing.T) { + sslCerts := utils.SSLCerts{} + sslCerts.SavePrivateCert(test.PrivateCert) + sdn := NewSDNHTTP(&sslCerts, "https://bdn-api.testnet.blxrbdn.com", sdnmessage.NodeModel{}, "") + + network := generateTestNetwork() + writeToFile(t, network, blockchainNetworkCacheFileName) + nodeModel := generateNodeModel() + writeToFile(t, nodeModel, nodeModelCacheFileName) + peers := generatePeers() + writeToFile(t, peers, potentialRelaysFileName) + accountModel := generateAccountModel() + writeToFile(t, accountModel, accountModelsFileName) + + assert.Nil(t, sdn.InitGateway(bxgateway.Ethereum, "Mainnet")) + + os.Remove(blockchainNetworkCacheFileName) + + assert.NotNil(t, sdn.InitGateway(bxgateway.Ethereum, "Mainnet")) + + os.Remove(nodeModelCacheFileName) + os.Remove(potentialRelaysFileName) + os.Remove(accountModelsFileName) +} + +func writeToFile(t *testing.T, data interface{}, fileName string) { + value, err := json.Marshal(data) + if err != nil { + t.FailNow() + } + + if utils.UpdateCacheFile("", fileName, value) != nil { + t.FailNow() + } +} diff --git a/connections/socket.go b/connections/socket.go new file mode 100644 index 0000000..07b368c --- /dev/null +++ b/connections/socket.go @@ -0,0 +1,77 @@ +package connections + +import ( + "crypto/tls" + "github.com/bloXroute-Labs/gateway/utils" + log "github.com/sirupsen/logrus" + "net" + "strconv" + "time" +) + +// Socket represents an in between interface between connection objects and network sockets +type Socket interface { + Read(b []byte) (int, error) + Write(b []byte) (int, error) + Close(string) error + + SetReadDeadline(t time.Time) error + LocalAddr() net.Addr + RemoteAddr() net.Addr + Properties() (utils.BxSSLProperties, error) +} + +const dialTimeout = 30 * time.Second + +// TLS wraps a raw TLS network connection to implement the Socket interface +type TLS struct { + *tls.Conn +} + +// NewTLS dials and creates a new TLS connection +func NewTLS(ip string, port int, certs *utils.SSLCerts) (*TLS, error) { + config, err := certs.LoadPrivateConfig() + if err != nil { + log.Errorf("servers: loadkeys: %s", err) + return nil, err + } + ipAddress := ip + ":" + strconv.Itoa(port) + ipConn, err := net.DialTimeout("tcp", ipAddress, dialTimeout) + if err != nil { + return nil, err + } + + tlsClient := tls.Client(ipConn, config) + err = tlsClient.Handshake() + if err != nil { + return nil, err + } + + return &TLS{Conn: tlsClient}, nil +} + +// NewTLSFromConn creates a new TLS wrapper on an existing TLS connection +func NewTLSFromConn(conn *tls.Conn) *TLS { + return &TLS{Conn: conn} +} + +// Properties returns the SSL properties embedded in TLS certificates +func (t TLS) Properties() (utils.BxSSLProperties, error) { + state := t.Conn.ConnectionState() + var ( + err error + bxSSLExtensions utils.BxSSLProperties + ) + for _, peerCertificate := range state.PeerCertificates { + bxSSLExtensions, err = utils.ParseBxCertificate(peerCertificate) + if err == nil { + break + } + } + return bxSSLExtensions, err +} + +// Close closes the underlying TLS connection +func (t TLS) Close(reason string) error { + return t.Conn.Close() +} diff --git a/connections/sslconn.go b/connections/sslconn.go new file mode 100644 index 0000000..2e4a544 --- /dev/null +++ b/connections/sslconn.go @@ -0,0 +1,341 @@ +package connections + +import ( + "bufio" + "bytes" + "fmt" + "github.com/bloXroute-Labs/gateway/bxmessage" + "github.com/bloXroute-Labs/gateway/utils" + log "github.com/sirupsen/logrus" + "sync" + "time" +) + +const ( + // RemoteInitiatedPort is a special constant used to indicate connections initiated from the remote + RemoteInitiatedPort = 0 + + // LocalInitiatedPort is a special constant used to indicate connections initiated locally + LocalInitiatedPort = 0 + + // PriorityQueueInterval represents the minimum amount of time that must be elapsed between non highest priority messages intended to sent along the connection + PriorityQueueInterval = 500 * time.Microsecond + + readPacketSize = 4096 +) + +// SSLConn represents the basic connection properties for connections opened between bloxroute nodes. SSLConn does not define any message handlers, and only implements the Conn interface. +type SSLConn struct { + Socket + writer *bufio.Writer + + connect func() (Socket, error) + ip string + port int64 + protocol bxmessage.Protocol + sslCerts *utils.SSLCerts + connectionOpen bool + lock *sync.Mutex + sendMessages chan bxmessage.Message + sendChannelSize int + buf bytes.Buffer + usePQ bool + pq *utils.MsgPriorityQueue + logMessages bool + extensions utils.BxSSLProperties + packet []byte + log *log.Entry + clock utils.Clock +} + +// NewSSLConnection constructs a new SSL connection. If socket is not nil, then the connection was initiated by the remote. +func NewSSLConnection(connect func() (Socket, error), sslCerts *utils.SSLCerts, ip string, port int64, protocol bxmessage.Protocol, usePQ bool, logMessages bool, sendChannelSize int, clock utils.Clock) *SSLConn { + conn := &SSLConn{ + connect: connect, + sslCerts: sslCerts, + ip: ip, + port: port, + protocol: protocol, + buf: bytes.Buffer{}, + usePQ: usePQ, + logMessages: logMessages, + lock: &sync.Mutex{}, + sendChannelSize: sendChannelSize, + packet: make([]byte, readPacketSize), + log: log.WithField("remoteAddr", fmt.Sprintf("%v:%v", ip, port)), + clock: clock, + } + return conn +} + +// sendLoop waits for messages on channel and send them to the socket +// terminates when the channel is closed +func (s *SSLConn) sendLoop() { + s.Log().Trace("starting send loop") + for { + select { + case msg, ok := <-s.sendMessages: + if !ok { + s.Log().Trace("stopping send loop (channel closed)") + return + } + s.packAndWrite(msg) + continueReading := true + for continueReading { + select { + case msg, ok = <-s.sendMessages: + if !ok { + s.Log().Trace("stopping send loop (channel closed)") + // before we close, try to send what is buffered + s.writer.Flush() + return + } + s.packAndWrite(msg) + default: + continueReading = false + } + } + err := s.writer.Flush() + if err != nil { + s.Log().Trace("stopping send loop (failed to Flush output buffer)") + return + } + } + } +} + +// packAndWrite is called by the sendLoop go routine +func (s *SSLConn) packAndWrite(msg bxmessage.Message) { + if !s.connectionOpen { + return + } + + buf, err := msg.Pack(s.protocol) + if err != nil { + s.Log().Warnf("can't pack message %v: %v. skipping", msg, err) + return + } + + // these lines can be enabled for logging exactly when we send transactions to the relays + //if s.logMessages { + // log.Tracef("sending %v to %v", msg, s) + //} + + _, err = s.writer.Write(buf) + if err != nil { + s.Log().Warnf("can't write message: %v. marking connection as closed", err) + _ = s.Close("could not write message to socket") + } +} + +// ID returns the underlying connection for checking identity +func (s *SSLConn) ID() Socket { + return s.Socket +} + +// Info returns connection details, include details parsed from certificates +func (s *SSLConn) Info() Info { + return Info{ + NodeID: s.extensions.NodeID, + AccountID: s.extensions.AccountID, + PeerIP: s.ip, + PeerPort: s.port, + LocalPort: LocalInitiatedPort, + ConnectionType: s.extensions.NodeType, + ConnectionState: "", + NetworkNum: 0, + FromMe: s.isInitiator(), + } +} + +// IsOpen indicates whether the socket connection is open +func (s SSLConn) IsOpen() bool { + return s.connectionOpen +} + +// Protocol provides the protocol version of the connection +func (s *SSLConn) Protocol() bxmessage.Protocol { + return s.protocol +} + +// SetProtocol sets the protocol version the connection is using +func (s *SSLConn) SetProtocol(p bxmessage.Protocol) { + s.protocol = p +} + +// Log returns the context logger for the SSL connection +func (s *SSLConn) Log() *log.Entry { + return s.log +} + +// Connect initializes a connection to a bloxroute node. If this is called when the remote addr is the one initiating the connection, then this function is does little besides mark some connection states as ready. Connect is also responsible for starting any goroutines relevant to the connection. +func (s *SSLConn) Connect() error { + s.pq = utils.NewMsgPriorityQueue(s.queueToMessageChan, PriorityQueueInterval) + s.buf.Reset() + + var err error + s.Socket, err = s.connect() + if err != nil { + s.connectionOpen = false + return err + } + // allocate a buffered writer to combine outgoing messages + s.writer = bufio.NewWriter(s.Socket) + + extensions, err := s.Properties() + if err != nil { + return err + } + s.extensions = extensions + s.connectionOpen = true + // start send loop now that connection is connected + s.sendMessages = make(chan bxmessage.Message, s.sendChannelSize) + go s.sendLoop() + + return nil +} + +// ReadMessages reads series of messages from the socket, placing each distinct message on the channel +func (s *SSLConn) ReadMessages(callBack func(msg bxmessage.MessageBytes), readDeadline time.Duration, headerLen int, readPayloadLen func([]byte) int) (int, error) { + + n, err := s.readWithDeadline(s.packet, readDeadline) + if err != nil { + s.Log().Debugf("connection closed while reading: %v", err) + _ = s.Close("connect closed by remote while reading") + return n, err + } + // TODO: why ReadMessages has to return every socket read? + s.buf.Write(s.packet[:n]) + for { + bufLen := s.buf.Len() + if bufLen < headerLen { + break + } + + payloadLen := readPayloadLen(s.buf.Bytes()) + if bufLen < headerLen+payloadLen { + break + } + // allocate an array for the message to protect from overrun by multiple go routines + msg := make([]byte, headerLen+payloadLen) + _, err = s.buf.Read(msg) + if err != nil { + s.Log().Warnf("encountered error while reading message: %v, skipping", err) + continue + } + callBack(msg) + } + return n, nil +} + +// readWithDeadline reads bytes from the connection onto a buffer +func (s *SSLConn) readWithDeadline(buf []byte, deadline time.Duration) (int, error) { + if !s.connectionOpen { + return 0, fmt.Errorf("connection is closing. Read from socket disabled") + } + _ = s.Socket.SetReadDeadline(s.clock.Now().Add(deadline)) + return s.Socket.Read(buf) +} + +// Send sends messages over the wire to the peer node +func (s *SSLConn) Send(msg bxmessage.Message) error { + var err error + if s.Socket == nil { + err = fmt.Errorf("trying to send a message to connection before it's connected") + s.Log().Debug(err) + return err + } + if !s.connectionOpen { + // note - can't use s.String() or s.s.RemoteAddr() here since RemoteAddr() may produce nil + err = fmt.Errorf("trying to send a message to %v:%v while it is closed", s.ip, s.port) + s.Log().Debug(err) + return err + } + // in order not to overload the python code - use priority queue and a gap of 0.5ms between sends + // this should be removed once relay/gw are in GOLANG + // Note: as of BX-2912 the pririty queue mechanism is disabled without an option to activate it + // TODO: remove PQ from code base + if true || !s.usePQ || msg.GetPriority() == bxmessage.HighestPriority { + s.queueToMessageChan(msg) + } else { + s.pq.Push(msg) + } + return nil +} + +// SendWithDelay sends messages over the wire to the peer node after waiting the requests delay +func (s *SSLConn) SendWithDelay(msg bxmessage.Message, delay time.Duration) error { + // avoid goroutine creation for no delay + if delay == 0 { + return s.Send(msg) + } + go func() { + s.clock.Sleep(delay) + err := s.Send(msg) + if err != nil { + s.Log().Errorf("could not send on conn %v", err) + } + }() + return nil +} + +func (s *SSLConn) queueToMessageChan(msg bxmessage.Message) { + // prevent a race with close + s.lock.Lock() + defer s.lock.Unlock() + if !s.connectionOpen { + return + } + select { + case s.sendMessages <- msg: + // all good if we are here + default: + _ = s.close("cannot place message on channel without blocking") + } +} + +// Close shuts down a connection. If the connection was initiated by this node, it can be reopened with another Connect call. If the connection was initiated by the remote, it cannot be reopened. +func (s *SSLConn) Close(reason string) error { + // prevent a race with writing to the sendMessages channel + s.lock.Lock() + defer s.lock.Unlock() + + return s.close(reason) +} + +// String represents a printable/readable identifier for the connection +func (s SSLConn) String() string { + if s.Socket == nil { + return fmt.Sprintf("%v:%v", s.ip, s.port) + } + return s.Socket.RemoteAddr().String() +} + +// isInitiator returns whether this node initiated the connection +func (s *SSLConn) isInitiator() bool { + return s.port != RemoteInitiatedPort +} + +// close should only be called when s.lock is already held +func (s *SSLConn) close(reason string) error { + // connection already closed + if !s.connectionOpen { + return nil + } + + s.connectionOpen = false + if s.sendMessages != nil { + // close channel to stop send loop if running + close(s.sendMessages) + } + + if s.Socket != nil { + err := s.Socket.Close(reason) + if err != nil { + s.Log().Debugf("unable to close connection: %v", err) + return err + } + s.Log().Infof("TLS is now closed: %v", reason) + } + return nil +} diff --git a/connections/websocket/ethereum.go b/connections/websocket/ethereum.go new file mode 100644 index 0000000..6241fcd --- /dev/null +++ b/connections/websocket/ethereum.go @@ -0,0 +1,140 @@ +package websocket + +import ( + "encoding/json" + "fmt" + "github.com/bloXroute-Labs/gateway/types" + "github.com/gorilla/websocket" + log "github.com/sirupsen/logrus" + "strconv" + "sync" +) + +// Ethereum is a websocket connection to an Ethereum RPC interface +type Ethereum struct { + hostAndPort string + ws *websocket.Conn + newPendingTransactionsChannel chan *types.BxTransaction + wg *sync.WaitGroup +} + +// EthereumResponseOfHash represents the Ethereum RPC response +type EthereumResponseOfHash struct { + JSONRPC string `json:"jsonrpc"` + ID interface{} `json:"id"` + Method string `json:"method"` + Params interface{} `json:"params"` +} + +// NewEthereum builds a new connection to an Ethereum RPC websocket +func NewEthereum(hostAndPort string, transactions chan *types.BxTransaction, wg *sync.WaitGroup) *Ethereum { + newEthereum := &Ethereum{ + hostAndPort: hostAndPort, + newPendingTransactionsChannel: transactions, + wg: wg, + } + return newEthereum +} + +// Start connects to the Ethereum node and starts its main event loop +func (e Ethereum) Start() error { + e.run() + return nil +} + +func (e Ethereum) run() { + log.Info("starting websocket client") + defer e.wg.Done() + + var ws *websocket.Conn = nil + var sendingAttempt int = 0 + + for { + //connect to websocket node + ws = e.connect(e.hostAndPort) + + sendingAttempt++ + + hashTransactionRequest := `{"id": "` + strconv.Itoa(sendingAttempt) + `", "method": "` + "eth_subscribe" + `", "params": ["` + "newPendingTransactions" + `"]}` + err := ws.WriteMessage(websocket.TextMessage, []byte(hashTransactionRequest)) + if err != nil { + log.Error(err) + continue + } + for { + _, hashOfNewTransaction, err := ws.ReadMessage() + if err != nil { + log.Error(err) + break + } + ethResponse := EthereumResponseOfHash{} + err = json.Unmarshal(hashOfNewTransaction, ðResponse) + if err != nil { + log.Error(err) + break + } + if ethResponse.Params != nil { + res := make(map[string]interface{}) + hash := make(map[string]string) + result := ethResponse.Params.(map[string]interface{}) + + res["subscription"] = result["subscription"].(string) + hash["txHash"] = result["result"].(string) + res["result"] = hash + ethResponse = EthereumResponseOfHash{ + JSONRPC: ethResponse.JSONRPC, + Method: "subscribe", + Params: res, + } + newHashTx, err := json.Marshal(ethResponse) + if err != nil { + log.Error(err) + break + } + //TODO ask eth node for content if not exist + //e.newPendingTransactionsChannel <- newHashTx + if false { + fmt.Println(newHashTx) + } + } + //TODO tx service from hash to content - common + //get content of transaction + //var connection = web3.NewWeb3(providers.NewHTTPProvider("63.34.22.69:8545", 10, false)) + //fmt.Println(connection) + //pointer, err := e.GetTransactionByHash("nn") + //fmt.Println(pointer.Data) + } + log.Info("reconnect") + } +} + +//func (e websocket) GetTransactionByHash(hash string) (*dto.TransactionResponse, error) { +// +// var provider providers.ProviderInterface +// params := make([]string, 1) +// params[0] = hash +// +// pointer := &dto.RequestResult{} +// +// err := provider.SendRequest(pointer, "eth_getTransactionByHash", params) +// +// if err != nil { +// fmt.Println(err) +// } +// return pointer.ToTransactionResponse() +//} + +func (e Ethereum) connect(ipAndPort string) *websocket.Conn { + dialer := websocket.DefaultDialer + var ws *websocket.Conn + + for { + wsSubscriber, _, err := dialer.Dial(ipAndPort, nil) + if err == nil { + ws = wsSubscriber + break + } + log.Error(err) + } + return ws +} diff --git a/connections_test/sslconn_test.go b/connections_test/sslconn_test.go new file mode 100644 index 0000000..bf84be3 --- /dev/null +++ b/connections_test/sslconn_test.go @@ -0,0 +1,47 @@ +package connections_test + +import ( + "github.com/bloXroute-Labs/gateway/bxmessage" + "github.com/bloXroute-Labs/gateway/connections" + "github.com/bloXroute-Labs/gateway/test/bxmock" + "github.com/bloXroute-Labs/gateway/utils" + "github.com/stretchr/testify/assert" + "runtime" + "testing" + "time" +) + +func TestSSLConn_ClosingFromSend(t *testing.T) { + startCount := runtime.NumGoroutine() + + _, s := sslConn(1) + assert.Equal(t, startCount, runtime.NumGoroutine()) + + // send loop started + _ = s.Connect() + assert.Equal(t, startCount+1, runtime.NumGoroutine()) + + am := bxmessage.Ack{} + _ = s.Send(&am) + assert.True(t, s.IsOpen()) + + _ = s.Send(&am) + assert.False(t, s.IsOpen()) + + time.Sleep(1 * time.Millisecond) + assert.Equal(t, startCount, runtime.NumGoroutine()) +} + +func sslConn(backlog int) (bxmock.MockTLS, *connections.SSLConn) { + ip := "127.0.0.1" + port := int64(3000) + + tls := bxmock.NewMockTLS(ip, port, "", utils.ExternalGateway, "") + certs := utils.TestCerts() + s := connections.NewSSLConnection( + func() (connections.Socket, error) { + return tls, nil + }, + &certs, ip, port, bxmessage.CurrentProtocol, false, false, backlog, utils.RealClock{}) + return tls, s +} diff --git a/constants.go b/constants.go new file mode 100644 index 0000000..5cc51fc --- /dev/null +++ b/constants.go @@ -0,0 +1,85 @@ +package bxgateway + +import "time" + +// MaxConnectionBacklog in addition to the socket write buffer and remote socket read buffer. +const MaxConnectionBacklog = 100000 + +// AllInterfaces binds a TCP server to accept on all interfaces. +// This is typically not recommended outside of development environments. +const AllInterfaces = "0.0.0.0" + +// MicroSecTimeFormat - use for representing tx "time" in feed +const MicroSecTimeFormat = "2006-01-02 15:04:05.000000" + +// SlowPingPong - ping/pong delay above it is considered a problem +const SlowPingPong = int64(100000) // 100 ms + +// SyncChunkSize - maximum SYNC message size for gateway +const SyncChunkSize = 500 * 1024 + +// TxStoreMaxSize - If number of Txs in TxStore is above TxStoreMaxSize cleanup will bring it back to TxStoreMaxSize (per network) +const TxStoreMaxSize = 200000 + +// BlockRecoveryTimeout - max time to wait for block recovery before canceling block +const BlockRecoveryTimeout = 10 * time.Second + +// Ethereum - string representation for the Ethereum protocol +const Ethereum = "Ethereum" + +// TimeDateLayoutISO - used to parse ISO time date format string +const TimeDateLayoutISO = "2006-01-02" + +// TimeLayoutISO - used to parse ISO time format string +const TimeLayoutISO = "2006-01-02 15:04:05-0700" + +// AsyncMsgChannelSize - size of async message channel +const AsyncMsgChannelSize = 500 + +// BxNotificationChannelSize - is the size of feed channels +const BxNotificationChannelSize = 1000 + +// MaxEthOnBlockCallRetries - max number of retries for eth RPC calls executed for onBlock feed +const MaxEthOnBlockCallRetries = 2 + +// EthOnBlockCallRetrySleepInterval - duration of sleep between RPC call retry attempts +const EthOnBlockCallRetrySleepInterval = 10 * time.Millisecond + +// MaxEthTxReceiptCallRetries - max number of retries for eth RPC calls executed for txReceipts feed +const MaxEthTxReceiptCallRetries = 5 + +// EthTxReceiptCallRetrySleepInterval - duration of sleep between RPC call retry attempts for txReceipts feed +const EthTxReceiptCallRetrySleepInterval = 2 * time.Millisecond + +// TaskCompletedEvent - sent as notification on onBlock feed after all RPC calls are completed +const TaskCompletedEvent = "TaskCompletedEvent" + +// TaskDisabledEvent - sent as notification on onBlock feed when a RPC call is disabled due to failure +const TaskDisabledEvent = "TaskDisabledEvent" + +// BDNBlocksMaxBlocksAway - gateway should not publish blocks to BDNBlocks feed that are older than best height from node minus BDNBlocksMaxBlocksAway +const BDNBlocksMaxBlocksAway = 50 + +// MaxOldBDNBlocksToSkipPublish is the max number of blocks beyond BDNBlocksMaxBlocksAway to skip publishing to BDNBlocks feed +const MaxOldBDNBlocksToSkipPublish = 3 + +// CleanedShortIDsChannelSize is the size of cleaned short ids channel +const CleanedShortIDsChannelSize = 100 + +// WSConnectionID - special node ID to identify the websocket connection +const WSConnectionID = "WSConnectionID" + +// DeliverToNodePercent is the % of transactions that should be delivered to the connected blockchain node +const DeliverToNodePercent = 20 + +// DefaultRoutingConfigFileName - routingConfig cache file name +const DefaultRoutingConfigFileName = "defaultRoutingConfig.json" + +// MaxAnnouncementFromNode restrict the size of the announcment message from the node +const MaxAnnouncementFromNode = 100 + +// GetMethod - get method for http +const GetMethod = "GET" + +// PostMethod - post method for http +const PostMethod = "POST" diff --git a/docker-entrypoint.sh b/docker-entrypoint.sh new file mode 100644 index 0000000..37f3432 --- /dev/null +++ b/docker-entrypoint.sh @@ -0,0 +1,23 @@ +#!/bin/sh +set -e + +# this script will start relayproxy + +PROGNAME=$(basename $0) + +USER="bloxroute" +GROUP="bloxroute" +WORKDIR="/app/bloxroute" +STARTUP="gateway $@" + +echo "$PROGNAME: Starting $STARTUP" +if [[ "$(id -u)" = '0' ]]; then + # if running as root, chown and step-down from root + find . \! -type l \! -user ${USER} -exec chown ${USER}:${GROUP} '{}' + + cd ${WORKDIR} + exec su-exec ${USER} ${STARTUP} +else + # allow the container to be started with `--user`, in this case we cannot use su-exec + cd ${WORKDIR} + exec ${STARTUP} +fi diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..12ce324 --- /dev/null +++ b/go.mod @@ -0,0 +1,34 @@ +module github.com/bloXroute-Labs/bxgateway-private-go/bxgateway + +go 1.16 + +require ( + github.com/bmizerany/assert v0.0.0-20160611221934-b7ed37b82869 + github.com/cpuguy83/go-md2man/v2 v2.0.0 // indirect + github.com/ethereum/go-ethereum v1.10.8 + github.com/evalphobia/logrus_fluent v0.5.4 + github.com/fluent/fluent-logger-golang v1.5.0 + github.com/golang/protobuf v1.5.2 + github.com/google/go-cmp v0.5.6 // indirect + github.com/gorilla/mux v1.8.0 + github.com/gorilla/websocket v1.4.2 + github.com/onsi/gomega v1.15.0 // indirect + github.com/orandin/lumberjackrus v1.0.1 + github.com/orcaman/concurrent-map v0.0.0-20210106121528-16402b402231 + github.com/satori/go.uuid v1.2.0 + github.com/sirupsen/logrus v1.8.1 + github.com/sourcegraph/jsonrpc2 v0.0.0-20200429184054-15c2290dcb37 + github.com/stretchr/testify v1.7.0 + github.com/struCoder/pidusage v0.1.3 + github.com/tinylib/msgp v1.1.5 // indirect + github.com/urfave/cli/v2 v2.3.0 + github.com/zhouzhuojie/conditions v0.2.3 + go.uber.org/atomic v1.4.0 + golang.org/x/crypto v0.0.0-20210322153248-0c34fe9e7dc2 + golang.org/x/sync v0.0.0-20210220032951-036812b2e83c + google.golang.org/grpc v1.35.1 + google.golang.org/protobuf v1.27.1 + gopkg.in/natefinch/lumberjack.v2 v2.0.0 // indirect + gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776 // indirect +// PLEASE DO NOT ADD gotest.tools v2.2.0+incompatible, this package sucks +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..790910b --- /dev/null +++ b/go.sum @@ -0,0 +1,670 @@ +cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU= +cloud.google.com/go v0.43.0/go.mod h1:BOSR3VbTLkk6FDC/TcffxP4NF/FFBGA5ku+jvKOP7pg= +cloud.google.com/go v0.44.1/go.mod h1:iSa0KzasP4Uvy3f1mN/7PiObzGgflwredwwASm/v6AU= +cloud.google.com/go v0.44.2/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY= +cloud.google.com/go v0.45.1/go.mod h1:RpBamKRgapWJb87xiFSdk4g1CME7QZg3uwTez+TSTjc= +cloud.google.com/go v0.46.3/go.mod h1:a6bKKbmY7er1mI7TEI4lsAkts/mkhTSZK8w33B4RAg0= +cloud.google.com/go v0.50.0/go.mod h1:r9sluTvynVuxRIOHXQEHMFffphuXHOMZMycpNR5e6To= +cloud.google.com/go v0.51.0/go.mod h1:hWtGJ6gnXH+KgDv+V0zFGDvpi07n3z8ZNj3T1RW0Gcw= +cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o= +cloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNFKH1/VBDHE= +cloud.google.com/go/bigtable v1.2.0/go.mod h1:JcVAOl45lrTmQfLj7T6TxyMzIN/3FGGcFm+2xVAli2o= +cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE= +cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I= +cloud.google.com/go/pubsub v1.1.0/go.mod h1:EwwdRX2sKPjnvnqCa270oGRyludottCI76h+R3AArQw= +cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw= +cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0ZeosJ0Rtdos= +collectd.org v0.3.0/go.mod h1:A/8DzQBkF6abtvrT2j/AU/4tiBgJWYyh0y/oB/4MlWE= +dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= +github.com/Azure/azure-pipeline-go v0.2.1/go.mod h1:UGSo8XybXnIGZ3epmeBw7Jdz+HiUVpqIlpz/HKHylF4= +github.com/Azure/azure-pipeline-go v0.2.2/go.mod h1:4rQ/NZncSvGqNkkOsNpOU1tgoNuIlp9AfUH5G1tvCHc= +github.com/Azure/azure-storage-blob-go v0.7.0/go.mod h1:f9YQKtsG1nMisotuTPpO0tjNuEjKRYAcJU8/ydDI++4= +github.com/Azure/go-autorest/autorest v0.9.0/go.mod h1:xyHB1BMZT0cuDHU7I0+g046+BFDTQ8rEZB0s4Yfa6bI= +github.com/Azure/go-autorest/autorest/adal v0.5.0/go.mod h1:8Z9fGy2MpX0PvDjB1pEgQTmVqjGhiHBW7RJJEciWzS0= +github.com/Azure/go-autorest/autorest/adal v0.8.0/go.mod h1:Z6vX6WXXuyieHAXwMj0S6HY6e6wcHn37qQMBQlvY3lc= +github.com/Azure/go-autorest/autorest/date v0.1.0/go.mod h1:plvfp3oPSKwf2DNjlBjWF/7vwR+cUD/ELuzDCXwHUVA= +github.com/Azure/go-autorest/autorest/date v0.2.0/go.mod h1:vcORJHLJEh643/Ioh9+vPmf1Ij9AEBM5FuBIXLmIy0g= +github.com/Azure/go-autorest/autorest/mocks v0.1.0/go.mod h1:OTyCOPRA2IgIlWxVYxBee2F5Gr4kF2zd2J5cFRaIDN0= +github.com/Azure/go-autorest/autorest/mocks v0.2.0/go.mod h1:OTyCOPRA2IgIlWxVYxBee2F5Gr4kF2zd2J5cFRaIDN0= +github.com/Azure/go-autorest/autorest/mocks v0.3.0/go.mod h1:a8FDP3DYzQ4RYfVAxAN3SVSiiO77gL2j2ronKKP0syM= +github.com/Azure/go-autorest/logger v0.1.0/go.mod h1:oExouG+K6PryycPJfVSxi/koC6LSNgds39diKLz7Vrc= +github.com/Azure/go-autorest/tracing v0.5.0/go.mod h1:r/s2XiOKccPW3HrqB+W0TQzfbtp2fGCgRFtBroKn4Dk= +github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ= +github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= +github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= +github.com/DATA-DOG/go-sqlmock v1.3.3/go.mod h1:f/Ixk793poVmq4qj/V1dPUg2JEAKC73Q5eFN3EC/SaM= +github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU= +github.com/StackExchange/wmi v0.0.0-20180116203802-5d049714c4a6 h1:fLjPD/aNc3UIOA6tDi6QXUemppXK3P9BI7mr2hd6gx8= +github.com/StackExchange/wmi v0.0.0-20180116203802-5d049714c4a6/go.mod h1:3eOhrUMpNV+6aFIbp5/iudMxNCF27Vw2OZgy4xEx0Fg= +github.com/VictoriaMetrics/fastcache v1.6.0 h1:C/3Oi3EiBCqufydp1neRZkqcwmEiuRT9c3fqvvgKm5o= +github.com/VictoriaMetrics/fastcache v1.6.0/go.mod h1:0qHz5QP0GMX4pfmMA/zt5RgfNuXJrTP0zS7DqpHGGTw= +github.com/aead/siphash v1.0.1/go.mod h1:Nywa3cDsYNNK3gaciGTWPwHt0wlpNV15vwmswBAUSII= +github.com/ajstarks/svgo v0.0.0-20180226025133-644b8db467af/go.mod h1:K08gAheRH3/J6wwsYMMT4xOr94bZjxIelGM0+d/wbFw= +github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= +github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= +github.com/allegro/bigcache v1.2.1-0.20190218064605-e24eb225f156 h1:eMwmnE/GDgah4HI848JfFxHt+iPb26b4zyfspmqY0/8= +github.com/allegro/bigcache v1.2.1-0.20190218064605-e24eb225f156/go.mod h1:Cb/ax3seSYIx7SuZdm2G2xzfwmv3TPSk2ucNfQESPXM= +github.com/andreyvit/diff v0.0.0-20170406064948-c7f18ee00883/go.mod h1:rCTlJbsFo29Kk6CurOXKm700vrz8f0KW0JNfpkRJY/8= +github.com/apache/arrow/go/arrow v0.0.0-20191024131854-af6fa24be0db/go.mod h1:VTxUBvSJ3s3eHAg65PNgrsn5BtqCRPdmyXh6rAfdxN0= +github.com/aws/aws-sdk-go-v2 v1.2.0/go.mod h1:zEQs02YRBw1DjK0PoJv3ygDYOFTre1ejlJWl8FwAuQo= +github.com/aws/aws-sdk-go-v2/config v1.1.1/go.mod h1:0XsVy9lBI/BCXm+2Tuvt39YmdHwS5unDQmxZOYe8F5Y= +github.com/aws/aws-sdk-go-v2/credentials v1.1.1/go.mod h1:mM2iIjwl7LULWtS6JCACyInboHirisUUdkBPoTHMOUo= +github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.0.2/go.mod h1:3hGg3PpiEjHnrkrlasTfxFqUsZ2GCk/fMUn4CbKgSkM= +github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.0.2/go.mod h1:45MfaXZ0cNbeuT0KQ1XJylq8A6+OpVV2E5kvY/Kq+u8= +github.com/aws/aws-sdk-go-v2/service/route53 v1.1.1/go.mod h1:rLiOUrPLW/Er5kRcQ7NkwbjlijluLsrIbu/iyl35RO4= +github.com/aws/aws-sdk-go-v2/service/sso v1.1.1/go.mod h1:SuZJxklHxLAXgLTc1iFXbEWkXs7QRTQpCLGaKIprQW0= +github.com/aws/aws-sdk-go-v2/service/sts v1.1.1/go.mod h1:Wi0EBZwiz/K44YliU0EKxqTCJGUfYTWXrrBwkq736bM= +github.com/aws/smithy-go v1.1.0/go.mod h1:EzMw8dbp/YJL4A5/sbhGddag+NPT7q084agLbB9LgIw= +github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= +github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8= +github.com/bmizerany/assert v0.0.0-20160611221934-b7ed37b82869 h1:DDGfHa7BWjL4YnC6+E63dPcxHo2sUxDIu8g3QgEJdRY= +github.com/bmizerany/assert v0.0.0-20160611221934-b7ed37b82869/go.mod h1:Ekp36dRnpXw/yCqJaO+ZrUyxD+3VXMFFr56k5XYrpB4= +github.com/bmizerany/pat v0.0.0-20170815010413-6226ea591a40/go.mod h1:8rLXio+WjiTceGBHIoTvn60HIbs7Hm7bcHjyrSqYB9c= +github.com/boltdb/bolt v1.3.1/go.mod h1:clJnj/oiGkjum5o1McbSZDSLxVThjynRyGBgiAx27Ps= +github.com/btcsuite/btcd v0.20.1-beta h1:Ik4hyJqN8Jfyv3S4AGBOmyouMsYE3EdYODkMbQjwPGw= +github.com/btcsuite/btcd v0.20.1-beta/go.mod h1:wVuoA8VJLEcwgqHBwHmzLRazpKxTv13Px/pDuV7OomQ= +github.com/btcsuite/btclog v0.0.0-20170628155309-84c8d2346e9f/go.mod h1:TdznJufoqS23FtqVCzL0ZqgP5MqXbb4fg/WgDys70nA= +github.com/btcsuite/btcutil v0.0.0-20190425235716-9e5f4b9a998d/go.mod h1:+5NJ2+qvTyV9exUAL/rxXi3DcLg2Ts+ymUAY5y4NvMg= +github.com/btcsuite/go-socks v0.0.0-20170105172521-4720035b7bfd/go.mod h1:HHNXQzUsZCxOoE+CPiyCTO6x34Zs86zZUiwtpXoGdtg= +github.com/btcsuite/goleveldb v0.0.0-20160330041536-7834afc9e8cd/go.mod h1:F+uVaaLLH7j4eDXPRvw78tMflu7Ie2bzYOH4Y8rRKBY= +github.com/btcsuite/snappy-go v0.0.0-20151229074030-0bdef8d06723/go.mod h1:8woku9dyThutzjeg+3xrA5iCpBRH8XEEg3lh6TiUghc= +github.com/btcsuite/websocket v0.0.0-20150119174127-31079b680792/go.mod h1:ghJtEyQwv5/p4Mg4C0fgbePVuGr935/5ddU9Z3TmDRY= +github.com/btcsuite/winsvc v1.0.0/go.mod h1:jsenWakMcC0zFBFurPLEAyrnc/teJEM1O46fmI40EZs= +github.com/c-bata/go-prompt v0.2.2/go.mod h1:VzqtzE2ksDBcdln8G7mk2RX9QyGjH+OVqOCSiVIqS34= +github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= +github.com/cespare/cp v0.1.0/go.mod h1:SOGHArjBr4JWaSDEVpWpo/hNg6RoKrls6Oh40hiwW+s= +github.com/cespare/xxhash v1.1.0 h1:a6HrQnmkObjyL+Gs60czilIUGqrzKutQD6XZog3p+ko= +github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc= +github.com/cespare/xxhash/v2 v2.1.1 h1:6MnRN8NT7+YBpUIWxHtefFZOKTAPgGjpQSxqLNn0+qY= +github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= +github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= +github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= +github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= +github.com/cloudflare/cloudflare-go v0.14.0/go.mod h1:EnwdgGMaFOruiPZRFSgn+TsQ3hQ7C/YWzIGLeu5c304= +github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= +github.com/consensys/bavard v0.1.8-0.20210406032232-f3452dc9b572/go.mod h1:Bpd0/3mZuaj6Sj+PqrmIquiOKy397AKGThQPaGzNXAQ= +github.com/consensys/gnark-crypto v0.4.1-0.20210426202927-39ac3d4b3f1f/go.mod h1:815PAHg3wvysy0SyIqanF8gZ0Y1wjk/hrDHD/iT88+Q= +github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= +github.com/cpuguy83/go-md2man/v2 v2.0.0 h1:EoUDS0afbrsXAZ9YQ9jdu/mZ2sXgT1/2yyNng4PGlyM= +github.com/cpuguy83/go-md2man/v2 v2.0.0/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= +github.com/cyberdelia/templates v0.0.0-20141128023046-ca7fffd4298c/go.mod h1:GyV+0YP4qX0UQ7r2MoYZ+AvYDp12OF5yg4q8rGnyNh4= +github.com/dave/jennifer v1.2.0/go.mod h1:fIb+770HOpJ2fmN9EPPKOqm1vMGhB+TwXKMZhrIygKg= +github.com/davecgh/go-spew v0.0.0-20171005155431-ecdeabc65495/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/deckarep/golang-set v0.0.0-20180603214616-504e848d77ea h1:j4317fAZh7X6GqbFowYdYdI0L9bwxL07jyPZIdepyZ0= +github.com/deckarep/golang-set v0.0.0-20180603214616-504e848d77ea/go.mod h1:93vsz/8Wt4joVM7c2AVqh+YRMiUSc14yDtF28KmMOgQ= +github.com/deepmap/oapi-codegen v1.6.0/go.mod h1:ryDa9AgbELGeB+YEXE1dR53yAjHwFvE9iAUlWl9Al3M= +github.com/deepmap/oapi-codegen v1.8.2/go.mod h1:YLgSKSDv/bZQB7N4ws6luhozi3cEdRktEqrX88CvjIw= +github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ= +github.com/dgryski/go-bitstream v0.0.0-20180413035011-3522498ce2c8/go.mod h1:VMaSuZ+SZcx/wljOQKvp5srsbCiKDEb6K2wC4+PiBmQ= +github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no= +github.com/dlclark/regexp2 v1.2.0/go.mod h1:2pZnwuY/m+8K6iRw6wQdMtk+rH5tNGR1i55kozfMjCc= +github.com/docker/docker v1.4.2-0.20180625184442-8e610b2b55bf/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= +github.com/dop251/goja v0.0.0-20200721192441-a695b0cdd498/go.mod h1:Mw6PkjjMXWbTj+nnj4s3QPXq1jaT0s5pC0iFD4+BOAA= +github.com/eclipse/paho.mqtt.golang v1.2.0/go.mod h1:H9keYFcgq3Qr5OUJm/JZI/i6U7joQ8SYLhZwfeOo6Ts= +github.com/edsrzf/mmap-go v1.0.0 h1:CEBF7HpRnUCSJgGUb5h1Gm7e3VkmVDrR8lvWVLtrOFw= +github.com/edsrzf/mmap-go v1.0.0/go.mod h1:YO35OhQPt3KJa3ryjFM5Bs14WD66h8eGKpfaBNrHW5M= +github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= +github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= +github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk= +github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= +github.com/ethereum/go-ethereum v1.10.8 h1:0UP5WUR8hh46ffbjJV7PK499+uGEyasRIfffS0vy06o= +github.com/ethereum/go-ethereum v1.10.8/go.mod h1:pJNuIUYfX5+JKzSD/BTdNsvJSZ1TJqmz0dVyXMAbf6M= +github.com/evalphobia/logrus_fluent v0.5.4 h1:G4BSBTm7+L+oanWfFtA/A5Y3pvL2OMxviczyZPYO5xc= +github.com/evalphobia/logrus_fluent v0.5.4/go.mod h1:hasyj+CXm3BDP1YhFk/rnTcjlegyqvkokV9A25cQsaA= +github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= +github.com/fjl/memsize v0.0.0-20190710130421-bcb5799ab5e5/go.mod h1:VvhXpOYNQvB+uIk2RvXzuaQtkQJzzIx6lSBe1xv7hi0= +github.com/fluent/fluent-logger-golang v1.5.0 h1:wBpSzYjRlzgwwAOw/3ig3Jaxrb5xTJlvBu7TRcgJeCg= +github.com/fluent/fluent-logger-golang v1.5.0/go.mod h1:2/HCT/jTy78yGyeNGQLGQsjF3zzzAuy6Xlk6FCMV5eU= +github.com/fogleman/gg v1.2.1-0.20190220221249-0403632d5b90/go.mod h1:R/bRT+9gY/C5z7JzPU0zXsXHKM4/ayA+zqcVNZzPa1k= +github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= +github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWoS4= +github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= +github.com/gballet/go-libpcsclite v0.0.0-20190607065134-2772fd86a8ff/go.mod h1:x7DCsMOv1taUwEWCzT4cmDeAkigA5/QCwUodaVOe8Ww= +github.com/getkin/kin-openapi v0.53.0/go.mod h1:7Yn5whZr5kJi6t+kShccXS8ae1APpYTW6yheSwk8Yi4= +github.com/getkin/kin-openapi v0.61.0/go.mod h1:7Yn5whZr5kJi6t+kShccXS8ae1APpYTW6yheSwk8Yi4= +github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= +github.com/glycerine/go-unsnap-stream v0.0.0-20180323001048-9f0cb55181dd/go.mod h1:/20jfyN9Y5QPEAprSgKAUr+glWDY39ZiUEAYOEv5dsE= +github.com/glycerine/goconvey v0.0.0-20190410193231-58a59202ab31/go.mod h1:Ogl1Tioa0aV7gstGFO7KhffUsb9M4ydbEbbxpcEDc24= +github.com/go-chi/chi/v5 v5.0.0/go.mod h1:BBug9lr0cqtdAhsu6R4AAdvufI0/XBzAQSsUqJpoZOs= +github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= +github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= +github.com/go-kit/kit v0.8.0 h1:Wz+5lgoB0kkuqLEc6NVmwRknTKP6dTGbSqvhZtBI/j0= +github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= +github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE= +github.com/go-logfmt/logfmt v0.4.0 h1:MP4Eh7ZCb31lleYCFuwm0oe4/YGak+5l1vA2NOE80nA= +github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk= +github.com/go-ole/go-ole v1.2.1 h1:2lOsA72HgjxAuMlKpFiCbHTvu44PIVkZ5hqm3RSdI/E= +github.com/go-ole/go-ole v1.2.1/go.mod h1:7FAglXiTm7HKlQRDeOQ6ZNUHidzCWXuZWq/1dTyBNF8= +github.com/go-openapi/jsonpointer v0.19.5/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg= +github.com/go-openapi/swag v0.19.5/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh66Z9tfKk= +github.com/go-sourcemap/sourcemap v2.1.2+incompatible/go.mod h1:F8jJfvm2KbVjc5NqelyYJmf/v5J0dwNLS2mL4sNA1Jg= +github.com/go-sql-driver/mysql v1.4.1/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w= +github.com/go-stack/stack v1.8.0 h1:5SgMzNM5HxrEjV0ww2lTmX6E2Izsfxas4+YHWRs3Lsk= +github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= +github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod h1:fyg7847qk6SyHyPtNmDHnmrv/HOrqktSC+C9fM+CJOE= +github.com/gofrs/uuid v3.3.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM= +github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= +github.com/gogo/protobuf v1.3.1/go.mod h1:SlYgWuQ5SjCEi6WLHjHCa1yvBfUnHcTbrrZtXPKa29o= +github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0/go.mod h1:E/TSTwGwJL78qG/PmXZO1EjYhfJinVAhrmmHX6Z8B9k= +github.com/golang/geo v0.0.0-20190916061304-5b978397cfec/go.mod h1:QZ0nwyI2jOfgRAoBvP+ab5aRr7c9x7lhGEJrKvBwjWI= +github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= +github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= +github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= +github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y= +github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= +github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= +github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= +github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= +github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= +github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8= +github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= +github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= +github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= +github.com/golang/protobuf v1.5.2 h1:ROPKBNFfQgOUMifHyP+KYbvpjbdoFNs+aK7DXlji0Tw= +github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= +github.com/golang/snappy v0.0.0-20180518054509-2e65f85255db/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= +github.com/golang/snappy v0.0.1/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= +github.com/golang/snappy v0.0.3 h1:fHPg5GQYlCeLIPB9BZqMVR5nR9A+IM5zcgeTdjMYmLA= +github.com/golang/snappy v0.0.3/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= +github.com/golangci/lint-1 v0.0.0-20181222135242-d2cdd8c08219/go.mod h1:/X8TswGSh1pIozq4ZwCfxS0WA5JGXguxk94ar/4c87Y= +github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= +github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= +github.com/google/flatbuffers v1.11.0/go.mod h1:1AeVuKshWv4vARoZatz6mlQ0JxURH0Kv5+zNeJKJCa8= +github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= +github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.4.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.6 h1:BKbKCqvP6I+rmFHt06ZmyQtvB8xAkWdhFyr0ZUNZcxQ= +github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/gofuzz v1.1.1-0.20200604201612-c04b05f3adfa/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= +github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= +github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= +github.com/google/pprof v0.0.0-20191218002539-d4f498aebedc/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= +github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/google/uuid v1.1.5/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= +github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= +github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= +github.com/gorilla/mux v1.8.0 h1:i40aqfkR1h2SlN9hojwV5ZA91wcXFOvkdNIeFDP5koI= +github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So= +github.com/gorilla/websocket v1.4.1/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= +github.com/gorilla/websocket v1.4.2 h1:+/TMaTYc4QFitKJxsQ7Yye35DkWvkdLcvGKqM+x0Ufc= +github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= +github.com/graph-gophers/graphql-go v0.0.0-20201113091052-beb923fada29/go.mod h1:9CQHMSxwO4MprSdzoIEobiHpoLtHm77vfxsvsIN5Vuc= +github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= +github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= +github.com/hashicorp/golang-lru v0.5.5-0.20210104140557-80c98217689d h1:dg1dEPuWpEqDnvIw251EVy4zlP8gWbsGj4BsUKCRpYs= +github.com/hashicorp/golang-lru v0.5.5-0.20210104140557-80c98217689d/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4= +github.com/holiman/bloomfilter/v2 v2.0.3 h1:73e0e/V0tCydx14a0SCYS/EWCxgwLZ18CZcZKVu0fao= +github.com/holiman/bloomfilter/v2 v2.0.3/go.mod h1:zpoh+gs7qcpqrHr3dB55AMiJwo0iURXE7ZOP9L9hSkA= +github.com/holiman/uint256 v1.2.0 h1:gpSYcPLWGv4sG43I2mVLiDZCNDh/EpGjSk8tmtxitHM= +github.com/holiman/uint256 v1.2.0/go.mod h1:y4ga/t+u+Xwd7CpDgZESaRcWy0I7XMlTMA25ApIH5Jw= +github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= +github.com/huin/goupnp v1.0.2 h1:RfGLP+h3mvisuWEyybxNq5Eft3NWhHLPeUN72kpKZoI= +github.com/huin/goupnp v1.0.2/go.mod h1:0dxJBVBHqTMjIUMkESDTNgOOx/Mw5wYIfyFmdzSamkM= +github.com/huin/goutil v0.0.0-20170803182201-1ca381bf3150/go.mod h1:PpLOETDnJ0o3iZrZfqZzyLl6l7F3c6L1oWn7OICBi6o= +github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= +github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= +github.com/influxdata/flux v0.65.1/go.mod h1:J754/zds0vvpfwuq7Gc2wRdVwEodfpCFM7mYlOw2LqY= +github.com/influxdata/influxdb v1.8.3/go.mod h1:JugdFhsvvI8gadxOI6noqNeeBHvWNTbfYGtiAn+2jhI= +github.com/influxdata/influxdb-client-go/v2 v2.4.0/go.mod h1:vLNHdxTJkIf2mSLvGrpj8TCcISApPoXkaxP8g9uRlW8= +github.com/influxdata/influxql v1.1.1-0.20200828144457-65d3ef77d385/go.mod h1:gHp9y86a/pxhjJ+zMjNXiQAA197Xk9wLxaz+fGG+kWk= +github.com/influxdata/line-protocol v0.0.0-20180522152040-32c6aa80de5e/go.mod h1:4kt73NQhadE3daL3WhR5EJ/J2ocX0PZzwxQ0gXJ7oFE= +github.com/influxdata/line-protocol v0.0.0-20200327222509-2487e7298839/go.mod h1:xaLFMmpvUxqXtVkUJfg9QmT88cDaCJ3ZKgdZ78oO8Qo= +github.com/influxdata/line-protocol v0.0.0-20210311194329-9aa0e372d097/go.mod h1:xaLFMmpvUxqXtVkUJfg9QmT88cDaCJ3ZKgdZ78oO8Qo= +github.com/influxdata/promql/v2 v2.12.0/go.mod h1:fxOPu+DY0bqCTCECchSRtWfc+0X19ybifQhZoQNF5D8= +github.com/influxdata/roaring v0.4.13-0.20180809181101-fc520f41fab6/go.mod h1:bSgUQ7q5ZLSO+bKBGqJiCBGAl+9DxyW63zLTujjUlOE= +github.com/influxdata/tdigest v0.0.0-20181121200506-bf2b5ad3c0a9/go.mod h1:Js0mqiSBE6Ffsg94weZZ2c+v/ciT8QRHFOap7EKDrR0= +github.com/influxdata/usage-client v0.0.0-20160829180054-6d3895376368/go.mod h1:Wbbw6tYNvwa5dlB6304Sd+82Z3f7PmVZHVKU637d4po= +github.com/jackpal/go-nat-pmp v1.0.2-0.20160603034137-1fa385a6f458 h1:6OvNmYgJyexcZ3pYbTI9jWx5tHo1Dee/tWbLMfPe2TA= +github.com/jackpal/go-nat-pmp v1.0.2-0.20160603034137-1fa385a6f458/go.mod h1:QPH045xvCAeXUZOxsnwmrtiCoxIr9eob+4orBN1SBKc= +github.com/jedisct1/go-minisign v0.0.0-20190909160543-45766022959e/go.mod h1:G1CVv03EnqU1wYL2dFwXxW2An0az9JTl/ZsqXQeBlkU= +github.com/jessevdk/go-flags v0.0.0-20141203071132-1679536dcc89/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI= +github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo= +github.com/jmespath/go-jmespath/internal/testify v1.5.1/go.mod h1:L3OGu8Wl2/fWfCI6z80xFu9LTZmf1ZRjMHUOPmWr69U= +github.com/jrick/logrotate v1.0.0/go.mod h1:LNinyqDIJnpAur+b8yyulnQw/wDuN1+BYKlTRt3OuAQ= +github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= +github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= +github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk= +github.com/jsternberg/zap-logfmt v1.0.0/go.mod h1:uvPs/4X51zdkcm5jXl5SYoN+4RK21K8mysFmDaM/h+o= +github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU= +github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= +github.com/jung-kurt/gofpdf v1.0.3-0.20190309125859-24315acbbda5/go.mod h1:7Id9E/uU8ce6rXgefFLlgrJj/GYY22cpxn+r32jIOes= +github.com/jwilder/encoding v0.0.0-20170811194829-b4e1701a28ef/go.mod h1:Ct9fl0F6iIOGgxJ5npU/IUOhOhqlVrGjyIZc8/MagT0= +github.com/karalabe/usb v0.0.0-20190919080040-51dc0efba356/go.mod h1:Od972xHfMJowv7NGVDiWVxk2zxnWgjLlJzE+F4F7AGU= +github.com/kisielk/errcheck v1.2.0/go.mod h1:/BMXB+zMLi60iA8Vv6Ksmxu/1UDYcXs4uQLJ+jE2L00= +github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= +github.com/kkdai/bstream v0.0.0-20161212061736-f391b8402d23/go.mod h1:J+Gs4SYgM6CZQHDETBtE9HaSEkGmuNXF86RwHhHUvq4= +github.com/klauspost/compress v1.4.0/go.mod h1:RyIbtBH6LamlWaDj8nUwkbUhJ87Yi3uG0guNDohfE1A= +github.com/klauspost/cpuid v0.0.0-20170728055534-ae7887de9fa5/go.mod h1:Pj4uuM528wm8OyEC2QMXAi2YiTZ96dNQPGgoMS4s3ek= +github.com/klauspost/crc32 v0.0.0-20161016154125-cb6bfca970f6/go.mod h1:+ZoRqAPRLkC4NPOvfYeR5KNOrY6TD+/sAC3HXPZgDYg= +github.com/klauspost/pgzip v1.0.2-0.20170402124221-0bf5dcad4ada/go.mod h1:Ch1tH69qFZu15pkjo5kYi6mth2Zzwzt50oCQKQE9RUs= +github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= +github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515 h1:T+h1c/A9Gawja4Y9mFVWj2vyii2bbUNDw3kt9VxK2EY= +github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= +github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= +github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= +github.com/labstack/echo/v4 v4.2.1/go.mod h1:AA49e0DZ8kk5jTOOCKNuPR6oTnBS0dYiM4FW1e6jwpg= +github.com/labstack/gommon v0.3.0/go.mod h1:MULnywXg0yavhxWKc+lOruYdAhDwPK9wf0OL7NoOu+k= +github.com/leanovate/gopter v0.2.9/go.mod h1:U2L/78B+KVFIx2VmW6onHJQzXtFb+p5y3y2Sh+Jxxv8= +github.com/lib/pq v1.0.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= +github.com/mailru/easyjson v0.0.0-20190614124828-94de47d64c63/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= +github.com/mailru/easyjson v0.0.0-20190626092158-b2ccc519800e/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= +github.com/matryer/moq v0.0.0-20190312154309-6cfb0558e1bd/go.mod h1:9ELz6aaclSIGnZBoaSLZ3NAl1VTufbOrXBPvtcy6WiQ= +github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU= +github.com/mattn/go-colorable v0.1.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= +github.com/mattn/go-colorable v0.1.7/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= +github.com/mattn/go-colorable v0.1.8/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= +github.com/mattn/go-ieproxy v0.0.0-20190610004146-91bb50d98149/go.mod h1:31jz6HNzdxOmlERGGEc4v/dMssOfmp2p5bT/okiKFFc= +github.com/mattn/go-ieproxy v0.0.0-20190702010315-6dee0af9227d/go.mod h1:31jz6HNzdxOmlERGGEc4v/dMssOfmp2p5bT/okiKFFc= +github.com/mattn/go-isatty v0.0.4/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= +github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= +github.com/mattn/go-isatty v0.0.9/go.mod h1:YNRxwqDuOph6SZLI9vUUz6OYw3QyUt7WiY2yME+cCiQ= +github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= +github.com/mattn/go-runewidth v0.0.3/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU= +github.com/mattn/go-runewidth v0.0.9 h1:Lm995f3rfxdpd6TSmuVCHVb/QhupuXlYr8sCI/QdE+0= +github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI= +github.com/mattn/go-sqlite3 v1.11.0/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc= +github.com/mattn/go-tty v0.0.0-20180907095812-13ff1204f104/go.mod h1:XPvLUNfbS4fJH25nqRHfWLMa1ONC8Amw+mIA639KxkE= +github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= +github.com/mschoch/smat v0.0.0-20160514031455-90eadee771ae/go.mod h1:qAyveg+e4CE+eKJXWVjKXM4ck2QobLqTDytGJbLLhJg= +github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= +github.com/naoina/go-stringutil v0.1.0/go.mod h1:XJ2SJL9jCtBh+P9q5btrd/Ylo8XwT/h1USek5+NqSA0= +github.com/naoina/toml v0.1.2-0.20170918210437-9fafd6967416/go.mod h1:NBIhNtsFMo3G2szEBne+bO4gS192HuIYRqfvOWb4i1E= +github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A= +github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE= +github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU= +github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U= +github.com/olekukonko/tablewriter v0.0.5 h1:P2Ga83D34wi1o9J6Wh1mRuqd4mF/x/lgBS7N7AbDhec= +github.com/olekukonko/tablewriter v0.0.5/go.mod h1:hPp6KlRPjbx+hW8ykQs1w3UBbZlj6HuIJcUGPhkA7kY= +github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= +github.com/onsi/ginkgo v1.7.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= +github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk= +github.com/onsi/ginkgo v1.14.0/go.mod h1:iSB4RoI2tjJc9BBv4NKIKWKya62Rps+oPG/Lv9klQyY= +github.com/onsi/ginkgo v1.16.4 h1:29JGrr5oVBm5ulCWet69zQkzWipVXIol6ygQUe/EzNc= +github.com/onsi/ginkgo v1.16.4/go.mod h1:dX+/inL/fNMqNlz0e9LfyB9TswhZpCVdJM/Z6Vvnwo0= +github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= +github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY= +github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo= +github.com/onsi/gomega v1.15.0 h1:WjP/FQ/sk43MRmnEcT+MlDw2TFvkrXlprrPST/IudjU= +github.com/onsi/gomega v1.15.0/go.mod h1:cIuvLEne0aoVhAgh/O6ac0Op8WWw9H6eYCriF+tEHG0= +github.com/opentracing/opentracing-go v1.0.2/go.mod h1:UkNAQd3GIcIGf0SeVgPpRdFStlNbqXla1AfSYxPUl2o= +github.com/opentracing/opentracing-go v1.0.3-0.20180606204148-bd9c31933947/go.mod h1:UkNAQd3GIcIGf0SeVgPpRdFStlNbqXla1AfSYxPUl2o= +github.com/opentracing/opentracing-go v1.1.0/go.mod h1:UkNAQd3GIcIGf0SeVgPpRdFStlNbqXla1AfSYxPUl2o= +github.com/orandin/lumberjackrus v1.0.1 h1:7ysDQ0MHD79zIFN9/EiDHjUcgopNi5ehtxFDy8rUkWo= +github.com/orandin/lumberjackrus v1.0.1/go.mod h1:xYLt6H8W93pKnQgUQaxsApS0Eb4BwHLOkxk5DVzf5H0= +github.com/orcaman/concurrent-map v0.0.0-20210106121528-16402b402231 h1:fa50YL1pzKW+1SsBnJDOHppJN9stOEwS+CRWyUtyYGU= +github.com/orcaman/concurrent-map v0.0.0-20210106121528-16402b402231/go.mod h1:Lu3tH6HLW3feq74c2GC+jIMS/K2CFcDWnWD9XkenwhI= +github.com/paulbellamy/ratecounter v0.2.0/go.mod h1:Hfx1hDpSGoqxkVVpBi/IlYD7kChlfo5C6hzIHwPqfFE= +github.com/peterh/liner v1.0.1-0.20180619022028-8c1271fcf47f/go.mod h1:xIteQHvHuaLYG9IFj6mSxM0fCKrs34IrEQUhOYuGPHc= +github.com/peterh/liner v1.1.1-0.20190123174540-a2c9a5303de7/go.mod h1:CRroGNssyjTd/qIG2FyxByd2S8JEAZXBl4qUrZf8GS0= +github.com/philhofer/fwd v1.0.0/go.mod h1:gk3iGcWd9+svBvR0sR+KPcfE+RNWozjowpeBVG3ZVNU= +github.com/philhofer/fwd v1.1.1 h1:GdGcTjf5RNAxwS4QLsiMzJYj5KEvPJD3Abr261yRQXQ= +github.com/philhofer/fwd v1.1.1/go.mod h1:gk3iGcWd9+svBvR0sR+KPcfE+RNWozjowpeBVG3ZVNU= +github.com/pierrec/lz4 v2.0.5+incompatible/go.mod h1:pdkljMzZIN41W+lC3N2tnIh5sFi+IEE17M5jbnwPHcY= +github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pkg/term v0.0.0-20180730021639-bffc007b7fd5/go.mod h1:eCbImbZ95eXtAUIbLAuAVnBnwf83mjf6QIVH8SHYwqQ= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= +github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo= +github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= +github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/prometheus/common v0.0.0-20181113130724-41aa239b4cce/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro= +github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= +github.com/prometheus/common v0.6.0/go.mod h1:eBmuwkDJBwy6iBfxCBob6t6dR6ENT/y+J+Zk0j9GMYc= +github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= +github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= +github.com/prometheus/tsdb v0.7.1 h1:YZcsG11NqnK4czYLrWd9mpEuAJIHVQLwdrleYfszMAA= +github.com/prometheus/tsdb v0.7.1/go.mod h1:qhTCs0VvXwvX/y3TZrWD7rabWM+ijKTux40TwIPHuXU= +github.com/retailnext/hllpp v1.0.1-0.20180308014038-101a6d2f8b52/go.mod h1:RDpi1RftBQPUCDRw6SmxeaREsAaRKnOclghuzp/WRzc= +github.com/rjeczalik/notify v0.9.1/go.mod h1:rKwnCoCGeuQnwBtTSPL9Dad03Vh2n40ePRrjvIXnJho= +github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= +github.com/rs/cors v1.7.0/go.mod h1:gFx+x8UowdsKA9AchylcLynDq+nNFfI8FkUZdN/jGCU= +github.com/russross/blackfriday/v2 v2.0.1 h1:lPqVAte+HuHNfhJ/0LC98ESWRz8afy9tM/0RK8m9o+Q= +github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/satori/go.uuid v1.2.0 h1:0uYX9dsZ2yD7q2RtLRtPSdGDWzjeM3TbMJP9utgA0ww= +github.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0= +github.com/segmentio/kafka-go v0.1.0/go.mod h1:X6itGqS9L4jDletMsxZ7Dz+JFWxM6JHfPOCvTvk+EJo= +github.com/segmentio/kafka-go v0.2.0/go.mod h1:X6itGqS9L4jDletMsxZ7Dz+JFWxM6JHfPOCvTvk+EJo= +github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo= +github.com/shirou/gopsutil v3.21.4-0.20210419000835-c7a38de76ee5+incompatible h1:Bn1aCHHRnjv4Bl16T8rcaFjYSrGrIZvpiGO6P3Q4GpU= +github.com/shirou/gopsutil v3.21.4-0.20210419000835-c7a38de76ee5+incompatible/go.mod h1:5b4v6he4MtMOwMlS0TUMTu2PcXUg8+E1lC7eC3UO/RA= +github.com/shurcooL/sanitized_anchor_name v1.0.0 h1:PdmoCO6wvbs+7yrJyMORt4/BmY5IYyJwS/kOiWx8mHo= +github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= +github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= +github.com/sirupsen/logrus v1.8.1 h1:dJKuHgqk1NNQlqoA6BTlM1Wf9DOH3NBjQyu0h9+AZZE= +github.com/sirupsen/logrus v1.8.1/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= +github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc= +github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA= +github.com/sourcegraph/jsonrpc2 v0.0.0-20200429184054-15c2290dcb37 h1:marA1XQDC7N870zmSFIoHZpIUduK80USeY0Rkuflgp4= +github.com/sourcegraph/jsonrpc2 v0.0.0-20200429184054-15c2290dcb37/go.mod h1:ZafdZgk/axhT1cvZAPOhw+95nz2I/Ra5qMlU4gTRwIo= +github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= +github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= +github.com/spf13/cobra v0.0.3/go.mod h1:1l0Ry5zgKvJasoi3XT1TypsSe7PqH0Sj9dhYf7v3XqQ= +github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= +github.com/status-im/keycard-go v0.0.0-20190316090335-8537d3370df4/go.mod h1:RZLeN1LMWmRsyYjvAu+I6Dm9QmlDaIIt+Y+4Kd7Tp+Q= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.2.0/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= +github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= +github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= +github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/struCoder/pidusage v0.1.3 h1:pZcSa6asBE38TJtW0Nui6GeCjLTpaT/jAnNP7dUTLSQ= +github.com/struCoder/pidusage v0.1.3/go.mod h1:pWBlW3YuSwRl6h7R5KbvA4N8oOqe9LjaKW5CwT1SPjI= +github.com/syndtr/goleveldb v1.0.1-0.20210305035536-64b5b1c73954 h1:xQdMZ1WLrgkkvOZ/LDQxjVxMLdby7osSh4ZEVa5sIjs= +github.com/syndtr/goleveldb v1.0.1-0.20210305035536-64b5b1c73954/go.mod h1:u2MKkTVTVJWe5D1rCvame8WqhBd88EuIwODJZ1VHCPM= +github.com/tinylib/msgp v1.0.2/go.mod h1:+d+yLhGm8mzTaHzB+wgMYrodPfmZrzkirds8fDWklFE= +github.com/tinylib/msgp v1.1.5 h1:2gXmtWueD2HefZHQe1QOy9HVzmFrLOVvsXwXBQ0ayy0= +github.com/tinylib/msgp v1.1.5/go.mod h1:eQsjooMTnV42mHu917E26IogZ2930nFyBQdofk10Udg= +github.com/tklauser/go-sysconf v0.3.5 h1:uu3Xl4nkLzQfXNsWn15rPc/HQCJKObbt1dKJeWp3vU4= +github.com/tklauser/go-sysconf v0.3.5/go.mod h1:MkWzOF4RMCshBAMXuhXJs64Rte09mITnppBXY/rYEFI= +github.com/tklauser/numcpus v0.2.2 h1:oyhllyrScuYI6g+h/zUvNXNp1wy7x8qQy3t/piefldA= +github.com/tklauser/numcpus v0.2.2/go.mod h1:x3qojaO3uyYt0i56EW/VUYs7uBvdl2fkfZFu0T9wgjM= +github.com/ttacon/chalk v0.0.0-20160626202418-22c06c80ed31/go.mod h1:onvgF043R+lC5RZ8IT9rBXDaEDnpnw/Cl+HFiw+v/7Q= +github.com/tyler-smith/go-bip39 v1.0.1-0.20181017060643-dbb3b84ba2ef/go.mod h1:sJ5fKU0s6JVwZjjcUEX2zFOnvq0ASQ2K9Zr6cf67kNs= +github.com/urfave/cli/v2 v2.3.0 h1:qph92Y649prgesehzOrQjdWyxFOp/QVM+6imKHad91M= +github.com/urfave/cli/v2 v2.3.0/go.mod h1:LJmUH05zAU44vOAcrfzZQKsZbVcdbOG8rtL3/XcUArI= +github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= +github.com/valyala/fasttemplate v1.0.1/go.mod h1:UQGH1tvbgY+Nz5t2n7tXsz52dQxojPUpymEIMZ47gx8= +github.com/valyala/fasttemplate v1.2.1/go.mod h1:KHLXt3tVN2HBp8eijSv/kGJopbvo7S+qRAEEKiv+SiQ= +github.com/willf/bitset v1.1.3/go.mod h1:RjeCKbqT1RxIR/KWY6phxZiaY1IyutSBfGjNPySAYV4= +github.com/xlab/treeprint v0.0.0-20180616005107-d6fb6747feb6/go.mod h1:ce1O1j6UtZfjr22oyGxGLbauSBp2YVXpARAosm7dHBg= +github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/zhouzhuojie/conditions v0.2.3 h1:TS3X6vA9CVXXteRdeXtpOw3hAar+01f0TI/dLp8qEvY= +github.com/zhouzhuojie/conditions v0.2.3/go.mod h1:Izhy98HD3MkfwGPz+p9ZV2JuqrpbHjaQbUq9iZHh+ZY= +go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= +go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8= +go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= +go.uber.org/atomic v1.3.2/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= +go.uber.org/atomic v1.4.0 h1:cxzIVoETapQEqDhQu3QfnvXAV4AlzcvUCxkVUFw3+EU= +go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= +go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0= +go.uber.org/zap v1.9.1/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= +golang.org/x/crypto v0.0.0-20170930174604-9419663f5a44/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= +golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20190909091759-094676da4a83/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20200820211705-5c72a883971a/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20201221181555-eec23a3978ad/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I= +golang.org/x/crypto v0.0.0-20210322153248-0c34fe9e7dc2 h1:It14KIkyBFYkHkwZ7k45minvA9aorojkyjGk9KJ5B/w= +golang.org/x/crypto v0.0.0-20210322153248-0c34fe9e7dc2/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4= +golang.org/x/exp v0.0.0-20180321215751-8460e604b9de/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/exp v0.0.0-20180807140117-3d87b88a115f/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/exp v0.0.0-20190125153040-c74c464bbbf2/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= +golang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm080zCjGlMRFzhUhsZKEZO7MGek= +golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY= +golang.org/x/exp v0.0.0-20191129062945-2f5052295587/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= +golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= +golang.org/x/image v0.0.0-20180708004352-c73c2afc3b81/go.mod h1:ux5Hcp/YLpHSI86hEcLt0YII63i6oz57MZXIpbrjZUs= +golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= +golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= +golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= +golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= +golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= +golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f/go.mod h1:5qLYkcX4OjUUV8bRuDixDT3tpyyb+LUpUlRWLxfhWrs= +golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE= +golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o= +golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc= +golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY= +golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= +golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= +golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200813134508-3edf25e44fcc/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= +golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= +golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.0.0-20210119194325-5f4716e94777/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20210220033124-5f55cee0dc0d/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20210428140749-89ef3d95e781/go.mod h1:OJAsFXCWl8Ukc7SiCT/9KSuxbyM7479/AVlXFRxuMCk= +golang.org/x/net v0.0.0-20210805182204-aaa1db679c0d h1:20cMwl2fHAzkJMEA+8J4JgqBQcQGzbisXo31MIeenXI= +golang.org/x/net v0.0.0-20210805182204-aaa1db679c0d/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= +golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20210220032951-036812b2e83c h1:5KslGYwFpkhGh+Q16bwMP3cOontH8FOep7tGV86Y7SQ= +golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20181107165924-66b7b1311ac8/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200107162124-548cf772de50/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200519105757-fe76b779f299/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200814200057-3d37ad5750ed/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200826173525-f9321e4c35a6/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210112080510-489259a85091/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210316164454-77fc1eacc6aa/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210324051608-47abb6519492/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210420205809-ac73e9fd8988/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210816183151-1e6c022a8912 h1:uCLL3g5wH2xjxVREVuAbP9JM5PPKjRbXKRa6IBjkzmU= +golang.org/x/sys v0.0.0-20210816183151-1e6c022a8912/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.6 h1:aRYxNxv6iGQlyVaZmk6ZgYEDa+Jg18DxebPSrd6bg1M= +golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.0.0-20201208040808-7e3f01d25324/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.0.0-20210220033141-f8bda1e9f3ba/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/tools v0.0.0-20180525024113-a5b4c53f6e8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20181030221726-6c7e314b6563/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190206041539-40960b6deb8e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= +golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191113191852-77e3bb0ad9e7/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191115202509-3a792d9c32b2/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191125144606-a911d9008d1f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191216173652-a0e659d51361/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20191227053925-7b8e75db28f4/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200108203644-89082a384178/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20201022035929-9cf592e881e9/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.0.0-20201224043029-2b0845dc783e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +gonum.org/v1/gonum v0.0.0-20180816165407-929014505bf4/go.mod h1:Y+Yx5eoAFn32cQvJDxZx5Dpnq+c3wtXuadVZAcxbbBo= +gonum.org/v1/gonum v0.0.0-20181121035319-3f7ecaa7e8ca/go.mod h1:Y+Yx5eoAFn32cQvJDxZx5Dpnq+c3wtXuadVZAcxbbBo= +gonum.org/v1/gonum v0.6.0/go.mod h1:9mxDZsDKxgMAuccQkewq682L+0eCu4dCN2yonUJTCLU= +gonum.org/v1/netlib v0.0.0-20181029234149-ec6d1f5cefe6/go.mod h1:wa6Ws7BG/ESfp6dHfk7C6KdzKA7wR7u/rKwOGE66zvw= +gonum.org/v1/netlib v0.0.0-20190313105609-8cb42192e0e0/go.mod h1:wa6Ws7BG/ESfp6dHfk7C6KdzKA7wR7u/rKwOGE66zvw= +gonum.org/v1/plot v0.0.0-20190515093506-e2840ee46a6b/go.mod h1:Wt8AAjI+ypCyYX3nZBvf6cAIx93T+c/OS2HFAYskSZc= +google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE= +google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M= +google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= +google.golang.org/api v0.9.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= +google.golang.org/api v0.13.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= +google.golang.org/api v0.14.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= +google.golang.org/api v0.15.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= +google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= +google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= +google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= +google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0= +google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= +google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= +google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190716160619-c506a9f90610/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= +google.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= +google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= +google.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8= +google.golang.org/genproto v0.0.0-20191108220845-16a3f7862a1a/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20191115194625-c23dd37a84c9/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20191216164720-4f79533eabd1/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20191230161307-f3c370f40bfb/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20200108215221-bd8f9a0ef82f/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013 h1:+kGHl1aib/qcwaRi1CbqBZ1rk19r85MNUf8HaBghugY= +google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= +google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= +google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= +google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= +google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= +google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY= +google.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= +google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= +google.golang.org/grpc v1.35.1 h1:I/Is1G9gVAEvrjXaHRzPN4E+PrUtCadWJb4jSyNrGLw= +google.golang.org/grpc v1.35.1/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU= +google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= +google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= +google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= +google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= +google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= +google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= +google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= +google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= +google.golang.org/protobuf v1.27.1 h1:SnqbnDw1V7RiZcXPx5MEeqPv2s79L9i7BJUlG/+RurQ= +google.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= +gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY= +gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= +gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= +gopkg.in/natefinch/lumberjack.v2 v2.0.0 h1:1Lc07Kr7qY4U2YPouBjpCLxpiyxIVoxqXgkXLknAOE8= +gopkg.in/natefinch/lumberjack.v2 v2.0.0/go.mod h1:l0ndWWf7gzL7RNwBG7wST/UCcT4T24xpD6X8LsfU/+k= +gopkg.in/natefinch/npipe.v2 v2.0.0-20160621034901-c1b8fa8bdcce h1:+JknDZhAj8YMt7GC73Ei8pv4MzjDUNPHgQWJdtMAaDU= +gopkg.in/natefinch/npipe.v2 v2.0.0-20160621034901-c1b8fa8bdcce/go.mod h1:5AcXVHNjg+BDxry382+8OKon8SEWiKktQR07RKPsv1c= +gopkg.in/olebedev/go-duktape.v3 v3.0.0-20200619000410-60c24ae608a6/go.mod h1:uAJfkITjFhyEEuUfm7bsmCZRbW5WRq8s9EY8HZ6hCns= +gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ= +gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= +gopkg.in/urfave/cli.v1 v1.20.0/go.mod h1:vuBzUtMdQeixQj8LVd+/98pzhxNGQoyuPBlsXHOQNO0= +gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.3/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= +gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776 h1:tQIYjPdBoyREyB9XMu+nnTclpTYkz2zFM+lzLJFO4gQ= +gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gotest.tools v2.2.0+incompatible/go.mod h1:DsYFclhRJ6vuDpmuTbkuFWG+y2sxOXAzmJt81HFBacw= +honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= +honnef.co/go/tools v0.1.3/go.mod h1:NgwopIslSNH47DimFoV78dnkksY2EFtX0ajyb3K/las= +rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8= +rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4= diff --git a/nodes/bx.go b/nodes/bx.go new file mode 100644 index 0000000..4bac81d --- /dev/null +++ b/nodes/bx.go @@ -0,0 +1,239 @@ +package nodes + +import ( + "context" + "encoding/hex" + "fmt" + "github.com/bloXroute-Labs/gateway" + "github.com/bloXroute-Labs/gateway/bxmessage" + "github.com/bloXroute-Labs/gateway/config" + "github.com/bloXroute-Labs/gateway/connections" + "github.com/bloXroute-Labs/gateway/connections/handler" + pbbase "github.com/bloXroute-Labs/gateway/protobuf" + "github.com/bloXroute-Labs/gateway/services" + "github.com/bloXroute-Labs/gateway/types" + "github.com/bloXroute-Labs/gateway/utils" + log "github.com/sirupsen/logrus" + "reflect" + "sync" + "time" +) + +const ( + pingInterval = 15 * time.Second +) + +// Bx is a base struct for bloxroute nodes +type Bx struct { + Abstract + BxConfig *config.Bx + ConnectionsLock *sync.RWMutex + Connections connections.ConnList + dataDir string +} + +// NewBx initializes a generic Bx node struct +func NewBx(bxConfig *config.Bx, dataDir string) Bx { + return Bx{ + BxConfig: bxConfig, + Connections: make(connections.ConnList, 0), + ConnectionsLock: &sync.RWMutex{}, + dataDir: dataDir, + } +} + +// OnConnEstablished - a callback function. Called when new connection is established +func (bn *Bx) OnConnEstablished(conn connections.Conn) error { + connInfo := conn.Info() + conn.Log().Infof("connection established, protocol version %v, network %v, on local port %v", + conn.Protocol(), connInfo.NetworkNum, conn.Info().LocalPort) + bn.ConnectionsLock.Lock() + defer bn.ConnectionsLock.Unlock() + bn.Connections = append(bn.Connections, conn) + return nil + +} + +// OnConnClosed - a callback function. Called when new connection is closed +func (bn *Bx) OnConnClosed(conn connections.Conn) error { + bn.ConnectionsLock.Lock() + defer bn.ConnectionsLock.Unlock() + for idx, connection := range bn.Connections { + if connection.ID() == conn.ID() { + if conn.Info().ConnectionType&utils.RelayTransaction != 0 { + bn.TxStore.Clear() + } + bn.Connections = append(bn.Connections[:idx], bn.Connections[idx+1:]...) + conn.Log().Debugf("connection closed and removed from connection pool") + return nil + } + } + err := fmt.Errorf("connection can't be removed from connection list - not found") + conn.Log().Debug(err) + return err +} + +// HandleMsg - a callback function. Generic handling for common bloXroute messages +func (bn *Bx) HandleMsg(msg bxmessage.Message, source connections.Conn) error { + switch msg.(type) { + case *bxmessage.SyncTxsMessage: + txs := msg.(*bxmessage.SyncTxsMessage) + for _, csi := range txs.ContentShortIds { + var shortID types.ShortID + var flags types.TxFlags + if len(csi.ShortIDs) > 0 { + shortID = csi.ShortIDs[0] + flags = csi.ShortIDFlags[0] + } + result := bn.TxStore.Add(csi.Hash, csi.Content, shortID, txs.GetNetworkNum(), false, flags, csi.Timestamp(), 0) + if result.NewTx || result.NewSID || result.NewContent { + source.Log().Tracef("TxStore sync: added hash %v newTx %v newContent %v newSid %v networkNum %v", + hex.EncodeToString(csi.Hash[:]), result.NewTx, result.NewContent, result.NewSID, result.Transaction.NetworkNum()) + } + } + + case *bxmessage.SyncReq: + syncReq := msg.(*bxmessage.SyncReq) + txCount := 0 + sentCount := 0 + syncTxs := &bxmessage.SyncTxsMessage{} + syncTxs.SetNetworkNum(syncReq.GetNetworkNum()) + priority := bxmessage.OnPongPriority + if source.Info().ConnectionType&utils.RelayProxy != 0 || source.Protocol() >= bxmessage.MinFastSyncProtocol { + priority = bxmessage.NormalPriority + } + + for txInfo := range bn.TxStore.Iter() { + if txInfo.NetworkNum() != syncTxs.GetNetworkNum() { + continue + } + if syncTxs.Add(txInfo) > bxgateway.SyncChunkSize { + syncTxs.SetPriority(priority) + // not checking error here as we have to finish the for loop to clear the Iterator goroutine + _ = source.Send(syncTxs) + sentCount += syncTxs.Count() + syncTxs = &bxmessage.SyncTxsMessage{} + syncTxs.SetNetworkNum(syncReq.GetNetworkNum()) + } + txCount++ + } + syncTxs.SetPriority(priority) + err := source.Send(syncTxs) + if err != nil { + return err + } + sentCount += syncTxs.Count() + syncDone := &bxmessage.SyncDone{} + syncDone.SetNetworkNum(syncReq.GetNetworkNum()) + syncDone.SetPriority(priority) + err = source.Send(syncDone) + if err != nil { + return err + } + + source.Log().Debugf("TxStore sync: done sending %v out of %v entries for network %v", sentCount, txCount, syncReq.GetNetworkNum()) + + case *bxmessage.SyncDone: + source.Log().Infof("completed transaction sync (%v entries)", bn.TxStore.Count()) + + case *bxmessage.TxCleanup: + cleanup := msg.(*bxmessage.TxCleanup) + go func() { + sizeBefore := bn.TxStore.Count() + startTime := time.Now() + bn.TxStore.RemoveShortIDs(&cleanup.ShortIDs, services.ReEntryProtection, "TxCleanup message") + source.Log().Debugf("TxStore cleanup (go routine) by txcleanup message took %v. Size before %v, size after %v, shortIds %v", + time.Now().Sub(startTime), sizeBefore, bn.TxStore.Count(), len(cleanup.ShortIDs)) + }() + + case *bxmessage.BlockConfirmation: + blockConfirmation := msg.(*bxmessage.BlockConfirmation) + go func() { + sizeBefore := bn.TxStore.Count() + startTime := time.Now() + bn.TxStore.RemoveHashes(&blockConfirmation.Hashes, services.ReEntryProtection, "BlockConfirmation message") + source.Log().Debugf("TxStore cleanup (go routine) by %v message took %v. Size before %v, size after %v, hashes %v", + bxmessage.BlockConfirmationType, time.Now().Sub(startTime), sizeBefore, bn.TxStore.Count(), len(blockConfirmation.Hashes)) + }() + + default: + source.Log().Errorf("unknown message type %v received", reflect.TypeOf(msg)) + } + return nil +} + +// DisconnectConn - disconnect a specific connection +func (bn *Bx) DisconnectConn(id types.NodeID) { + bn.ConnectionsLock.Lock() + defer bn.ConnectionsLock.Unlock() + for _, conn := range bn.Connections { + if id == conn.Info().NodeID { + // closing in a new go routine in order to avoid deadlock while Close method acquiring ConnectionsLock + go conn.Close("disconnect requested by bxapi") + } + } +} + +// Peers provides a list of current peers for the requested type +func (bn *Bx) Peers(_ context.Context, req *pbbase.PeersRequest) (*pbbase.PeersReply, error) { + var nodeType utils.NodeType = -1 // all types + switch req.Type { + case "gw", "gateway": + nodeType = utils.Gateway + case "relay": + nodeType = utils.Relay + } + resp := &pbbase.PeersReply{} + bn.ConnectionsLock.RLock() + defer bn.ConnectionsLock.RUnlock() + + for _, conn := range bn.Connections { + connInfo := conn.Info() + if connInfo.ConnectionType&nodeType == 0 { + continue + } + connType := connInfo.ConnectionType.String() + peer := &pbbase.Peer{ + Ip: connInfo.PeerIP, + NodeId: string(connInfo.NodeID), + Type: connType, + State: connInfo.ConnectionState, + Network: uint32(connInfo.NetworkNum), + Initiator: connInfo.FromMe, + AccountId: string(connInfo.AccountID), + Port: conn.Info().LocalPort, + } + if bxConn, ok := conn.(*handler.BxConn); ok { + peer.MinMsFromPeer, peer.MinMsToPeer, peer.SlowTrafficCount, peer.MinMsRoundTrip = bxConn.GetMinLatencies() + } + resp.Peers = append(resp.Peers, peer) + + } + return resp, nil +} + +// PingLoop send a ping request every pingInterval to gateway, relays, and proxies. Can't use broadcast due to geo constrains +func (bn *Bx) PingLoop() { + pingTicker := time.NewTicker(pingInterval) + ping := &bxmessage.Ping{} + to := utils.Gateway | utils.RelayProxy | utils.Relay + for { + select { + case <-pingTicker.C: + bn.ConnectionsLock.RLock() + count := 0 + for _, conn := range bn.Connections { + if conn.Info().ConnectionType&to != 0 { + err := conn.Send(ping) + if err != nil { + conn.Log().Errorf("error sending ping message: %v", err) + continue + } + count++ + } + } + bn.ConnectionsLock.RUnlock() + log.Tracef("ping message sent to %v connections", count) + } + } +} diff --git a/nodes/fluentd.go b/nodes/fluentd.go new file mode 100644 index 0000000..b03a238 --- /dev/null +++ b/nodes/fluentd.go @@ -0,0 +1,46 @@ +package nodes + +import ( + "github.com/bloXroute-Labs/gateway/types" + "github.com/evalphobia/logrus_fluent" + log "github.com/sirupsen/logrus" + "strings" +) + +// InitFluentD - initialise logging +func InitFluentD(fluentDEnabled bool, fluentDHost string, nodeID types.NodeID) error { + + Formatter := new(log.TextFormatter) + Formatter.TimestampFormat = "2006-01-02T15:04:05.000000" + Formatter.FullTimestamp = true + + if fluentDEnabled { + hook, err := logrus_fluent.NewWithConfig(logrus_fluent.Config{ + Host: fluentDHost, + Port: 24224, + MarshalAsJSON: true, + }) + if err != nil { + log.Warnf("Failed to create fluentd config with error %v", err) + return nil + } + hook.SetLevels([]log.Level{ + log.PanicLevel, + log.FatalLevel, + log.ErrorLevel, + log.WarnLevel, + log.InfoLevel, + }) + hook.SetTag("bx.go.log") + hook.SetMessageField("msg") + hook.AddCustomizer(func(entry *log.Entry, data log.Fields) { + data["level"] = strings.ToUpper(entry.Level.String()) + data["timestamp"] = entry.Time.Format(Formatter.TimestampFormat) + data["instance"] = nodeID + }) + + log.AddHook(hook) + log.Infof("connection established with fluentd hook at %v:%v", hook.Fluent.FluentHost, hook.Fluent.FluentPort) + } + return nil +} diff --git a/nodes/gateway.go b/nodes/gateway.go new file mode 100644 index 0000000..5d60236 --- /dev/null +++ b/nodes/gateway.go @@ -0,0 +1,793 @@ +package nodes + +import ( + "bytes" + "context" + "encoding/hex" + "encoding/json" + "fmt" + "github.com/bloXroute-Labs/gateway" + "github.com/bloXroute-Labs/gateway/blockchain" + "github.com/bloXroute-Labs/gateway/blockchain/network" + "github.com/bloXroute-Labs/gateway/bxmessage" + "github.com/bloXroute-Labs/gateway/config" + "github.com/bloXroute-Labs/gateway/connections" + "github.com/bloXroute-Labs/gateway/connections/handler" + pb "github.com/bloXroute-Labs/gateway/protobuf" + "github.com/bloXroute-Labs/gateway/sdnmessage" + "github.com/bloXroute-Labs/gateway/servers" + "github.com/bloXroute-Labs/gateway/services" + baseservices "github.com/bloXroute-Labs/gateway/services" + "github.com/bloXroute-Labs/gateway/services/loggers" + "github.com/bloXroute-Labs/gateway/services/statistics" + "github.com/bloXroute-Labs/gateway/types" + "github.com/bloXroute-Labs/gateway/utils" + "github.com/bloXroute-Labs/gateway/version" + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/crypto" + log "github.com/sirupsen/logrus" + "go.uber.org/atomic" + "golang.org/x/sync/errgroup" + "math/big" + "net/http" + "os" + "path" + "reflect" + "runtime" + "strings" + "time" +) + +const ( + flasbotAuthHeader = "X-Flashbots-Signature" + timeToAvoidReEntry = 30 * time.Minute +) + +type gateway struct { + Bx + pb.UnimplementedGatewayServer + context context.Context + cancel context.CancelFunc + + sdn *connections.SDNHTTP + accountID types.AccountID + bridge blockchain.Bridge + feedChan chan types.Notification + asyncMsgChannel chan services.MsgInfo + bdnStats *bxmessage.BdnPerformanceStats + blockProcessor services.BlockProcessor + pendingTxs services.HashHistory + possiblePendingTxs services.HashHistory + txTrace loggers.TxTrace + blockchainPeers []types.NodeEndpoint + stats statistics.Stats + bdnBlocks services.HashHistory + wsProvider blockchain.WSProvider + syncedWithRelay atomic.Bool + + bestBlockHeight int + bdnBlocksSkipCount int + seenMEVMinerBundles baseservices.HashHistory + seenMEVSearchers baseservices.HashHistory +} + +// NewGateway returns a new gateway node to send messages from a blockchain node to the relay network +func NewGateway(parent context.Context, bxConfig *config.Bx, bridge blockchain.Bridge, ws blockchain.WSProvider, blockchainPeers []types.NodeEndpoint) (Node, error) { + ctx, cancel := context.WithCancel(parent) + + g := &gateway{ + Bx: NewBx(bxConfig, "datadir"), + bridge: bridge, + wsProvider: ws, + context: ctx, + cancel: cancel, + blockchainPeers: blockchainPeers, + pendingTxs: services.NewHashHistory("pendingTxs", 15*time.Minute), + possiblePendingTxs: services.NewHashHistory("possiblePendingTxs", 15*time.Minute), + bdnBlocks: services.NewHashHistory("bdnBlocks", 15*time.Minute), + seenMEVMinerBundles: baseservices.NewHashHistory("mevMinerBundle", 30*time.Minute), + seenMEVSearchers: baseservices.NewHashHistory("mevSearcher", 30*time.Minute), + } + g.asyncMsgChannel = services.NewAsyncMsgChannel(g) + + // create tx store service pass to eth client + assigner := services.NewEmptyShortIDAssigner() + txStore := services.NewBxTxStore(30*time.Minute, 3*24*time.Hour, 10*time.Minute, assigner, services.NewHashHistory("seenTxs", 30*time.Minute), nil, timeToAvoidReEntry) + g.TxStore = &txStore + g.bdnStats = bxmessage.NewBDNStats() + g.blockProcessor = services.NewRLPBlockProcessor(g.TxStore) + + // set empty default stats, Run function will override it + g.stats = statistics.NewStats(false, "127.0.0.1", "", nil, false) + + return g, nil +} + +func (g *gateway) isSyncWithRelay() bool { + return g.syncedWithRelay.Load() +} + +func (g *gateway) setSyncWithRelay() { + g.syncedWithRelay.Store(true) +} + +func (g *gateway) Run() error { + defer g.cancel() + + var err error + + privateCertDir := path.Join(g.BxConfig.DataDir, "ssl") + gatewayType := g.BxConfig.NodeType + + privateCertFile, privateKeyFile, registrationOnlyCertFile, registrationOnlyKeyFile := utils.GetCertDir(g.BxConfig.RegistrationCertDir, privateCertDir, strings.ToLower(gatewayType.String())) + sslCerts := utils.NewSSLCertsFromFiles(privateCertFile, privateKeyFile, registrationOnlyCertFile, registrationOnlyKeyFile) + if g.accountID, err = sslCerts.GetAccountID(); err != nil { + return err + } + log.Infof("ssl certificate is successfully loaded") + + _, err = os.Stat(".dockerignore") + isDocker := !os.IsNotExist(err) + hostname, _ := os.Hostname() + + blockchainPeerEndpoint := types.NodeEndpoint{IP: "", Port: 0, PublicKey: ""} + if len(g.blockchainPeers) > 0 { + blockchainPeerEndpoint.IP = g.blockchainPeers[0].IP + blockchainPeerEndpoint.Port = g.blockchainPeers[0].Port + blockchainPeerEndpoint.PublicKey = g.blockchainPeers[0].PublicKey + } + + nodeModel := sdnmessage.NodeModel{ + NodeType: gatewayType.String(), + ExternalIP: g.BxConfig.ExternalIP, + ExternalPort: g.BxConfig.ExternalPort, + BlockchainIP: blockchainPeerEndpoint.IP, + NodePublicKey: blockchainPeerEndpoint.PublicKey, + BlockchainPort: blockchainPeerEndpoint.Port, + ProgramName: types.BloxrouteGoGateway, + SourceVersion: version.BuildVersion, + IsDocker: isDocker, + Hostname: hostname, + OsVersion: runtime.GOOS, + ProtocolVersion: bxmessage.CurrentProtocol, + IsGatewayMiner: g.BxConfig.BlocksOnly, + NodeStartTime: time.Now().Format(bxgateway.TimeLayoutISO), + } + + g.sdn = connections.NewSDNHTTP(&sslCerts, g.BxConfig.SDNURL, nodeModel, g.BxConfig.DataDir) + var group errgroup.Group + + err = g.sdn.InitGateway(bxgateway.Ethereum, g.BxConfig.BlockchainNetwork) + if err != nil { + return err + } + + if g.BxConfig.MevBuilderURI != "" && g.sdn.AccountModel().MevBuilder == "" { + panic(fmt.Errorf("account %v is not allowed for mev builder service, closing the gateway. Please contact support@bloxroute.com to enable running as mev builder", g.sdn.AccountModel().AccountID)) + } + + if g.BxConfig.MevMinerURI != "" && g.sdn.AccountModel().MevMiner == "" { + panic(fmt.Errorf( + "account %v is not allowed for mev miner service, closing the gateway. Please contact support@bloxroute.com to enable running as mev miner", + g.sdn.AccountModel().AccountID, + )) + } + + var txTraceLogger *log.Logger = nil + if g.BxConfig.TxTrace.Enabled { + txTraceLogger, err = CreateCustomLogger( + g.BxConfig.AppName, + int(g.BxConfig.ExternalPort), + "txtrace", + g.BxConfig.TxTrace.MaxFileSize, + g.BxConfig.TxTrace.MaxBackupFiles, + 1, + log.TraceLevel, + ) + if err != nil { + return fmt.Errorf("failed to create TxTrace logger: %v", err) + } + } + g.txTrace = loggers.NewTxTrace(txTraceLogger) + + if g.sdn.AccountTier() != sdnmessage.ATierElite && len(g.blockchainPeers) == 0 { + panic("No blockchain node specified. Enterprise-Elite account is required in order to run gateway without a blockchain node.") + } + + networkNum := g.sdn.NetworkNum() + relayHost, relayPort, err := g.sdn.BestRelay(g.BxConfig) + if err != nil { + return err + } + + err = g.pushBlockchainConfig() + if err != nil { + return fmt.Errorf("could process initial blockchain configuration: %v", err) + } + group.Go(g.handleBridgeMessages) + group.Go(g.TxStore.Start) + + g.feedChan = make(chan types.Notification, bxgateway.BxNotificationChannelSize) + accountModel := g.sdn.AccountModel() + feedProvider := servers.NewFeedManager(g.context, g, g.feedChan, + fmt.Sprintf(":%v", g.BxConfig.WebsocketPort), networkNum, + g.wsProvider, g.BxConfig.ManageWSServer, accountModel, g.sdn.GetCustomerAccountModel, + g.BxConfig.WebsocketTLSEnabled, privateCertFile, privateKeyFile) + if g.BxConfig.WebsocketEnabled || g.BxConfig.WebsocketTLSEnabled { + group.Go(feedProvider.Start) + } + + usePQ := g.BxConfig.PrioritySending + log.Infof("gateway %v (%v) starting, connecting to relay %v:%v", g.sdn.NodeID, g.BxConfig.Environment, relayHost, relayPort) + relay := handler.NewOutboundRelay(g, + &sslCerts, relayHost, relayPort, g.sdn.NodeID, utils.Relay, usePQ, &g.sdn.Networks, true, false, utils.RealClock{}, false) + relay.SetNetworkNum(networkNum) + + if err := InitFluentD(g.BxConfig.FluentDEnabled, g.BxConfig.FluentDHost, g.sdn.NodeID); err != nil { + return err + } + + g.stats = statistics.NewStats(g.BxConfig.FluentDEnabled, g.BxConfig.FluentDHost, g.sdn.NodeID, &g.sdn.Networks, g.BxConfig.LogNetworkContent) + + go g.PingLoop() + + group.Go(relay.Start) + + go g.sendStatsOnInterval(15*time.Minute, relay.BxConn) + + if g.BxConfig.Enabled { + grpcServer := newGatewayGRPCServer(g, g.BxConfig.Host, g.BxConfig.Port, g.BxConfig.User, g.BxConfig.Password) + group.Go(grpcServer.Start) + } + + err = group.Wait() + if err != nil { + return err + } + + return nil +} + +func (g *gateway) broadcast(msg bxmessage.Message, source connections.Conn, to utils.NodeType) types.BroadcastResults { + g.ConnectionsLock.RLock() + defer g.ConnectionsLock.RUnlock() + results := types.BroadcastResults{} + + for _, conn := range g.Connections { + // if connection type is not in target - skip + if conn.Info().ConnectionType&to == 0 { + continue + } + + results.RelevantPeers++ + if !conn.IsOpen() || source != nil && conn.ID() == source.ID() { + results.NotOpenPeers++ + continue + } + + err := conn.Send(msg) + if err != nil { + conn.Log().Errorf("error writing to connection, closing") + results.ErrorPeers++ + continue + } + if conn.Info().IsGateway() { + results.SentGatewayPeers++ + } + } + return results +} + +func (g *gateway) pushBlockchainConfig() error { + blockchainNetwork, err := g.sdn.FindNetwork(g.sdn.NetworkNum()) + if err != nil { + return err + } + + blockchainAttributes := blockchainNetwork.DefaultAttributes + chainDifficulty, ok := blockchainAttributes.ChainDifficulty.(string) + if !ok { + return fmt.Errorf("could not parse total difficulty: %v", blockchainAttributes.ChainDifficulty) + } + td, ok := new(big.Int).SetString(chainDifficulty, 16) + if !ok { + return fmt.Errorf("could not parse total difficulty: %v", blockchainAttributes.ChainDifficulty) + } + + genesis := common.HexToHash(blockchainAttributes.GenesisHash) + ignoreBlockTimeout := time.Second * time.Duration(blockchainNetwork.BlockInterval*blockchainNetwork.IgnoreBlockIntervalCount) + ethConfig := network.EthConfig{ + Network: uint64(blockchainAttributes.NetworkID), + TotalDifficulty: td, + Head: genesis, + Genesis: genesis, + IgnoreBlockTimeout: ignoreBlockTimeout, + } + + return g.bridge.UpdateNetworkConfig(ethConfig) +} + +func (g *gateway) publishBlock(bxBlock *types.BxBlock, feedName types.FeedType) error { + blockHeight := int(bxBlock.Number.Int64()) + if feedName == types.BDNBlocksFeed { + if !g.bdnBlocks.SetIfAbsent(bxBlock.Hash().String(), 15*time.Minute) { + log.Debugf("Block %v with height %v was already published with feed %v", bxBlock.Hash(), bxBlock.Number, types.BDNBlocksFeed) + return nil + } + if len(g.blockchainPeers) > 0 && blockHeight < g.bestBlockHeight { + log.Debugf("block %v (%v) is too far behind best block height %v from node - not publishing to bdnBlocks", bxBlock.Number, bxBlock.Hash(), g.bestBlockHeight) + return nil + } + if g.bestBlockHeight != 0 && utils.Abs(blockHeight-g.bestBlockHeight) > bxgateway.BDNBlocksMaxBlocksAway { + if blockHeight > g.bestBlockHeight { + g.bdnBlocksSkipCount++ + } + if g.bdnBlocksSkipCount <= bxgateway.MaxOldBDNBlocksToSkipPublish { + log.Debugf("block %v (%v) is too far away from best block height %v - not publishing to bdnBlocks", bxBlock.Number, bxBlock.Hash(), g.bestBlockHeight) + return nil + } + log.Debugf("publishing block from BDN with height %v that is far away from current best block height %v - resetting bestBlockHeight to zero", blockHeight, g.bestBlockHeight) + g.bestBlockHeight = 0 + } + g.bdnBlocksSkipCount = 0 + if len(g.blockchainPeers) == 0 && blockHeight > g.bestBlockHeight { + g.bestBlockHeight = blockHeight + } + } + + blockNotification, err := g.bridge.BxBlockToCanonicFormat(bxBlock) + if err != nil { + return err + } + blockNotification.SetNotificationType(feedName) + log.Debugf("Received block for %v, block hash: %v, block height: %v, notify the feed", feedName, bxBlock.Hash(), bxBlock.Number) + g.notify(blockNotification) + if feedName == types.NewBlocksFeed { + g.bestBlockHeight = blockHeight + g.bdnBlocksSkipCount = 0 + + onBlockNotification := *blockNotification + onBlockNotification.SetNotificationType(types.OnBlockFeed) + log.Debugf("Received block for %v, block hash: %v, block height: %v, notify the feed", types.OnBlockFeed, bxBlock.Hash(), bxBlock.Number) + g.notify(&onBlockNotification) + + txReceiptNotification := *blockNotification + txReceiptNotification.SetNotificationType(types.TxReceiptsFeed) + log.Debugf("Received block for %v, block hash: %v, block height: %v, notify the feed", types.TxReceiptsFeed, bxBlock.Hash(), bxBlock.Number) + g.notify(&txReceiptNotification) + } + return nil +} + +func (g *gateway) publishPendingTx(txHash types.SHA256Hash, bxTx *types.BxTransaction, fromNode bool) { + if g.pendingTxs.Exists(txHash.String()) { + return + } + + if fromNode || g.possiblePendingTxs.Exists(txHash.String()) { + if bxTx != nil { + // if already has tx content, tx is pending and notify it + pendingTxsNotification := types.CreatePendingTransactionNotification(bxTx) + g.notify(pendingTxsNotification) + g.pendingTxs.Add(txHash.String(), 15*time.Minute) + } else if fromNode { + // not asking for tx content as we expect it to happen anyway + g.possiblePendingTxs.Add(txHash.String(), 15*time.Minute) + } + } +} + +func (g *gateway) handleBridgeMessages() error { + var err error + for { + select { + case txsFromNode := <-g.bridge.ReceiveNodeTransactions(): + // if we are not yet synced with relay - ignore the transactions from the node + if !g.isSyncWithRelay() { + continue + } + blockchainConnection := connections.NewBlockchainConn(txsFromNode.PeerEndpoint) + for _, blockchainTx := range txsFromNode.Transactions { + tx := bxmessage.NewTx(blockchainTx.Hash(), blockchainTx.Content(), g.sdn.NetworkNum(), types.TFLocalRegion, types.EmptyAccountID) + g.processTransaction(tx, blockchainConnection) + } + case txAnnouncement := <-g.bridge.ReceiveTransactionHashesAnnouncement(): + // if we are not yet synced with relay - ignore the announcement from the node + if !g.isSyncWithRelay() { + continue + } + // if announcement message has many transaction we are probably after reconnect with the node - we should ignore it in order not to over load the client feed + if len(txAnnouncement.Hashes) > bxgateway.MaxAnnouncementFromNode { + log.Debugf("skipped tx announcement of size %v", len(txAnnouncement.Hashes)) + continue + } + requests := make([]types.SHA256Hash, 0) + for _, hash := range txAnnouncement.Hashes { + bxTx, exists := g.TxStore.Get(hash) + if !exists { + log.Tracef("msgTx: from Blockchain, hash %v, event TxAnnouncedByBlockchainNode, peerID: %v", hash, txAnnouncement.PeerID) + requests = append(requests, hash) + } else { + log.Tracef("msgTx: from Blockchain, hash %v, event TxAnnouncedByBlockchainNodeIgnoreSeen, peerID: %v", hash, txAnnouncement.PeerID) + } + g.publishPendingTx(hash, bxTx, true) + } + + if len(requests) > 0 && txAnnouncement.PeerID != bxgateway.WSConnectionID { + err = g.bridge.RequestTransactionsFromNode(txAnnouncement.PeerID, requests) + if err == blockchain.ErrChannelFull { + log.Warnf("transaction requests channel is full, skipping request for %v hashes", len(requests)) + } else if err != nil { + log.Errorf("could not request transactions over bridge: %v", err) + return err + } + } + case blockFromNode := <-g.bridge.ReceiveBlockFromNode(): + blockchainConnection := connections.NewBlockchainConn(g.blockchainPeers[0]) + g.processBlockFromBlockchain(blockFromNode.Block, blockchainConnection) + case _ = <-g.bridge.ReceiveNoActiveBlockchainPeersAlert(): + if g.sdn.AccountTier() != sdnmessage.ATierElite { + panic("Gateway does not have an active blockchain connection. Enterprise-Elite account is required in order to run gateway without a blockchain node.") + } + } + } +} + +func (g *gateway) NodeStatus() connections.NodeStatus { + var capabilities types.CapabilityFlags + if g.BxConfig.MevBuilderURI != "" { + capabilities |= types.CapabilityMevBuilder + } + + if g.BxConfig.MevMinerURI != "" { + capabilities |= types.CapabilityMevMiner + } + + return connections.NodeStatus{ + Capabilities: capabilities, + Version: version.BuildVersion, + } +} + +func (g *gateway) HandleMsg(msg bxmessage.Message, source connections.Conn, background connections.MsgHandlingOptions) error { + startTime := time.Now() + var err error + if background { + g.asyncMsgChannel <- services.MsgInfo{Msg: msg, Source: source} + return nil + } + switch msg.(type) { + case *bxmessage.Tx: + tx := msg.(*bxmessage.Tx) + g.processTransaction(tx, source) + case *bxmessage.Broadcast: + broadcastMsg := msg.(*bxmessage.Broadcast) + blockHash := broadcastMsg.Hash() + + bxBlock, missingShortIDsCount, err := g.blockProcessor.ProcessBroadcast(broadcastMsg) + switch { + case err == services.ErrAlreadyProcessed: + source.Log().Debugf("received duplicate block %v, skipping", blockHash) + g.stats.AddGatewayBlockEvent("GatewayProcessBlockFromBDNIgnoreSeen", source, blockHash, broadcastMsg.GetNetworkNum(), 1, startTime, 0, len(broadcastMsg.Block()), 0, len(broadcastMsg.ShortIDs()), 0, 0, bxBlock) + return nil + case err == services.ErrMissingShortIDs: + if !g.isSyncWithRelay() { + source.Log().Debugf("TxStore sync is in progress - Ignoring block %v from bdn with unknown %v shortIDs", blockHash, missingShortIDsCount) + return nil + } + // TODO - list the missing shortIDs in trace. + source.Log().Debugf("could not decompress block %v, missing shortIDs count: %v", blockHash, missingShortIDsCount) + g.stats.AddGatewayBlockEvent("GatewayProcessBlockFromBDNRequiredRecovery", source, blockHash, broadcastMsg.GetNetworkNum(), 1, startTime, 0, len(broadcastMsg.Block()), 0, len(broadcastMsg.ShortIDs()), 0, missingShortIDsCount, bxBlock) + return nil + case err != nil: + source.Log().Errorf("could not decompress block %v, err: %v", blockHash, err) + broadcastBlockHex := hex.EncodeToString(broadcastMsg.Block()) + source.Log().Debugf("could not decompress block %v, err: %v, contents: %v", blockHash, err, broadcastBlockHex) + return nil + } + + source.Log().Infof("processing block %v from BDN, block number: %v, txs count: %v", blockHash, bxBlock.Number, len(bxBlock.Txs)) + g.processBlockFromBDN(bxBlock) + // TODO decompress should not be 0 - calculate it in the BxBlock struct or add the original size in the broadcast msg + g.stats.AddGatewayBlockEvent("GatewayProcessBlockFromBDN", source, blockHash, broadcastMsg.GetNetworkNum(), 1, startTime, 0, len(broadcastMsg.Block()), 0, len(broadcastMsg.ShortIDs()), len(bxBlock.Txs), 0, bxBlock) + case *bxmessage.RefreshBlockchainNetwork: + go g.sdn.GetBlockchainNetwork() + case *bxmessage.Txs: + // TODO: check if this is the message type we want to use? + txsMessage := msg.(*bxmessage.Txs) + for _, txsItem := range txsMessage.Items() { + g.TxStore.Add(txsItem.Hash, txsItem.Content, txsItem.ShortID, g.sdn.NetworkNum(), false, 0, time.Now(), 0) + } + case *bxmessage.SyncDone: + g.setSyncWithRelay() + err = g.Bx.HandleMsg(msg, source) + case *bxmessage.MEVBundle: + mevBundle := msg.(*bxmessage.MEVBundle) + go g.handleMEVBundleMessage(*mevBundle, source) + case *bxmessage.MEVSearcher: + mevSearcher := msg.(*bxmessage.MEVSearcher) + go g.handleMEVSearcherMessage(*mevSearcher, source) + case *bxmessage.ErrorNotification: + errorNotification := msg.(*bxmessage.ErrorNotification) + + if errorNotification.ErrorType == types.ErrorTypeTemporary { + panic(fmt.Errorf("gateway received temporary error notification from relay %v with error %v", source, errorNotification.Reason)) + } else if errorNotification.ErrorType == types.ErrorTypePermanent { + log.Errorf("received permanent error notification from relay %v, with error %v. closing the connection", source, errorNotification.Reason) + // TODO should also close the gateway while notify the bridge and other go routine (web socket server, ...) + os.Exit(0) + } + + default: + err = g.Bx.HandleMsg(msg, source) + } + return err +} + +func (g *gateway) processTransaction(tx *bxmessage.Tx, source connections.Conn) { + startTime := time.Now() + sentToBlockchainNode := false + sentToBDN := false + var broadcastRes types.BroadcastResults + txResult := g.TxStore.Add(tx.Hash(), tx.Content(), tx.ShortID(), tx.GetNetworkNum(), false, tx.Flags(), tx.Timestamp(), 0) + eventName := "TxProcessedByGatewayFromPeerIgnoreSeen" + if txResult.NewContent || txResult.NewSID || txResult.Reprocess { + eventName = "TxProcessedByGatewayFromPeer" + } + + if txResult.NewContent || txResult.Reprocess { + if txResult.NewContent { + newTxsNotification := types.CreateNewTransactionNotification(txResult.Transaction) + g.notify(newTxsNotification) + g.publishPendingTx(txResult.Transaction.Hash(), txResult.Transaction, source.Info().ConnectionType == utils.Blockchain) + } + + if !source.Info().IsRelay() { + broadcastRes = g.broadcast(tx, source, utils.RelayTransaction) + sentToBDN = true + if !txResult.Reprocess { + g.bdnStats.LogNewTxFromNode(types.NodeEndpoint{IP: source.Info().PeerIP, Port: int(source.Info().PeerPort)}) + } + } + + if source.Info().ConnectionType != utils.Blockchain { + if (!g.BxConfig.BlocksOnly && tx.Flags()&types.TFDeliverToNode != 0) || g.BxConfig.AllTransactions { + _ = g.bridge.SendTransactionsFromBDN([]*types.BxTransaction{txResult.Transaction}) + sentToBlockchainNode = true + g.bdnStats.LogTxSentToNode() + } + if !txResult.Reprocess { + g.bdnStats.LogNewTxFromBDN() + } + } + + g.txTrace.Log(tx.Hash(), source) + } else if source.Info().ConnectionType == utils.Blockchain { + g.bdnStats.LogDuplicateTxFromNode(types.NodeEndpoint{IP: source.Info().PeerIP, Port: int(source.Info().PeerPort), PublicKey: source.Info().PeerEnode}) + } + + log.Tracef("msgTx: from %v, hash %v, flags %v, new Tx %v, new content %v, new shortid %v, event %v, sentToBDN: %v, sentToBlockchainNode: %v, handling duration %v", source, tx.Hash(), tx.Flags(), txResult.NewTx, txResult.NewContent, txResult.NewSID, eventName, sentToBDN, sentToBlockchainNode, time.Now().Sub(startTime)) + g.stats.AddTxsByShortIDsEvent(eventName, source, txResult.Transaction, tx.ShortID(), source.Info().NodeID, broadcastRes.RelevantPeers, broadcastRes.SentGatewayPeers, startTime, tx.GetPriority(), txResult.DebugData) +} + +func (g *gateway) processBlockFromBlockchain(bxBlock *types.BxBlock, source connections.Blockchain) { + startTime := time.Now() + + blockchainEndpoint := types.NodeEndpoint{IP: source.Info().PeerIP, Port: int(source.Info().PeerPort), PublicKey: source.Info().PeerEnode} + g.bdnStats.LogNewBlockMessageFromNode(blockchainEndpoint) + + blockHash := bxBlock.Hash() + // even though it is not from BDN, still sending this block in the feed in case the node sent the block first + err := g.publishBlock(bxBlock, types.BDNBlocksFeed) + if err != nil { + source.Log().Errorf("Failed to publish block %v from blockchain node with %v", bxBlock.Hash(), err) + } + + err = g.publishBlock(bxBlock, types.NewBlocksFeed) + if err != nil { + source.Log().Errorf("Failed to publish block %v from blockchain node with %v", bxBlock.Hash(), err) + } + + broadcastMessage, usedShortIDs, err := g.blockProcessor.BxBlockToBroadcast(bxBlock, g.sdn.NetworkNum(), g.sdn.GetMinTxAge()) + if err == services.ErrAlreadyProcessed { + source.Log().Debugf("received duplicate block %v, skipping", blockHash) + // TODO same as line 378 - should calculate BxBlock size + g.stats.AddGatewayBlockEvent("GatewayReceivedBlockFromBlockchainNodeIgnoreSeen", source, blockHash, g.sdn.NetworkNum(), 1, startTime, 0, 0, 0, 0, len(bxBlock.Txs), len(usedShortIDs), bxBlock) + return + } else if err != nil { + source.Log().Errorf("could not compress block: %v", err) + return + } + + // if not synced avoid sending to bdn (low compression rate block) + if !g.isSyncWithRelay() { + source.Log().Debugf("TxSync not completed. Not sending block %v to the bdn", bxBlock.Hash()) + return + } + + source.Log().Debugf("compressed block from blockchain node: hash %v, compressed %v short IDs", bxBlock.Hash(), len(usedShortIDs)) + source.Log().Infof("propagating block %v from blockchain node to BDN, block number: %v, txs count: %v", bxBlock.Hash(), bxBlock.Number, len(bxBlock.Txs)) + + _ = g.broadcast(broadcastMessage, source, utils.RelayBlock) + + g.bdnStats.LogNewBlockFromNode(g.blockchainPeers[0]) + // TODO same as line 378 - should calculate BxBlock size + g.stats.AddGatewayBlockEvent("GatewayReceivedBlockFromBlockchainNode", source, blockHash, g.sdn.NetworkNum(), 1, startTime, 0, 0, int(broadcastMessage.Size()), len(broadcastMessage.ShortIDs()), len(bxBlock.Txs), len(usedShortIDs), bxBlock) +} + +func (g *gateway) processBlockFromBDN(bxBlock *types.BxBlock) { + err := g.bridge.SendBlockToNode(bxBlock) + if err != nil { + log.Errorf("unable to send block from BDN to node: %v", err) + } + g.bdnStats.LogNewBlockFromBDN() + err = g.publishBlock(bxBlock, types.BDNBlocksFeed) + if err != nil { + log.Errorf("Failed to publish BDN block with %v, block hash: %v, block height: %v", err, bxBlock.Hash(), bxBlock.Number) + } +} + +func (g *gateway) notify(notification types.Notification) { + if g.BxConfig.WebsocketEnabled { + select { + case g.feedChan <- notification: + default: + log.Warnf("gateway feed channel is full. Can't add %v without blocking. Ignoring hash %v", reflect.TypeOf(notification), notification.GetHash()) + } + } +} + +func (g gateway) handleMEVBundleMessage(mevBundle bxmessage.MEVBundle, source connections.Conn) { + if source.Info().IsRelay() { + if g.BxConfig.MevMinerURI == "" { + log.Warnf("received mevBundle message, but mev miner uri is empty. Message %v from %v in network %v", mevBundle.Hash(), mevBundle.SourceID(), mevBundle.GetNetworkNum()) + return + } + if g.seenMEVMinerBundles.SetIfAbsent(mevBundle.Hash().String(), time.Minute*30) { + mevBundle.ID, mevBundle.JSONRPC = "1", "2.0" + mevRPC, err := json.Marshal(mevBundle) + if err != nil { + log.Errorf("failed to marshal mevBundle payload for: %v, hash: %v, params: %v, error: %v", g.BxConfig.MevMinerURI, mevBundle.Hash(), string(mevBundle.Params), err) + return + } + + httpClient := http.Client{} + defer httpClient.CloseIdleConnections() + + req, err := http.NewRequest(http.MethodPost, g.BxConfig.MevMinerURI, bytes.NewReader(mevRPC)) + if err != nil { + log.Errorf("failed to create new mevBundle http request for: %v, hash: %v, %v", g.BxConfig.MevMinerURI, mevBundle.Hash(), err) + return + } + req.Header.Add("Content-Type", "application/json") + + resp, err := httpClient.Do(req) + if err != nil { + log.Errorf("failed to perform mevBundle request for: %v, hash: %v, %v", g.BxConfig.MevMinerURI, mevBundle.Hash(), err) + return + } + + if resp.StatusCode != http.StatusOK { + log.Errorf("invalid mevBundle status code from: %v, hash: %v,code: %v", g.BxConfig.MevMinerURI, mevBundle.Hash(), resp.StatusCode) + return + } + } + return + } + + mevBundle.SetHash() + mevBundle.SetNetworkNum(g.sdn.NetworkNum()) + broadcastRes := g.broadcast(&mevBundle, source, utils.RelayTransaction) + log.Tracef("broadcast mevBundle message msg: %v in network %v to relays, result: [%v]", mevBundle.Hash(), mevBundle.GetNetworkNum(), broadcastRes) +} + +// TODO: think about remove code duplication and merge this func with handleMEVBundleMessage +func (g gateway) handleMEVSearcherMessage(mevSearcher bxmessage.MEVSearcher, source connections.Conn) { + if source.Info().IsRelay() { + if g.BxConfig.MevBuilderURI == "" { + log.Warnf("received mevSearcher message, but mev-builder-uri is empty. Message %v from %v in network %v", mevSearcher.Hash(), mevSearcher.SourceID(), mevSearcher.GetNetworkNum()) + return + } + if g.seenMEVSearchers.SetIfAbsent(mevSearcher.Hash().String(), time.Minute*30) { + mevSearcher.ID, mevSearcher.JSONRPC = "1", "2.0" + mevRPC, err := json.Marshal(mevSearcher) + if err != nil { + log.Errorf("failed to marshal mevSearcher payload for: %v, hash: %v, params: %v, error: %v", g.BxConfig.MevBuilderURI, mevSearcher.Hash(), string(mevSearcher.Params), err) + return + } + + httpClient := http.Client{} + defer httpClient.CloseIdleConnections() + + req, err := http.NewRequest(http.MethodPost, g.BxConfig.MevBuilderURI, bytes.NewReader(mevRPC)) + if err != nil { + log.Errorf("failed to create new mevSearcher http request for: %v, hash: %v, %v", g.BxConfig.MevBuilderURI, mevSearcher.Hash(), err) + return + } + req.Header.Add("Content-Type", "application/json") + + // For this case we always have only 1 element + var mevSearcherAuth string + for _, auth := range mevSearcher.Auth() { + mevSearcherAuth = auth + break + } + + req.Header.Add(flasbotAuthHeader, mevSearcherAuth) + resp, err := httpClient.Do(req) + if err != nil { + log.Errorf("failed to perform mevSearcher request for: %v, hash: %v, %v", g.BxConfig.MevBuilderURI, mevSearcher.Hash(), err) + return + } + + if resp.StatusCode != http.StatusOK { + log.Errorf("invalid mevSearcher status code from: %v, hash: %v,code: %v", g.BxConfig.MevBuilderURI, mevSearcher.Hash(), resp.StatusCode) + return + } + } + return + } + + mevSearcher.SetHash() + mevSearcher.SetNetworkNum(g.sdn.NetworkNum()) + broadcastRes := g.broadcast(&mevSearcher, source, utils.RelayTransaction) + log.Tracef("broadcast mevSearcher message msg: %v in network %v to relays, result: [%v]", mevSearcher.Hash(), mevSearcher.GetNetworkNum(), broadcastRes) +} + +func (g *gateway) Peers(ctx context.Context, req *pb.PeersRequest) (*pb.PeersReply, error) { + return g.Bx.Peers(ctx, req) +} + +func (g *gateway) Version(_ context.Context, _ *pb.VersionRequest) (*pb.VersionReply, error) { + resp := &pb.VersionReply{ + Version: version.BuildVersion, + BuildDate: version.BuildDate, + } + return resp, nil +} + +func (g *gateway) BlxrTx(_ context.Context, req *pb.BlxrTxRequest) (*pb.BlxrTxReply, error) { + tx := bxmessage.Tx{} + tx.SetTimestamp(time.Now()) + + txContent, err := hex.DecodeString(req.GetTransaction()) + if err != nil { + log.Errorf("failed to decode transaction %v sent via GRPC blxrtx: %v", req.GetTransaction(), err) + return &pb.BlxrTxReply{}, err + } + tx.SetContent(txContent) + + hashAsByteArr := crypto.Keccak256(txContent) + var hash types.SHA256Hash + copy(hash[:], hashAsByteArr) + tx.SetHash(hash) + + // TODO: take the account ID from the authentication meta data + tx.SetAccountID(g.sdn.NodeModel().AccountID) + tx.SetNetworkNum(g.sdn.NetworkNum()) + + grpc := connections.NewRPCConn(g.accountID, "", g.sdn.NetworkNum(), utils.GRPC) + g.HandleMsg(&tx, grpc, connections.RunForeground) + return &pb.BlxrTxReply{TxHash: tx.Hash().String()}, nil +} + +func (g *gateway) sendStatsOnInterval(interval time.Duration, relayConn *handler.BxConn) { + ticker := time.NewTicker(interval) + for { + select { + case <-ticker.C: + rss, err := utils.GetAppMemoryUsage() + if err != nil { + log.Tracef("Failed to get Process RSS size: %v", err) + } + g.bdnStats.SetMemoryUtilization(rss) + + closedIntervalBDNStatsMsg := g.bdnStats.CloseInterval() + err = relayConn.Send(&closedIntervalBDNStatsMsg) + if err != nil { + log.Debugf("failed to send BDN performance stats: %v", err) + } + closedIntervalBDNStatsMsg.Log() + } + } +} diff --git a/nodes/gateway_test.go b/nodes/gateway_test.go new file mode 100644 index 0000000..61ba76a --- /dev/null +++ b/nodes/gateway_test.go @@ -0,0 +1,547 @@ +package nodes + +import ( + "context" + "github.com/bloXroute-Labs/gateway" + "github.com/bloXroute-Labs/gateway/blockchain" + "github.com/bloXroute-Labs/gateway/blockchain/eth" + "github.com/bloXroute-Labs/gateway/blockchain/network" + "github.com/bloXroute-Labs/gateway/bxmessage" + "github.com/bloXroute-Labs/gateway/config" + "github.com/bloXroute-Labs/gateway/connections" + "github.com/bloXroute-Labs/gateway/connections/handler" + "github.com/bloXroute-Labs/gateway/sdnmessage" + "github.com/bloXroute-Labs/gateway/services" + "github.com/bloXroute-Labs/gateway/services/loggers" + "github.com/bloXroute-Labs/gateway/test/bxmock" + "github.com/bloXroute-Labs/gateway/types" + "github.com/bloXroute-Labs/gateway/utils" + "github.com/ethereum/go-ethereum/common" + ethtypes "github.com/ethereum/go-ethereum/core/types" + "github.com/sirupsen/logrus" + "github.com/stretchr/testify/assert" + "math/big" + "sync" + "testing" + "time" +) + +var ( + networkNum types.NetworkNum = 5 + blockchainIPEndpoint = types.NodeEndpoint{IP: "127.0.0.1", Port: 8001} +) + +func setup() (blockchain.Bridge, *gateway) { + sslCerts := utils.TestCerts() + nm := sdnmessage.NodeModel{ + NodeType: "EXTERNAL_GATEWAY", + BlockchainNetworkNum: networkNum, + ExternalIP: "172.0.0.1", + } + + sdn := connections.NewSDNHTTP(&sslCerts, "", nm, "") + sdn.Networks = sdnmessage.BlockchainNetworks{5: bxmock.MockNetwork(networkNum, "Ethereum", "Mainnet", 0)} + + logConfig := config.Log{ + AppName: "gateway-test", + FileName: "test-logfile", + FileLevel: logrus.TraceLevel, + ConsoleLevel: logrus.TraceLevel, + MaxSize: 100, + MaxBackups: 2, + MaxAge: 1, + TxTrace: config.TxTraceLog{ + Enabled: true, + MaxFileSize: 100, + MaxBackupFiles: 3, + }, + } + bxConfig := &config.Bx{ + Log: &logConfig, + NodeType: utils.Gateway, + } + + bridge := blockchain.NewBxBridge(eth.Converter{}) + var blockchainPeers []types.NodeEndpoint + blockchainPeers = append(blockchainPeers, types.NodeEndpoint{IP: "123.45.6.78", Port: 123}) + node, _ := NewGateway(context.Background(), bxConfig, bridge, bxmock.NewMockWSProvider(), blockchainPeers) + + g := node.(*gateway) + g.sdn = sdn + g.bdnStats = bxmessage.NewBDNStats() + g.txTrace = loggers.NewTxTrace(nil) + g.setSyncWithRelay() + return bridge, g +} + +func newBP() (*services.BxTxStore, services.BlockProcessor) { + txStore := services.NewBxTxStore(time.Minute, time.Minute, time.Minute, services.NewEmptyShortIDAssigner(), services.NewHashHistory("seenTxs", time.Minute), nil, 30*time.Minute) + bp := services.NewRLPBlockProcessor(&txStore) + return &txStore, bp +} + +func addRelayConn(g *gateway) (bxmock.MockTLS, *handler.Relay) { + mockTLS := bxmock.NewMockTLS("1.1.1.1", 1800, "", utils.Relay, "") + relayConn := handler.NewRelay(g, + func() (connections.Socket, error) { + return &mockTLS, nil + }, + &utils.SSLCerts{}, "1.1.1.1", 1800, "", utils.Relay, true, &g.sdn.Networks, true, true, connections.LocalInitiatedPort, utils.RealClock{}, + false) + + // set connection as established and ready for broadcast + _ = relayConn.Connect() + hello := bxmessage.Hello{ + NodeID: "1234", + Protocol: relayConn.Protocol(), + } + b, _ := hello.Pack(relayConn.Protocol()) + relayConn.ProcessMessage(b) + + // advance ack message + ackBytes, err := mockTLS.MockAdvanceSent() + if err != nil { + panic(err) + } + var ack bxmessage.Ack + err = ack.Unpack(ackBytes, relayConn.Protocol()) + if err != nil { + panic(err) + } + + // advance sync req + syncReqBytes, err := mockTLS.MockAdvanceSent() + if err != nil { + panic(err) + } + var syncReq bxmessage.SyncReq + err = syncReq.Unpack(syncReqBytes, relayConn.Protocol()) + if err != nil { + panic(err) + } + + return mockTLS, relayConn +} + +func bxBlockFromEth(b blockchain.Bridge, height uint64, parentHash types.SHA256Hash) *types.BxBlock { + ethBlock := bxmock.NewEthBlock(height, common.BytesToHash(parentHash.Bytes())) + blockInfo := ð.BlockInfo{Block: ethBlock} + blockInfo.SetTotalDifficulty(big.NewInt(int64(10 * height))) + bxBlock, _ := b.BlockBlockchainToBDN(blockInfo) + return bxBlock +} + +func TestGateway_PushBlockchainConfig(t *testing.T) { + networkID := int64(1) + td := "40000" + hash := "0xd4e56740f876aef8c010b86a40d5f56745a118d0906a34e69aec8c0db1cb8fa3" + + bridge, g := setup() + + var blockchainNetwork sdnmessage.BlockchainNetwork + blockchainNetwork.DefaultAttributes.NetworkID = networkID + blockchainNetwork.DefaultAttributes.ChainDifficulty = td + blockchainNetwork.DefaultAttributes.GenesisHash = hash + + g.sdn.Networks[5] = &blockchainNetwork + + var ethConfig network.EthConfig + wg := sync.WaitGroup{} + wg.Add(1) + + go func() { + defer wg.Done() + ethConfig = <-bridge.ReceiveNetworkConfigUpdates() + }() + + err := g.pushBlockchainConfig() + assert.Nil(t, err) + + wg.Wait() + assert.NotNil(t, ethConfig) + + expectedTD, _ := new(big.Int).SetString("40000", 16) + expectedHash := common.HexToHash(hash) + + assert.Equal(t, uint64(networkID), ethConfig.Network) + assert.Equal(t, expectedTD, ethConfig.TotalDifficulty) + assert.Equal(t, expectedHash, ethConfig.Genesis) + assert.Equal(t, expectedHash, ethConfig.Head) +} + +func TestGateway_HandleTransactionFromBlockchain(t *testing.T) { + bridge, g := setup() + mockTLS, relayConn := addRelayConn(g) + + go func() { + err := g.handleBridgeMessages() + assert.Nil(t, err) + }() + + ethTx, ethTxBytes := bxmock.NewSignedEthTxBytes(ethtypes.DynamicFeeTxType, 1, nil) + bdnTx, err := bridge.TransactionBlockchainToBDN(ethTx) + assert.Nil(t, err) + + // send transactions over bridge from blockchain connection + txs := []*types.BxTransaction{bdnTx} + err = bridge.SendTransactionsToBDN(txs, blockchainIPEndpoint) + assert.Nil(t, err) + + msgBytes, err := mockTLS.MockAdvanceSent() + assert.Nil(t, err) + + // transaction is broadcast to relays + var sentTx bxmessage.Tx + err = sentTx.Unpack(msgBytes, relayConn.Protocol()) + assert.Nil(t, err) + + assert.Equal(t, ethTx.Hash().Bytes(), sentTx.Hash().Bytes()) + assert.Equal(t, ethTxBytes, sentTx.Content()) +} + +func TestGateway_HandleTransactionHashesFromBlockchain(t *testing.T) { + bridge, g := setup() + + go func() { + err := g.handleBridgeMessages() + assert.Nil(t, err) + }() + + peerID := "go-ethereum-1" + hashes := []types.SHA256Hash{ + types.GenerateSHA256Hash(), + types.GenerateSHA256Hash(), + types.GenerateSHA256Hash(), + types.GenerateSHA256Hash(), + } + + // hash 0 should be skipped since no content available + g.TxStore.Add(hashes[0], types.TxContent{}, 1, networkNum, false, 0, time.Now(), 0) + g.TxStore.Add(hashes[1], types.TxContent{1, 2, 3}, types.ShortIDEmpty, networkNum, false, 0, time.Now(), 0) + + err := bridge.AnnounceTransactionHashes(peerID, hashes) + assert.Nil(t, err) + + request := <-bridge.ReceiveTransactionHashesRequest() + + assert.Equal(t, len(hashes)-2, len(request.Hashes)) + assert.Equal(t, hashes[2], request.Hashes[0]) + assert.Equal(t, hashes[3], request.Hashes[1]) + + assert.Equal(t, peerID, request.PeerID) +} + +func TestGateway_HandleTransactionFromRPC(t *testing.T) { + bridge, g := setup() + mockTLS, relayConn := addRelayConn(g) + + ethTx, txMessage := bxmock.NewSignedEthTxMessage(ethtypes.LegacyTxType, 1, nil, networkNum) + txMessage.SetFlags(types.TFDeliverToNode) + + err := g.HandleMsg(txMessage, connections.NewRPCConn("", "", networkNum, utils.Websocket), connections.RunForeground) + assert.Nil(t, err) + + // transaction should be broadcast both to blockchain node and BDN + txsSentToBlockchain := <-bridge.ReceiveBDNTransactions() + assert.Equal(t, 1, len(txsSentToBlockchain)) + txSentToBlockchain := txsSentToBlockchain[0] + + msgBytes, err := mockTLS.MockAdvanceSent() + assert.Nil(t, err) + + var txSentToBDN bxmessage.Tx + err = txSentToBDN.Unpack(msgBytes, relayConn.Protocol()) + assert.Nil(t, err) + + assert.Equal(t, ethTx.Hash().Bytes(), txSentToBDN.Hash().Bytes()) + assert.Equal(t, txSentToBlockchain.Hash(), txSentToBDN.Hash()) +} + +func TestGateway_HandleTransactionFromRelay(t *testing.T) { + bridge, g := setup() + _, relayConn := addRelayConn(g) + + deliveredEthTx, deliveredTxMessage := bxmock.NewSignedEthTxMessage(ethtypes.LegacyTxType, 1, nil, networkNum) + deliveredTxMessage.SetFlags(types.TFDeliverToNode) + + err := g.HandleMsg(deliveredTxMessage, relayConn, connections.RunForeground) + assert.Nil(t, err) + + bdnTxs := <-bridge.ReceiveBDNTransactions() + assert.Equal(t, 1, len(bdnTxs)) + + bdnTx := bdnTxs[0] + assert.Equal(t, deliveredEthTx.Hash().Bytes(), bdnTx.Hash().Bytes()) + + _, undeliveredTxMessage := bxmock.NewSignedEthTxMessage(ethtypes.LegacyTxType, 1, nil, networkNum) + + err = g.HandleMsg(undeliveredTxMessage, relayConn, connections.RunForeground) + assert.Nil(t, err) + + select { + case <-bridge.ReceiveBDNTransactions(): + assert.Fail(t, "unexpectedly received txs when TFDeliverToNode not set") + default: + } +} + +func TestGateway_ReprocessTransactionFromRelay(t *testing.T) { + bridge, g := setup() + _, relayConn := addRelayConn(g) + + // send new tx + ethTx, ethTxMsg := bxmock.NewSignedEthTxMessage(ethtypes.LegacyTxType, 1, nil, networkNum) + err := g.HandleMsg(ethTxMsg, relayConn, connections.RunForeground) + assert.Nil(t, err) + + hash, _ := types.NewSHA256HashFromString(ethTx.Hash().String()) + _, exists := g.TxStore.Get(hash) + assert.True(t, exists) + + // reprocess resent tx + resentTxMessage := ethTxMsg + resentTxMessage.SetFlags(types.TFDeliverToNode) + err = g.HandleMsg(resentTxMessage, relayConn, connections.RunForeground) + assert.Nil(t, err) + + bdnTxs := <-bridge.ReceiveBDNTransactions() + assert.Equal(t, 1, len(bdnTxs)) + bdnTx := bdnTxs[0] + assert.Equal(t, ethTx.Hash().Bytes(), bdnTx.Hash().Bytes()) + + // only reprocess once + err = g.HandleMsg(resentTxMessage, relayConn, connections.RunForeground) + assert.Nil(t, err) + select { + case <-bridge.ReceiveBDNTransactions(): + assert.Fail(t, "unexpectedly reprocessed tx more than once") + default: + } +} + +func TestGateway_HandleTransactionFromRelayBlocksOnly(t *testing.T) { + bridge, g := setup() + _, relayConn := addRelayConn(g) + g.BxConfig.BlocksOnly = true + + _, freeTx := bxmock.NewSignedEthTxMessage(ethtypes.LegacyTxType, 1, nil, networkNum) + freeTx.SetFlags(types.TFDeliverToNode) + + _, paidTx := bxmock.NewSignedEthTxMessage(ethtypes.LegacyTxType, 2, nil, networkNum) + paidTx.SetFlags(types.TFPaidTx | types.TFDeliverToNode) + + err := g.HandleMsg(paidTx, relayConn, connections.RunForeground) + assert.Nil(t, err) + + select { + case <-bridge.ReceiveBDNTransactions(): + assert.Fail(t, "unexpectedly received txs when --blocks-only set") + default: + } +} + +func TestGateway_HandleBlockFromBlockchain(t *testing.T) { + bridge, g := setup() + mockTLS, relayConn := addRelayConn(g) + + go func() { + err := g.handleBridgeMessages() + assert.Nil(t, err) + }() + + ethBlock := bxmock.NewEthBlock(10, common.Hash{}) + bxBlock, err := bridge.BlockBlockchainToBDN(eth.NewBlockInfo(ethBlock, nil)) + assert.Nil(t, err) + + // send block over bridge from blockchain connection + err = bridge.SendBlockToBDN(bxBlock, blockchainIPEndpoint) + assert.Nil(t, err) + + msgBytes, err := mockTLS.MockAdvanceSent() + assert.Nil(t, err) + + // block is broadcast to relays + var sentBroadcast bxmessage.Broadcast + err = sentBroadcast.Unpack(msgBytes, relayConn.Protocol()) + assert.Nil(t, err) + + assert.Equal(t, ethBlock.Hash().Bytes(), sentBroadcast.Hash().Bytes()) + assert.Equal(t, networkNum, sentBroadcast.GetNetworkNum()) + assert.Equal(t, 0, len(sentBroadcast.ShortIDs())) + + // try sending block again + err = bridge.SendBlockToBDN(bxBlock, blockchainIPEndpoint) + assert.Nil(t, err) + + // times out, nothing sent (already processed) + msgBytes, err = mockTLS.MockAdvanceSent() + assert.NotNil(t, err, "unexpected bytes %v", string(msgBytes)) +} + +func TestGateway_HandleBlockFromRelay(t *testing.T) { + bridge, g := setup() + _, relayConn := addRelayConn(g) + + // separate service instance, to avoid already processed errors + txStore, bp := newBP() + + ethBlock := bxmock.NewEthBlock(10, common.Hash{}) + bxBlock, _ := bridge.BlockBlockchainToBDN(eth.NewBlockInfo(ethBlock, nil)) + + // compress a transaction + bxTransaction, _ := bridge.TransactionBlockchainToBDN(ethBlock.Transactions()[0]) + txStore.Add(bxTransaction.Hash(), bxTransaction.Content(), 1, networkNum, false, 0, time.Now(), 0) + g.TxStore.Add(bxTransaction.Hash(), bxTransaction.Content(), 1, networkNum, false, 0, time.Now(), 0) + + broadcastMessage, _, err := bp.BxBlockToBroadcast(bxBlock, networkNum, g.sdn.GetMinTxAge()) + assert.Nil(t, err) + + err = g.HandleMsg(broadcastMessage, relayConn, connections.RunForeground) + assert.Nil(t, err) + + receivedBxBlock := <-bridge.ReceiveBlockFromBDN() + if receivedBxBlock == nil { + t.FailNow() + } + assert.Equal(t, bxBlock.Hash(), receivedBxBlock.Hash()) + assert.Equal(t, bxBlock.Header, receivedBxBlock.Header) + assert.Equal(t, bxBlock.Trailer, receivedBxBlock.Trailer) + assert.True(t, bxBlock.Equals(receivedBxBlock)) + + // duplicate, no processing + err = g.HandleMsg(broadcastMessage, relayConn, connections.RunForeground) + assert.Nil(t, err) + + select { + case <-bridge.ReceiveBlockFromBDN(): + assert.Fail(t, "unexpectedly processed block again") + default: + } +} + +func TestGateway_ValidateHeightBDNBlocksWithNode(t *testing.T) { + bridge, g := setup() + g.feedChan = make(chan types.Notification, bxgateway.BxNotificationChannelSize) + g.BxConfig.WebsocketEnabled = true + + // block from node + heightFromNode := 10 + expectFeedNotification(t, bridge, g, types.NewBlocksFeed, heightFromNode, heightFromNode, 0) + + // skip 1st too far ahead block from BDN + tooFarAheadHeight := heightFromNode + bxgateway.BDNBlocksMaxBlocksAway + 1 + expectNoFeedNotification(t, bridge, g, types.BDNBlocksFeed, tooFarAheadHeight, heightFromNode, 1) + + // skip 2nd too far ahead block from BDN + offset := 5 + expectNoFeedNotification(t, bridge, g, types.BDNBlocksFeed, tooFarAheadHeight+offset, heightFromNode, 2) + + // skip 3rd too far ahead block from BDN + offset++ + expectNoFeedNotification(t, bridge, g, types.BDNBlocksFeed, tooFarAheadHeight+offset, heightFromNode, 3) + + // should publish block from BDN after 3 skipped, clear best height, clear skip count + offset++ + expectFeedNotification(t, bridge, g, types.BDNBlocksFeed, tooFarAheadHeight+offset, 0, 0) + + // block from node - set bestBlockHeight + offset++ + bestBlockHeight := tooFarAheadHeight + offset + expectFeedNotification(t, bridge, g, types.NewBlocksFeed, tooFarAheadHeight+offset, bestBlockHeight, 0) + + // skip 1st too far ahead block from BDN + tooFarAheadHeight = tooFarAheadHeight + offset + bxgateway.BDNBlocksMaxBlocksAway + 1 + expectNoFeedNotification(t, bridge, g, types.BDNBlocksFeed, tooFarAheadHeight, bestBlockHeight, 1) + + // skip 2nd too far ahead block from BDN + offset = 1 + expectNoFeedNotification(t, bridge, g, types.BDNBlocksFeed, tooFarAheadHeight+offset, bestBlockHeight, 2) + + // skip 3rd too far block from BDN + offset++ + expectNoFeedNotification(t, bridge, g, types.BDNBlocksFeed, tooFarAheadHeight+offset, bestBlockHeight, 3) + + // old block from BDN - skip + tooOldHeight := bestBlockHeight - bxgateway.BDNBlocksMaxBlocksAway - 1 + expectNoFeedNotification(t, bridge, g, types.BDNBlocksFeed, tooOldHeight, bestBlockHeight, 3) + + // publish 4th too far ahead block, clear best height and skipped block count + offset++ + expectFeedNotification(t, bridge, g, types.BDNBlocksFeed, tooFarAheadHeight+offset, 0, 0) + + // block from node - set bestBlockHeight + expectFeedNotification(t, bridge, g, types.NewBlocksFeed, bestBlockHeight+1, bestBlockHeight+1, 0) + + // skip 1st too far ahead block from BDN + offset++ + expectNoFeedNotification(t, bridge, g, types.BDNBlocksFeed, tooFarAheadHeight+offset, bestBlockHeight+1, 1) + + // block from node - set bestBlockHeight and clear skipped block count + expectFeedNotification(t, bridge, g, types.NewBlocksFeed, bestBlockHeight+2, bestBlockHeight+2, 0) +} + +func TestGateway_ValidateHeightBDNBlocksWithoutNode(t *testing.T) { + bridge, g := setup() + g.feedChan = make(chan types.Notification, bxgateway.BxNotificationChannelSize) + g.BxConfig.WebsocketEnabled = true + g.blockchainPeers = []types.NodeEndpoint{} + + // first block from BDN (no blockchain node) + heightFromNode := 10 + expectFeedNotification(t, bridge, g, types.BDNBlocksFeed, heightFromNode, heightFromNode, 0) + + // skip 1st too far block from BDN + tooFarHeight := heightFromNode + bxgateway.BDNBlocksMaxBlocksAway + 1 + expectNoFeedNotification(t, bridge, g, types.BDNBlocksFeed, tooFarHeight, heightFromNode, 1) + + // skip 2nd too far block from BDN + expectNoFeedNotification(t, bridge, g, types.BDNBlocksFeed, tooFarHeight+5, heightFromNode, 2) + + // skip 3rd too far block from BDN + expectNoFeedNotification(t, bridge, g, types.BDNBlocksFeed, tooFarHeight+6, heightFromNode, 3) + + // should publish block from BDN after 3 skipped, reset best height, clear skip count + expectFeedNotification(t, bridge, g, types.BDNBlocksFeed, tooFarHeight+7, tooFarHeight+7, 0) +} + +func expectNoFeedNotification(t *testing.T, bridge blockchain.Bridge, g *gateway, feedName types.FeedType, blockHeight int, expectedBestBlockHeight int, expectedSkipBlockCount int) { + ethBlock := bxmock.NewEthBlock(uint64(blockHeight), common.Hash{}) + bxBlock, _ := bridge.BlockBlockchainToBDN(eth.NewBlockInfo(ethBlock, nil)) + err := g.publishBlock(bxBlock, feedName) + assert.Nil(t, err) + select { + case <-g.feedChan: + assert.Fail(t, "received unexpected feed notification") + default: + } + assert.Equal(t, expectedBestBlockHeight, g.bestBlockHeight) + assert.Equal(t, expectedSkipBlockCount, g.bdnBlocksSkipCount) +} + +func expectFeedNotification(t *testing.T, bridge blockchain.Bridge, g *gateway, feedName types.FeedType, blockHeight int, expectedBestBlockHeight int, expectedSkipBlockCount int) { + ethBlock := bxmock.NewEthBlock(uint64(blockHeight), common.Hash{}) + bxBlock, _ := bridge.BlockBlockchainToBDN(eth.NewBlockInfo(ethBlock, nil)) + err := g.publishBlock(bxBlock, feedName) + assert.Nil(t, err) + select { + case <-g.feedChan: + default: + assert.Fail(t, "did not receive expected feed notification") + } + if feedName == types.NewBlocksFeed { + // onBlock notification + select { + case <-g.feedChan: + default: + assert.Fail(t, "did not receive expected feed notification") + } + // txReceipt Notification + select { + case <-g.feedChan: + default: + assert.Fail(t, "did not receive expected feed notification") + } + } + assert.Equal(t, expectedBestBlockHeight, g.bestBlockHeight) + assert.Equal(t, expectedSkipBlockCount, g.bdnBlocksSkipCount) +} diff --git a/nodes/gatewaygrpcserver.go b/nodes/gatewaygrpcserver.go new file mode 100644 index 0000000..541e81f --- /dev/null +++ b/nodes/gatewaygrpcserver.go @@ -0,0 +1,77 @@ +package nodes + +import ( + "context" + "errors" + "fmt" + pb "github.com/bloXroute-Labs/gateway/protobuf" + "github.com/bloXroute-Labs/gateway/rpc" + log "github.com/sirupsen/logrus" + "google.golang.org/grpc" + "net" +) + +type gatewayGRPCServer struct { + gateway *gateway + listenAddr string + encodedAuth string + server *grpc.Server +} + +func newGatewayGRPCServer(gateway *gateway, host string, port int, user string, secret string) gatewayGRPCServer { + grpcHostPort := fmt.Sprintf("%v:%v", host, port) + + var encodedAuth string + if user != "" && secret != "" { + encodedAuth = rpc.EncodeUserSecret(user, secret) + } else { + encodedAuth = "" + } + + return gatewayGRPCServer{ + gateway: gateway, + listenAddr: grpcHostPort, + encodedAuth: encodedAuth, + } +} + +func (ggs *gatewayGRPCServer) Start() error { + ggs.run() + return nil +} + +func (ggs *gatewayGRPCServer) Stop() { + server := ggs.server + if server != nil { + ggs.server.Stop() + } +} + +func (ggs *gatewayGRPCServer) run() { + listener, err := net.Listen("tcp", ggs.listenAddr) + if err != nil { + log.Fatalf("failed to listen: %v", err) + } + + ggs.server = grpc.NewServer(grpc.UnaryInterceptor(ggs.authenticate)) + pb.RegisterGatewayServer(ggs.server, ggs.gateway) + + log.Infof("GRPC server is starting on %v", ggs.listenAddr) + if err := ggs.server.Serve(listener); err != nil { + log.Fatalf("failed to serve: %v", err) + } +} + +func (ggs *gatewayGRPCServer) authenticate(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (interface{}, error) { + if ggs.encodedAuth != "" { + auth, err := rpc.ReadAuthMetadata(ctx) + if err != nil { + return nil, err + } + + if auth != ggs.encodedAuth { + return nil, errors.New("provided auth information was incorrect") + } + } + return handler(ctx, req) +} diff --git a/nodes/gatewaygrpcserver_test.go b/nodes/gatewaygrpcserver_test.go new file mode 100644 index 0000000..8e42f5f --- /dev/null +++ b/nodes/gatewaygrpcserver_test.go @@ -0,0 +1,104 @@ +package nodes + +import ( + "context" + "github.com/bloXroute-Labs/gateway/config" + pb "github.com/bloXroute-Labs/gateway/protobuf" + "github.com/bloXroute-Labs/gateway/rpc" + "github.com/bloXroute-Labs/gateway/test" + "github.com/bloXroute-Labs/gateway/version" + "github.com/stretchr/testify/assert" + "testing" + "time" +) + +func spawnGRPCServer(port int, user string, password string) (*gateway, *gatewayGRPCServer) { + serverConfig := config.NewGRPC("0.0.0.0", port, user, password) + _, g := setup() + g.BxConfig.GRPC = serverConfig + s := newGatewayGRPCServer(g, serverConfig.Host, serverConfig.Port, serverConfig.User, serverConfig.Password) + go func() { + _ = s.Start() + }() + + // small sleep for goroutine to start + time.Sleep(1 * time.Millisecond) + + return g, &s +} + +func TestGatewayGRPCServerNoAuth(t *testing.T) { + port := test.NextTestPort() + + _, s := spawnGRPCServer(port, "", "") + defer s.Stop() + + clientConfig := config.NewGRPC("127.0.0.1", port, "", "") + res, err := rpc.GatewayCall(clientConfig, func(ctx context.Context, client pb.GatewayClient) (interface{}, error) { + return client.Version(ctx, &pb.VersionRequest{}) + }) + + assert.Nil(t, err) + + versionReply, ok := res.(*pb.VersionReply) + assert.True(t, ok) + assert.Equal(t, version.BuildVersion, versionReply.GetVersion()) +} + +func TestGatewayGRPCServerAuth(t *testing.T) { + port := test.NextTestPort() + + _, s := spawnGRPCServer(port, "user", "password") + defer s.Stop() + + authorizedClientConfig := config.NewGRPC("127.0.0.1", port, "user", "password") + res, err := rpc.GatewayCall(authorizedClientConfig, func(ctx context.Context, client pb.GatewayClient) (interface{}, error) { + return client.Version(ctx, &pb.VersionRequest{}) + }) + + assert.Nil(t, err) + + versionReply, ok := res.(*pb.VersionReply) + assert.True(t, ok) + assert.Equal(t, version.BuildVersion, versionReply.GetVersion()) + + unauthorizedClientConfig := config.NewGRPC("127.0.0.1", port, "user", "wrongpassword") + res, err = rpc.GatewayCall(unauthorizedClientConfig, func(ctx context.Context, client pb.GatewayClient) (interface{}, error) { + return client.Version(ctx, &pb.VersionRequest{}) + }) + + assert.Nil(t, res) + assert.NotNil(t, err) +} + +func TestGatewayGRPCServerPeers(t *testing.T) { + port := test.NextTestPort() + + g, s := spawnGRPCServer(port, "", "") + defer s.Stop() + + clientConfig := config.NewGRPC("127.0.0.1", port, "", "") + + peersCall := func() *pb.PeersReply { + res, err := rpc.GatewayCall(clientConfig, func(ctx context.Context, client pb.GatewayClient) (interface{}, error) { + return client.Peers(ctx, &pb.PeersRequest{}) + }) + + assert.Nil(t, err) + + peersReply, ok := res.(*pb.PeersReply) + assert.True(t, ok) + return peersReply + } + + peers := peersCall() + assert.Equal(t, 0, len(peers.GetPeers())) + + _, conn := addRelayConn(g) + + peers = peersCall() + assert.Equal(t, 1, len(peers.GetPeers())) + + peer := peers.GetPeers()[0] + assert.Equal(t, conn.Info().PeerIP, peer.Ip) +} diff --git a/nodes/log.go b/nodes/log.go new file mode 100644 index 0000000..098bb29 --- /dev/null +++ b/nodes/log.go @@ -0,0 +1,125 @@ +package nodes + +import ( + "fmt" + "github.com/bloXroute-Labs/gateway/config" + "github.com/orandin/lumberjackrus" + "github.com/sirupsen/logrus" + log "github.com/sirupsen/logrus" + "io" + "io/ioutil" + "os" + "strings" +) + +// writerHook is a hook that writes logs of specified logLevels to specified writer +// This hook is used to separate stdout and stderr log output, as this is not supported natively by logrus +type writerHook struct { + writer io.Writer + logLevels []log.Level +} + +func stdoutWriter(level log.Level) *writerHook { + var levels []log.Level + + // stdout should never write WARN, ERROR, etc. logs, since that's handled by stderr + // (increasing log levels = more verbose logs) + if level >= log.InfoLevel { + levels = log.AllLevels[log.InfoLevel : level+1] + } + return &writerHook{ + writer: os.Stdout, + logLevels: levels, + } +} + +func stderrWriter(level log.Level) *writerHook { + // stderr should never write INFO or more verbose logs, cap the levels represented + if level >= log.InfoLevel { + level = log.WarnLevel + } + return &writerHook{ + writer: os.Stderr, + logLevels: log.AllLevels[:level+1], + } +} + +// Fire will be called when some logging function is called with current hook +// It will format log entry to string and write it to appropriate writer +func (hook *writerHook) Fire(entry *log.Entry) error { + line, err := entry.Bytes() + if err != nil { + return err + } + _, err = hook.writer.Write(line) + return err +} + +// Levels define on which log levels this hook would trigger +func (hook *writerHook) Levels() []log.Level { + return hook.logLevels +} + +// InitLogs - initialise logging +func InitLogs(logConfig *config.Log, version string) error { + fileHook, formatter, err := createLogFileHook(logConfig.FileName, logConfig.MaxSize, logConfig.MaxBackups, logConfig.MaxAge, logConfig.FileLevel) + if err != nil { + return err + } + + log.SetFormatter(formatter) + log.SetLevel(log.TraceLevel) + + // send logs to nowhere by default, use hooks for separate stdout/stderr + log.SetOutput(ioutil.Discard) + + log.AddHook(stdoutWriter(logConfig.ConsoleLevel)) + log.AddHook(stderrWriter(logConfig.ConsoleLevel)) + log.AddHook(fileHook) + + log.Debugf("log initiated.") + log.Infof("%v (%v) is starting with arguments %v", logConfig.AppName, version, strings.Join(os.Args[1:], " ")) + return nil +} + +// CreateCustomLogger creates a new custom logrus instance +func CreateCustomLogger(appName string, port int, fileName string, maxSize int, maxBackups int, maxAge int, logFileLevel log.Level) (*log.Logger, error) { + customLogger := logrus.New() + + fileHook, formatter, err := createLogFileHook(fmt.Sprintf("logs/%v-%v-%v.log", fileName, appName, port), maxSize, maxBackups, maxAge, logFileLevel) + if err != nil { + return nil, err + } + + customLogger.SetFormatter(formatter) + customLogger.SetLevel(logFileLevel) + + // send logs to nowhere by default, use hook for redirection to file + customLogger.SetOutput(ioutil.Discard) + customLogger.AddHook(fileHook) + + return customLogger, nil +} + +func createLogFileHook(fileName string, maxSize int, maxBackups int, maxAge int, logFileLevel log.Level) (*lumberjackrus.Hook, *log.TextFormatter, error) { + formatter := new(log.TextFormatter) + formatter.TimestampFormat = "2006-01-02T15:04:05.000000" + formatter.FullTimestamp = true + formatter.DisableColors = true + + fileHook, err := lumberjackrus.NewHook( + &lumberjackrus.LogFile{ + Filename: fileName, + MaxSize: maxSize, + MaxBackups: maxBackups, + MaxAge: maxAge, + Compress: false, + LocalTime: false, + }, + logFileLevel, + formatter, + &lumberjackrus.LogFileOpts{}, + ) + + return fileHook, formatter, err +} diff --git a/nodes/node.go b/nodes/node.go new file mode 100644 index 0000000..3664626 --- /dev/null +++ b/nodes/node.go @@ -0,0 +1,26 @@ +package nodes + +import ( + "fmt" + "github.com/bloXroute-Labs/gateway/services" + "github.com/urfave/cli/v2" +) + +// Node represents the basic node interface +type Node interface { + Run() error +} + +// Abstract represents a basic bloxroute node interface +type Abstract struct { + TxStore services.TxStore +} + +func notImplError(funcName string) error { + return fmt.Errorf("func %v not implemented", funcName) +} + +// Run starts running the abstract node +func (an Abstract) Run(ctx *cli.Context) error { + return notImplError("Run") +} diff --git a/protobuf/gateway.pb.go b/protobuf/gateway.pb.go new file mode 100644 index 0000000..61d147b --- /dev/null +++ b/protobuf/gateway.pb.go @@ -0,0 +1,1418 @@ +// Code generated by protoc-gen-go. DO NOT EDIT. +// versions: +// protoc-gen-go v1.25.0 +// protoc v3.17.1 +// source: gateway.proto + +// The gateway service definition. + +package gateway + +import ( + proto "github.com/golang/protobuf/proto" + protoreflect "google.golang.org/protobuf/reflect/protoreflect" + protoimpl "google.golang.org/protobuf/runtime/protoimpl" + timestamppb "google.golang.org/protobuf/types/known/timestamppb" + reflect "reflect" + sync "sync" +) + +const ( + // Verify that this generated code is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) + // Verify that runtime/protoimpl is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) +) + +// This is a compile-time assertion that a sufficiently up-to-date version +// of the legacy proto package is being used. +const _ = proto.ProtoPackageIsVersion4 + +type VersionRequest struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields +} + +func (x *VersionRequest) Reset() { + *x = VersionRequest{} + if protoimpl.UnsafeEnabled { + mi := &file_gateway_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *VersionRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*VersionRequest) ProtoMessage() {} + +func (x *VersionRequest) ProtoReflect() protoreflect.Message { + mi := &file_gateway_proto_msgTypes[0] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use VersionRequest.ProtoReflect.Descriptor instead. +func (*VersionRequest) Descriptor() ([]byte, []int) { + return file_gateway_proto_rawDescGZIP(), []int{0} +} + +type VersionReply struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Version string `protobuf:"bytes,1,opt,name=version,proto3" json:"version,omitempty"` + BuildDate string `protobuf:"bytes,2,opt,name=build_date,json=buildDate,proto3" json:"build_date,omitempty"` +} + +func (x *VersionReply) Reset() { + *x = VersionReply{} + if protoimpl.UnsafeEnabled { + mi := &file_gateway_proto_msgTypes[1] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *VersionReply) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*VersionReply) ProtoMessage() {} + +func (x *VersionReply) ProtoReflect() protoreflect.Message { + mi := &file_gateway_proto_msgTypes[1] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use VersionReply.ProtoReflect.Descriptor instead. +func (*VersionReply) Descriptor() ([]byte, []int) { + return file_gateway_proto_rawDescGZIP(), []int{1} +} + +func (x *VersionReply) GetVersion() string { + if x != nil { + return x.Version + } + return "" +} + +func (x *VersionReply) GetBuildDate() string { + if x != nil { + return x.BuildDate + } + return "" +} + +type StopRequest struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields +} + +func (x *StopRequest) Reset() { + *x = StopRequest{} + if protoimpl.UnsafeEnabled { + mi := &file_gateway_proto_msgTypes[2] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *StopRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*StopRequest) ProtoMessage() {} + +func (x *StopRequest) ProtoReflect() protoreflect.Message { + mi := &file_gateway_proto_msgTypes[2] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use StopRequest.ProtoReflect.Descriptor instead. +func (*StopRequest) Descriptor() ([]byte, []int) { + return file_gateway_proto_rawDescGZIP(), []int{2} +} + +type StopReply struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields +} + +func (x *StopReply) Reset() { + *x = StopReply{} + if protoimpl.UnsafeEnabled { + mi := &file_gateway_proto_msgTypes[3] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *StopReply) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*StopReply) ProtoMessage() {} + +func (x *StopReply) ProtoReflect() protoreflect.Message { + mi := &file_gateway_proto_msgTypes[3] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use StopReply.ProtoReflect.Descriptor instead. +func (*StopReply) Descriptor() ([]byte, []int) { + return file_gateway_proto_rawDescGZIP(), []int{3} +} + +type PeersRequest struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Type string `protobuf:"bytes,1,opt,name=type,proto3" json:"type,omitempty"` +} + +func (x *PeersRequest) Reset() { + *x = PeersRequest{} + if protoimpl.UnsafeEnabled { + mi := &file_gateway_proto_msgTypes[4] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *PeersRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*PeersRequest) ProtoMessage() {} + +func (x *PeersRequest) ProtoReflect() protoreflect.Message { + mi := &file_gateway_proto_msgTypes[4] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use PeersRequest.ProtoReflect.Descriptor instead. +func (*PeersRequest) Descriptor() ([]byte, []int) { + return file_gateway_proto_rawDescGZIP(), []int{4} +} + +func (x *PeersRequest) GetType() string { + if x != nil { + return x.Type + } + return "" +} + +type Peer struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Ip string `protobuf:"bytes,1,opt,name=ip,proto3" json:"ip,omitempty"` + NodeId string `protobuf:"bytes,2,opt,name=node_id,json=nodeId,proto3" json:"node_id,omitempty"` + Type string `protobuf:"bytes,3,opt,name=type,proto3" json:"type,omitempty"` + State string `protobuf:"bytes,4,opt,name=state,proto3" json:"state,omitempty"` + Network uint32 `protobuf:"varint,5,opt,name=network,proto3" json:"network,omitempty"` + Initiator bool `protobuf:"varint,6,opt,name=initiator,proto3" json:"initiator,omitempty"` + MinMsFromPeer int64 `protobuf:"varint,7,opt,name=min_ms_from_peer,json=minMsFromPeer,proto3" json:"min_ms_from_peer,omitempty"` + MinMsToPeer int64 `protobuf:"varint,8,opt,name=min_ms_to_peer,json=minMsToPeer,proto3" json:"min_ms_to_peer,omitempty"` + SlowTrafficCount int64 `protobuf:"varint,9,opt,name=slow_traffic_count,json=slowTrafficCount,proto3" json:"slow_traffic_count,omitempty"` + MinMsRoundTrip int64 `protobuf:"varint,10,opt,name=min_ms_round_trip,json=minMsRoundTrip,proto3" json:"min_ms_round_trip,omitempty"` + AccountId string `protobuf:"bytes,11,opt,name=account_id,json=accountId,proto3" json:"account_id,omitempty"` + AccountTier string `protobuf:"bytes,12,opt,name=account_tier,json=accountTier,proto3" json:"account_tier,omitempty"` + Port int64 `protobuf:"varint,13,opt,name=port,proto3" json:"port,omitempty"` +} + +func (x *Peer) Reset() { + *x = Peer{} + if protoimpl.UnsafeEnabled { + mi := &file_gateway_proto_msgTypes[5] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *Peer) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*Peer) ProtoMessage() {} + +func (x *Peer) ProtoReflect() protoreflect.Message { + mi := &file_gateway_proto_msgTypes[5] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use Peer.ProtoReflect.Descriptor instead. +func (*Peer) Descriptor() ([]byte, []int) { + return file_gateway_proto_rawDescGZIP(), []int{5} +} + +func (x *Peer) GetIp() string { + if x != nil { + return x.Ip + } + return "" +} + +func (x *Peer) GetNodeId() string { + if x != nil { + return x.NodeId + } + return "" +} + +func (x *Peer) GetType() string { + if x != nil { + return x.Type + } + return "" +} + +func (x *Peer) GetState() string { + if x != nil { + return x.State + } + return "" +} + +func (x *Peer) GetNetwork() uint32 { + if x != nil { + return x.Network + } + return 0 +} + +func (x *Peer) GetInitiator() bool { + if x != nil { + return x.Initiator + } + return false +} + +func (x *Peer) GetMinMsFromPeer() int64 { + if x != nil { + return x.MinMsFromPeer + } + return 0 +} + +func (x *Peer) GetMinMsToPeer() int64 { + if x != nil { + return x.MinMsToPeer + } + return 0 +} + +func (x *Peer) GetSlowTrafficCount() int64 { + if x != nil { + return x.SlowTrafficCount + } + return 0 +} + +func (x *Peer) GetMinMsRoundTrip() int64 { + if x != nil { + return x.MinMsRoundTrip + } + return 0 +} + +func (x *Peer) GetAccountId() string { + if x != nil { + return x.AccountId + } + return "" +} + +func (x *Peer) GetAccountTier() string { + if x != nil { + return x.AccountTier + } + return "" +} + +func (x *Peer) GetPort() int64 { + if x != nil { + return x.Port + } + return 0 +} + +type PeersReply struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Peers []*Peer `protobuf:"bytes,1,rep,name=peers,proto3" json:"peers,omitempty"` +} + +func (x *PeersReply) Reset() { + *x = PeersReply{} + if protoimpl.UnsafeEnabled { + mi := &file_gateway_proto_msgTypes[6] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *PeersReply) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*PeersReply) ProtoMessage() {} + +func (x *PeersReply) ProtoReflect() protoreflect.Message { + mi := &file_gateway_proto_msgTypes[6] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use PeersReply.ProtoReflect.Descriptor instead. +func (*PeersReply) Descriptor() ([]byte, []int) { + return file_gateway_proto_rawDescGZIP(), []int{6} +} + +func (x *PeersReply) GetPeers() []*Peer { + if x != nil { + return x.Peers + } + return nil +} + +type SendTXRequest struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields +} + +func (x *SendTXRequest) Reset() { + *x = SendTXRequest{} + if protoimpl.UnsafeEnabled { + mi := &file_gateway_proto_msgTypes[7] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *SendTXRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*SendTXRequest) ProtoMessage() {} + +func (x *SendTXRequest) ProtoReflect() protoreflect.Message { + mi := &file_gateway_proto_msgTypes[7] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use SendTXRequest.ProtoReflect.Descriptor instead. +func (*SendTXRequest) Descriptor() ([]byte, []int) { + return file_gateway_proto_rawDescGZIP(), []int{7} +} + +type Transaction struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Content string `protobuf:"bytes,1,opt,name=content,proto3" json:"content,omitempty"` +} + +func (x *Transaction) Reset() { + *x = Transaction{} + if protoimpl.UnsafeEnabled { + mi := &file_gateway_proto_msgTypes[8] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *Transaction) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*Transaction) ProtoMessage() {} + +func (x *Transaction) ProtoReflect() protoreflect.Message { + mi := &file_gateway_proto_msgTypes[8] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use Transaction.ProtoReflect.Descriptor instead. +func (*Transaction) Descriptor() ([]byte, []int) { + return file_gateway_proto_rawDescGZIP(), []int{8} +} + +func (x *Transaction) GetContent() string { + if x != nil { + return x.Content + } + return "" +} + +type Transactions struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Transactions []*Transaction `protobuf:"bytes,1,rep,name=transactions,proto3" json:"transactions,omitempty"` +} + +func (x *Transactions) Reset() { + *x = Transactions{} + if protoimpl.UnsafeEnabled { + mi := &file_gateway_proto_msgTypes[9] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *Transactions) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*Transactions) ProtoMessage() {} + +func (x *Transactions) ProtoReflect() protoreflect.Message { + mi := &file_gateway_proto_msgTypes[9] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use Transactions.ProtoReflect.Descriptor instead. +func (*Transactions) Descriptor() ([]byte, []int) { + return file_gateway_proto_rawDescGZIP(), []int{9} +} + +func (x *Transactions) GetTransactions() []*Transaction { + if x != nil { + return x.Transactions + } + return nil +} + +type BxTransaction struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Hash string `protobuf:"bytes,1,opt,name=hash,proto3" json:"hash,omitempty"` + ShortIds []uint64 `protobuf:"varint,2,rep,packed,name=short_ids,json=shortIds,proto3" json:"short_ids,omitempty"` + AddTime *timestamppb.Timestamp `protobuf:"bytes,3,opt,name=add_time,json=addTime,proto3" json:"add_time,omitempty"` +} + +func (x *BxTransaction) Reset() { + *x = BxTransaction{} + if protoimpl.UnsafeEnabled { + mi := &file_gateway_proto_msgTypes[10] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *BxTransaction) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*BxTransaction) ProtoMessage() {} + +func (x *BxTransaction) ProtoReflect() protoreflect.Message { + mi := &file_gateway_proto_msgTypes[10] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use BxTransaction.ProtoReflect.Descriptor instead. +func (*BxTransaction) Descriptor() ([]byte, []int) { + return file_gateway_proto_rawDescGZIP(), []int{10} +} + +func (x *BxTransaction) GetHash() string { + if x != nil { + return x.Hash + } + return "" +} + +func (x *BxTransaction) GetShortIds() []uint64 { + if x != nil { + return x.ShortIds + } + return nil +} + +func (x *BxTransaction) GetAddTime() *timestamppb.Timestamp { + if x != nil { + return x.AddTime + } + return nil +} + +type GetBxTransactionRequest struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Hash string `protobuf:"bytes,1,opt,name=hash,proto3" json:"hash,omitempty"` +} + +func (x *GetBxTransactionRequest) Reset() { + *x = GetBxTransactionRequest{} + if protoimpl.UnsafeEnabled { + mi := &file_gateway_proto_msgTypes[11] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *GetBxTransactionRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*GetBxTransactionRequest) ProtoMessage() {} + +func (x *GetBxTransactionRequest) ProtoReflect() protoreflect.Message { + mi := &file_gateway_proto_msgTypes[11] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use GetBxTransactionRequest.ProtoReflect.Descriptor instead. +func (*GetBxTransactionRequest) Descriptor() ([]byte, []int) { + return file_gateway_proto_rawDescGZIP(), []int{11} +} + +func (x *GetBxTransactionRequest) GetHash() string { + if x != nil { + return x.Hash + } + return "" +} + +type GetBxTransactionResponse struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Tx *BxTransaction `protobuf:"bytes,1,opt,name=tx,proto3" json:"tx,omitempty"` +} + +func (x *GetBxTransactionResponse) Reset() { + *x = GetBxTransactionResponse{} + if protoimpl.UnsafeEnabled { + mi := &file_gateway_proto_msgTypes[12] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *GetBxTransactionResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*GetBxTransactionResponse) ProtoMessage() {} + +func (x *GetBxTransactionResponse) ProtoReflect() protoreflect.Message { + mi := &file_gateway_proto_msgTypes[12] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use GetBxTransactionResponse.ProtoReflect.Descriptor instead. +func (*GetBxTransactionResponse) Descriptor() ([]byte, []int) { + return file_gateway_proto_rawDescGZIP(), []int{12} +} + +func (x *GetBxTransactionResponse) GetTx() *BxTransaction { + if x != nil { + return x.Tx + } + return nil +} + +type TxStoreRequest struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields +} + +func (x *TxStoreRequest) Reset() { + *x = TxStoreRequest{} + if protoimpl.UnsafeEnabled { + mi := &file_gateway_proto_msgTypes[13] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *TxStoreRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*TxStoreRequest) ProtoMessage() {} + +func (x *TxStoreRequest) ProtoReflect() protoreflect.Message { + mi := &file_gateway_proto_msgTypes[13] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use TxStoreRequest.ProtoReflect.Descriptor instead. +func (*TxStoreRequest) Descriptor() ([]byte, []int) { + return file_gateway_proto_rawDescGZIP(), []int{13} +} + +type TxStoreNetworkData struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Network uint64 `protobuf:"varint,4,opt,name=network,proto3" json:"network,omitempty"` + TxCount uint64 `protobuf:"varint,1,opt,name=tx_count,json=txCount,proto3" json:"tx_count,omitempty"` + ShortIdCount uint64 `protobuf:"varint,2,opt,name=short_id_count,json=shortIdCount,proto3" json:"short_id_count,omitempty"` + OldestTx *BxTransaction `protobuf:"bytes,3,opt,name=oldest_tx,json=oldestTx,proto3" json:"oldest_tx,omitempty"` +} + +func (x *TxStoreNetworkData) Reset() { + *x = TxStoreNetworkData{} + if protoimpl.UnsafeEnabled { + mi := &file_gateway_proto_msgTypes[14] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *TxStoreNetworkData) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*TxStoreNetworkData) ProtoMessage() {} + +func (x *TxStoreNetworkData) ProtoReflect() protoreflect.Message { + mi := &file_gateway_proto_msgTypes[14] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use TxStoreNetworkData.ProtoReflect.Descriptor instead. +func (*TxStoreNetworkData) Descriptor() ([]byte, []int) { + return file_gateway_proto_rawDescGZIP(), []int{14} +} + +func (x *TxStoreNetworkData) GetNetwork() uint64 { + if x != nil { + return x.Network + } + return 0 +} + +func (x *TxStoreNetworkData) GetTxCount() uint64 { + if x != nil { + return x.TxCount + } + return 0 +} + +func (x *TxStoreNetworkData) GetShortIdCount() uint64 { + if x != nil { + return x.ShortIdCount + } + return 0 +} + +func (x *TxStoreNetworkData) GetOldestTx() *BxTransaction { + if x != nil { + return x.OldestTx + } + return nil +} + +type TxStoreReply struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + TxCount uint64 `protobuf:"varint,1,opt,name=tx_count,json=txCount,proto3" json:"tx_count,omitempty"` + ShortIdCount uint64 `protobuf:"varint,2,opt,name=short_id_count,json=shortIdCount,proto3" json:"short_id_count,omitempty"` + NetworkData []*TxStoreNetworkData `protobuf:"bytes,3,rep,name=network_data,json=networkData,proto3" json:"network_data,omitempty"` +} + +func (x *TxStoreReply) Reset() { + *x = TxStoreReply{} + if protoimpl.UnsafeEnabled { + mi := &file_gateway_proto_msgTypes[15] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *TxStoreReply) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*TxStoreReply) ProtoMessage() {} + +func (x *TxStoreReply) ProtoReflect() protoreflect.Message { + mi := &file_gateway_proto_msgTypes[15] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use TxStoreReply.ProtoReflect.Descriptor instead. +func (*TxStoreReply) Descriptor() ([]byte, []int) { + return file_gateway_proto_rawDescGZIP(), []int{15} +} + +func (x *TxStoreReply) GetTxCount() uint64 { + if x != nil { + return x.TxCount + } + return 0 +} + +func (x *TxStoreReply) GetShortIdCount() uint64 { + if x != nil { + return x.ShortIdCount + } + return 0 +} + +func (x *TxStoreReply) GetNetworkData() []*TxStoreNetworkData { + if x != nil { + return x.NetworkData + } + return nil +} + +type BlxrTxRequest struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Transaction string `protobuf:"bytes,1,opt,name=transaction,proto3" json:"transaction,omitempty"` +} + +func (x *BlxrTxRequest) Reset() { + *x = BlxrTxRequest{} + if protoimpl.UnsafeEnabled { + mi := &file_gateway_proto_msgTypes[16] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *BlxrTxRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*BlxrTxRequest) ProtoMessage() {} + +func (x *BlxrTxRequest) ProtoReflect() protoreflect.Message { + mi := &file_gateway_proto_msgTypes[16] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use BlxrTxRequest.ProtoReflect.Descriptor instead. +func (*BlxrTxRequest) Descriptor() ([]byte, []int) { + return file_gateway_proto_rawDescGZIP(), []int{16} +} + +func (x *BlxrTxRequest) GetTransaction() string { + if x != nil { + return x.Transaction + } + return "" +} + +type BlxrTxReply struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + TxHash string `protobuf:"bytes,1,opt,name=tx_hash,json=txHash,proto3" json:"tx_hash,omitempty"` +} + +func (x *BlxrTxReply) Reset() { + *x = BlxrTxReply{} + if protoimpl.UnsafeEnabled { + mi := &file_gateway_proto_msgTypes[17] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *BlxrTxReply) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*BlxrTxReply) ProtoMessage() {} + +func (x *BlxrTxReply) ProtoReflect() protoreflect.Message { + mi := &file_gateway_proto_msgTypes[17] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use BlxrTxReply.ProtoReflect.Descriptor instead. +func (*BlxrTxReply) Descriptor() ([]byte, []int) { + return file_gateway_proto_rawDescGZIP(), []int{17} +} + +func (x *BlxrTxReply) GetTxHash() string { + if x != nil { + return x.TxHash + } + return "" +} + +var File_gateway_proto protoreflect.FileDescriptor + +var file_gateway_proto_rawDesc = []byte{ + 0x0a, 0x0d, 0x67, 0x61, 0x74, 0x65, 0x77, 0x61, 0x79, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12, + 0x07, 0x67, 0x61, 0x74, 0x65, 0x77, 0x61, 0x79, 0x1a, 0x1f, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, + 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2f, 0x74, 0x69, 0x6d, 0x65, 0x73, 0x74, + 0x61, 0x6d, 0x70, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x22, 0x10, 0x0a, 0x0e, 0x56, 0x65, 0x72, + 0x73, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x22, 0x47, 0x0a, 0x0c, 0x56, + 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x70, 0x6c, 0x79, 0x12, 0x18, 0x0a, 0x07, 0x76, + 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x76, 0x65, + 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x12, 0x1d, 0x0a, 0x0a, 0x62, 0x75, 0x69, 0x6c, 0x64, 0x5f, 0x64, + 0x61, 0x74, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x62, 0x75, 0x69, 0x6c, 0x64, + 0x44, 0x61, 0x74, 0x65, 0x22, 0x0d, 0x0a, 0x0b, 0x53, 0x74, 0x6f, 0x70, 0x52, 0x65, 0x71, 0x75, + 0x65, 0x73, 0x74, 0x22, 0x0b, 0x0a, 0x09, 0x53, 0x74, 0x6f, 0x70, 0x52, 0x65, 0x70, 0x6c, 0x79, + 0x22, 0x22, 0x0a, 0x0c, 0x50, 0x65, 0x65, 0x72, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, + 0x12, 0x12, 0x0a, 0x04, 0x74, 0x79, 0x70, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, + 0x74, 0x79, 0x70, 0x65, 0x22, 0x8e, 0x03, 0x0a, 0x04, 0x50, 0x65, 0x65, 0x72, 0x12, 0x0e, 0x0a, + 0x02, 0x69, 0x70, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, 0x69, 0x70, 0x12, 0x17, 0x0a, + 0x07, 0x6e, 0x6f, 0x64, 0x65, 0x5f, 0x69, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, + 0x6e, 0x6f, 0x64, 0x65, 0x49, 0x64, 0x12, 0x12, 0x0a, 0x04, 0x74, 0x79, 0x70, 0x65, 0x18, 0x03, + 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x74, 0x79, 0x70, 0x65, 0x12, 0x14, 0x0a, 0x05, 0x73, 0x74, + 0x61, 0x74, 0x65, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x73, 0x74, 0x61, 0x74, 0x65, + 0x12, 0x18, 0x0a, 0x07, 0x6e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x18, 0x05, 0x20, 0x01, 0x28, + 0x0d, 0x52, 0x07, 0x6e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x12, 0x1c, 0x0a, 0x09, 0x69, 0x6e, + 0x69, 0x74, 0x69, 0x61, 0x74, 0x6f, 0x72, 0x18, 0x06, 0x20, 0x01, 0x28, 0x08, 0x52, 0x09, 0x69, + 0x6e, 0x69, 0x74, 0x69, 0x61, 0x74, 0x6f, 0x72, 0x12, 0x27, 0x0a, 0x10, 0x6d, 0x69, 0x6e, 0x5f, + 0x6d, 0x73, 0x5f, 0x66, 0x72, 0x6f, 0x6d, 0x5f, 0x70, 0x65, 0x65, 0x72, 0x18, 0x07, 0x20, 0x01, + 0x28, 0x03, 0x52, 0x0d, 0x6d, 0x69, 0x6e, 0x4d, 0x73, 0x46, 0x72, 0x6f, 0x6d, 0x50, 0x65, 0x65, + 0x72, 0x12, 0x23, 0x0a, 0x0e, 0x6d, 0x69, 0x6e, 0x5f, 0x6d, 0x73, 0x5f, 0x74, 0x6f, 0x5f, 0x70, + 0x65, 0x65, 0x72, 0x18, 0x08, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0b, 0x6d, 0x69, 0x6e, 0x4d, 0x73, + 0x54, 0x6f, 0x50, 0x65, 0x65, 0x72, 0x12, 0x2c, 0x0a, 0x12, 0x73, 0x6c, 0x6f, 0x77, 0x5f, 0x74, + 0x72, 0x61, 0x66, 0x66, 0x69, 0x63, 0x5f, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x18, 0x09, 0x20, 0x01, + 0x28, 0x03, 0x52, 0x10, 0x73, 0x6c, 0x6f, 0x77, 0x54, 0x72, 0x61, 0x66, 0x66, 0x69, 0x63, 0x43, + 0x6f, 0x75, 0x6e, 0x74, 0x12, 0x29, 0x0a, 0x11, 0x6d, 0x69, 0x6e, 0x5f, 0x6d, 0x73, 0x5f, 0x72, + 0x6f, 0x75, 0x6e, 0x64, 0x5f, 0x74, 0x72, 0x69, 0x70, 0x18, 0x0a, 0x20, 0x01, 0x28, 0x03, 0x52, + 0x0e, 0x6d, 0x69, 0x6e, 0x4d, 0x73, 0x52, 0x6f, 0x75, 0x6e, 0x64, 0x54, 0x72, 0x69, 0x70, 0x12, + 0x1d, 0x0a, 0x0a, 0x61, 0x63, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x5f, 0x69, 0x64, 0x18, 0x0b, 0x20, + 0x01, 0x28, 0x09, 0x52, 0x09, 0x61, 0x63, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x49, 0x64, 0x12, 0x21, + 0x0a, 0x0c, 0x61, 0x63, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x5f, 0x74, 0x69, 0x65, 0x72, 0x18, 0x0c, + 0x20, 0x01, 0x28, 0x09, 0x52, 0x0b, 0x61, 0x63, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x54, 0x69, 0x65, + 0x72, 0x12, 0x12, 0x0a, 0x04, 0x70, 0x6f, 0x72, 0x74, 0x18, 0x0d, 0x20, 0x01, 0x28, 0x03, 0x52, + 0x04, 0x70, 0x6f, 0x72, 0x74, 0x22, 0x31, 0x0a, 0x0a, 0x50, 0x65, 0x65, 0x72, 0x73, 0x52, 0x65, + 0x70, 0x6c, 0x79, 0x12, 0x23, 0x0a, 0x05, 0x70, 0x65, 0x65, 0x72, 0x73, 0x18, 0x01, 0x20, 0x03, + 0x28, 0x0b, 0x32, 0x0d, 0x2e, 0x67, 0x61, 0x74, 0x65, 0x77, 0x61, 0x79, 0x2e, 0x50, 0x65, 0x65, + 0x72, 0x52, 0x05, 0x70, 0x65, 0x65, 0x72, 0x73, 0x22, 0x0f, 0x0a, 0x0d, 0x53, 0x65, 0x6e, 0x64, + 0x54, 0x58, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x22, 0x27, 0x0a, 0x0b, 0x54, 0x72, 0x61, + 0x6e, 0x73, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x18, 0x0a, 0x07, 0x63, 0x6f, 0x6e, 0x74, + 0x65, 0x6e, 0x74, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x63, 0x6f, 0x6e, 0x74, 0x65, + 0x6e, 0x74, 0x22, 0x48, 0x0a, 0x0c, 0x54, 0x72, 0x61, 0x6e, 0x73, 0x61, 0x63, 0x74, 0x69, 0x6f, + 0x6e, 0x73, 0x12, 0x38, 0x0a, 0x0c, 0x74, 0x72, 0x61, 0x6e, 0x73, 0x61, 0x63, 0x74, 0x69, 0x6f, + 0x6e, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x14, 0x2e, 0x67, 0x61, 0x74, 0x65, 0x77, + 0x61, 0x79, 0x2e, 0x54, 0x72, 0x61, 0x6e, 0x73, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x0c, + 0x74, 0x72, 0x61, 0x6e, 0x73, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x22, 0x77, 0x0a, 0x0d, + 0x42, 0x78, 0x54, 0x72, 0x61, 0x6e, 0x73, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x12, 0x0a, + 0x04, 0x68, 0x61, 0x73, 0x68, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x68, 0x61, 0x73, + 0x68, 0x12, 0x1b, 0x0a, 0x09, 0x73, 0x68, 0x6f, 0x72, 0x74, 0x5f, 0x69, 0x64, 0x73, 0x18, 0x02, + 0x20, 0x03, 0x28, 0x04, 0x52, 0x08, 0x73, 0x68, 0x6f, 0x72, 0x74, 0x49, 0x64, 0x73, 0x12, 0x35, + 0x0a, 0x08, 0x61, 0x64, 0x64, 0x5f, 0x74, 0x69, 0x6d, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, + 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, + 0x75, 0x66, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x52, 0x07, 0x61, 0x64, + 0x64, 0x54, 0x69, 0x6d, 0x65, 0x22, 0x2d, 0x0a, 0x17, 0x47, 0x65, 0x74, 0x42, 0x78, 0x54, 0x72, + 0x61, 0x6e, 0x73, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, + 0x12, 0x12, 0x0a, 0x04, 0x68, 0x61, 0x73, 0x68, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, + 0x68, 0x61, 0x73, 0x68, 0x22, 0x42, 0x0a, 0x18, 0x47, 0x65, 0x74, 0x42, 0x78, 0x54, 0x72, 0x61, + 0x6e, 0x73, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, + 0x12, 0x26, 0x0a, 0x02, 0x74, 0x78, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x16, 0x2e, 0x67, + 0x61, 0x74, 0x65, 0x77, 0x61, 0x79, 0x2e, 0x42, 0x78, 0x54, 0x72, 0x61, 0x6e, 0x73, 0x61, 0x63, + 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x02, 0x74, 0x78, 0x22, 0x10, 0x0a, 0x0e, 0x54, 0x78, 0x53, 0x74, + 0x6f, 0x72, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x22, 0xa4, 0x01, 0x0a, 0x12, 0x54, + 0x78, 0x53, 0x74, 0x6f, 0x72, 0x65, 0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x44, 0x61, 0x74, + 0x61, 0x12, 0x18, 0x0a, 0x07, 0x6e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x18, 0x04, 0x20, 0x01, + 0x28, 0x04, 0x52, 0x07, 0x6e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x12, 0x19, 0x0a, 0x08, 0x74, + 0x78, 0x5f, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x18, 0x01, 0x20, 0x01, 0x28, 0x04, 0x52, 0x07, 0x74, + 0x78, 0x43, 0x6f, 0x75, 0x6e, 0x74, 0x12, 0x24, 0x0a, 0x0e, 0x73, 0x68, 0x6f, 0x72, 0x74, 0x5f, + 0x69, 0x64, 0x5f, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x18, 0x02, 0x20, 0x01, 0x28, 0x04, 0x52, 0x0c, + 0x73, 0x68, 0x6f, 0x72, 0x74, 0x49, 0x64, 0x43, 0x6f, 0x75, 0x6e, 0x74, 0x12, 0x33, 0x0a, 0x09, + 0x6f, 0x6c, 0x64, 0x65, 0x73, 0x74, 0x5f, 0x74, 0x78, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, + 0x16, 0x2e, 0x67, 0x61, 0x74, 0x65, 0x77, 0x61, 0x79, 0x2e, 0x42, 0x78, 0x54, 0x72, 0x61, 0x6e, + 0x73, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x08, 0x6f, 0x6c, 0x64, 0x65, 0x73, 0x74, 0x54, + 0x78, 0x22, 0x8f, 0x01, 0x0a, 0x0c, 0x54, 0x78, 0x53, 0x74, 0x6f, 0x72, 0x65, 0x52, 0x65, 0x70, + 0x6c, 0x79, 0x12, 0x19, 0x0a, 0x08, 0x74, 0x78, 0x5f, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x18, 0x01, + 0x20, 0x01, 0x28, 0x04, 0x52, 0x07, 0x74, 0x78, 0x43, 0x6f, 0x75, 0x6e, 0x74, 0x12, 0x24, 0x0a, + 0x0e, 0x73, 0x68, 0x6f, 0x72, 0x74, 0x5f, 0x69, 0x64, 0x5f, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x18, + 0x02, 0x20, 0x01, 0x28, 0x04, 0x52, 0x0c, 0x73, 0x68, 0x6f, 0x72, 0x74, 0x49, 0x64, 0x43, 0x6f, + 0x75, 0x6e, 0x74, 0x12, 0x3e, 0x0a, 0x0c, 0x6e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x5f, 0x64, + 0x61, 0x74, 0x61, 0x18, 0x03, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x1b, 0x2e, 0x67, 0x61, 0x74, 0x65, + 0x77, 0x61, 0x79, 0x2e, 0x54, 0x78, 0x53, 0x74, 0x6f, 0x72, 0x65, 0x4e, 0x65, 0x74, 0x77, 0x6f, + 0x72, 0x6b, 0x44, 0x61, 0x74, 0x61, 0x52, 0x0b, 0x6e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x44, + 0x61, 0x74, 0x61, 0x22, 0x31, 0x0a, 0x0d, 0x42, 0x6c, 0x78, 0x72, 0x54, 0x78, 0x52, 0x65, 0x71, + 0x75, 0x65, 0x73, 0x74, 0x12, 0x20, 0x0a, 0x0b, 0x74, 0x72, 0x61, 0x6e, 0x73, 0x61, 0x63, 0x74, + 0x69, 0x6f, 0x6e, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0b, 0x74, 0x72, 0x61, 0x6e, 0x73, + 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x22, 0x26, 0x0a, 0x0b, 0x42, 0x6c, 0x78, 0x72, 0x54, 0x78, + 0x52, 0x65, 0x70, 0x6c, 0x79, 0x12, 0x17, 0x0a, 0x07, 0x74, 0x78, 0x5f, 0x68, 0x61, 0x73, 0x68, + 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x74, 0x78, 0x48, 0x61, 0x73, 0x68, 0x32, 0xff, + 0x02, 0x0a, 0x07, 0x47, 0x61, 0x74, 0x65, 0x77, 0x61, 0x79, 0x12, 0x38, 0x0a, 0x06, 0x42, 0x6c, + 0x78, 0x72, 0x54, 0x78, 0x12, 0x16, 0x2e, 0x67, 0x61, 0x74, 0x65, 0x77, 0x61, 0x79, 0x2e, 0x42, + 0x6c, 0x78, 0x72, 0x54, 0x78, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x14, 0x2e, 0x67, + 0x61, 0x74, 0x65, 0x77, 0x61, 0x79, 0x2e, 0x42, 0x6c, 0x78, 0x72, 0x54, 0x78, 0x52, 0x65, 0x70, + 0x6c, 0x79, 0x22, 0x00, 0x12, 0x35, 0x0a, 0x05, 0x50, 0x65, 0x65, 0x72, 0x73, 0x12, 0x15, 0x2e, + 0x67, 0x61, 0x74, 0x65, 0x77, 0x61, 0x79, 0x2e, 0x50, 0x65, 0x65, 0x72, 0x73, 0x52, 0x65, 0x71, + 0x75, 0x65, 0x73, 0x74, 0x1a, 0x13, 0x2e, 0x67, 0x61, 0x74, 0x65, 0x77, 0x61, 0x79, 0x2e, 0x50, + 0x65, 0x65, 0x72, 0x73, 0x52, 0x65, 0x70, 0x6c, 0x79, 0x22, 0x00, 0x12, 0x42, 0x0a, 0x0e, 0x54, + 0x78, 0x53, 0x74, 0x6f, 0x72, 0x65, 0x53, 0x75, 0x6d, 0x6d, 0x61, 0x72, 0x79, 0x12, 0x17, 0x2e, + 0x67, 0x61, 0x74, 0x65, 0x77, 0x61, 0x79, 0x2e, 0x54, 0x78, 0x53, 0x74, 0x6f, 0x72, 0x65, 0x52, + 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x15, 0x2e, 0x67, 0x61, 0x74, 0x65, 0x77, 0x61, 0x79, + 0x2e, 0x54, 0x78, 0x53, 0x74, 0x6f, 0x72, 0x65, 0x52, 0x65, 0x70, 0x6c, 0x79, 0x22, 0x00, 0x12, + 0x4e, 0x0a, 0x05, 0x47, 0x65, 0x74, 0x54, 0x78, 0x12, 0x20, 0x2e, 0x67, 0x61, 0x74, 0x65, 0x77, + 0x61, 0x79, 0x2e, 0x47, 0x65, 0x74, 0x42, 0x78, 0x54, 0x72, 0x61, 0x6e, 0x73, 0x61, 0x63, 0x74, + 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x21, 0x2e, 0x67, 0x61, 0x74, + 0x65, 0x77, 0x61, 0x79, 0x2e, 0x47, 0x65, 0x74, 0x42, 0x78, 0x54, 0x72, 0x61, 0x6e, 0x73, 0x61, + 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, + 0x32, 0x0a, 0x04, 0x53, 0x74, 0x6f, 0x70, 0x12, 0x14, 0x2e, 0x67, 0x61, 0x74, 0x65, 0x77, 0x61, + 0x79, 0x2e, 0x53, 0x74, 0x6f, 0x70, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x12, 0x2e, + 0x67, 0x61, 0x74, 0x65, 0x77, 0x61, 0x79, 0x2e, 0x53, 0x74, 0x6f, 0x70, 0x52, 0x65, 0x70, 0x6c, + 0x79, 0x22, 0x00, 0x12, 0x3b, 0x0a, 0x07, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x12, 0x17, + 0x2e, 0x67, 0x61, 0x74, 0x65, 0x77, 0x61, 0x79, 0x2e, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, + 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x15, 0x2e, 0x67, 0x61, 0x74, 0x65, 0x77, 0x61, + 0x79, 0x2e, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x70, 0x6c, 0x79, 0x22, 0x00, + 0x42, 0x42, 0x5a, 0x40, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x62, + 0x6c, 0x6f, 0x58, 0x72, 0x6f, 0x75, 0x74, 0x65, 0x2d, 0x4c, 0x61, 0x62, 0x73, 0x2f, 0x62, 0x78, + 0x67, 0x61, 0x74, 0x65, 0x77, 0x61, 0x79, 0x2d, 0x70, 0x72, 0x69, 0x76, 0x61, 0x74, 0x65, 0x2d, + 0x67, 0x6f, 0x2f, 0x62, 0x78, 0x67, 0x61, 0x74, 0x65, 0x77, 0x61, 0x79, 0x2f, 0x67, 0x61, 0x74, + 0x65, 0x77, 0x61, 0x79, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, +} + +var ( + file_gateway_proto_rawDescOnce sync.Once + file_gateway_proto_rawDescData = file_gateway_proto_rawDesc +) + +func file_gateway_proto_rawDescGZIP() []byte { + file_gateway_proto_rawDescOnce.Do(func() { + file_gateway_proto_rawDescData = protoimpl.X.CompressGZIP(file_gateway_proto_rawDescData) + }) + return file_gateway_proto_rawDescData +} + +var file_gateway_proto_msgTypes = make([]protoimpl.MessageInfo, 18) +var file_gateway_proto_goTypes = []interface{}{ + (*VersionRequest)(nil), // 0: gateway.VersionRequest + (*VersionReply)(nil), // 1: gateway.VersionReply + (*StopRequest)(nil), // 2: gateway.StopRequest + (*StopReply)(nil), // 3: gateway.StopReply + (*PeersRequest)(nil), // 4: gateway.PeersRequest + (*Peer)(nil), // 5: gateway.Peer + (*PeersReply)(nil), // 6: gateway.PeersReply + (*SendTXRequest)(nil), // 7: gateway.SendTXRequest + (*Transaction)(nil), // 8: gateway.Transaction + (*Transactions)(nil), // 9: gateway.Transactions + (*BxTransaction)(nil), // 10: gateway.BxTransaction + (*GetBxTransactionRequest)(nil), // 11: gateway.GetBxTransactionRequest + (*GetBxTransactionResponse)(nil), // 12: gateway.GetBxTransactionResponse + (*TxStoreRequest)(nil), // 13: gateway.TxStoreRequest + (*TxStoreNetworkData)(nil), // 14: gateway.TxStoreNetworkData + (*TxStoreReply)(nil), // 15: gateway.TxStoreReply + (*BlxrTxRequest)(nil), // 16: gateway.BlxrTxRequest + (*BlxrTxReply)(nil), // 17: gateway.BlxrTxReply + (*timestamppb.Timestamp)(nil), // 18: google.protobuf.Timestamp +} +var file_gateway_proto_depIdxs = []int32{ + 5, // 0: gateway.PeersReply.peers:type_name -> gateway.Peer + 8, // 1: gateway.Transactions.transactions:type_name -> gateway.Transaction + 18, // 2: gateway.BxTransaction.add_time:type_name -> google.protobuf.Timestamp + 10, // 3: gateway.GetBxTransactionResponse.tx:type_name -> gateway.BxTransaction + 10, // 4: gateway.TxStoreNetworkData.oldest_tx:type_name -> gateway.BxTransaction + 14, // 5: gateway.TxStoreReply.network_data:type_name -> gateway.TxStoreNetworkData + 16, // 6: gateway.Gateway.BlxrTx:input_type -> gateway.BlxrTxRequest + 4, // 7: gateway.Gateway.Peers:input_type -> gateway.PeersRequest + 13, // 8: gateway.Gateway.TxStoreSummary:input_type -> gateway.TxStoreRequest + 11, // 9: gateway.Gateway.GetTx:input_type -> gateway.GetBxTransactionRequest + 2, // 10: gateway.Gateway.Stop:input_type -> gateway.StopRequest + 0, // 11: gateway.Gateway.Version:input_type -> gateway.VersionRequest + 17, // 12: gateway.Gateway.BlxrTx:output_type -> gateway.BlxrTxReply + 6, // 13: gateway.Gateway.Peers:output_type -> gateway.PeersReply + 15, // 14: gateway.Gateway.TxStoreSummary:output_type -> gateway.TxStoreReply + 12, // 15: gateway.Gateway.GetTx:output_type -> gateway.GetBxTransactionResponse + 3, // 16: gateway.Gateway.Stop:output_type -> gateway.StopReply + 1, // 17: gateway.Gateway.Version:output_type -> gateway.VersionReply + 12, // [12:18] is the sub-list for method output_type + 6, // [6:12] is the sub-list for method input_type + 6, // [6:6] is the sub-list for extension type_name + 6, // [6:6] is the sub-list for extension extendee + 0, // [0:6] is the sub-list for field type_name +} + +func init() { file_gateway_proto_init() } +func file_gateway_proto_init() { + if File_gateway_proto != nil { + return + } + if !protoimpl.UnsafeEnabled { + file_gateway_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*VersionRequest); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_gateway_proto_msgTypes[1].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*VersionReply); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_gateway_proto_msgTypes[2].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*StopRequest); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_gateway_proto_msgTypes[3].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*StopReply); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_gateway_proto_msgTypes[4].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*PeersRequest); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_gateway_proto_msgTypes[5].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*Peer); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_gateway_proto_msgTypes[6].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*PeersReply); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_gateway_proto_msgTypes[7].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*SendTXRequest); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_gateway_proto_msgTypes[8].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*Transaction); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_gateway_proto_msgTypes[9].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*Transactions); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_gateway_proto_msgTypes[10].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*BxTransaction); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_gateway_proto_msgTypes[11].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*GetBxTransactionRequest); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_gateway_proto_msgTypes[12].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*GetBxTransactionResponse); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_gateway_proto_msgTypes[13].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*TxStoreRequest); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_gateway_proto_msgTypes[14].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*TxStoreNetworkData); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_gateway_proto_msgTypes[15].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*TxStoreReply); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_gateway_proto_msgTypes[16].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*BlxrTxRequest); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_gateway_proto_msgTypes[17].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*BlxrTxReply); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + } + type x struct{} + out := protoimpl.TypeBuilder{ + File: protoimpl.DescBuilder{ + GoPackagePath: reflect.TypeOf(x{}).PkgPath(), + RawDescriptor: file_gateway_proto_rawDesc, + NumEnums: 0, + NumMessages: 18, + NumExtensions: 0, + NumServices: 1, + }, + GoTypes: file_gateway_proto_goTypes, + DependencyIndexes: file_gateway_proto_depIdxs, + MessageInfos: file_gateway_proto_msgTypes, + }.Build() + File_gateway_proto = out.File + file_gateway_proto_rawDesc = nil + file_gateway_proto_goTypes = nil + file_gateway_proto_depIdxs = nil +} diff --git a/protobuf/gateway.proto b/protobuf/gateway.proto new file mode 100644 index 0000000..c29c283 --- /dev/null +++ b/protobuf/gateway.proto @@ -0,0 +1,103 @@ +syntax = "proto3"; +import "google/protobuf/timestamp.proto"; + +option go_package = "github.com/bloXroute-Labs/bxgateway-private-go/bxgateway/gateway"; + +// The gateway service definition. +package gateway; +service Gateway { + rpc BlxrTx (BlxrTxRequest) returns (BlxrTxReply) {} + rpc Peers (PeersRequest) returns (PeersReply) {} + rpc TxStoreSummary(TxStoreRequest) returns (TxStoreReply) {} + rpc GetTx(GetBxTransactionRequest) returns (GetBxTransactionResponse) {} + rpc Stop (StopRequest) returns (StopReply){} + rpc Version (VersionRequest) returns (VersionReply){} +} + +message VersionRequest{ +} + +message VersionReply{ + string version = 1; + string build_date =2; +} + +message StopRequest{ +} + +message StopReply{ +} + +message PeersRequest { + string type = 1; +} + +message Peer { + string ip = 1; + string node_id = 2; + string type = 3; + string state = 4; + uint32 network = 5; + bool initiator = 6; + int64 min_ms_from_peer = 7; + int64 min_ms_to_peer = 8; + int64 slow_traffic_count = 9; + int64 min_ms_round_trip = 10; + string account_id = 11; + string account_tier = 12; + int64 port = 13; +} + + +message PeersReply { + repeated Peer peers = 1; +} + +message SendTXRequest{ +} + +message Transaction { + string content = 1; +} + +message Transactions { + repeated Transaction transactions = 1; +} + +message BxTransaction { + string hash = 1; + repeated uint64 short_ids = 2; + google.protobuf.Timestamp add_time = 3; +} + +message GetBxTransactionRequest { + string hash = 1; +} + +message GetBxTransactionResponse { + BxTransaction tx = 1; +} + +message TxStoreRequest {} + +message TxStoreNetworkData{ + uint64 network = 4; + uint64 tx_count = 1; + uint64 short_id_count = 2; + BxTransaction oldest_tx = 3; +} + +message TxStoreReply { + uint64 tx_count = 1; + uint64 short_id_count = 2; + repeated TxStoreNetworkData network_data = 3; + +} + +message BlxrTxRequest { + string transaction = 1; +} + +message BlxrTxReply { + string tx_hash = 1; +} diff --git a/protobuf/gateway_grpc.pb.go b/protobuf/gateway_grpc.pb.go new file mode 100644 index 0000000..5dd7182 --- /dev/null +++ b/protobuf/gateway_grpc.pb.go @@ -0,0 +1,281 @@ +// Code generated by protoc-gen-go-grpc. DO NOT EDIT. + +package gateway + +import ( + context "context" + grpc "google.golang.org/grpc" + codes "google.golang.org/grpc/codes" + status "google.golang.org/grpc/status" +) + +// This is a compile-time assertion to ensure that this generated file +// is compatible with the grpc package it is being compiled against. +// Requires gRPC-Go v1.32.0 or later. +const _ = grpc.SupportPackageIsVersion7 + +// GatewayClient is the client API for Gateway service. +// +// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream. +type GatewayClient interface { + BlxrTx(ctx context.Context, in *BlxrTxRequest, opts ...grpc.CallOption) (*BlxrTxReply, error) + Peers(ctx context.Context, in *PeersRequest, opts ...grpc.CallOption) (*PeersReply, error) + TxStoreSummary(ctx context.Context, in *TxStoreRequest, opts ...grpc.CallOption) (*TxStoreReply, error) + GetTx(ctx context.Context, in *GetBxTransactionRequest, opts ...grpc.CallOption) (*GetBxTransactionResponse, error) + Stop(ctx context.Context, in *StopRequest, opts ...grpc.CallOption) (*StopReply, error) + Version(ctx context.Context, in *VersionRequest, opts ...grpc.CallOption) (*VersionReply, error) +} + +type gatewayClient struct { + cc grpc.ClientConnInterface +} + +func NewGatewayClient(cc grpc.ClientConnInterface) GatewayClient { + return &gatewayClient{cc} +} + +func (c *gatewayClient) BlxrTx(ctx context.Context, in *BlxrTxRequest, opts ...grpc.CallOption) (*BlxrTxReply, error) { + out := new(BlxrTxReply) + err := c.cc.Invoke(ctx, "/gateway.Gateway/BlxrTx", in, out, opts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *gatewayClient) Peers(ctx context.Context, in *PeersRequest, opts ...grpc.CallOption) (*PeersReply, error) { + out := new(PeersReply) + err := c.cc.Invoke(ctx, "/gateway.Gateway/Peers", in, out, opts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *gatewayClient) TxStoreSummary(ctx context.Context, in *TxStoreRequest, opts ...grpc.CallOption) (*TxStoreReply, error) { + out := new(TxStoreReply) + err := c.cc.Invoke(ctx, "/gateway.Gateway/TxStoreSummary", in, out, opts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *gatewayClient) GetTx(ctx context.Context, in *GetBxTransactionRequest, opts ...grpc.CallOption) (*GetBxTransactionResponse, error) { + out := new(GetBxTransactionResponse) + err := c.cc.Invoke(ctx, "/gateway.Gateway/GetTx", in, out, opts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *gatewayClient) Stop(ctx context.Context, in *StopRequest, opts ...grpc.CallOption) (*StopReply, error) { + out := new(StopReply) + err := c.cc.Invoke(ctx, "/gateway.Gateway/Stop", in, out, opts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *gatewayClient) Version(ctx context.Context, in *VersionRequest, opts ...grpc.CallOption) (*VersionReply, error) { + out := new(VersionReply) + err := c.cc.Invoke(ctx, "/gateway.Gateway/Version", in, out, opts...) + if err != nil { + return nil, err + } + return out, nil +} + +// GatewayServer is the server API for Gateway service. +// All implementations must embed UnimplementedGatewayServer +// for forward compatibility +type GatewayServer interface { + BlxrTx(context.Context, *BlxrTxRequest) (*BlxrTxReply, error) + Peers(context.Context, *PeersRequest) (*PeersReply, error) + TxStoreSummary(context.Context, *TxStoreRequest) (*TxStoreReply, error) + GetTx(context.Context, *GetBxTransactionRequest) (*GetBxTransactionResponse, error) + Stop(context.Context, *StopRequest) (*StopReply, error) + Version(context.Context, *VersionRequest) (*VersionReply, error) + mustEmbedUnimplementedGatewayServer() +} + +// UnimplementedGatewayServer must be embedded to have forward compatible implementations. +type UnimplementedGatewayServer struct { +} + +func (UnimplementedGatewayServer) BlxrTx(context.Context, *BlxrTxRequest) (*BlxrTxReply, error) { + return nil, status.Errorf(codes.Unimplemented, "method BlxrTx not implemented") +} +func (UnimplementedGatewayServer) Peers(context.Context, *PeersRequest) (*PeersReply, error) { + return nil, status.Errorf(codes.Unimplemented, "method Peers not implemented") +} +func (UnimplementedGatewayServer) TxStoreSummary(context.Context, *TxStoreRequest) (*TxStoreReply, error) { + return nil, status.Errorf(codes.Unimplemented, "method TxStoreSummary not implemented") +} +func (UnimplementedGatewayServer) GetTx(context.Context, *GetBxTransactionRequest) (*GetBxTransactionResponse, error) { + return nil, status.Errorf(codes.Unimplemented, "method GetTx not implemented") +} +func (UnimplementedGatewayServer) Stop(context.Context, *StopRequest) (*StopReply, error) { + return nil, status.Errorf(codes.Unimplemented, "method Stop not implemented") +} +func (UnimplementedGatewayServer) Version(context.Context, *VersionRequest) (*VersionReply, error) { + return nil, status.Errorf(codes.Unimplemented, "method Version not implemented") +} +func (UnimplementedGatewayServer) mustEmbedUnimplementedGatewayServer() {} + +// UnsafeGatewayServer may be embedded to opt out of forward compatibility for this service. +// Use of this interface is not recommended, as added methods to GatewayServer will +// result in compilation errors. +type UnsafeGatewayServer interface { + mustEmbedUnimplementedGatewayServer() +} + +func RegisterGatewayServer(s grpc.ServiceRegistrar, srv GatewayServer) { + s.RegisterService(&Gateway_ServiceDesc, srv) +} + +func _Gateway_BlxrTx_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(BlxrTxRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(GatewayServer).BlxrTx(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: "/gateway.Gateway/BlxrTx", + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(GatewayServer).BlxrTx(ctx, req.(*BlxrTxRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _Gateway_Peers_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(PeersRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(GatewayServer).Peers(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: "/gateway.Gateway/Peers", + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(GatewayServer).Peers(ctx, req.(*PeersRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _Gateway_TxStoreSummary_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(TxStoreRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(GatewayServer).TxStoreSummary(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: "/gateway.Gateway/TxStoreSummary", + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(GatewayServer).TxStoreSummary(ctx, req.(*TxStoreRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _Gateway_GetTx_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(GetBxTransactionRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(GatewayServer).GetTx(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: "/gateway.Gateway/GetTx", + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(GatewayServer).GetTx(ctx, req.(*GetBxTransactionRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _Gateway_Stop_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(StopRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(GatewayServer).Stop(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: "/gateway.Gateway/Stop", + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(GatewayServer).Stop(ctx, req.(*StopRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _Gateway_Version_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(VersionRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(GatewayServer).Version(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: "/gateway.Gateway/Version", + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(GatewayServer).Version(ctx, req.(*VersionRequest)) + } + return interceptor(ctx, in, info, handler) +} + +// Gateway_ServiceDesc is the grpc.ServiceDesc for Gateway service. +// It's only intended for direct use with grpc.RegisterService, +// and not to be introspected or modified (even as a copy) +var Gateway_ServiceDesc = grpc.ServiceDesc{ + ServiceName: "gateway.Gateway", + HandlerType: (*GatewayServer)(nil), + Methods: []grpc.MethodDesc{ + { + MethodName: "BlxrTx", + Handler: _Gateway_BlxrTx_Handler, + }, + { + MethodName: "Peers", + Handler: _Gateway_Peers_Handler, + }, + { + MethodName: "TxStoreSummary", + Handler: _Gateway_TxStoreSummary_Handler, + }, + { + MethodName: "GetTx", + Handler: _Gateway_GetTx_Handler, + }, + { + MethodName: "Stop", + Handler: _Gateway_Stop_Handler, + }, + { + MethodName: "Version", + Handler: _Gateway_Version_Handler, + }, + }, + Streams: []grpc.StreamDesc{}, + Metadata: "gateway.proto", +} diff --git a/protobuf/generate.sh b/protobuf/generate.sh new file mode 100755 index 0000000..51ba86f --- /dev/null +++ b/protobuf/generate.sh @@ -0,0 +1 @@ +protoc --go_out=. --go_opt=paths=source_relative --go-grpc_out=. --go-grpc_opt=paths=source_relative gateway.proto \ No newline at end of file diff --git a/rpc/auth.go b/rpc/auth.go new file mode 100644 index 0000000..dc0f86e --- /dev/null +++ b/rpc/auth.go @@ -0,0 +1,73 @@ +package rpc + +import ( + "context" + "encoding/base64" + "errors" + "fmt" + "google.golang.org/grpc" + "google.golang.org/grpc/metadata" + "strings" +) + +type blxrCredentials struct { + authorization string +} + +func (bc blxrCredentials) GetRequestMetadata(ctx context.Context, uri ...string) (map[string]string, error) { + return map[string]string{ + "authorization": bc.authorization, + }, nil +} + +func (bc blxrCredentials) RequireTransportSecurity() bool { + return false +} + +// NewBLXRCredentials constructs a new bloxroute GRPC auth scheme from a raw auth header +func NewBLXRCredentials(authorization string) grpc.DialOption { + return grpc.WithPerRPCCredentials(blxrCredentials{authorization: authorization}) +} + +// NewBLXRCredentialsFromUserPassword constructs a new bloxroute GRPC auth scheme from an RPC user and secret +func NewBLXRCredentialsFromUserPassword(user string, secret string) grpc.DialOption { + return grpc.WithPerRPCCredentials(blxrCredentials{authorization: EncodeUserSecret(user, secret)}) +} + +// EncodeUserSecret produces a base64 encoded auth header of a user and secret +func EncodeUserSecret(user string, secret string) string { + data := fmt.Sprintf("%v:%v", user, secret) + return base64.StdEncoding.EncodeToString([]byte(data)) +} + +// DecodeAuthHeader produces the user and secret from an encoded auth header +func DecodeAuthHeader(authHeader string) (user string, secret string, err error) { + data, err := base64.StdEncoding.DecodeString(authHeader) + if err != nil { + return + } + + splitData := strings.Split(string(data), ":") + if len(splitData) != 2 { + err = fmt.Errorf("improperly formatted decoded auth header: %v", string(data)) + return + } + + user = splitData[0] + secret = splitData[1] + return +} + +// ReadAuthMetadata reads auth info from the RPC connection context +func ReadAuthMetadata(ctx context.Context) (string, error) { + md, ok := metadata.FromIncomingContext(ctx) + if !ok { + return "", errors.New("could not read metadata from context") + } + + values := md.Get("authorization") + if len(values) == 0 { + return "", errors.New("no auth information was provided") + } + return values[0], nil +} diff --git a/rpc/auth_test.go b/rpc/auth_test.go new file mode 100644 index 0000000..41d22cc --- /dev/null +++ b/rpc/auth_test.go @@ -0,0 +1,24 @@ +package rpc + +import ( + "github.com/stretchr/testify/assert" + "testing" +) + +const ( + user = "d2ab93ba-1a8b-4187-994e-c63896c936e3" + secret = "a61681a9456ca319a90036e8361dbc73" + authHeader = "ZDJhYjkzYmEtMWE4Yi00MTg3LTk5NGUtYzYzODk2YzkzNmUzOmE2MTY4MWE5NDU2Y2EzMTlhOTAwMzZlODM2MWRiYzcz" +) + +func TestEncodeUserSecret(t *testing.T) { + result := EncodeUserSecret(user, secret) + assert.Equal(t, authHeader, result) +} + +func TestDecodeAuthHeader(t *testing.T) { + decodedUser, decodedSecret, err := DecodeAuthHeader(authHeader) + assert.Nil(t, err) + assert.Equal(t, user, decodedUser) + assert.Equal(t, secret, decodedSecret) +} diff --git a/rpc/client.go b/rpc/client.go new file mode 100644 index 0000000..ead87d6 --- /dev/null +++ b/rpc/client.go @@ -0,0 +1,73 @@ +package rpc + +import ( + "context" + "encoding/json" + "fmt" + "github.com/bloXroute-Labs/gateway/config" + pb "github.com/bloXroute-Labs/gateway/protobuf" + "google.golang.org/grpc" +) + +// AuthOption parses authentication info from the provided CLI context +func AuthOption(grpcConfig *config.GRPC) (authOption grpc.DialOption, included bool) { + if grpcConfig.AuthEnabled { + included = true + + if grpcConfig.EncodedAuthSet { + authOption = NewBLXRCredentials(grpcConfig.EncodedAuth) + } else { + authOption = NewBLXRCredentialsFromUserPassword(grpcConfig.User, grpcConfig.Password) + } + } + return +} + +func connectInsecure(grpcConfig *config.GRPC) (*grpc.ClientConn, error) { + address := fmt.Sprintf("%v:%v", grpcConfig.Host, grpcConfig.Port) + authOption, required := AuthOption(grpcConfig) + + if required { + return grpc.Dial(address, grpc.WithInsecure(), authOption) + } + return grpc.Dial(address, grpc.WithInsecure()) +} + +// GatewayClient returns a ready-to-use GRPC gateway client +func GatewayClient(grpcConfig *config.GRPC) (pb.GatewayClient, error) { + conn, err := connectInsecure(grpcConfig) + + if err != nil { + return nil, fmt.Errorf("could not connect to gateway GRPC: %v", err) + } + pbConn := pb.NewGatewayClient(conn) + return pbConn, nil +} + +// GatewayCall executes a GRPC gateway call +func GatewayCall(grpcConfig *config.GRPC, call func(ctx context.Context, client pb.GatewayClient) (interface{}, error)) (interface{}, error) { + client, err := GatewayClient(grpcConfig) + if err != nil { + return nil, err + } + + callContext, cancel := context.WithTimeout(context.Background(), grpcConfig.Timeout) + defer cancel() + + return call(callContext, client) +} + +// GatewayConsoleCall executes a GRPC gateway call and logs the output to stdout as JSON +func GatewayConsoleCall(grpcConfig *config.GRPC, call func(ctx context.Context, client pb.GatewayClient) (interface{}, error)) error { + result, err := GatewayCall(grpcConfig, call) + if err != nil { + return err + } + + b, err := json.MarshalIndent(result, "", " ") + if err != nil { + return fmt.Errorf("could not marshal JSON: %v", err) + } + fmt.Println(string(b)) + return nil +} diff --git a/run_docker.sh b/run_docker.sh new file mode 100755 index 0000000..7812156 --- /dev/null +++ b/run_docker.sh @@ -0,0 +1,57 @@ +#!/usr/bin/env bash +# This script can be used to run a bloXroute gateway running with Go. +# The script takes three arguments +# CERT_PATH: Your SSL certificates from the portal. This should be a path to +# the parent directory of the external_gateway folder. +# The external_gateway folder should contain a registration_only folder with certs inside +# LOGS_PATH: Where you want a logs folder to be created with a gateway.log file inside +# EXTERNAL_IP: Optional argument for certain regions of china where you need to set external ip manually +# Example usage: +# +# ./run_docker.sh /home/ubuntu/gw /home/ubuntu/gw/logs +# +# It creates a container named bxgateway-go running in the background +# Websockets are accessible at ws://localhost:28334/ws +# To test them you can use wscat -c ws://localhost:28334/ws -H "Authorization:" +# If you see Connected, websockets are available + +CERT_PATH=${1:-/home/ec2-user/ssl} +LOGS_PATH=${2:-/home/ec2-user/bxgateway-private-go/logs} +BLOCKCHAIN_NETWORK=${3:-"Mainnet"} +EXTERNAL_IP=${4:-""} + +# Modify this to hardcode the relay IP you want to connect to +RELAY_IP="" +LOG_LEVEL="debug" +IMAGE_TAG="test" +ENV="testnet" + +if [[ ${ENV} == "mainnet" ]]; then + CA_CERT_URL="https://s3.amazonaws.com/credentials.blxrbdn.com/ca/ca_cert.pem" +else + CA_CERT_URL="https://s3.amazonaws.com/credentials.bxrtest.com/ca/ca_cert.pem" +fi + +mkdir -p "$CERT_PATH"/external_gateway/ca +curl $CA_CERT_URL -o "$CERT_PATH"/external_gateway/ca/ca_cert.pem + +ARGS="--ws --port 1802" +if [[ "${EXTERNAL_IP}" != "" ]]; then + ARGS="${ARGS} --external-ip ${EXTERNAL_IP}" +fi + +if [[ "${RELAY_IP}" != "" ]]; then + ARGS="${ARGS} --relay-ip ${RELAY_IP}" +fi + +if [[ "${LOG_LEVEL}" != "" ]]; then + ARGS="${ARGS} --log-level ${LOG_LEVEL}" +fi + +ARGS="${ARGS} --blockchain-network ${BLOCKCHAIN_NETWORK}" + +docker pull bloxroute/bloxroute-gateway-go:$IMAGE_TAG +docker rm -f bxgateway-go +docker run --name bxgateway-go -d -v "$LOGS_PATH":/app/bloxroute/logs \ + -v "$CERT_PATH":/app/bloxroute/ssl/${ENV} -p 1802:1802 -p 127.0.0.1:6060:6060 \ + -p 28334:28333 --restart=on-failure bloxroute/bloxroute-gateway-go:$IMAGE_TAG "$ARGS --env ${ENV}" \ No newline at end of file diff --git a/sdnmessage/accounts.go b/sdnmessage/accounts.go new file mode 100644 index 0000000..46b6093 --- /dev/null +++ b/sdnmessage/accounts.go @@ -0,0 +1,246 @@ +package sdnmessage + +import ( + "github.com/bloXroute-Labs/gateway/types" + "time" +) + +// AccountRequest represents a request to bxapi for account details +// Either field is nullable, so fields must be pointers. +type AccountRequest struct { + AccountID *types.AccountID `json:"account_id"` + PeerID *types.NodeID `json:"peer_id"` +} + +// AccountResponse represents a response from bxapi with account details +type AccountResponse struct { + NodeID *types.NodeID `json:"node_id"` // will be none if a pushed down update from bxapi + Account *Account `json:"account"` +} + +// AccountTier represents a tier name +type AccountTier string + +// AccountTier types enumeration +const ( + ATierElite AccountTier = "EnterpriseElite" + ATierEnterprise AccountTier = "Enterprise" + ATierProfessional AccountTier = "Professional" + ATierDeveloper AccountTier = "Developer" + ATierIntroductory AccountTier = "Introductory" +) + +// IsElite indicates whether the account tier is elite +func (at AccountTier) IsElite() bool { + return at == ATierElite +} + +// IsEnterprise indicates whether the account tier is considered enterprise +func (at AccountTier) IsEnterprise() bool { + return at == ATierEnterprise +} + +// ReceivesUnpaidTxs indicates whether the account tier receives unpaid txs (only >= ATierProfessional) +func (at AccountTier) ReceivesUnpaidTxs() bool { + return at == ATierElite || at == ATierEnterprise || at == ATierProfessional +} + +// TimeIntervalType represents an time interval type +type TimeIntervalType string + +// TimeIntervalType enumeration +const ( + TimeIntervalDaily TimeIntervalType = "DAILY" + TimeIntervalWithout TimeIntervalType = "WITHOUT_INTERVAL" +) + +// BDNServiceType represents a BDN service type +type BDNServiceType string + +// BDNServiceType enumeration +const ( + BDNServiceMsgQuota BDNServiceType = "MSG_QUOTA" + BDNServicePermit BDNServiceType = "PERMIT" +) + +// BDNServiceBehaviorType represents various flags for service handling behaviors +type BDNServiceBehaviorType string + +// BDNServiceBehaviorType enumeration +const ( + // BehaviorNoAction means + BehaviorNoAction BDNServiceBehaviorType = "NO_ACTION" + // BehaviorBlock means to block transaction propagation + BehaviorBlock BDNServiceBehaviorType = "BLOCK" + // BehaviorAlert means issue customer alert + BehaviorAlert BDNServiceBehaviorType = "ALERT" + // BehaviorAuditLog means log audit entry + BehaviorAuditLog BDNServiceBehaviorType = "AUDIT_LOG" + // BlockAlert means + BlockAlert BDNServiceBehaviorType = "BLOCK_ALERT" + // AuditAlert means + AuditAlert BDNServiceBehaviorType = "AUDIT_ALERT" +) + +// BDNService represents a service model config +// This struct is roughly equivalent to 'BdnServiceModel' in Python +type BDNService struct { + TimeInterval TimeIntervalType `json:"interval"` + ServiceType BDNServiceType `json:"service_type"` + Limit int `json:"limit"` + BehaviorLimitOK BDNServiceBehaviorType `json:"behavior_limit_ok"` + BehaviorLimitFail BDNServiceBehaviorType `json:"behavior_limit_fail"` +} + +// BDNQuotaService represents quota service model configs +type BDNQuotaService struct { + ExpireDate string `json:"expire_date"` + MsgQuota BDNService `json:"msg_quota"` + ExpireDateTime time.Time +} + +// FeedProperties represent feed in BDN service +type FeedProperties struct { + AllowFiltering bool `json:"allow_filtering"` + AvailableFields []string `json:"available_fields"` +} + +// BDNBasicService is a placeholder for service model configs +type BDNBasicService interface{} + +// BDNFeedService is a placeholder for service model configs +type BDNFeedService struct { + ExpireDate string `json:"expire_date"` + Feed FeedProperties `json:"feed"` +} + +// BDNPrivateRelayService is a placeholder for service model configs +type BDNPrivateRelayService interface{} + +// Account represents the account structure fetched from bxapi +type Account struct { + AccountInfo + SecretHash string `json:"secret_hash"` + FreeTransactions BDNQuotaService `json:"tx_free"` + PaidTransactions BDNQuotaService `json:"tx_paid"` + CloudAPI BDNBasicService `json:"cloud_api"` + NewTransactionStreaming BDNFeedService `json:"new_transaction_streaming"` + NewBlockStreaming BDNFeedService `json:"new_block_streaming"` + PendingTransactionStreaming BDNFeedService `json:"new_pending_transaction_streaming"` + TransactionStateFeed BDNFeedService `json:"transaction_state_feed"` + OnBlockFeed BDNFeedService `json:"on_block_feed"` + TransactionReceiptFeed BDNFeedService `json:"transaction_receipts_feed"` + PrivateRelay BDNPrivateRelayService `json:"private_relays"` + PrivateTransaction BDNQuotaService `json:"private_transaction"` + TxTraceRateLimitation BDNQuotaService `json:"tx_trace_rate_limitation"` +} + +// AccountInfo represents basic info about the account model +// This struct is roughly equivalent to `AccountTemplate` in Python +type AccountInfo struct { + AccountID types.AccountID `json:"account_id"` + LogicalAccountID string `json:"logical_account_id"` + Certificate string `json:"certificate"` + ExpireDate string `json:"expire_date"` + BlockchainProtocol string `json:"blockchain_protocol"` + BlockchainNetwork string `json:"blockchain_network"` + TierName AccountTier `json:"tier_name"` + Miner bool `json:"is_miner"` + MevBuilder string `json:"mev_builder"` + MevMiner string `json:"mev_miner"` +} + +// DefaultEnterpriseAccount default enterprise account +var DefaultEnterpriseAccount = Account{ + AccountInfo: AccountInfo{ + AccountID: "", + LogicalAccountID: "", + Certificate: "", + ExpireDate: "2999-12-31", + BlockchainProtocol: "Ethereum", + BlockchainNetwork: "Mainnet", + TierName: ATierEnterprise, + Miner: false, + }, + FreeTransactions: BDNQuotaService{ + ExpireDate: "2999-12-31", + MsgQuota: BDNService{ + TimeInterval: TimeIntervalDaily, + ServiceType: BDNServiceMsgQuota, + Limit: 1, + }, + ExpireDateTime: time.Now().Add(time.Hour), + }, + PaidTransactions: BDNQuotaService{ + ExpireDate: "2999-12-31", + MsgQuota: BDNService{ + TimeInterval: TimeIntervalDaily, + ServiceType: BDNServiceMsgQuota, + Limit: 1, + }, + ExpireDateTime: time.Now().Add(time.Hour), + }, + CloudAPI: nil, + NewTransactionStreaming: BDNFeedService{ + ExpireDate: "2999-12-31", + Feed: FeedProperties{ + AllowFiltering: true, + AvailableFields: []string{"all"}, + }, + }, + NewBlockStreaming: BDNFeedService{ + ExpireDate: "2999-12-31", + Feed: FeedProperties{ + AllowFiltering: true, + AvailableFields: []string{"all"}, + }, + }, + PendingTransactionStreaming: BDNFeedService{ + ExpireDate: "2999-12-31", + Feed: FeedProperties{ + AllowFiltering: true, + AvailableFields: []string{"all"}, + }, + }, + TransactionStateFeed: BDNFeedService{ + ExpireDate: "2999-12-31", + Feed: FeedProperties{ + AllowFiltering: false, + AvailableFields: nil, + }, + }, + OnBlockFeed: BDNFeedService{ + ExpireDate: "2999-12-31", + Feed: FeedProperties{ + AllowFiltering: false, + AvailableFields: nil, + }, + }, + TransactionReceiptFeed: BDNFeedService{ + ExpireDate: "2999-12-31", + Feed: FeedProperties{ + AllowFiltering: false, + AvailableFields: nil, + }, + }, + PrivateRelay: nil, + PrivateTransaction: BDNQuotaService{ + ExpireDate: "2999-12-31", + MsgQuota: BDNService{ + TimeInterval: TimeIntervalDaily, + ServiceType: BDNServiceMsgQuota, + Limit: 1, + }, + ExpireDateTime: time.Now().Add(time.Hour), + }, + TxTraceRateLimitation: BDNQuotaService{ + ExpireDate: "2999-12-31", + MsgQuota: BDNService{ + TimeInterval: TimeIntervalDaily, + ServiceType: BDNServiceMsgQuota, + Limit: 1, + }, + ExpireDateTime: time.Now().Add(time.Hour), + }, + SecretHash: "", +} diff --git a/sdnmessage/audit_counters_update.go b/sdnmessage/audit_counters_update.go new file mode 100644 index 0000000..e5b2481 --- /dev/null +++ b/sdnmessage/audit_counters_update.go @@ -0,0 +1,18 @@ +package sdnmessage + +// AccountAuditCountersUpdate represents an audit counter update for an account +type AccountAuditCountersUpdate struct { + AccountID string `json:"account_id"` + TxPaidCounter int `json:"tx_paid_counter"` +} + +// AuditCountersUpdate represents an audit counter phase update for accounts +type AuditCountersUpdate struct { + AccountUpdates []AccountAuditCountersUpdate `json:"account_updates"` + PhaseKey int `json:"phase_key"` +} + +// AuditCountersUpdateRequest represents a request from the SDN for an AuditCountersUpdate +type AuditCountersUpdateRequest struct { + PhaseKey int `json:"phase_key"` +} diff --git a/sdnmessage/blockchain_network.go b/sdnmessage/blockchain_network.go new file mode 100644 index 0000000..cefdaec --- /dev/null +++ b/sdnmessage/blockchain_network.go @@ -0,0 +1,76 @@ +package sdnmessage + +import ( + "fmt" + "github.com/bloXroute-Labs/gateway/types" +) + +// BlockchainNetwork represents network config for a given blockchain network being routed by bloxroute +type BlockchainNetwork struct { + AllowTimeReuseSenderNonce float64 `json:"allowed_time_reuse_sender_nonce"` + AllowGasPriceChangeReuseSenderNonce float64 `json:"allowed_gas_price_change_reuse_sender_nonce"` + BlockConfirmationsCount int64 `json:"block_confirmations_count"` + BlockHoldTimeoutS float64 `json:"block_hold_timeout_s"` + BlockInterval int64 `json:"block_interval"` + BlockRecoveryTimeoutS int64 `json:"block_recovery_timeout_s"` + DefaultAttributes struct { + BlockchainNetMagic int64 `json:"blockchain_net_magic,omitempty"` + BlockchainPort int64 `json:"blockchain_port,omitempty"` + BlockchainServices int64 `json:"blockchain_services,omitempty"` + BlockchainVersion int64 `json:"blockchain_version,omitempty"` + // string?? doesn't work... + ChainDifficulty interface{} `json:"chain_difficulty,omitempty"` + GenesisHash string `json:"genesis_hash,omitempty"` + NetworkID int64 `json:"network_id,omitempty"` + } `json:"default_attributes"` + EnableBlockCompression bool `json:"enable_block_compression"` + EnableCheckSenderNonce bool `json:"enable_check_sender_nonce"` + EnableNetworkContentLogs bool `json:"enable_network_content_logs"` + EnableRecordingTxDetectionTimeLocation bool `json:"enable_recording_tx_detection_time_location"` + Environment string `json:"environment"` + FinalTxConfirmationsCount int64 `json:"final_tx_confirmations_count"` + IgnoreBlockIntervalCount int64 `json:"ignore_block_interval_count"` + LogCompressedBlockDebugInfoOnRelay bool `json:"log_compressed_block_debug_info_on_relay"` + MaxBlockSizeBytes int64 `json:"max_block_size_bytes"` + MaxTxSizeBytes int64 `json:"max_tx_size_bytes"` + MediumTxNetworkFee int64 `json:"medium_tx_network_fee"` + MempoolExpectedTransactionsCount int64 `json:"mempool_expected_transactions_count"` + MinTxAgeSeconds float64 `json:"min_tx_age_seconds"` + MinTxNetworkFee float64 `json:"min_tx_network_fee"` + Network string `json:"network"` + NetworkNum types.NetworkNum `json:"network_num"` + Protocol string `json:"protocol"` + RemovedTransactionsHistoryExpirationS int64 `json:"removed_transactions_history_expiration_s"` + SdnID string `json:"sdn_id"` + SendCompressedTxsAfterBlock bool `json:"send_compressed_txs_after_block"` + TxContentsMemoryLimitBytes int64 `json:"tx_contents_memory_limit_bytes"` + TxPercentToLogByHash float64 `json:"tx_percent_to_log_by_hash"` + TxPercentToLogBySid float64 `json:"tx_percent_to_log_by_sid"` + TxSyncIntervalS float64 `json:"tx_sync_interval_s"` + TxSyncSyncContent bool `json:"tx_sync_sync_content"` + Type string `json:"type"` + EnableTxTrace bool `json:"enable_tx_trace"` + InjectPoa bool `json:"inject_poa"` + AllowedFromTier string `json:"allowed_from_tier"` + SendCrossGeo bool `json:"send_cross_geo"` +} + +// BlockchainNetworks represents the full message returned from bxapi +type BlockchainNetworks map[types.NetworkNum]*BlockchainNetwork + +// UpdateFrom - sets updates from a network +func (bcn *BlockchainNetwork) UpdateFrom(network *BlockchainNetwork) { + bcn.AllowTimeReuseSenderNonce = network.AllowTimeReuseSenderNonce + bcn.AllowGasPriceChangeReuseSenderNonce = network.AllowGasPriceChangeReuseSenderNonce + bcn.TxPercentToLogByHash = network.TxPercentToLogByHash + bcn.EnableTxTrace = network.EnableTxTrace + bcn.EnableCheckSenderNonce = network.EnableCheckSenderNonce +} + +// FindNetwork finds a BlockchainNetwork instance by its number and allow update +func (bcns *BlockchainNetworks) FindNetwork(networkNum types.NetworkNum) (*BlockchainNetwork, error) { + if network, exists := (*bcns)[networkNum]; exists { + return network, nil + } + return nil, fmt.Errorf("can't find blockchain network %v", networkNum) +} diff --git a/sdnmessage/connected_peers.go b/sdnmessage/connected_peers.go new file mode 100644 index 0000000..b67e67f --- /dev/null +++ b/sdnmessage/connected_peers.go @@ -0,0 +1,22 @@ +package sdnmessage + +import ( + "github.com/bloXroute-Labs/gateway/types" +) + +// ConnectedPeer represents info about a connected peer to a relay +type ConnectedPeer struct { + NodeID types.NodeID `json:"node_id"` + ExternalIP string `json:"external_ip"` + ConnectionType string `json:"connection_type"` + ConnectionState string `json:"connection_state"` + NetworkNum types.NetworkNum `json:"network_num"` + FromMe bool `json:"from_me"` +} + +// ConnectedPeers represents both the request from bxapi to fetch the relay's connected peers, +// as well as the response back containing said peers +type ConnectedPeers struct { + RequestID string `json:"request_id"` + ConnectedPeers []ConnectedPeer `json:"connected_peers"` +} diff --git a/sdnmessage/disconnect_relays.go b/sdnmessage/disconnect_relays.go new file mode 100644 index 0000000..8c8a61f --- /dev/null +++ b/sdnmessage/disconnect_relays.go @@ -0,0 +1,9 @@ +package sdnmessage + +import ( + "github.com/bloXroute-Labs/gateway/types" +) + +// DisconnectRelays represents the set of connections for the relay to disconnect, as sent down +// from bxapi +type DisconnectRelays = []types.NodeID diff --git a/sdnmessage/firewall_rule.go b/sdnmessage/firewall_rule.go new file mode 100644 index 0000000..859ea63 --- /dev/null +++ b/sdnmessage/firewall_rule.go @@ -0,0 +1,24 @@ +package sdnmessage + +import ( + "github.com/bloXroute-Labs/gateway/types" + "time" +) + +// FirewallRule is SDN P2P message that sent to proxy +type FirewallRule struct { + AccountID types.AccountID `json:"account_id"` + PeerID types.NodeID `json:"node_id"` + Duration int `json:"duration"` + expirationTime time.Time +} + +// GetExpirationTime - returns the expirationTime of a rule +func (firewallRule *FirewallRule) GetExpirationTime() time.Time { + return firewallRule.expirationTime +} + +// SetExpirationTime - sets the expirationTime of a rule +func (firewallRule *FirewallRule) SetExpirationTime(expirationTime time.Time) { + firewallRule.expirationTime = expirationTime +} diff --git a/sdnmessage/node_event.go b/sdnmessage/node_event.go new file mode 100644 index 0000000..0b1fa83 --- /dev/null +++ b/sdnmessage/node_event.go @@ -0,0 +1,46 @@ +package sdnmessage + +import ( + "fmt" + "github.com/bloXroute-Labs/gateway/types" +) + +// NodeEventType represents a type of node event being reported to the SDN +type NodeEventType string + +// NodeEventType enumerations +const ( + NeOnline NodeEventType = "ONLINE" + NeOffline NodeEventType = "OFFLINE" + NePeerConnEstablished NodeEventType = "PEER_CONN_ESTABLISHED" + NePeerConnClosed NodeEventType = "PEER_CONN_CLOSED" +) + +// NodeEvent represents a node event and its context being reported to the SDN +// In most cases, NodeID refers to the peer +type NodeEvent struct { + NodeID types.NodeID `json:"node_id"` + EventType NodeEventType `json:"event_type"` + PeerIP string `json:"peer_ip"` + PeerPort int `json:"peer_port"` + Timestamp string `json:"timestamp"` + EventID string `json:"event_id"` + Payload string `json:"payload"` +} + +// NewNodeConnectionEvent returns an online NodeEvent for a peer. +func NewNodeConnectionEvent(peerID types.NodeID, networkNum types.NetworkNum) NodeEvent { + return NodeEvent{ + NodeID: peerID, + Payload: fmt.Sprint(networkNum), + EventType: NePeerConnEstablished, + } +} + +// NewNodeDisconnectionEvent returns an offline NodeEvent for a peer. +func NewNodeDisconnectionEvent(peerID types.NodeID) NodeEvent { + return NodeEvent{ + NodeID: peerID, + EventType: NePeerConnClosed, + } +} diff --git a/sdnmessage/node_model.go b/sdnmessage/node_model.go new file mode 100644 index 0000000..e99605c --- /dev/null +++ b/sdnmessage/node_model.go @@ -0,0 +1,75 @@ +package sdnmessage + +import ( + "bytes" + "encoding/json" + "fmt" + "github.com/bloXroute-Labs/gateway/types" + log "github.com/sirupsen/logrus" +) + +// NodeModel represents metadata on a given node in the bloxroute network +type NodeModel struct { + NodeType string `json:"node_type"` + ExternalPort int64 `json:"external_port"` + NonSSLPort int `json:"non_ssl_port"` + ExternalIP string `json:"external_ip"` + Online bool `json:"online"` + SdnConnectionAlive bool `json:"sdn_connection_alive"` + Network string `json:"network"` + Protocol string `json:"protocol"` + NodeID types.NodeID `json:"node_id"` + SidStart interface{} `json:"sid_start"` + SidEnd interface{} `json:"sid_end"` + NextSidStart interface{} `json:"next_sid_start"` + NextSidEnd interface{} `json:"next_sid_end"` + SidExpireTime int `json:"sid_expire_time"` + LastPongTime float64 `json:"last_pong_time"` + IsGatewayMiner bool `json:"is_gateway_miner"` + IsInternalGateway bool `json:"is_internal_gateway"` + SourceVersion string `json:"source_version"` + ProtocolVersion interface{} `json:"protocol_version"` + BlockchainNetworkNum types.NetworkNum `json:"blockchain_network_num"` + BlockchainIP string `json:"blockchain_ip"` + BlockchainPort int `json:"blockchain_port"` + Hostname string `json:"hostname"` + SdnID interface{} `json:"sdn_id"` + OsVersion string `json:"os_version"` + Continent interface{} `json:"continent"` + SplitRelays bool `json:"split_relays"` + Country interface{} `json:"country"` + Region interface{} `json:"region"` + Idx int64 `json:"idx"` + HasFullyUpdatedTxService bool `json:"has_fully_updated_tx_service"` + SyncTxsStatus bool `json:"sync_txs_status"` + NodeStartTime string `json:"node_start_time"` + NodePublicKey string `json:"node_public_key"` + BaselineRouteRedundancy int `json:"baseline_route_redundancy"` + BaselineSourceRedundancy int `json:"baseline_source_redundancy"` + PrivateIP interface{} `json:"private_ip"` + Csr string `json:"csr"` + Cert string `json:"cert"` + PlatformProvider interface{} `json:"platform_provider"` + AccountID types.AccountID `json:"account_id"` + LatestSourceVersion interface{} `json:"latest_source_version"` + ShouldUpdateSourceVersion bool `json:"should_update_source_version"` + AssigningShortIds bool `json:"assigning_short_ids"` + NodePrivileges string `json:"node_privileges"` + FirstSeenTime interface{} `json:"first_seen_time"` + IsDocker bool `json:"is_docker"` + UsingPrivateIPConnection bool `json:"using_private_ip_connection"` + PrivateNode bool `json:"private_node"` + ProgramName string `json:"program_name"` + RelayType string `json:"relay_type"` +} + +// Pack serializes a NodeModel into a buffer for sending +func (nm NodeModel) Pack() []byte { + buf := new(bytes.Buffer) + err := json.NewEncoder(buf).Encode(nm) + if err != nil { + nodeModel, _ := json.MarshalIndent(nm, "", "\t") + log.Error(fmt.Errorf("unable to encode node model with account id %v error %v", nodeModel, err)) + } + return buf.Bytes() +} diff --git a/sdnmessage/peer.go b/sdnmessage/peer.go new file mode 100644 index 0000000..c0ba351 --- /dev/null +++ b/sdnmessage/peer.go @@ -0,0 +1,64 @@ +package sdnmessage + +import ( + "fmt" + "github.com/bloXroute-Labs/gateway/types" +) + +// Peer represents a peer returned by the SDN for a node to connect to +type Peer struct { + AssigningShortIds bool `json:"assigning_short_ids"` + Attributes Attributes `json:"attributes"` + Idx int64 `json:"idx"` + IP string `json:"ip"` + IsInternalGateway bool `json:"is_internal_gateway"` + NodeID types.NodeID `json:"node_id"` + NodeType string `json:"node_type"` + NonSslPort int64 `json:"non_ssl_port"` + Port int64 `json:"port"` + PrivateNetwork bool +} + +// Attributes - contains peer attributes +type Attributes struct { + Continent string `json:"continent"` + Country string `json:"country"` + NodePublicKey interface{} `json:"node_public_key"` + Region string `json:"region"` + PlatformProvider string `json:"platform_provider"` + PrivateNode bool `json:"private_node"` + RelayType string `json:"relay_type"` +} + +// Matches return if a given peer is equal on ip/port/nodeID fields (generally good enough for an equals-type comparison) +func (p Peer) Matches(ip string, port int64, nodeID types.NodeID) bool { + return p.IP == ip && p.Port == port && p.NodeID == nodeID +} + +// String returns a formatted representation of the peer model +func (p Peer) String() string { + return fmt.Sprintf("%v@%v:%v", p.NodeID, p.IP, p.Port) +} + +// Peers represents a list of Peer from the SDN +type Peers []Peer + +// Contains returns if the given peer is in the peer list +func (pl Peers) Contains(p Peer) bool { + for _, peer := range pl { + if peer.NodeID == p.NodeID && peer.IP == p.IP && peer.Port == p.Port { + return true + } + } + return false +} + +// ContainsPeerID returns if the given peer ID matches one of the peers in the peer list +func (pl Peers) ContainsPeerID(peerID types.NodeID) bool { + for _, peer := range pl { + if peer.NodeID == peerID { + return true + } + } + return false +} diff --git a/sdnmessage/routing.go b/sdnmessage/routing.go new file mode 100644 index 0000000..4a28f8e --- /dev/null +++ b/sdnmessage/routing.go @@ -0,0 +1,22 @@ +package sdnmessage + +// SDNRoutingConfig represents configuration sent down from the SDN intended for controlling routing behavior +// Currently, the relay proxy is expected to only use TransactionPaidDelay +type SDNRoutingConfig struct { + HighLatencyThreshold int `json:"high_latency_threshold_ms"` + UltraHighLatencyThreshold int `json:"ultra_high_latency_threshold_ms"` + LowBandwidthThroughputThreshold int `json:"low_bandwidth_threshold_sent_throughput_bytes"` + UltraLowBandwidthThroughputThreshold int `json:"ultra_low_bandwidth_threshold_sent_throughput_bytes"` + LowBandwidthBacklogThreshold int `json:"low_bandwidth_threshold_backlog_bytes"` + UltraLowBandwidthBacklogThreshold int `json:"ultra_low_bandwidth_threshold_backlog_bytes"` + ConnectionHealthUpdateInterval int `json:"connection_health_update_interval_s"` + EnableRoutingTables bool `json:"enable_routing_tables"` + MaxTransactionElapsedTime int `json:"max_transaction_elapsed_time_s"` + BroadcastAllChinaRelays bool `json:"broadcast_to_all_china_relays"` + NoRoutesToSameCountry bool `json:"no_routes_to_same_country"` + UnpaidTransactionPropagationDelay int `json:"unpaid_transaction_propagation_delay_ms"` + TransactionPropagationDelay int `json:"transaction_propagation_delay_ms"` + CENProcessingDelay int `json:"cen_processing_delay_ms"` + DirectRouteBuffer int `json:"direct_route_buffer_ms"` + ATRNoPrivateIPForwardingRoutes bool `json:"atr_no_private_ip_forwarding_routes"` +} diff --git a/sdnmessage/synccheck.go b/sdnmessage/synccheck.go new file mode 100644 index 0000000..3d95f7c --- /dev/null +++ b/sdnmessage/synccheck.go @@ -0,0 +1,8 @@ +package sdnmessage + +// TransactionSyncCheck is a check to determine whether the relay or proxy is synced from +// its perspective +type TransactionSyncCheck struct { + RequestID string `json:"request_id"` + IsSynced bool `json:"is_tx_service_synced"` +} diff --git a/servers/clienthandler.go b/servers/clienthandler.go new file mode 100644 index 0000000..98132d7 --- /dev/null +++ b/servers/clienthandler.go @@ -0,0 +1,885 @@ +package servers + +import ( + "context" + "crypto/tls" + "encoding/base64" + "encoding/hex" + "encoding/json" + "fmt" + "github.com/bloXroute-Labs/gateway" + "github.com/bloXroute-Labs/gateway/blockchain" + "github.com/bloXroute-Labs/gateway/bxmessage" + "github.com/bloXroute-Labs/gateway/connections" + "github.com/bloXroute-Labs/gateway/sdnmessage" + "github.com/bloXroute-Labs/gateway/types" + "github.com/bloXroute-Labs/gateway/utils" + "github.com/ethereum/go-ethereum/common/hexutil" + ethtypes "github.com/ethereum/go-ethereum/core/types" + "github.com/ethereum/go-ethereum/rlp" + "github.com/gorilla/websocket" + "github.com/satori/go.uuid" + log "github.com/sirupsen/logrus" + "github.com/sourcegraph/jsonrpc2" + websocketjsonrpc2 "github.com/sourcegraph/jsonrpc2/websocket" + "github.com/zhouzhuojie/conditions" + "net/http" + "regexp" + "strconv" + "strings" + "sync" + "time" +) + +type clientHandler struct { + feedManager *FeedManager + server *http.Server +} + +// TxResponse - response of the jsonrpc params +type TxResponse struct { + Subscription string `json:"subscription"` + Result TxResult `json:"result"` +} + +// TxResult - request of jsonrpc params +type TxResult struct { + TxHash *string `json:"txHash,omitempty"` + TxContents types.BlockchainTransaction `json:"txContents,omitempty"` + LocalRegion *bool `json:"localRegion,omitempty"` + Time *string `json:"time,omitempty"` + RawTx *string `json:"rawTx,omitempty"` +} + +// BlockResponse - response of the jsonrpc params +type BlockResponse struct { + Subscription string `json:"subscription"` + Result types.Notification `json:"result"` +} + +type handlerObj struct { + FeedManager *FeedManager + ClientReq *clientReq + remoteAddress string + connectionAccount sdnmessage.Account +} + +type clientReq struct { + includes []string + feed types.FeedType + expr conditions.Expr + calls *map[string]*RPCCall +} + +type subscriptionRequest struct { + feed types.FeedType + options subscriptionOptions +} + +type subscriptionOptions struct { + Include []string `json:"Include"` + Filters string `json:"Filters"` + CallParams []map[string]string `json:"Call-Params"` +} + +// RPCCall represents customer call executed for onBlock feed +type RPCCall struct { + commandMethod string + blockOffset int + callName string + callPayload map[string]string + active bool +} + +func newCall(name string) *RPCCall { + return &RPCCall{ + callName: name, + callPayload: make(map[string]string), + active: true, + } +} + +func (c *RPCCall) validatePayload(method string, requiredFields []string) error { + for _, field := range requiredFields { + _, ok := c.callPayload[field] + if !ok { + return fmt.Errorf("expected %v element in request payload for %v", field, method) + } + } + return nil +} + +func (c *RPCCall) string() string { + payloadBytes, err := json.Marshal(c.callPayload) + if err != nil { + log.Errorf("failed to convert eth call to string: %v", err) + return c.callName + } + + return fmt.Sprintf("%+v", struct { + commandMethod string + blockOffset int + callName string + callPayload string + active bool + }{ + commandMethod: c.commandMethod, + blockOffset: c.blockOffset, + callName: c.callName, + callPayload: string(payloadBytes), + active: c.active, + }) +} + +type rpcPingResponse struct { + Pong string `json:"pong"` +} + +type rpcTxResponse struct { + TxHash string `json:"txHash"` +} + +var upgrader = websocket.Upgrader{} + +var txContentFields = []string{"tx_contents.nonce", "tx_contents.tx_hash", + "tx_contents.gas_price", "tx_contents.gas", "tx_contents.to", "tx_contents.value", "tx_contents.input", + "tx_contents.v", "tx_contents.r", "tx_contents.s", "tx_contents.from", "tx_contents.type", "tx_contents.access_list", + "tx_contents.chain_id", "tx_contents.max_priority_fee_per_gas", "tx_contents.max_fee_per_gas"} + +var validTxParams = append(txContentFields, "tx_contents", "tx_hash", "local_region", "time", "raw_tx") + +var validBlockParams = append(txContentFields, "hash", "header", "transactions", "uncles") + +var validOnBlockParams = []string{"name", "response", "block_height", "tag"} + +var validTxReceiptParams = []string{"block_hash", "block_number", "contract_address", + "cumulative_gas_used", "effective_gas_price", "from", "gas_used", "logs", "logs_bloom", + "status", "to", "transaction_hash", "transaction_index", "type"} + +var validParams = map[types.FeedType][]string{ + types.NewTxsFeed: validTxParams, + types.BDNBlocksFeed: validBlockParams, + types.NewBlocksFeed: validBlockParams, + types.PendingTxsFeed: validTxParams, + types.OnBlockFeed: validOnBlockParams, + types.TxReceiptsFeed: validTxReceiptParams, +} + +var defaultTxParams = append(txContentFields, "tx_hash", "local_region", "time") + +var availableFilters = []string{"gas", "gas_price", "value", "to", "from", "method_id", "type", "chain_id", "max_fee_per_gas", "max_priority_fee_per_gas"} + +var availableFeeds = []types.FeedType{types.NewTxsFeed, types.NewBlocksFeed, types.BDNBlocksFeed, types.PendingTxsFeed, types.OnBlockFeed, types.TxReceiptsFeed} + +// NewWSServer creates and returns a new websocket server managed by FeedManager +func NewWSServer(feedManager *FeedManager) *http.Server { + handler := http.NewServeMux() + handler.HandleFunc("/ws", func(responseWriter http.ResponseWriter, request *http.Request) { + connectionAccountID, connectionSecretHash := getAccountIDSecretHashFromReq(request, feedManager.websocketTLSEnabled) + connectionAccountModel := sdnmessage.Account{} + serverAccountID := feedManager.accountModel.AccountID + // if gateway received request from a customer with a different account id, it should verify it with the SDN. + //if the gateway does not have permission to verify account id (which mostly happen with external gateways), + //SDN will return StatusUnauthorized and fail this connection. if SDN return any other error - + //assuming the issue is with the SDN and set default enterprise account for the customer. in order to send request to the gateway, + //customer must be enterprise / elite account + var err error + if connectionAccountID != serverAccountID { + connectionAccountModel, err = feedManager.getCustomerAccountModel(connectionAccountID) + if err != nil { + if strings.Contains(strconv.FormatInt(http.StatusUnauthorized, 10), err.Error()) { + log.Errorf("Account %v is not authorized to get other account %v information", serverAccountID, connectionAccountID) + return + } + log.Errorf("Failed to get customer account model, account id: %v, error: %v", connectionAccountID, err) + connectionAccountModel = sdnmessage.DefaultEnterpriseAccount + connectionAccountModel.AccountID = connectionAccountID + connectionAccountModel.SecretHash = connectionSecretHash + } + if connectionAccountModel.TierName != sdnmessage.ATierEnterprise && connectionAccountModel.TierName != sdnmessage.ATierElite { + log.Errorf("Customer account %v must be enterprise / enterprise elite but it is %v", connectionAccountID, connectionAccountModel.TierName) + return + } + } else { + connectionAccountModel = feedManager.accountModel + } + if connectionAccountModel.SecretHash != connectionSecretHash && connectionSecretHash != "" { + log.Errorf("Account %v sent a different secret hash: %v then set in the account model: %v", connectionAccountID, connectionSecretHash, connectionAccountModel.SecretHash) + return + } + handleWSClientConnection(feedManager, responseWriter, request, connectionAccountModel) + }) + + server := http.Server{ + Addr: feedManager.addr, + Handler: handler, + } + return &server +} + +// handleWsClientConnection - when new http connection is made we get here upgrade to ws, and start handling +func handleWSClientConnection(feedManager *FeedManager, w http.ResponseWriter, r *http.Request, accountModel sdnmessage.Account) { + log.Debugf("New web-socket connection from %v", r.RemoteAddr) + connection, err := upgrader.Upgrade(w, r, nil) + if err != nil { + log.Errorf("error upgrading HTTP server connection to the WebSocket protocol - %v", err.Error()) + return + } + + handler := jsonrpc2.AsyncHandler(&handlerObj{FeedManager: feedManager, remoteAddress: r.RemoteAddr, connectionAccount: accountModel}) + _ = jsonrpc2.NewConn(r.Context(), websocketjsonrpc2.NewObjectStream(connection), handler) +} + +func getAccountIDSecretHashFromReq(request *http.Request, websocketTLSEnabled bool) (accountID types.AccountID, secretHash string) { + tokenString := request.Header.Get("Authorization") + if tokenString != "" { + payload, err := base64.StdEncoding.DecodeString(tokenString) + if err != nil { + log.Errorf("RemoteAddr: %v RequestURI: %v. DecodeString error: %v. invalid tokenString: %v.", request.RemoteAddr, request.RequestURI, err.Error(), tokenString) + return + } + accountIDAndHash := strings.SplitN(string(payload), ":", 2) + if len(accountIDAndHash) == 1 { + log.Errorf("RemoteAddr: %v RequestURI: %v. invalid tokenString- %v palyoad: %v.", request.RemoteAddr, request.RequestURI, tokenString, payload) + return + } + accountID = types.AccountID(accountIDAndHash[0]) + secretHash = accountIDAndHash[1] + } else if websocketTLSEnabled { + if request.TLS != nil && len(request.TLS.PeerCertificates) > 0 { + accountID = utils.GetAccountIDFromBxCertificate(request.TLS.PeerCertificates[0].Extensions) + } + } + if accountID == "" { + log.Errorf("RemoteAddr: %v RequestURI: %v. missing authorization from method: %v.", request.RemoteAddr, request.RequestURI, request.Method) + } + return +} + +func (ch *clientHandler) runWSServer() { + log.Infof("starting websocket server") + var err error + if ch.feedManager.websocketTLSEnabled { + ch.server.TLSConfig = &tls.Config{ + ClientAuth: tls.RequestClientCert, + } + err = ch.server.ListenAndServeTLS(ch.feedManager.certFile, ch.feedManager.keyFile) + } else { + err = ch.server.ListenAndServe() + } + if err != nil { + log.Errorf("could not listen on %v. error: %v", ch.feedManager.addr, err) + } +} + +func (ch *clientHandler) shutdownWSServer() { + log.Infof("shutting down websocket server") + ch.feedManager.UnsubscribeAll() + err := ch.server.Shutdown(ch.feedManager.context) + if err != nil { + log.Errorf("encountered error shutting down websocket server %v: %v", ch.feedManager.addr, err) + } +} + +func (ch *clientHandler) manageWSServer() { + for { + select { + case syncStatus := <-ch.feedManager.blockchainWS.ReceiveNodeSyncStatusUpdate(): + if syncStatus == blockchain.Unsynced { + ch.shutdownWSServer() + } else { + ch.server = NewWSServer(ch.feedManager) + go ch.runWSServer() + } + } + } +} + +// SendErrorMsg formats and sends an RPC error message back to the client +func SendErrorMsg(ctx context.Context, code RPCErrorCode, data string, conn *jsonrpc2.Conn, req *jsonrpc2.Request) { + rpcError := &jsonrpc2.Error{ + Code: int64(code), + Message: errorMsg[code], + } + rpcError.SetError(data) + err := conn.ReplyWithError(ctx, req.ID, rpcError) + if err != nil { + log.Errorf("could not respond to client with error message: %v", err) + } +} + +func reply(ctx context.Context, conn *jsonrpc2.Conn, ID jsonrpc2.ID, result interface{}) error { + if err := conn.Reply(ctx, ID, result); err != nil { + return err + } + return nil +} + +func (h *handlerObj) validateFeed(feedName types.FeedType, feedStreaming sdnmessage.BDNFeedService, includes []string, filters []string) error { + expireDateTime, _ := time.Parse(bxgateway.TimeDateLayoutISO, feedStreaming.ExpireDate) + if time.Now().UTC().After(expireDateTime) { + return fmt.Errorf("%v is not allowed or date has been expired", feedName) + } + if feedStreaming.Feed.AllowFiltering && utils.Exists("all", feedStreaming.Feed.AvailableFields) { + return nil + } + for _, include := range includes { + if !utils.Exists(include, feedStreaming.Feed.AvailableFields) { + return fmt.Errorf("including %v: %v is not allowed", feedName, include) + } + } + if !feedStreaming.Feed.AllowFiltering && len(filters) > 0 { + return fmt.Errorf("filtering in %v is not allowed", feedName) + } + return nil +} + +// Handle - handling client request +func (h *handlerObj) Handle(ctx context.Context, conn *jsonrpc2.Conn, req *jsonrpc2.Request) { + start := time.Now() + defer func() { + log.Debugf("websocket handling for %v ended. Duration %v", RPCRequestType(req.Method), time.Now().Sub(start)) + }() + switch RPCRequestType(req.Method) { + case RPCSubscribe: + request, err := h.createClientReq(req) + if err != nil { + SendErrorMsg(ctx, InvalidParams, err.Error(), conn, req) + return + } + feedName := request.feed + if h.FeedManager.blockchainWS == nil && (feedName != types.NewTxsFeed && feedName != types.BDNBlocksFeed) { + errMsg := fmt.Sprintf("%v feed requires --eth-ws-uri startup parameter", feedName) + SendErrorMsg(ctx, InvalidParams, errMsg, conn, req) + return + } + subscriptionID, feedChan, errSubscribe := h.FeedManager.Subscribe(request.feed, conn) + if errSubscribe != nil { + SendErrorMsg(ctx, InvalidParams, errSubscribe.Error(), conn, req) + return + } + defer h.FeedManager.Unsubscribe(*subscriptionID) + if err = reply(ctx, conn, req.ID, subscriptionID); err != nil { + log.Errorf("error reply to subscriptionID: %v : %v ", subscriptionID, err) + SendErrorMsg(ctx, InternalError, string(rune(websocket.CloseMessage)), conn, req) + return + } + + for { + select { + case notification, ok := <-(*feedChan): + if !ok { + if h.FeedManager.subscriptionExists(*subscriptionID) { + SendErrorMsg(ctx, InternalError, string(rune(websocket.CloseMessage)), conn, req) + } + return + } + switch feedName { + case types.NewTxsFeed: + tx := (*notification).(*types.NewTransactionNotification) + if h.sendTxNotification(ctx, subscriptionID, request, conn, tx) != nil { + return + } + case types.PendingTxsFeed: + tx := (*notification).(*types.PendingTransactionNotification) + if h.sendTxNotification(ctx, subscriptionID, request, conn, &tx.NewTransactionNotification) != nil { + return + } + case types.BDNBlocksFeed, types.NewBlocksFeed: + block := (*notification).(*types.BlockNotification) + if h.sendNotification(ctx, subscriptionID, request, conn, block) != nil { + return + } + case types.TxReceiptsFeed: + if h.FeedManager.blockchainWS == nil { + return + } + block := (*notification).(*types.BlockNotification) + var wg sync.WaitGroup + for _, tx := range block.Transactions { + wg.Add(1) + go func(t types.EthTransaction) { + defer wg.Done() + response, err := h.FeedManager.blockchainWS.FetchTransactionReceipt([]interface{}{t.Hash.Format(true)}, blockchain.RPCOptions{RetryAttempts: bxgateway.MaxEthTxReceiptCallRetries, RetryInterval: bxgateway.EthTxReceiptCallRetrySleepInterval}) + if err != nil || response == nil { + log.Errorf("failed to fetch transaction receipt for %v in block %v: %v", t.Hash, block.BlockHash, err) + return + } + txReceiptNotification := types.NewTxReceiptNotification(response.(map[string]interface{})) + if h.sendNotification(ctx, subscriptionID, request, conn, txReceiptNotification) != nil { + log.Errorf("failed to send tx receipt for %v", t.Hash) + return + } + }(tx) + } + wg.Wait() + log.Debugf("finished fetching transaction receipts for block %v", block.BlockHash) + case types.OnBlockFeed: + if h.FeedManager.blockchainWS == nil { + return + } + block := (*notification).(*types.BlockNotification) + blockHeightStr := block.Header.Number.String() + hashStr := block.BlockHash.String() + var wg sync.WaitGroup + for _, c := range *request.calls { + wg.Add(1) + go func(call *RPCCall) { + defer wg.Done() + if !call.active { + return + } + tag := "0x" + strconv.FormatInt(int64(int(block.Header.Number.Uint64())+call.blockOffset), 16) + payload, err := h.FeedManager.blockchainWS.ConstructRPCCallPayload(call.commandMethod, call.callPayload, tag) + if err != nil { + return + } + response, err := h.FeedManager.blockchainWS.CallRPC(call.commandMethod, payload, blockchain.RPCOptions{RetryAttempts: bxgateway.MaxEthOnBlockCallRetries, RetryInterval: bxgateway.EthOnBlockCallRetrySleepInterval}) + if err != nil { + log.Debugf("disabling failed onBlock call %v: %v", call.callName, err) + call.active = false + taskDisabledNotification := types.NewOnBlockNotification(bxgateway.TaskDisabledEvent, call.string(), blockHeightStr, tag, hashStr) + if h.sendNotification(ctx, subscriptionID, request, conn, taskDisabledNotification) != nil { + log.Errorf("failed to send TaskDisabledNotification for %v", call.callName) + } + return + } + onBlockNotification := types.NewOnBlockNotification(call.callName, response.(string), blockHeightStr, tag, hashStr) + if h.sendNotification(ctx, subscriptionID, request, conn, onBlockNotification) != nil { + return + } + }(c) + } + wg.Wait() + taskCompletedNotification := types.NewOnBlockNotification(bxgateway.TaskCompletedEvent, "", blockHeightStr, blockHeightStr, hashStr) + if h.sendNotification(ctx, subscriptionID, request, conn, taskCompletedNotification) != nil { + log.Errorf("failed to send TaskCompletedEvent on block %v", blockHeightStr) + return + } + } + } + } + case RPCUnsubscribe: + var params []string + _ = json.Unmarshal(*req.Params, ¶ms) + if len(params) != 1 { + err := fmt.Errorf("params %v with incorrect length", params) + SendErrorMsg(ctx, InvalidParams, err.Error(), conn, req) + return + } + uid, _ := uuid.FromString(params[0]) + if err := h.FeedManager.Unsubscribe(uid); err != nil { + log.Infof("subscription id %v was not found", uid) + } + case RPCTx: + if h.FeedManager.accountModel.AccountID != h.connectionAccount.AccountID { + err := fmt.Errorf("blxr_tx is not allowed when account authentication is different from the node account") + log.Errorf("%v. account auth: %v, node account: %v ", err, h.connectionAccount.AccountID, h.FeedManager.accountModel.AccountID) + SendErrorMsg(ctx, InvalidRequest, err.Error(), conn, req) + return + } + var params struct { + Transaction string `json:"transaction"` + } + err := json.Unmarshal(*req.Params, ¶ms) + if err != nil { + log.Errorf("unmarshal req.Params error - %v", err.Error()) + SendErrorMsg(ctx, InvalidParams, err.Error(), conn, req) + return + } + + txBytes, err := hex.DecodeString(params.Transaction) + if err != nil { + log.Errorf("invalid hex string: %v", err) + SendErrorMsg(ctx, InvalidParams, err.Error(), conn, req) + return + } + + // Ethereum transactions encoded for RPC interfaces is slightly different from the RLP encoded format, so much decoded + reencode the transaction for consistency. + // Specifically, note `UnmarshalBinary` should be used for RPC interfaces, and rlp.DecodeBytes should be used for the wire protocol. + var ethTx ethtypes.Transaction + err = ethTx.UnmarshalBinary(txBytes) + if err != nil { + log.Errorf("could not decode Ethereum transaction: %v", err) + SendErrorMsg(ctx, InvalidParams, err.Error(), conn, req) + return + } + txContent, _ := rlp.EncodeToBytes(ðTx) + + var hash types.SHA256Hash + copy(hash[:], ethTx.Hash().Bytes()) + + tx := bxmessage.NewTx(hash, txContent, h.FeedManager.networkNum, types.TFPaidTx|types.TFLocalRegion|types.TFDeliverToNode, h.connectionAccount.AccountID) + ws := connections.NewRPCConn(h.connectionAccount.AccountID, h.remoteAddress, h.FeedManager.networkNum, utils.Websocket) + + // call the Handler. Don't invoke in a go routine + _ = h.FeedManager.node.HandleMsg(tx, ws, connections.RunForeground) + + response := rpcTxResponse{ + TxHash: tx.Hash().String(), + } + if err = reply(ctx, conn, req.ID, response); err != nil { + log.Errorf("%v reply error - %v", RPCTx, err) + return + } + log.Infof("blxr_tx: Hash - 0x%v", response.TxHash) + case RPCPing: + response := rpcPingResponse{ + Pong: time.Now().UTC().Format(bxgateway.MicroSecTimeFormat), + } + if err := reply(ctx, conn, req.ID, response); err != nil { + log.Errorf("%v reply error - %v", RPCPing, err) + } + case RPCMevSearcher: + params := struct { + MevMethod string `json:"mev_method"` + Payload json.RawMessage `json:"payload"` + MevBuilders map[string]string `json:"mev_builders"` + }{} + + err := json.Unmarshal(*req.Params, ¶ms) + if err != nil { + log.Errorf("failed to unmarshal req.Params for mevSearcher, error: %v", err.Error()) + SendErrorMsg(ctx, InvalidParams, err.Error(), conn, req) + return + } + + mevSearcher, err := bxmessage.NewMEVSearcher(params.MevMethod, params.MevBuilders, params.Payload) + if err != nil { + log.Errorf("failed to create new mevSearcher: %v", err) + SendErrorMsg(ctx, InvalidParams, err.Error(), conn, req) + return + } + + ws := connections.NewRPCConn(h.connectionAccount.AccountID, h.remoteAddress, h.FeedManager.networkNum, utils.Websocket) + err = h.FeedManager.node.HandleMsg(&mevSearcher, ws, connections.RunForeground) + if err != nil { + log.Errorf("failed to process mevSearcher message: %v", err) + SendErrorMsg(ctx, InvalidParams, err.Error(), conn, req) + return + } + + if err := reply(ctx, conn, req.ID, map[string]string{"status": "ok"}); err != nil { + log.Errorf("%v mev searcher error: %v", RPCMevSearcher, err) + } + case RPCMevBuilder: + params := struct { + MevMethod string `json:"mev_method"` + Payload json.RawMessage `json:"payload"` + MevMinerNames []string `json:"mev_miner_names"` + }{} + + err := json.Unmarshal(*req.Params, ¶ms) + if err != nil { + log.Errorf("failed to unmarshal req.Params for mevBundle from mev-builder, error: %v", err.Error()) + SendErrorMsg(ctx, InvalidParams, err.Error(), conn, req) + return + } + + mevBundle, err := bxmessage.NewMEVBundle(params.MevMethod, params.MevMinerNames, params.Payload) + if err != nil { + log.Errorf("failed to create new mevBundle: %v", err) + SendErrorMsg(ctx, InvalidParams, err.Error(), conn, req) + return + } + + ws := connections.NewRPCConn(h.connectionAccount.AccountID, h.remoteAddress, h.FeedManager.networkNum, utils.Websocket) + err = h.FeedManager.node.HandleMsg(&mevBundle, ws, connections.RunForeground) + if err != nil { + log.Errorf("failed to process mevBundle message: %v", err) + SendErrorMsg(ctx, InvalidParams, err.Error(), conn, req) + return + } + + if err := reply(ctx, conn, req.ID, map[string]string{"status": "ok"}); err != nil { + log.Errorf("%v mev builder error: %v", RPCMevBuilder, err) + } + + default: + err := fmt.Errorf("got unsupported method name: %v", req.Method) + SendErrorMsg(ctx, MethodNotFound, err.Error(), conn, req) + return + } +} + +// sendNotification - build a response according to client request and notify client +func (h *handlerObj) sendNotification(ctx context.Context, subscriptionID *uuid.UUID, clientReq *clientReq, conn *jsonrpc2.Conn, notification types.Notification) error { + response := BlockResponse{ + Subscription: subscriptionID.String(), + } + content := notification.WithFields(clientReq.includes) + response.Result = content + err := conn.Notify(ctx, "subscribe", response) + if err != nil { + log.Errorf("error reply to subscriptionID: %v : %v ", subscriptionID, err.Error()) + return err + } + return nil +} + +// sendTxNotification - build a response according to client request and notify client +func (h *handlerObj) sendTxNotification(ctx context.Context, subscriptionID *uuid.UUID, clientReq *clientReq, conn *jsonrpc2.Conn, tx *types.NewTransactionNotification) error { + hasTxContent := false + + response := TxResponse{ + Subscription: subscriptionID.String(), + Result: TxResult{}, + } + + if clientReq.expr != nil { + txFilters := tx.Filters(clientReq.expr.Args()) + if txFilters == nil { + return nil + } + //Evaluate if we should send the tx + shouldSend, err := conditions.Evaluate(clientReq.expr, txFilters) + if err != nil { + log.Errorf("error evaluate Filters. feed: %v. method: %v. Filters: %v. remote adress: %v. account id: %v error - %v tx: %v.", + clientReq.feed, clientReq.includes[0], clientReq.expr.String(), h.remoteAddress, h.connectionAccount.AccountID, err.Error(), txFilters) + return nil + } + if !shouldSend { + return nil + } + } + + for _, param := range clientReq.includes { + if strings.Contains(param, "tx_contents") { + hasTxContent = true + } + switch param { + case "tx_hash": + txHash := tx.GetHash() + response.Result.TxHash = &txHash + case "time": + timeNow := time.Now().Format(bxgateway.MicroSecTimeFormat) + response.Result.Time = &timeNow + case "local_region": + localRegion := tx.LocalRegion() + response.Result.LocalRegion = &localRegion + case "raw_tx": + rawTx := hexutil.Encode(tx.RawTx()) + response.Result.RawTx = &rawTx + } + } + if hasTxContent { + txContent := tx.WithFields(clientReq.includes) + if txContent.(*types.NewTransactionNotification).BlockchainTransaction == nil { + return nil + } + response.Result.TxContents = txContent.(*types.NewTransactionNotification).BlockchainTransaction + } + + err := conn.Notify(ctx, "subscribe", response) + if err != nil { + log.Errorf("error reply to subscriptionID: %v : %v ", subscriptionID, err.Error()) + return err + } + + return nil +} + +func (h *handlerObj) createClientReq(req *jsonrpc2.Request) (*clientReq, error) { + request := subscriptionRequest{} + var rpcParams []json.RawMessage + err := json.Unmarshal(*req.Params, &rpcParams) + if err != nil { + return nil, err + } + if len(rpcParams) < 2 { + log.Debugf("invalid param from request id: %v. method: %v. params: %s. remote adress: %v account id: %v.", + req.ID, req.Method, *req.Params, h.remoteAddress, h.connectionAccount.AccountID) + return nil, fmt.Errorf("number of params must be at least length 2. requested params: %s", *req.Params) + } + err = json.Unmarshal(rpcParams[0], &request.feed) + if err != nil { + return nil, err + } + if !types.Exists(request.feed, availableFeeds) { + log.Debugf("invalid param from request id: %v. method: %v. params: %s. remote adress: %v account id: %v", + req.ID, req.Method, *req.Params, h.remoteAddress, h.connectionAccount.AccountID) + return nil, fmt.Errorf("got unsupported feed name %v. possible feeds are %v", request.feed, availableFeeds) + } + err = json.Unmarshal(rpcParams[1], &request.options) + if err != nil { + return nil, err + } + if request.options.Include == nil { + log.Debugf("invalid param from request id: %v. method: %v. params: %s. remote adress: %v account id: %v.", + req.ID, req.Method, *req.Params, h.remoteAddress, h.connectionAccount.AccountID) + return nil, fmt.Errorf("got unsupported params %v", string(rpcParams[1])) + } + var requestedFields []string + if len(request.options.Include) == 0 { + switch request.feed { + case types.BDNBlocksFeed, types.NewBlocksFeed: + requestedFields = validBlockParams + case types.NewTxsFeed: + requestedFields = defaultTxParams + case types.PendingTxsFeed: + requestedFields = defaultTxParams + case types.OnBlockFeed: + requestedFields = validParams[types.OnBlockFeed] + case types.TxReceiptsFeed: + requestedFields = validParams[types.TxReceiptsFeed] + } + } + for _, param := range request.options.Include { + switch request.feed { + case types.BDNBlocksFeed: + if !utils.Exists(param, validParams[types.BDNBlocksFeed]) { + return nil, fmt.Errorf("got unsupported param %v", param) + } + case types.NewTxsFeed: + if !utils.Exists(param, validTxParams) { + return nil, fmt.Errorf("got unsupported param %v", param) + } + case types.PendingTxsFeed: + if !utils.Exists(param, validTxParams) { + return nil, fmt.Errorf("got unsupported param %v", param) + } + case types.OnBlockFeed: + if !utils.Exists(param, validParams[types.OnBlockFeed]) { + return nil, fmt.Errorf("got unsupported param %v", param) + } + case types.TxReceiptsFeed: + if !utils.Exists(param, validParams[types.TxReceiptsFeed]) { + return nil, fmt.Errorf("got unsupported param %v", param) + } + } + if param == "tx_contents" { + requestedFields = append(requestedFields, txContentFields...) + } + requestedFields = append(requestedFields, param) + } + request.options.Include = requestedFields + + var expr conditions.Expr + if request.options.Filters != "" { + // Parse the condition language and get expression + p := conditions.NewParser(strings.NewReader(strings.ToLower(strings.Replace(request.options.Filters, "'", "\"", -1)))) + expr, err = p.Parse() + if err != nil { + log.Debugf("error parsing Filters from request id: %v. method: %v. params: %s. remote adress: %v account id: %v error - %v", + req.ID, req.Method, *req.Params, h.remoteAddress, h.connectionAccount.AccountID, err.Error()) + return nil, fmt.Errorf("error parsing Filters %v", err.Error()) + + } + err = h.evaluateFilters(expr) + if err != nil { + log.Debugf("error evalued Filters from request id: %v. method: %v. params: %s. remote adress: %v account id: %v error - %v", + req.ID, req.Method, *req.Params, h.remoteAddress, h.connectionAccount.AccountID, err.Error()) + return nil, fmt.Errorf("error evaluat Filters- %v", err.Error()) + } + if expr != nil { + log.Infof("GetTxContentAndFilters string - %s, GetTxContentAndFilters args - %s", expr, expr.Args()) + } + } + + // check if valid feed + var filters []string + if expr != nil { + filters = expr.Args() + } + + feedStreaming := sdnmessage.BDNFeedService{} + switch request.feed { + case types.NewTxsFeed: + feedStreaming = h.connectionAccount.NewTransactionStreaming + case types.PendingTxsFeed: + feedStreaming = h.connectionAccount.PendingTransactionStreaming + case types.BDNBlocksFeed, types.NewBlocksFeed: + feedStreaming = h.connectionAccount.NewBlockStreaming + case types.OnBlockFeed: + feedStreaming = h.connectionAccount.OnBlockFeed + case types.TxReceiptsFeed: + feedStreaming = h.connectionAccount.TransactionReceiptFeed + } + err = h.validateFeed(request.feed, feedStreaming, request.options.Include, filters) + if err != nil { + return nil, err + } + + calls := make(map[string]*RPCCall) + if request.feed == types.OnBlockFeed { + for idx, callParams := range request.options.CallParams { + if callParams == nil { + return nil, fmt.Errorf("call-params cannot be nil") + } + call := newCall(strconv.Itoa(idx)) + for param, value := range callParams { + switch param { + case "method": + isValidMethod := utils.Exists(value, h.FeedManager.blockchainWS.GetValidRPCCallMethods()) + if !isValidMethod { + return nil, fmt.Errorf("invalid method %v provided. Supported methods: %v", value, h.FeedManager.blockchainWS.GetValidRPCCallMethods()) + } + call.commandMethod = value + case "tag": + if value == "latest" { + call.blockOffset = 0 + break + } + blockOffset, err := strconv.Atoi(value) + if err != nil || blockOffset > 0 { + return nil, fmt.Errorf("invalid value %v provided for tag. Supported values: latest, 0 or a negative number", value) + } + call.blockOffset = blockOffset + case "name": + _, nameExists := calls[value] + if nameExists { + return nil, fmt.Errorf("unique name must be provided for each call: call %v already exists", value) + } + call.callName = value + default: + isValidPayloadField := utils.Exists(param, h.FeedManager.blockchainWS.GetValidRPCCallPayloadFields()) + if !isValidPayloadField { + return nil, fmt.Errorf("invalid payload field %v provided. Supported fields: %v", param, h.FeedManager.blockchainWS.GetValidRPCCallPayloadFields()) + } + call.callPayload[param] = value + } + } + requiredFields, ok := h.FeedManager.blockchainWS.GetRequiredPayloadFieldsForRPCMethod(call.commandMethod) + if !ok { + return nil, fmt.Errorf("unexpectedly, unable to find required fields for method %v", call.commandMethod) + } + err = call.validatePayload(call.commandMethod, requiredFields) + if err != nil { + return nil, err + } + calls[call.callName] = call + } + } + + clientRequest := &clientReq{} + clientRequest.includes = request.options.Include + clientRequest.feed = request.feed + clientRequest.expr = expr + clientRequest.calls = &calls + return clientRequest, nil +} + +// filtersHasEmptyValue - checks if some filter has empty value like "Filters":"({to})" +func (h *handlerObj) filtersHasEmptyValue(rawFilters string) error { + rex := regexp.MustCompile(`\(([^)]+)\)`) + out := rex.FindAllStringSubmatch(rawFilters, -1) + for _, i := range out { + for _, filter := range availableFilters { + if i[1] == filter || filter == rawFilters { + return fmt.Errorf("%v", i[1]) + } + } + } + return nil +} + +// evaluateFilters - evaluating if the Filters provided by the user are ok +func (h *handlerObj) evaluateFilters(expr conditions.Expr) error { + isEmptyValue := h.filtersHasEmptyValue(expr.String()) + if isEmptyValue != nil { + return isEmptyValue + } + //Evaluate if we should send the tx + _, err := conditions.Evaluate(expr, types.EmptyFilteredTransactionMap) + return err +} diff --git a/servers/clienthandler_test.go b/servers/clienthandler_test.go new file mode 100644 index 0000000..f6bc213 --- /dev/null +++ b/servers/clienthandler_test.go @@ -0,0 +1,234 @@ +package servers + +import ( + "context" + "encoding/json" + "fmt" + "github.com/bloXroute-Labs/gateway" + "github.com/bloXroute-Labs/gateway/blockchain" + "github.com/bloXroute-Labs/gateway/sdnmessage" + "github.com/bloXroute-Labs/gateway/test/bxmock" + "github.com/bloXroute-Labs/gateway/test/fixtures" + "github.com/bloXroute-Labs/gateway/types" + "github.com/gorilla/websocket" + uuid "github.com/satori/go.uuid" + "github.com/stretchr/testify/assert" + "golang.org/x/sync/errgroup" + "net/http" + "testing" + "time" +) + +var accountIDToAccountModel = map[types.AccountID]sdnmessage.Account{ + "a": {AccountInfo: sdnmessage.AccountInfo{AccountID: "a", TierName: sdnmessage.ATierElite}, SecretHash: "123456"}, + "b": {AccountInfo: sdnmessage.AccountInfo{AccountID: "b", TierName: sdnmessage.ATierDeveloper}, SecretHash: "7891011"}, + "c": {AccountInfo: sdnmessage.AccountInfo{AccountID: "c", TierName: sdnmessage.ATierElite}}, + "gw": { + AccountInfo: sdnmessage.AccountInfo{ + AccountID: "gw", + ExpireDate: "2999-12-31", + TierName: sdnmessage.ATierEnterprise, + }, + SecretHash: "secret", + NewTransactionStreaming: sdnmessage.BDNFeedService{ + ExpireDate: "2999-12-31", + Feed: sdnmessage.FeedProperties{ + AllowFiltering: true, + AvailableFields: []string{"all"}, + }, + }, + }, +} + +func getMockCustomerAccountModel(accountID types.AccountID) (sdnmessage.Account, error) { + var err error + if accountID == "d" { + err = fmt.Errorf("Timeout error") + } + return accountIDToAccountModel[accountID], err +} + +func TestClientHandler(t *testing.T) { + t.Skipf("Causing crash with make tests - should be check") + g := bxmock.MockBxListener{} + feedChan := make(chan types.Notification) + url := "127.0.0.1:28332" + wsURL := fmt.Sprintf("ws://%s/ws", url) + gwAccount, _ := getMockCustomerAccountModel("gw") + fm := NewFeedManager(context.Background(), g, feedChan, url, types.NetworkNum(1), bxmock.NewMockWSProvider(), true, gwAccount, getMockCustomerAccountModel, false, "", "") + var group errgroup.Group + group.Go(fm.Start) + time.Sleep(10 * time.Millisecond) + + dialer := websocket.DefaultDialer + headers := make(http.Header) + + // pass - different account for server and client + dummyAuthHeader := "YToxMjM0NTY=" + headers.Set("Authorization", dummyAuthHeader) + ws, _, err := dialer.Dial(wsURL, headers) + assert.Nil(t, err) + + t.Run("wsClient", func(t *testing.T) { + handlePingRequest(t, ws) + }) + + // fail for tier type + dummyAuthHeader = "Yjo3ODkxMDEx" + headers.Set("Authorization", dummyAuthHeader) + ws, _, err = dialer.Dial(wsURL, headers) + assert.NotNil(t, err) + + // fail for secret hash + dummyAuthHeader = "Yzo3ODkxMDEx" + headers.Set("Authorization", dummyAuthHeader) + ws, _, err = dialer.Dial(wsURL, headers) + assert.NotNil(t, err) + + // fail for timeout - account should set to enterprise + dummyAuthHeader = "ZDo3ODkxMDEx" + headers.Set("Authorization", dummyAuthHeader) + ws, _, err = dialer.Dial(wsURL, headers) + assert.Nil(t, err) + + t.Run("wsClient", func(t *testing.T) { + handlePingRequest(t, ws) + }) + + // pass - same account for server and client + dummyAuthHeader = "Z3c6c2VjcmV0" + headers.Set("Authorization", dummyAuthHeader) + ws, _, err = dialer.Dial(wsURL, headers) + assert.Nil(t, err) + + t.Run("wsClient", func(t *testing.T) { + handlePingRequest(t, ws) + handleBlxrTxRequestLegacyTx(t, ws) + handleBlxrTxRequestAccessListTx(t, ws) + handleBlxrTxRequestDynamicFeeTx(t, ws) + handleSubscribe(t, fm, ws) + testWSShutdown(t, fm, ws) + }) +} + +func handleSubscribe(t *testing.T, fm *FeedManager, ws *websocket.Conn) { + subscribeMsg := writeMsgToWsAndReadResponse(ws, []byte(`{"id": "1", "method": "subscribe", "params": ["newTxs", {"include": ["tx_hash"]}]}`)) + clientRes := getClientResponse(subscribeMsg) + subscriptionID, err := uuid.FromString(fmt.Sprintf("%v", clientRes.Result)) + assert.Nil(t, err) + _, exists := fm.idToClientSubscription[subscriptionID] + assert.True(t, exists) +} + +func testWSShutdown(t *testing.T, fm *FeedManager, ws *websocket.Conn) { + subscribeMsg := writeMsgToWsAndReadResponse(ws, []byte(`{"id": "1", "method": "subscribe", "params": ["newTxs", {"include": ["tx_hash"]}]}`)) + clientRes := getClientResponse(subscribeMsg) + subscriptionID, err := uuid.FromString(fmt.Sprintf("%v", clientRes.Result)) + assert.Nil(t, err) + _, exists := fm.idToClientSubscription[subscriptionID] + assert.True(t, exists) + + subscribeMsg2 := writeMsgToWsAndReadResponse(ws, []byte(`{"id": "2", "method": "subscribe", "params": ["newTxs", {"include": ["tx_hash"]}]}`)) + clientRes2 := getClientResponse(subscribeMsg2) + subscriptionID2, err := uuid.FromString(fmt.Sprintf("%v", clientRes2.Result)) + assert.Nil(t, err) + _, exists = fm.idToClientSubscription[subscriptionID2] + assert.True(t, exists) + + fm.blockchainWS.UpdateNodeSyncStatus(blockchain.Unsynced) + time.Sleep(time.Millisecond) + _, exists = fm.idToClientSubscription[subscriptionID] + assert.False(t, exists) + _, exists = fm.idToClientSubscription[subscriptionID2] + assert.False(t, exists) +} + +func handlePingRequest(t *testing.T, ws *websocket.Conn) { + timeClientSendsRequest := time.Now().UTC() + msg := writeMsgToWsAndReadResponse(ws, []byte(`{"id": "1", "method": "ping"}`)) + timeClientReceivesResponse := time.Now().UTC() + + clientRes := getClientResponse(msg) + res := parsePingResult(clientRes.Result) + timeServerReceivesRequest, err := time.Parse(bxgateway.MicroSecTimeFormat, res.Pong) + assert.Nil(t, err) + assert.True(t, timeClientReceivesResponse.After(timeServerReceivesRequest)) + assert.True(t, timeServerReceivesRequest.After(timeClientSendsRequest)) +} + +func handleBlxrTxRequestLegacyTx(t *testing.T, ws *websocket.Conn) { + reqPayload := fmt.Sprintf(`{"id": "1", "method": "blxr_tx", "params": {"transaction": "%s"}}`, fixtures.LegacyTransaction) + msg := writeMsgToWsAndReadResponse(ws, []byte(reqPayload)) + clientRes := getClientResponse(msg) + res := parseBlxrTxResult(clientRes.Result) + assert.Equal(t, fixtures.LegacyTransactionHash[2:], res.TxHash) +} + +func handleBlxrTxRequestAccessListTx(t *testing.T, ws *websocket.Conn) { + reqPayload := fmt.Sprintf(`{"id": "1", "method": "blxr_tx", "params": {"transaction": "%s"}}`, fixtures.AccessListTransactionForRPCInterface) + msg := writeMsgToWsAndReadResponse(ws, []byte(reqPayload)) + clientRes := getClientResponse(msg) + res := parseBlxrTxResult(clientRes.Result) + assert.Equal(t, fixtures.AccessListTransactionHash[2:], res.TxHash) +} + +func handleBlxrTxRequestDynamicFeeTx(t *testing.T, ws *websocket.Conn) { + reqPayload := fmt.Sprintf(`{"id": "1", "method": "blxr_tx", "params": {"transaction": "%s"}}`, fixtures.DynamicFeeTransactionForRPCInterface) + msg := writeMsgToWsAndReadResponse(ws, []byte(reqPayload)) + clientRes := getClientResponse(msg) + res := parseBlxrTxResult(clientRes.Result) + assert.Equal(t, fixtures.DynamicFeeTransactionHash[2:], res.TxHash) +} + +type clientResponse struct { + Jsonrpc string `json:"JSONRPC"` + ID string `json:"id"` + Result interface{} `json:"result"` +} + +func getClientResponse(msg []byte) (cr clientResponse) { + res := clientResponse{} + err := json.Unmarshal(msg, &res) + if err != nil { + panic(err) + } + return res +} + +func parsePingResult(rpcResponse interface{}) (pr rpcPingResponse) { + res := rpcPingResponse{} + b, err := json.Marshal(rpcResponse) + if err != nil { + panic(err) + } + err = json.Unmarshal(b, &res) + if err != nil { + panic(err) + } + return res +} + +func parseBlxrTxResult(rpcResponse interface{}) (tr rpcTxResponse) { + res := rpcTxResponse{} + b, err := json.Marshal(rpcResponse) + if err != nil { + panic(err) + } + err = json.Unmarshal(b, &res) + if err != nil { + panic(err) + } + return res +} + +func writeMsgToWsAndReadResponse(conn *websocket.Conn, msg []byte) (response []byte) { + err := conn.WriteMessage(websocket.TextMessage, msg) + if err != nil { + panic(err) + } + _, response, err = conn.ReadMessage() + if err != nil { + panic(err) + } + return response +} diff --git a/servers/constants.go b/servers/constants.go new file mode 100644 index 0000000..cae4877 --- /dev/null +++ b/servers/constants.go @@ -0,0 +1,45 @@ +package servers + +// RPCErrorCode represents an error condition while processing an RPC request +type RPCErrorCode int64 + +// RPCErrorCode types +const ( + // ParseError - json parse error + ParseError RPCErrorCode = -32700 + + // InvalidRequest - invalid request + InvalidRequest RPCErrorCode = -32600 + + // MethodNotFound - method not found + MethodNotFound RPCErrorCode = -32601 + + // InvalidParams - invalid params + InvalidParams RPCErrorCode = -32602 + + // InternalError - internal error + InternalError RPCErrorCode = -32603 + + // AccountIDError - invalid account ID + AccountIDError RPCErrorCode = -32004 +) + +var errorMsg = map[RPCErrorCode]string{ + MethodNotFound: "Invalid method", + InvalidParams: "Invalid params", + AccountIDError: "Invalid account ID", + InternalError: "Internal error", +} + +// RPCRequestType represents the JSON-RPC methods that are callable +type RPCRequestType string + +// RPCRequestType enumeration +const ( + RPCSubscribe RPCRequestType = "subscribe" + RPCUnsubscribe RPCRequestType = "unsubscribe" + RPCTx RPCRequestType = "blxr_tx" + RPCPing RPCRequestType = "ping" + RPCMevSearcher RPCRequestType = "blxr_mev_searcher" + RPCMevBuilder RPCRequestType = "blxr_mev_builder" +) diff --git a/servers/feedmanager.go b/servers/feedmanager.go new file mode 100644 index 0000000..01ccbd1 --- /dev/null +++ b/servers/feedmanager.go @@ -0,0 +1,167 @@ +package servers + +import ( + "context" + "fmt" + "github.com/bloXroute-Labs/gateway" + "github.com/bloXroute-Labs/gateway/blockchain" + "github.com/bloXroute-Labs/gateway/connections" + "github.com/bloXroute-Labs/gateway/sdnmessage" + "github.com/bloXroute-Labs/gateway/types" + "github.com/satori/go.uuid" + log "github.com/sirupsen/logrus" + "github.com/sourcegraph/jsonrpc2" + "sync" +) + +// ClientSubscription contains client subscription feed and websocket connection +type ClientSubscription struct { + feed chan *types.Notification + feedType types.FeedType + connection *jsonrpc2.Conn +} + +// FeedManager - feed manager fields +type FeedManager struct { + feedChan chan types.Notification + idToClientSubscription map[uuid.UUID]ClientSubscription + lock sync.RWMutex + addr string + node connections.BxListener + networkNum types.NetworkNum + blockchainWS blockchain.WSProvider + manageWSServer bool + accountModel sdnmessage.Account + getCustomerAccountModel func(types.AccountID) (sdnmessage.Account, error) + websocketTLSEnabled bool + certFile string + keyFile string + + context context.Context + cancel context.CancelFunc +} + +// NewFeedManager - create a new feedManager +func NewFeedManager(parent context.Context, node connections.BxListener, feedChan chan types.Notification, + addr string, networkNum types.NetworkNum, ws blockchain.WSProvider, manageWSServer bool, + accountModel sdnmessage.Account, getCustomerAccountModel func(types.AccountID) (sdnmessage.Account, error), + websocketTLSEnabled bool, certFile string, keyFile string) *FeedManager { + ctx, cancel := context.WithCancel(parent) + newServer := &FeedManager{ + feedChan: feedChan, + idToClientSubscription: make(map[uuid.UUID]ClientSubscription), + addr: addr, + node: node, + networkNum: networkNum, + blockchainWS: ws, + manageWSServer: manageWSServer, + accountModel: accountModel, + getCustomerAccountModel: getCustomerAccountModel, + websocketTLSEnabled: websocketTLSEnabled, + certFile: certFile, + keyFile: keyFile, + context: ctx, + cancel: cancel, + } + return newServer +} + +// Start - start feed manager +func (f *FeedManager) Start() error { + log.Infof("starting feed provider on addr: %v", f.addr) + defer f.cancel() + + ch := clientHandler{ + feedManager: f, + server: NewWSServer(f), + } + go ch.runWSServer() + if f.manageWSServer { + go ch.manageWSServer() + } + f.run() + return nil +} + +// Subscribe - subscribe a client to a desired feed +func (f *FeedManager) Subscribe(feedName types.FeedType, conn *jsonrpc2.Conn) (*uuid.UUID, *chan *types.Notification, error) { + if !types.Exists(feedName, availableFeeds) { + err := fmt.Errorf("got unsupported feed name %v", feedName) + log.Error(err.Error()) + return nil, nil, err + } + + id := uuid.NewV4() + clientSubscription := ClientSubscription{ + feed: make(chan *types.Notification, bxgateway.BxNotificationChannelSize), + feedType: feedName, + connection: conn, + } + + f.lock.Lock() + f.idToClientSubscription[id] = clientSubscription + f.lock.Unlock() + + return &id, &clientSubscription.feed, nil +} + +// Unsubscribe - unsubscribe a client from feed +func (f *FeedManager) Unsubscribe(subscriptionID uuid.UUID) error { + f.lock.Lock() + defer f.lock.Unlock() + + clientSub, exists := f.idToClientSubscription[subscriptionID] + if !exists { + return fmt.Errorf("subscription %v was not found", subscriptionID) + } + close(clientSub.feed) + delete(f.idToClientSubscription, subscriptionID) + err := clientSub.connection.Close() + if err != nil && err != jsonrpc2.ErrClosed { + return fmt.Errorf("encountered error closing websocket connection with ID %v", subscriptionID) + } + return nil +} + +// UnsubscribeAll - unsubscribes all client subscriptions +func (f *FeedManager) UnsubscribeAll() { + for subscriptionID := range f.idToClientSubscription { + err := f.Unsubscribe(subscriptionID) + if err != nil { + log.Errorf("failed to unsubscribe subscription with ID %v: %v", subscriptionID, err) + } + } +} + +// run - getting newTx or pendingTx and pass to client via common channel +func (f *FeedManager) run() { + for { + notification, ok := <-f.feedChan + if !ok { + log.Errorf("feed manager can not pull from feed channel") + break + } + f.lock.RLock() + for uid, clientSub := range f.idToClientSubscription { + if clientSub.feedType == notification.NotificationType() { + select { + case clientSub.feed <- ¬ification: + default: + log.Warnf("can't send %v to channel %v without blocking. Ignored hash %v", clientSub.feedType, uid, notification.GetHash()) + } + } + + } + f.lock.RUnlock() + } +} + +func (f *FeedManager) subscriptionExists(subscriptionID uuid.UUID) bool { + f.lock.RLock() + defer f.lock.RUnlock() + + if _, exists := f.idToClientSubscription[subscriptionID]; exists { + return true + } + return false +} diff --git a/servers/websocket.go b/servers/websocket.go new file mode 100644 index 0000000..7e3fab9 --- /dev/null +++ b/servers/websocket.go @@ -0,0 +1,57 @@ +package servers + +import ( + "fmt" + "github.com/gorilla/websocket" + log "github.com/sirupsen/logrus" + "github.com/sourcegraph/jsonrpc2" + websocketjsonrpc2 "github.com/sourcegraph/jsonrpc2/websocket" + "net/http" +) + +// WebsocketRPCServer represents a simple RPC server +type WebsocketRPCServer struct { + host string + port int + handler jsonrpc2.Handler +} + +// NewWebsocketRPCServer initializes an RPC server with any RPC handler +func NewWebsocketRPCServer(host string, port int, handler jsonrpc2.Handler) WebsocketRPCServer { + return WebsocketRPCServer{ + host: host, + port: port, + handler: handler, + } +} + +// Run starts the RPC Server +func (ws *WebsocketRPCServer) Run() error { + listenAddr := fmt.Sprintf("%v:%v", ws.host, ws.port) + + handler := http.NewServeMux() + handler.HandleFunc("/ws", func(w http.ResponseWriter, r *http.Request) { + log.Info("got a connection, upgrading to websockets") + upgrader := websocket.Upgrader{} + connection, err := upgrader.Upgrade(w, r, nil) + if err != nil { + log.Errorf("error upgrading HTTP server connection to websocket protocol: %v", err) + return + } + handler := jsonrpc2.AsyncHandler(ws.handler) + jc := jsonrpc2.NewConn(r.Context(), websocketjsonrpc2.NewObjectStream(connection), handler) + <-jc.DisconnectNotify() + + err = connection.Close() + if err != nil { + log.Errorf("error closing connection: %v", err) + } + }) + log.Infof("starting rpc server on %v", listenAddr) + + err := http.ListenAndServe(listenAddr, handler) + if err != nil { + panic(fmt.Errorf("could not listen on %v. error: %v", listenAddr, err)) + } + return nil +} diff --git a/services/asyncmsghandler.go b/services/asyncmsghandler.go new file mode 100644 index 0000000..c6c801a --- /dev/null +++ b/services/asyncmsghandler.go @@ -0,0 +1,43 @@ +package services + +import ( + "github.com/bloXroute-Labs/gateway" + "github.com/bloXroute-Labs/gateway/bxmessage" + "github.com/bloXroute-Labs/gateway/connections" + log "github.com/sirupsen/logrus" +) + +// MsgInfo is a struct that stores a msg and its source connection +type MsgInfo struct { + Msg bxmessage.Message + Source connections.Conn +} + +// AsyncMsgHandler is a struct that handles messages asynchronously +type AsyncMsgHandler struct { + AsyncMsgChannel chan MsgInfo + listener connections.BxListener +} + +// NewAsyncMsgChannel returns a new instance of AsyncMsgHandler +func NewAsyncMsgChannel(listener connections.BxListener) chan MsgInfo { + handler := &AsyncMsgHandler{ + AsyncMsgChannel: make(chan MsgInfo, bxgateway.AsyncMsgChannelSize), + listener: listener, + } + go handler.HandleMsgAsync() + return handler.AsyncMsgChannel +} + +// HandleMsgAsync handles messages pushed onto the channel of AsyncMsgHandler +func (amh AsyncMsgHandler) HandleMsgAsync() { + for { + messageInfo, ok := <-amh.AsyncMsgChannel + if !ok { + log.Error("unexpected termination of AsyncMsgHandler. AsyncMsgChannel was closed.") + return + } + log.Tracef("async handling of %v from %v", messageInfo.Msg, messageInfo.Source.ID().RemoteAddr()) + _ = amh.listener.HandleMsg(messageInfo.Msg, messageInfo.Source, connections.RunForeground) + } +} diff --git a/services/blockprocessor.go b/services/blockprocessor.go new file mode 100644 index 0000000..51c8787 --- /dev/null +++ b/services/blockprocessor.go @@ -0,0 +1,188 @@ +package services + +import ( + "errors" + "fmt" + "github.com/bloXroute-Labs/gateway/bxmessage" + "github.com/bloXroute-Labs/gateway/types" + "github.com/ethereum/go-ethereum/rlp" + log "github.com/sirupsen/logrus" + "math/big" + "sync" + "time" +) + +// error constants for identifying special processing casess +var ( + ErrAlreadyProcessed = errors.New("already processed") + ErrMissingShortIDs = errors.New("missing short IDs") +) + +// BxBlockConverter is the service interface for converting broadcast messages to/from bx blocks +type BxBlockConverter interface { + BxBlockToBroadcast(*types.BxBlock, types.NetworkNum, time.Duration) (*bxmessage.Broadcast, types.ShortIDList, error) + BxBlockFromBroadcast(*bxmessage.Broadcast) (*types.BxBlock, types.ShortIDList, error) +} + +// BlockProcessor is the service interface for processing broadcast messages +type BlockProcessor interface { + BxBlockConverter + + ShouldProcess(hash types.SHA256Hash) bool + ProcessBroadcast(*bxmessage.Broadcast) (block *types.BxBlock, missingShortIDsCount int, err error) +} + +// NewRLPBlockProcessor returns a BlockProcessor for Ethereum blocks encoded in broadcast messages +func NewRLPBlockProcessor(txStore TxStore) BlockProcessor { + bp := &rlpBlockProcessor{ + txStore: txStore, + processedBlocks: NewHashHistory("processedBlocks", 30*time.Minute), + lock: &sync.Mutex{}, + } + return bp +} + +type rlpBlockProcessor struct { + txStore TxStore + processedBlocks HashHistory + lock *sync.Mutex +} + +type bxCompressedTransaction struct { + IsFullTransaction bool + Transaction []byte +} + +type bxBlockRLP struct { + Header rlp.RawValue + Txs []bxCompressedTransaction + Trailer rlp.RawValue + TotalDifficulty *big.Int + Number *big.Int +} + +func (bp *rlpBlockProcessor) ProcessBroadcast(broadcast *bxmessage.Broadcast) (*types.BxBlock, int, error) { + bxBlock, missingShortIDs, err := bp.BxBlockFromBroadcast(broadcast) + if err == ErrMissingShortIDs { + log.Debugf("block %v from BDN is missing %v short IDs", broadcast.Hash(), len(missingShortIDs)) + return nil, len(missingShortIDs), ErrMissingShortIDs + } else if err != nil { + return nil, len(missingShortIDs), err + } + + return bxBlock, len(missingShortIDs), nil +} + +func (bp *rlpBlockProcessor) BxBlockToBroadcast(block *types.BxBlock, networkNum types.NetworkNum, minTxAge time.Duration) (*bxmessage.Broadcast, types.ShortIDList, error) { + bp.lock.Lock() + defer bp.lock.Unlock() + + blockHash := block.Hash() + if !bp.ShouldProcess(blockHash) { + return nil, nil, ErrAlreadyProcessed + } + + usedShortIDs := make(types.ShortIDList, 0) + txs := make([]bxCompressedTransaction, 0, len(block.Txs)) + maxTimestampForCompression := time.Now().Add(-minTxAge) + // compress transactions in block if short ID is known + for _, tx := range block.Txs { + txHash := tx.Hash() + + bxTransaction, ok := bp.txStore.Get(txHash) + if ok && bxTransaction.AddTime().Before(maxTimestampForCompression) { + shortIDs := bxTransaction.ShortIDs() + if len(shortIDs) > 0 { + shortID := shortIDs[0] + usedShortIDs = append(usedShortIDs, shortID) + txs = append(txs, bxCompressedTransaction{ + IsFullTransaction: false, + Transaction: []byte{}, + }) + continue + } + } + txs = append(txs, bxCompressedTransaction{ + IsFullTransaction: true, + Transaction: tx.Content(), + }) + } + + rlpBlock := bxBlockRLP{ + Header: block.Header, + Txs: txs, + Trailer: block.Trailer, + TotalDifficulty: block.TotalDifficulty, + Number: block.Number, + } + encodedBlock, err := rlp.EncodeToBytes(rlpBlock) + if err != nil { + return nil, usedShortIDs, err + } + + bp.markProcessed(blockHash) + broadcastMessage := bxmessage.NewBlockBroadcast(block.Hash(), encodedBlock, usedShortIDs, networkNum) + return broadcastMessage, usedShortIDs, nil +} + +// BxBlockFromBroadcast processes the encoded compressed block in a broadcast message, replacing all short IDs with their stored transaction contents +func (bp *rlpBlockProcessor) BxBlockFromBroadcast(broadcast *bxmessage.Broadcast) (*types.BxBlock, types.ShortIDList, error) { + bp.lock.Lock() + defer bp.lock.Unlock() + + blockHash := broadcast.Hash() + if !bp.ShouldProcess(blockHash) { + return nil, nil, ErrAlreadyProcessed + } + + shortIDs := broadcast.ShortIDs() + var bxTransactions []*types.BxTransaction + var missingShortIDs types.ShortIDList + var err error + + // looking for missing sids + for _, sid := range shortIDs { + bxTransaction, err := bp.txStore.GetTxByShortID(sid) + if err == nil { // sid exists in TxStore + bxTransactions = append(bxTransactions, bxTransaction) + } else { + missingShortIDs = append(missingShortIDs, sid) + } + } + + if len(missingShortIDs) > 0 { + return nil, missingShortIDs, ErrMissingShortIDs + } + + var rlpBlock bxBlockRLP + if err = rlp.DecodeBytes(broadcast.Block(), &rlpBlock); err != nil { + return nil, missingShortIDs, err + } + + compressedTransactionCount := 0 + txs := make([]*types.BxBlockTransaction, 0, len(rlpBlock.Txs)) + + for _, tx := range rlpBlock.Txs { + if !tx.IsFullTransaction { + if compressedTransactionCount >= len(bxTransactions) { + return nil, missingShortIDs, fmt.Errorf("could not decompress bad block: more empty transactions than short IDs provided") + } + txs = append(txs, types.NewRawBxBlockTransaction(bxTransactions[compressedTransactionCount].Content())) + compressedTransactionCount++ + } else { + txs = append(txs, types.NewRawBxBlockTransaction(tx.Transaction)) + } + } + + bp.markProcessed(blockHash) + block := types.NewRawBxBlock(broadcast.Hash(), rlpBlock.Header, txs, rlpBlock.Trailer, rlpBlock.TotalDifficulty, rlpBlock.Number) + return block, missingShortIDs, err +} + +func (bp *rlpBlockProcessor) ShouldProcess(hash types.SHA256Hash) bool { + return !bp.processedBlocks.Exists(hash.String()) +} + +func (bp *rlpBlockProcessor) markProcessed(hash types.SHA256Hash) { + bp.processedBlocks.Add(hash.String(), 10*time.Minute) +} diff --git a/services/blockprocessor_test.go b/services/blockprocessor_test.go new file mode 100644 index 0000000..1f71fde --- /dev/null +++ b/services/blockprocessor_test.go @@ -0,0 +1,213 @@ +package services + +import ( + "github.com/bloXroute-Labs/gateway/bxmessage" + "github.com/bloXroute-Labs/gateway/test" + "github.com/bloXroute-Labs/gateway/test/bxmock" + "github.com/bloXroute-Labs/gateway/test/fixtures" + "github.com/bloXroute-Labs/gateway/types" + "github.com/ethereum/go-ethereum/common" + ethtypes "github.com/ethereum/go-ethereum/core/types" + "github.com/ethereum/go-ethereum/rlp" + "github.com/stretchr/testify/assert" + "math/big" + "testing" + "time" +) + +func TestRLPBlockProcessor_BxBlockToBroadcast(t *testing.T) { + store := newTestBxTxStore() + bp := NewRLPBlockProcessor(&store) + clock := bxmock.MockClock{} + clock.SetTime(time.Now()) + + blockHash := types.GenerateSHA256Hash() + header, _ := rlp.EncodeToBytes(test.GenerateBytes(300)) + trailer, _ := rlp.EncodeToBytes(test.GenerateBytes(350)) + + // note that txs[0] is a huge tx + txs := []*types.BxBlockTransaction{ + types.NewBxBlockTransaction(types.GenerateSHA256Hash(), test.GenerateBytes(25000)), + types.NewBxBlockTransaction(types.GenerateSHA256Hash(), test.GenerateBytes(250)), + types.NewBxBlockTransaction(types.GenerateSHA256Hash(), test.GenerateBytes(250)), + types.NewBxBlockTransaction(types.GenerateSHA256Hash(), test.GenerateBytes(250)), + types.NewBxBlockTransaction(types.GenerateSHA256Hash(), test.GenerateBytes(250)), + } + + // create delay the txs[0], so it passes the age check + store.Add(txs[0].Hash(), txs[0].Content(), 1, testNetworkNum, false, 0, clock.Now().Add(-2*time.Second), 0) + + // The txs[2] will not be included in shortID since it's too recent + store.Add(txs[3].Hash(), txs[3].Content(), 2, testNetworkNum, false, 0, clock.Now(), 0) + + bxBlock, err := types.NewBxBlock(blockHash, header, txs, trailer, big.NewInt(10000), big.NewInt(10)) + assert.Nil(t, err) + + // assume the blockchain network MinTxAgeSecond is 2 + broadcastMessage, shortIDs, err := bp.BxBlockToBroadcast(bxBlock, testNetworkNum, time.Second*2) + assert.Nil(t, err) + + // only the first shortID exists, the second Tx didn't get added into shortID + assert.Equal(t, 1, len(shortIDs)) + assert.Contains(t, shortIDs, types.ShortID(1)) + assert.NotContains(t, shortIDs, types.ShortID(2)) + + // check that block is definitely compressed (tx 0 is huge) + assert.Less(t, len(broadcastMessage.Block()), 2000) + + // duplicate, skip this time + // assume the blockchain network MinTxAgeSecond is 2 + _, _, err = bp.BxBlockToBroadcast(bxBlock, testNetworkNum, time.Second*2) + assert.Equal(t, ErrAlreadyProcessed, err) + + // duplicate, skip from other direction too + _, _, err = bp.BxBlockFromBroadcast(broadcastMessage) + assert.Equal(t, ErrAlreadyProcessed, err) + + // decompress same block works after clearing processed list + bp.(*rlpBlockProcessor).processedBlocks = NewHashHistory("processedBlocks", 30*time.Minute) + decodedBxBlock, missingShortIDs, err := bp.BxBlockFromBroadcast(broadcastMessage) + assert.Nil(t, err) + assert.Equal(t, 0, len(missingShortIDs)) + + assert.Equal(t, header, decodedBxBlock.Header) + assert.Equal(t, trailer, decodedBxBlock.Trailer) + + for i, tx := range decodedBxBlock.Txs { + assert.Equal(t, txs[i].Content(), tx.Content()) + } +} + +func TestRLPBlockProcessor_BroadcastToBxBlockMissingShortIDs(t *testing.T) { + broadcast := &bxmessage.Broadcast{} + _ = broadcast.Unpack(common.Hex2Bytes(fixtures.BroadcastMessageWithShortIDs), 0) + + store := newTestBxTxStore() + bp := NewRLPBlockProcessor(&store) + + bxBlock, missingShortIDs, err := bp.BxBlockFromBroadcast(broadcast) + assert.NotNil(t, err) + assert.Nil(t, bxBlock) + assert.Equal(t, 2, len(missingShortIDs)) + + // ok to reprocess, not successfully seen yet + _, _, err = bp.BxBlockFromBroadcast(broadcast) + assert.NotEqual(t, ErrAlreadyProcessed, err) +} + +func TestRLPBlockProcessor_BroadcastToBxBlockShortIDs(t *testing.T) { + broadcast := &bxmessage.Broadcast{} + _ = broadcast.Unpack(common.Hex2Bytes(fixtures.BroadcastMessageWithShortIDs), 0) + + store := newTestBxTxStore() + bp := NewRLPBlockProcessor(&store) + + txHash1, _ := types.NewSHA256HashFromString(fixtures.BroadcastTransactionHash1) + txContent1 := common.Hex2Bytes(fixtures.BroadcastTransactionContent1) + txHash2, _ := types.NewSHA256HashFromString(fixtures.BroadcastTransactionHash2) + txContent2 := common.Hex2Bytes(fixtures.BroadcastTransactionContent2) + + txContents := [][]byte{txContent1, txContent2} + + store.Add(txHash1, txContent1, 1, testNetworkNum, false, types.TFPaidTx, time.Now(), testChainID) + store.Add(txHash2, txContent2, 2, testNetworkNum, false, types.TFPaidTx, time.Now(), testChainID) + + bxBlock, missingShortIDs, err := bp.BxBlockFromBroadcast(broadcast) + assert.Nil(t, err) + assert.NotNil(t, bxBlock) + assert.Equal(t, 0, len(missingShortIDs)) + + // note: this hash does not actually match the block contents (test data was generated as such) + assert.Equal(t, broadcast.Hash(), bxBlock.Hash()) + + // check transactions have been decompressed + assert.Equal(t, 2, len(bxBlock.Txs)) + for i, blockTx := range bxBlock.Txs { + assert.Equal(t, txContents[i], blockTx.Content()) + } + + // verify integrity of other fields + var ethHeader ethtypes.Header + if err := rlp.DecodeBytes(bxBlock.Header, ðHeader); err != nil { + t.Fatal(err) + } + + var uncles []*ethtypes.Header + if err := rlp.DecodeBytes(bxBlock.Trailer, &uncles); err != nil { + t.Fatal(err) + } + assert.Equal(t, 1, len(uncles)) + assert.Equal(t, common.Hex2Bytes(fixtures.BroadcastUncleParentHash), uncles[0].ParentHash.Bytes()) + + assert.Equal(t, fixtures.BroadcastDifficulty, bxBlock.TotalDifficulty) + assert.Equal(t, fixtures.BroadcastBlockNumber, bxBlock.Number) + + // duplicate, skip this time + _, _, err = bp.BxBlockFromBroadcast(broadcast) + assert.Equal(t, ErrAlreadyProcessed, err) +} + +func TestRLPBlockProcessor_BroadcastToBxBlockFullTxs(t *testing.T) { + broadcast := &bxmessage.Broadcast{} + _ = broadcast.Unpack(common.Hex2Bytes(fixtures.BroadcastMessageFullTxs), 0) + + store := newTestBxTxStore() + bp := NewRLPBlockProcessor(&store) + + txHash1, _ := types.NewSHA256HashFromString(fixtures.BroadcastTransactionHash1) + txContent1 := common.Hex2Bytes(fixtures.BroadcastTransactionContent1) + + store.Add(txHash1, txContent1, 1, testNetworkNum, false, types.TFPaidTx, time.Now(), testChainID) + + bxBlock, missingShortIDs, err := bp.BxBlockFromBroadcast(broadcast) + assert.Nil(t, err) + assert.NotNil(t, bxBlock) + assert.Equal(t, 0, len(missingShortIDs)) + + // note: this hash does not actually match the block contents (test data was generated as such) + assert.Equal(t, broadcast.Hash(), bxBlock.Hash()) + + assert.Equal(t, 1, len(bxBlock.Txs)) + assert.NotEqual(t, txContent1, bxBlock.Txs[0].Content()) + + // verify integrity of other fields + var ethHeader ethtypes.Header + if err := rlp.DecodeBytes(bxBlock.Header, ðHeader); err != nil { + t.Fatal(err) + } + assert.Equal(t, common.Hex2Bytes(fixtures.BroadcastMessageFullTxsBlockHash), ethHeader.Hash().Bytes()) + + var uncles []*ethtypes.Header + if err := rlp.DecodeBytes(bxBlock.Trailer, &uncles); err != nil { + t.Fatal(err) + } + assert.Equal(t, 2, len(uncles)) + + assert.Equal(t, fixtures.BroadcastDifficulty, bxBlock.TotalDifficulty) + assert.Equal(t, fixtures.BroadcastBlockNumber, bxBlock.Number) +} + +func TestRLPBlockProcessor_ProcessBroadcast(t *testing.T) { + broadcast := &bxmessage.Broadcast{} + _ = broadcast.Unpack(common.Hex2Bytes(fixtures.BroadcastMessageWithShortIDs), 0) + + txHash1, _ := types.NewSHA256HashFromString(fixtures.BroadcastTransactionHash1) + txContent1 := common.Hex2Bytes(fixtures.BroadcastTransactionContent1) + txHash2, _ := types.NewSHA256HashFromString(fixtures.BroadcastTransactionHash2) + txContent2 := common.Hex2Bytes(fixtures.BroadcastTransactionContent2) + + store := newTestBxTxStore() + store.Add(txHash1, txContent1, 1, testNetworkNum, false, types.TFPaidTx, time.Now(), testChainID) + store.Add(txHash2, txContent2, 2, testNetworkNum, false, types.TFPaidTx, time.Now(), testChainID) + + bp := NewRLPBlockProcessor(&store) + + bxBlock, _, err := bp.ProcessBroadcast(broadcast) + assert.Nil(t, err) + + assert.NotNil(t, bxBlock) + assert.Equal(t, broadcast.Hash(), bxBlock.Hash()) + + _, _, err = bp.ProcessBroadcast(broadcast) + assert.Equal(t, ErrAlreadyProcessed, err) +} diff --git a/services/bxtxstore.go b/services/bxtxstore.go new file mode 100644 index 0000000..600bdc3 --- /dev/null +++ b/services/bxtxstore.go @@ -0,0 +1,381 @@ +package services + +import ( + "encoding/hex" + "fmt" + "github.com/bloXroute-Labs/gateway" + pbbase "github.com/bloXroute-Labs/gateway/protobuf" + "github.com/bloXroute-Labs/gateway/types" + "github.com/bloXroute-Labs/gateway/utils" + "github.com/orcaman/concurrent-map" + log "github.com/sirupsen/logrus" + "runtime/debug" + "sort" + "sync" + "time" +) + +// BxTxStore represents the storage of transaction info for a given node +type BxTxStore struct { + clock utils.Clock + hashToContent cmap.ConcurrentMap + shortIDToHash cmap.ConcurrentMap + + seenTxs HashHistory + timeToAvoidReEntry time.Duration + + cleanupFreq time.Duration + maxTxAge time.Duration + noSIDAge time.Duration + quit chan bool + lock sync.Mutex + assigner ShortIDAssigner + cleanedShortIDsChannel chan types.ShortIDsByNetwork +} + +// NewBxTxStore creates a new BxTxStore to store and processes all relevant transactions +func NewBxTxStore(cleanupFreq time.Duration, maxTxAge time.Duration, noSIDAge time.Duration, + assigner ShortIDAssigner, seenTxs HashHistory, cleanedShortIDsChannel chan types.ShortIDsByNetwork, + timeToAvoidReEntry time.Duration) BxTxStore { + return newBxTxStore(utils.RealClock{}, cleanupFreq, maxTxAge, noSIDAge, assigner, seenTxs, cleanedShortIDsChannel, timeToAvoidReEntry) +} + +func newBxTxStore(clock utils.Clock, cleanupFreq time.Duration, maxTxAge time.Duration, + noSIDAge time.Duration, assigner ShortIDAssigner, seenTxs HashHistory, cleanedShortIDsChannel chan types.ShortIDsByNetwork, timeToAvoidReEntry time.Duration) BxTxStore { + return BxTxStore{ + clock: clock, + hashToContent: cmap.New(), + shortIDToHash: cmap.New(), + seenTxs: seenTxs, + timeToAvoidReEntry: timeToAvoidReEntry, + cleanupFreq: cleanupFreq, + maxTxAge: maxTxAge, + noSIDAge: noSIDAge, + quit: make(chan bool), + assigner: assigner, + cleanedShortIDsChannel: cleanedShortIDsChannel, + } +} + +// Start initializes all relevant goroutines for the BxTxStore +func (t *BxTxStore) Start() error { + t.cleanup() + return nil +} + +// Stop closes all running go routines for BxTxStore +func (t *BxTxStore) Stop() { + t.quit <- true + <-t.quit +} + +// Clear removes all elements from txs and shortIDToHash +func (t *BxTxStore) Clear() { + t.hashToContent.Clear() + t.shortIDToHash.Clear() + log.Debugf("Cleared tx service.") +} + +// Count indicates the number of stored transaction in BxTxStore +func (t *BxTxStore) Count() int { + return t.hashToContent.Count() +} + +// remove deletes a single transaction, including its shortIDs +func (t *BxTxStore) remove(hash string, reEntryProtection bool, reason string) { + if tx, ok := t.hashToContent.Get(hash); ok { + bxTransaction := tx.(*types.BxTransaction) + for _, shortID := range bxTransaction.ShortIDs() { + t.shortIDToHash.Remove(fmt.Sprint(shortID)) + } + t.hashToContent.Remove(hash) + // if asked, add the hash to the history map so we remember this transaction for some time + // and prevent if from being added back to the TxStore + if reEntryProtection { + t.seenTxs.Add(hash, t.timeToAvoidReEntry) + } + log.Tracef("TxStore: transaction %v, network %v, shortIDs %v removed (%v). reEntryProtection %v", + bxTransaction.Hash(), bxTransaction.NetworkNum(), bxTransaction.ShortIDs(), reason, reEntryProtection) + } +} + +// RemoveShortIDs deletes a series of transactions by their short IDs. RemoveShortIDs can take a potentially large short ID array, so it should be passed by reference. +func (t *BxTxStore) RemoveShortIDs(shortIDs *types.ShortIDList, reEntryProtection bool, reason string) { + // note - it is OK for hashesToRemove to hold the same hash multiple times. + hashesToRemove := make(types.SHA256HashList, 0) + for _, shortID := range *shortIDs { + strShortID := fmt.Sprint(shortID) + if hash, ok := t.shortIDToHash.Get(strShortID); ok { + hashesToRemove = append(hashesToRemove, hash.(types.SHA256Hash)) + } + } + t.RemoveHashes(&hashesToRemove, reEntryProtection, reason) +} + +// GetTxByShortID lookup a transaction by its shortID. return error if not found +func (t *BxTxStore) GetTxByShortID(shortID types.ShortID) (*types.BxTransaction, error) { + if h, ok := t.shortIDToHash.Get(fmt.Sprint(shortID)); ok { + hash := h.(types.SHA256Hash) + if tx, exists := t.hashToContent.Get(string(hash[:])); exists { + return tx.(*types.BxTransaction), nil + } + return nil, fmt.Errorf("transaction content for shortID %v and hash %v does not exist", shortID, hash) + } + return nil, fmt.Errorf("transaction with shortID %v does not exist", shortID) +} + +// RemoveHashes deletes a series of transactions by their hash from BxTxStore. RemoveHashes can take a potentially large hash array, so it should be passed by reference. +func (t *BxTxStore) RemoveHashes(hashes *types.SHA256HashList, reEntryProtection bool, reason string) { + for _, hash := range *hashes { + t.remove(string(hash[:]), reEntryProtection, reason) + } +} + +// Iter returns a channel iterator for all transactions in BxTxStore +func (t *BxTxStore) Iter() (iter <-chan *types.BxTransaction) { + newChan := make(chan *types.BxTransaction) + go func() { + for elem := range t.hashToContent.IterBuffered() { + tx := elem.Val.(*types.BxTransaction) + if t.clock.Now().Sub(tx.AddTime()) < t.maxTxAge { + newChan <- tx + } + } + close(newChan) + }() + return newChan +} + +// Add adds a new transaction to BxTxStore +func (t *BxTxStore) Add(hash types.SHA256Hash, content types.TxContent, shortID types.ShortID, networkNum types.NetworkNum, + _ bool, flags types.TxFlags, timestamp time.Time, _ int64) TransactionResult { + if shortID == types.ShortIDEmpty && len(content) == 0 { + debug.PrintStack() + panic("Bad usage of Add function - content and shortID can't be both missing") + } + result := TransactionResult{} + if t.clock.Now().Sub(timestamp) > t.maxTxAge { + result.Transaction = types.NewBxTransaction(hash, networkNum, flags, timestamp) + result.DebugData = fmt.Sprintf("Transaction is too old - %v", timestamp) + return result + } + + hashStr := string(hash[:]) + // if the hash is in history we treat it as IgnoreSeen + if t.seenTxs.Exists(hashStr) { + // if the hash is in history, but we get a shortID for it, it means that the hash was not in the ATR history + //and some GWs may get and use this shortID. In such a case we should remove the hash from history and allow + //it to be added to the TxStore + if shortID != types.ShortIDEmpty { + t.seenTxs.Remove(hashStr) + } else { + result.Transaction = types.NewBxTransaction(hash, networkNum, flags, timestamp) + result.DebugData = fmt.Sprintf("Transaction already seen and deleted from store") + result.AlreadySeen = true + return result + } + } + + bxTransaction := types.NewBxTransaction(hash, networkNum, flags, timestamp) + if result.NewTx = t.hashToContent.SetIfAbsent(hashStr, bxTransaction); !result.NewTx { + tx, exists := t.hashToContent.Get(hashStr) + if !exists { + log.Warnf("couldn't Get an existing transaction %v, network %v, flags %v, shortID %v, content %v", + hash, networkNum, flags, shortID, hex.EncodeToString(content[:])) + result.Transaction = bxTransaction + result.DebugData = fmt.Sprintf("Transaction deleted by other GO routine") + return result + } + bxTransaction = tx.(*types.BxTransaction) + } + + // make sure we are the only process that makes changes to the transaction + bxTransaction.Lock() + + if !bxTransaction.Flags().IsPaid() && flags.IsPaid() { + result.Reprocess = true + bxTransaction.AddFlags(types.TFPaidTx) + } + if !bxTransaction.Flags().ShouldDeliverToNode() && flags.ShouldDeliverToNode() { + result.Reprocess = true + bxTransaction.AddFlags(types.TFDeliverToNode) + } + + // if shortID was not provided, assign shortID (if we are running as assigner) + // note that assigner.Next() provides ShortIDEmpty if we are not assigning + // if we assigned shortID, result.AssignedShortID hold non ShortIDEmpty value + if result.NewTx && shortID == types.ShortIDEmpty { + shortID = t.assigner.Next() + result.AssignedShortID = shortID + } + + result.NewSID = bxTransaction.AddShortID(shortID) + result.NewContent = bxTransaction.SetContent(content) + result.Transaction = bxTransaction + bxTransaction.Unlock() + + if result.NewSID { + t.shortIDToHash.Set(fmt.Sprint(shortID), bxTransaction.Hash()) + } + + return result +} + +type networkData struct { + maxAge time.Duration + ages []int + cleanAge int + cleanNoSID int +} + +func (t *BxTxStore) clean() (cleaned int, cleanedShortIDs types.ShortIDsByNetwork) { + currTime := t.clock.Now() + + var networks = make(map[types.NetworkNum]*networkData) + cleanedShortIDs = make(types.ShortIDsByNetwork) + + for item := range t.hashToContent.IterBuffered() { + bxTransaction := item.Val.(*types.BxTransaction) + netData, netDataExists := networks[bxTransaction.NetworkNum()] + if !netDataExists { + netData = &networkData{} + networks[bxTransaction.NetworkNum()] = netData + } + txAge := int(currTime.Sub(bxTransaction.AddTime()) / time.Second) + networks[bxTransaction.NetworkNum()].ages = append(networks[bxTransaction.NetworkNum()].ages, txAge) + } + + for net, netData := range networks { + // if we are below the number of allowed Txs, no need to do anything + if len(netData.ages) <= bxgateway.TxStoreMaxSize { + networks[net].maxAge = t.maxTxAge + continue + } + // per network, sort ages in ascending order + sort.Ints(netData.ages) + // in order to avoid many cleanup msgs, cleanup only 90% of the TxStoreMaxSize + networks[net].maxAge = time.Duration(netData.ages[int(bxgateway.TxStoreMaxSize*0.9)-1]) * time.Second + if networks[net].maxAge > t.maxTxAge { + networks[net].maxAge = t.maxTxAge + } + log.Debugf("TxStore size for network %v is %v. Cleaning %v transactions older than %v", + net, len(netData.ages), len(netData.ages)-bxgateway.TxStoreMaxSize, networks[net].maxAge) + } + + for item := range t.hashToContent.IterBuffered() { + bxTransaction := item.Val.(*types.BxTransaction) + networkNum := bxTransaction.NetworkNum() + netData, netDataExists := networks[networkNum] + removeReason := "" + txAge := currTime.Sub(bxTransaction.AddTime()) + + if netDataExists && txAge > netData.maxAge { + removeReason = fmt.Sprintf("transation age %v is greater than %v", txAge, netData.maxAge) + netData.cleanAge++ + } else { + if txAge > t.noSIDAge && len(bxTransaction.ShortIDs()) == 0 { + removeReason = fmt.Sprintf("transation age %v but no short ID", txAge) + netData.cleanNoSID++ + } + } + + if removeReason != "" { + // remove the transaction by hash from both maps + // no need to add the hash to the history as it is deleted after long time + // dec-5-2021: add to hash history to prevent a lot of reentry (BSC, Polygon) + t.remove(item.Key, ReEntryProtection, removeReason) + cleanedShortIDs[networkNum] = append(cleanedShortIDs[networkNum], bxTransaction.ShortIDs()...) + } + } + + for net, netData := range networks { + log.Debugf("TxStore network %v #txs before cleanup %v cleaned %v missing SID entries and %v aged entries", + net, len(netData.ages), netData.cleanNoSID, netData.cleanAge) + cleaned += netData.cleanNoSID + netData.cleanAge + } + + return cleaned, cleanedShortIDs +} + +// CleanNow performs an immediate cleanup of the TxStore +func (t *BxTxStore) CleanNow() { + mapSizeBeforeClean := t.Count() + timeStart := t.clock.Now() + cleaned, cleanedShortIDs := t.clean() + log.Debugf("TxStore cleaned %v entries in %v. size before clean: %v size after clean: %v", + cleaned, t.clock.Now().Sub(timeStart), mapSizeBeforeClean, t.Count()) + if t.cleanedShortIDsChannel != nil && len(cleanedShortIDs) > 0 { + t.cleanedShortIDsChannel <- cleanedShortIDs + } +} + +func (t *BxTxStore) cleanup() { + ticker := time.NewTicker(t.cleanupFreq) + for { + select { + case <-ticker.C: + t.CleanNow() + case <-t.quit: + t.quit <- true + ticker.Stop() + return + } + } +} + +// Get returns a single transaction from the transaction service +func (t *BxTxStore) Get(hash types.SHA256Hash) (*types.BxTransaction, bool) { + tx, ok := t.hashToContent.Get(string(hash[:])) + if !ok { + return nil, ok + } + return tx.(*types.BxTransaction), ok +} + +// HasContent returns if a given transaction is in the transaction service +func (t *BxTxStore) HasContent(hash types.SHA256Hash) bool { + tx, ok := t.Get(hash) + if !ok { + return false + } + return tx.Content() != nil +} + +// Summarize returns some info about the tx service +func (t *BxTxStore) Summarize() *pbbase.TxStoreReply { + networks := make(map[types.NetworkNum]*pbbase.TxStoreNetworkData) + res := pbbase.TxStoreReply{ + TxCount: uint64(t.hashToContent.Count()), + ShortIdCount: uint64(t.shortIDToHash.Count()), + } + + for item := range t.hashToContent.IterBuffered() { + bxTransaction, ok := item.Val.(*types.BxTransaction) + if !ok { + continue + } + networkData, exists := networks[bxTransaction.NetworkNum()] + if !exists { + networkData = &pbbase.TxStoreNetworkData{} + networkData.OldestTx = bxTransaction.Protobuf() + networkData.TxCount++ + networkData.Network = uint64(bxTransaction.NetworkNum()) + networkData.ShortIdCount += uint64(len(bxTransaction.ShortIDs())) + networks[bxTransaction.NetworkNum()] = networkData + + continue + } + oldestTx := networkData.OldestTx + oldestTxTS := oldestTx.AddTime + if bxTransaction.AddTime().Before(oldestTxTS.AsTime()) { + networkData.OldestTx = bxTransaction.Protobuf() + } + networkData.TxCount++ + networkData.ShortIdCount += uint64(len(bxTransaction.ShortIDs())) + } + for _, netData := range networks { + res.NetworkData = append(res.NetworkData, netData) + } + + return &res +} diff --git a/services/bxtxstore_benchmark_test.go b/services/bxtxstore_benchmark_test.go new file mode 100644 index 0000000..19f03e7 --- /dev/null +++ b/services/bxtxstore_benchmark_test.go @@ -0,0 +1,176 @@ +package services + +import ( + "crypto/sha256" + "fmt" + "github.com/bloXroute-Labs/gateway/types" + "github.com/struCoder/pidusage" + "math/rand" + "os" + "sync" + "testing" + "time" +) + +type TestManager struct { + listTxHashes types.SHA256HashList + quit1 chan bool + quit2 chan bool +} + +func generateSixHundredThousandTx(t *BxTxStore) { + timeNow := time.Now() + hashMap := make(map[types.SHA256Hash]bool) + + for i := 0; i < 600000; { + hash := generateRandTxHash() + if _, ok := hashMap[hash]; ok { + continue + } + hashMap[hash] = true + content := generateRandTxContent() + result := t.Add(hash, content, types.ShortIDEmpty, testNetworkNum, false, types.TFPaidTx, time.Now(), testChainID) + result.Transaction.SetAddTime(timeNow.Add(time.Duration(-33*i) * time.Millisecond)) + if result.NewTx { + i++ + } + } +} + +func generateRandTxContent() []byte { + num := 200 + rand.Intn(100) + slice := make([]byte, num) + if _, err := rand.Read(slice); err != nil { + panic(err) + } + return slice +} + +func generateRandTxHash() types.SHA256Hash { + slice := make([]byte, 32) + if _, err := rand.Read(slice[29:]); err != nil { + panic(err) + } + var hash types.SHA256Hash + copy(hash[:], slice) + + return hash +} + +func BenchmarkTxService(b *testing.B) { + txManager := NewBxTxStore(10*time.Second, 300*time.Minute, 10*time.Minute, NewEmptyShortIDAssigner(), NewHashHistory("seenTxs", 30*time.Minute), nil, 30*time.Minute) + go func() { + _ = txManager.Start() + }() + generateSixHundredThousandTx(&txManager) + items := txManager.Count() + fmt.Println("number of tx: ", items) + + var durationForAdd time.Duration + durationForAdd = 0 + testManager := TestManager{ + quit1: make(chan bool), + quit2: make(chan bool), + } + lock := sync.Mutex{} + + go func() { + ticker := time.NewTicker(33 * time.Millisecond) + tickerTImeForGet := time.NewTicker(10 * time.Second) + tickerMapSize := time.NewTicker(30 * time.Second) + for { + select { + case <-ticker.C: + currTime := time.Now() + result := txManager.Add(generateRandTxHash(), generateRandTxContent(), 1, testNetworkNum, + false, types.TFPaidTx, time.Now(), testChainID) + after := time.Now().Sub(currTime) + durationForAdd += after + lock.Lock() + testManager.listTxHashes = append(testManager.listTxHashes, result.Transaction.Hash()) + lock.Unlock() + case <-tickerTImeForGet.C: + fmt.Println("duration for add after 300 adds: ", durationForAdd) + durationForAdd = 0 + case <-tickerMapSize.C: + sysInfo, _ := pidusage.GetStat(os.Getpid()) + fmt.Println("map size after 30 sec: ", txManager.Count(), " rss: ", int64(sysInfo.Memory)) + case <-testManager.quit1: + testManager.quit1 <- true + ticker.Stop() + tickerMapSize.Stop() + tickerTImeForGet.Stop() + return + } + } + }() + + go func() { + ticker := time.NewTicker(15 * time.Second) + var newList types.SHA256HashList + for { + select { + case <-ticker.C: + currTime := time.Now() + fmt.Println("len list hash before removing: ", len(testManager.listTxHashes)) + lock.Lock() + testManager.listTxHashes = testManager.listTxHashes[len(testManager.listTxHashes)-200 : len(testManager.listTxHashes)] + copy(newList, testManager.listTxHashes[len(testManager.listTxHashes)-200:len(testManager.listTxHashes)]) + testManager.listTxHashes = newList + lock.Unlock() + fmt.Println("len list hash after removing: ", len(testManager.listTxHashes)) + fmt.Println("len map before removing: ", txManager.Count()) + currTime = time.Now() + txManager.RemoveHashes(&newList, ReEntryProtection, "test") + fmt.Println("len map after removing: ", txManager.Count()) + after := time.Now().Sub(currTime) + fmt.Printf("time take to remove %v hash %v:\n ", len(newList), after) + case <-testManager.quit2: + testManager.quit2 <- true + ticker.Stop() + return + } + } + }() + time.Sleep(11 * time.Minute) + //stop + testManager.quit1 <- true + <-testManager.quit1 + testManager.quit2 <- true + <-testManager.quit2 + txManager.Stop() +} + +func TestRemoveTxsByShortIDs(t *testing.T) { + txService := NewBxTxStore(10*time.Second, 300*time.Second, 30*time.Second, NewEmptyShortIDAssigner(), NewHashHistory("seenTxs", 30*time.Minute), nil, 30*time.Minute) + + content := generateRandTxContent() + h := sha256.New() + h.Write(content) + hash := types.SHA256Hash{} + copy(hash[:], h.Sum(nil)) + + // add new transaction without short ID + result := txService.Add(hash, content, types.ShortIDEmpty, testNetworkNum, false, types.TFPaidTx, time.Now(), testChainID) + if !result.NewTx || txService.Count() != 1 { + t.Error("Failed to add transaction") + } + // remove some not existing transactions + txService.RemoveShortIDs(&types.ShortIDList{1, 3, 5, 6}, ReEntryProtection, "test") + if txService.Count() != 1 { + t.Error("Incorrect number of transactions in BxTxStore") + } + // assign 2 shortIDs to the existing transaction + txService.Add(hash, content, types.ShortID(1001), testNetworkNum, false, types.TFPaidTx, time.Now(), testChainID) + result = txService.Add(hash, content, types.ShortID(1002), testNetworkNum, false, types.TFPaidTx, time.Now(), testChainID) + + if result.NewTx || len(result.Transaction.ShortIDs()) != 2 || txService.Count() != 1 { + t.Error("something went wrong") + } + + txService.RemoveShortIDs(&types.ShortIDList{1002}, ReEntryProtection, "test") + if txService.Count() != 0 { + t.Error("Failed to remove transaction by shortId") + } + +} diff --git a/services/bxtxstore_test.go b/services/bxtxstore_test.go new file mode 100644 index 0000000..34ee8fe --- /dev/null +++ b/services/bxtxstore_test.go @@ -0,0 +1,299 @@ +package services + +import ( + "github.com/bloXroute-Labs/gateway" + "github.com/bloXroute-Labs/gateway/test/bxmock" + "github.com/bloXroute-Labs/gateway/types" + "github.com/stretchr/testify/assert" + "strconv" + "testing" + "time" +) + +const testNetworkNum types.NetworkNum = 5 +const testChainID int64 = 1 + +func newTestBxTxStore() BxTxStore { + return newBxTxStore(&bxmock.MockClock{}, 30*time.Second, 30*time.Second, 10*time.Second, + NewEmptyShortIDAssigner(), NewHashHistory("seenTxs", 30*time.Minute), nil, 30*time.Minute) +} + +func TestBxTxStore_Add(t *testing.T) { + store := newTestBxTxStore() + + hash1 := types.SHA256Hash{1} + content1 := types.TxContent{1} + hash2 := types.SHA256Hash{2} + content2 := types.TxContent{2} + hash3 := types.SHA256Hash{3} + content3 := types.TxContent{3} + + // add content first + result11 := store.Add(hash1, content1, types.ShortIDEmpty, testNetworkNum, false, 0, time.Now(), testChainID) + assert.True(t, result11.NewTx) + assert.True(t, result11.NewContent) + assert.False(t, result11.NewSID) + assert.False(t, result11.Reprocess) + + // reprocess paid tx + result12 := store.Add(hash1, content1, types.ShortIDEmpty, testNetworkNum, false, types.TFPaidTx, time.Now(), testChainID) + assert.False(t, result12.NewTx) + assert.False(t, result12.NewContent) + assert.False(t, result12.NewSID) + assert.True(t, result12.Reprocess) + + // only reprocess paid tx once + result13 := store.Add(hash1, content1, types.ShortIDEmpty, testNetworkNum, false, types.TFPaidTx, time.Now(), testChainID) + assert.False(t, result13.NewTx) + assert.False(t, result13.NewContent) + assert.False(t, result13.NewSID) + assert.False(t, result13.Reprocess) + + // then add short ID + result14 := store.Add(hash1, content1, 1, testNetworkNum, false, types.TFPaidTx, time.Now(), testChainID) + assert.False(t, result14.NewTx) + assert.False(t, result14.NewContent) + assert.True(t, result14.NewSID) + assert.False(t, result14.Reprocess) + + // try adding some nonsense that should all be ignored + result15 := store.Add(hash1, content2, 1, testNetworkNum, false, types.TFPaidTx, time.Now(), testChainID) + assert.False(t, result15.NewTx) + assert.False(t, result15.NewContent) + assert.False(t, result15.NewSID) + assert.False(t, result15.Reprocess) + + for tx := range store.Iter() { + assert.Equal(t, hash1, tx.Hash()) + assert.Equal(t, content1, tx.Content()) + assert.Equal(t, types.ShortID(1), tx.ShortIDs()[0]) + } + + // add short ID first + result21 := store.Add(hash2, types.TxContent{}, 2, testNetworkNum, false, 0, time.Now(), testChainID) + assert.True(t, result21.NewTx) + assert.False(t, result21.NewContent) + assert.True(t, result21.NewSID) + assert.False(t, result21.Reprocess) + + // reprocess deliverToNode tx + result22 := store.Add(hash2, types.TxContent{}, 2, testNetworkNum, false, types.TFDeliverToNode, time.Now(), testChainID) + assert.False(t, result22.NewTx) + assert.False(t, result22.NewContent) + assert.False(t, result22.NewSID) + assert.True(t, result22.Reprocess) + + // then add content + result23 := store.Add(hash2, content2, 2, testNetworkNum, false, types.TFDeliverToNode, time.Now(), testChainID) + assert.False(t, result23.NewTx) + assert.True(t, result23.NewContent) + assert.False(t, result23.NewSID) + assert.False(t, result23.Reprocess) + + // add content and short ID together + result31 := store.Add(hash3, content3, 3, testNetworkNum, false, types.TFDeliverToNode, time.Now(), testChainID) + assert.True(t, result31.NewTx) + assert.True(t, result31.NewContent) + assert.True(t, result31.NewSID) + assert.False(t, result31.Reprocess) + +} + +func TestBxTxStore_clean(t *testing.T) { + clock := bxmock.MockClock{} + + cleanedShortIDsChan := make(chan types.ShortIDsByNetwork) + store := newBxTxStore(&clock, 30*time.Second, 30*time.Second, 10*time.Second, NewEmptyShortIDAssigner(), NewHashHistory("seenTxs", 30*time.Minute), cleanedShortIDsChan, 30*time.Minute) + + hash1 := types.SHA256Hash{1} + content1 := types.TxContent{1} + hash2 := types.SHA256Hash{2} + content2 := types.TxContent{2} + + // add content first, no shortID + result1 := store.Add(hash1, content1, types.ShortIDEmpty, testNetworkNum, false, types.TFPaidTx, time.Now(), testChainID) + assert.True(t, result1.NewTx) + assert.True(t, result1.NewContent) + assert.False(t, result1.NewSID) + assert.Equal(t, store.Count(), 1) + result1.Transaction.SetAddTime(clock.Now()) + + result2 := store.Add(hash2, content2, 2, testNetworkNum, false, types.TFPaidTx, time.Now(), testChainID) + assert.True(t, result2.NewTx) + assert.True(t, result2.NewContent) + assert.True(t, result2.NewSID) + assert.Equal(t, store.Count(), 2) + result2.Transaction.SetAddTime(clock.Now()) + + clock.IncTime(20 * time.Second) + cleaned, cleanedShortIDs := store.clean() + assert.Equal(t, 0, len(cleanedShortIDs[testNetworkNum])) + assert.Equal(t, store.Count(), 1) + assert.Equal(t, cleaned, 1) +} + +func TestBxTxStore_clean256K(t *testing.T) { + otherNetworkTxs := 20 + extra := 10 + clock := bxmock.MockClock{} + + store := newBxTxStore(&clock, 30*time.Second, 300*time.Hour, 10*time.Second, NewEmptyShortIDAssigner(), NewHashHistory("seenTxs", 30*time.Minute), nil, 30*time.Minute) + // add some Tx from a different network to check that these will not be cleaned + for i := 0; i < otherNetworkTxs; i++ { + var h types.SHA256Hash + var c types.TxContent + copy(h[:], strconv.Itoa(i)) + result1 := store.Add(h, c, types.ShortID(i+1), testNetworkNum+1, false, types.TFPaidTx, time.Now(), testChainID) + assert.True(t, result1.NewTx) + assert.False(t, result1.NewContent) + assert.True(t, result1.NewSID) + result1.Transaction.SetAddTime(clock.Now()) + clock.IncTime(time.Second) + } + + count := bxgateway.TxStoreMaxSize + extra + for i := otherNetworkTxs; i < otherNetworkTxs+count; i++ { + var h types.SHA256Hash + var c types.TxContent + copy(h[:], strconv.Itoa(i)) + result1 := store.Add(h, c, types.ShortID(i+1), testNetworkNum, false, types.TFPaidTx, time.Now(), testChainID) + assert.True(t, result1.NewTx) + assert.False(t, result1.NewContent) + assert.True(t, result1.NewSID) + result1.Transaction.SetAddTime(clock.Now()) + clock.IncTime(time.Second) + } + assert.Equal(t, count+otherNetworkTxs, store.Count()) + assert.Equal(t, count+otherNetworkTxs, store.hashToContent.Count()) + assert.Equal(t, count+otherNetworkTxs, store.shortIDToHash.Count()) + + cleaned, cleanedShortIDs := store.clean() + assert.Equal(t, bxgateway.TxStoreMaxSize*0.9+otherNetworkTxs, store.Count()) + assert.Equal(t, bxgateway.TxStoreMaxSize*0.1+extra, len(cleanedShortIDs[testNetworkNum])) + assert.Equal(t, bxgateway.TxStoreMaxSize*0.9+otherNetworkTxs, store.hashToContent.Count()) + assert.Equal(t, bxgateway.TxStoreMaxSize*0.9+otherNetworkTxs, store.shortIDToHash.Count()) + assert.Equal(t, bxgateway.TxStoreMaxSize*0.1+extra, store.seenTxs.Count()) + + assert.Equal(t, bxgateway.TxStoreMaxSize*0.1+extra, cleaned) +} + +func TestHistory(t *testing.T) { + clock := bxmock.MockClock{} + cleanedShortIDsChan := make(chan types.ShortIDsByNetwork) + store := newBxTxStore(&clock, 30*time.Minute, 3*24*time.Hour, 10*time.Minute, + NewEmptyShortIDAssigner(), newHashHistory("seenTxs", &clock, 30*time.Minute), cleanedShortIDsChan, 30*time.Minute) + shortIDsByNetwork := make(types.ShortIDsByNetwork) + go func() { + for { + cleanedShortIDs := <-cleanedShortIDsChan + shortIDsByNetwork[testNetworkNum] = append(shortIDsByNetwork[testNetworkNum], cleanedShortIDs[testNetworkNum]...) + } + }() + + hash1 := types.SHA256Hash{1} + content1 := types.TxContent{1} + hash2 := types.SHA256Hash{2} + content2 := types.TxContent{2} + shortID2 := types.ShortID(2) + + // add content first + result := store.Add(hash1, content1, types.ShortIDEmpty, testNetworkNum, false, types.TFPaidTx, time.Now(), testChainID) + assert.True(t, result.NewTx) + assert.True(t, result.NewContent) + assert.False(t, result.NewSID) + result1 := store.Add(hash2, content2, shortID2, testNetworkNum, false, types.TFPaidTx, time.Now(), testChainID) + assert.True(t, result1.NewTx) + assert.True(t, result1.NewContent) + assert.True(t, result1.NewSID) + + // remove it + store.RemoveHashes(&types.SHA256HashList{hash1}, ReEntryProtection, "test") + // make sure size is 1 (hash2) + assert.Equal(t, 1, store.Count()) + assert.Equal(t, 1, store.seenTxs.Count()) + + // add it again - should not get in due to history + result = store.Add(hash1, content1, types.ShortIDEmpty, testNetworkNum, false, types.TFPaidTx, clock.Now(), testChainID) + assert.False(t, result.NewTx) + assert.False(t, result.NewContent) + assert.False(t, result.NewSID) + // move time behind history + clock.IncTime(1*time.Minute + 24*time.Hour) + // force cleanup + store.CleanNow() + //assert.Equal(t, 1, len(cleanedShortIDsChan)) + // add it again - this time it should get in + result = store.Add(hash1, content1, types.ShortIDEmpty, testNetworkNum, false, types.TFPaidTx, clock.Now(), testChainID) + assert.True(t, result.NewTx) + assert.True(t, result.NewContent) + assert.False(t, result.NewSID) + // make sure hash2 is already in store + tx, err := store.GetTxByShortID(shortID2) + assert.Nil(t, err) + assert.Equal(t, content2, tx.Content()) + // make sure size is 2 + assert.Equal(t, 2, store.Count()) +} + +func TestGetTxByShortID(t *testing.T) { + store := newTestBxTxStore() + + hash1 := types.SHA256Hash{1} + content1 := types.TxContent{1} + tx, err := store.GetTxByShortID(1) + assert.Nil(t, tx) + assert.NotNil(t, err) + + // add content first, then short ID + result11 := store.Add(hash1, content1, types.ShortIDEmpty, testNetworkNum, false, types.TFPaidTx, time.Now(), testChainID) + assert.True(t, result11.NewTx) + assert.True(t, result11.NewContent) + assert.False(t, result11.NewSID) + tx, err = store.GetTxByShortID(1) + assert.Nil(t, tx) + assert.NotNil(t, err) + + result12 := store.Add(hash1, content1, 1, testNetworkNum, false, types.TFPaidTx, time.Now(), testChainID) + assert.False(t, result12.NewTx) + assert.False(t, result12.NewContent) + assert.True(t, result12.NewSID) + tx, err = store.GetTxByShortID(1) + assert.NotNil(t, tx) + assert.Nil(t, err) + assert.Equal(t, content1, tx.Content()) + + store.remove(string(hash1[:]), ReEntryProtection, "TestGetTxByShortID") + tx, err = store.GetTxByShortID(1) + assert.Nil(t, tx) + assert.NotNil(t, err) +} + +func TestHistoryTxWithShortID(t *testing.T) { + clock := bxmock.MockClock{} + + cleanedShortIDsChan := make(chan types.ShortIDsByNetwork) + store := newBxTxStore(&clock, 30*time.Second, 30*time.Second, 10*time.Second, NewEmptyShortIDAssigner(), newHashHistory("seenTxs", &clock, 60*time.Minute), cleanedShortIDsChan, 30*time.Minute) + hash1 := types.SHA256Hash{1} + content1 := types.TxContent{1} + + // add content first, no shortID + result1 := store.Add(hash1, content1, types.ShortIDEmpty, testNetworkNum, false, types.TFPaidTx, clock.Now(), testChainID) + assert.True(t, result1.NewTx) + assert.True(t, result1.NewContent) + assert.False(t, result1.NewSID) + assert.Equal(t, store.Count(), 1) + + clock.IncTime(40 * time.Second) + go store.CleanNow() + <-cleanedShortIDsChan + // tx should not be added since it is history + result := store.Add(hash1, content1, types.ShortIDEmpty, testNetworkNum, false, types.TFPaidTx, clock.Now(), testChainID) + assert.False(t, result.NewTx) + assert.False(t, result.NewContent) + assert.True(t, result.AlreadySeen) + + // tx should be added because it has short id + result2 := store.Add(hash1, content1, 1, testNetworkNum, false, types.TFPaidTx, clock.Now(), testChainID) + assert.True(t, result2.NewTx) + assert.True(t, result2.NewSID) +} diff --git a/services/ethtxstore.go b/services/ethtxstore.go new file mode 100644 index 0000000..78cf329 --- /dev/null +++ b/services/ethtxstore.go @@ -0,0 +1,221 @@ +package services + +import ( + "fmt" + "github.com/bloXroute-Labs/gateway/sdnmessage" + "github.com/bloXroute-Labs/gateway/types" + "github.com/bloXroute-Labs/gateway/utils" + cmap "github.com/orcaman/concurrent-map" + log "github.com/sirupsen/logrus" + "math/big" + "time" +) + +// TODO : move ethtxstore and related tests outside of bxgateway package + +const ( + cleanNonceInterval = 10 * time.Second + timeToAvoidReEntry = 24 * time.Hour +) + +// EthTxStore represents transaction storage and validation for Ethereum transactions +type EthTxStore struct { + BxTxStore + nonceTracker +} + +// NewEthTxStore returns new manager for Ethereum transactions +func NewEthTxStore(clock utils.Clock, cleanupInterval time.Duration, maxTxAge time.Duration, + noSIDAge time.Duration, assigner ShortIDAssigner, hashHistory HashHistory, cleanedShortIDsChannel chan types.ShortIDsByNetwork, networkConfig sdnmessage.BlockchainNetworks) *EthTxStore { + return &EthTxStore{ + BxTxStore: newBxTxStore(clock, cleanupInterval, maxTxAge, noSIDAge, assigner, hashHistory, cleanedShortIDsChannel, timeToAvoidReEntry), + nonceTracker: newNonceTracker(clock, networkConfig, cleanNonceInterval), + } +} + +// Add validates an Ethereum transaction and checks that its nonce has not been seen before +func (t *EthTxStore) Add(hash types.SHA256Hash, content types.TxContent, shortID types.ShortID, + network types.NetworkNum, validate bool, flags types.TxFlags, timestamp time.Time, networkChainID int64) TransactionResult { + result := t.BxTxStore.Add(hash, content, shortID, network, false, flags, timestamp, networkChainID) + + // if no new content we can leave + if !result.NewContent { + return result + } + + reuseNonceActive := t.isReuseNonceActive(network) + // if validation is not needed and reuse nonce is not active we can leave to avoid parsing of the transaction + if !validate && !reuseNonceActive { + return result + } + + // note: we need to continue and validate even if the content is coming from the BDN. This can still + // be a reuseNonce case and we should start track the transaction for future reuse nonce with it. + + // upon first seeing tx, validate that it's an Ethereum transaction. Extract Sender only if reuse Nonce is Active + blockchainTx, err := result.Transaction.BlockchainTransaction(reuseNonceActive) + if err != nil { + result.FailedValidation = true + return result + } + + ethTx := blockchainTx.(*types.EthTransaction) + txChainID := ethTx.ChainID.Int64() + if networkChainID != 0 && txChainID != 0 && networkChainID != txChainID { + log.Errorf("chainID mismatch for hash %v - content chainID %v networkNum %v networkChainID %v", hash, txChainID, network, networkChainID) + // remove the tx from the TxStore but allow it to get back in + hashToRemove := make(types.SHA256HashList, 1) + hashToRemove[0] = result.Transaction.Hash() + t.BxTxStore.RemoveHashes(&hashToRemove, ReEntryProtection, "chainID mismatch") + result.FailedValidation = true + return result + + } + // validation done. If reuse nonce is not active we better return + if !reuseNonceActive { + return result + } + seenNonce, otherTx := t.track(ethTx, network) + if !seenNonce { + return result + } + + // if we have shortID we should not block this transaction even in case of reuse nonce. + // we called t.track() to keep track of this transaction so it may block other transactions. + if shortID != types.ShortIDEmpty { + log.Tracef("reuse nonce detected but ignored since having shortID (%v). "+ + "New transaction %v is reusing nonce with existing tx %v", + shortID, result.Transaction.Hash(), otherTx) + return result + } + + result.ReuseSenderNonce = true + result.DebugData = otherTx + log.Tracef("reuse nonce detected. New transaction %v is reusing nonce with existing tx %v", + result.Transaction.Hash(), otherTx) + // remove the tx from the TxStore but allow it to get back in + hashToRemove := make(types.SHA256HashList, 1) + hashToRemove[0] = result.Transaction.Hash() + t.BxTxStore.RemoveHashes(&hashToRemove, NoReEntryProtection, "reuse nonce detected") + return result +} + +// Stop halts the nonce tracker in addition to regular tx service cleanup +func (t *EthTxStore) Stop() { + t.BxTxStore.Stop() + t.nonceTracker.quit <- true + <-t.nonceTracker.quit +} + +type trackedTx struct { + tx *types.EthTransaction + + // txs with a gas fees higher than both of this are not considered duplicates + gasFeeCap types.EthBigInt + gasTipCap types.EthBigInt + + expireTime time.Time // after this time, txs with same key are not considered duplicates +} + +type nonceTracker struct { + clock utils.Clock + addressNonceToTx cmap.ConcurrentMap + cleanInterval time.Duration + networkConfig sdnmessage.BlockchainNetworks + quit chan bool +} + +func fromNonceKey(from types.EthAddress, nonce types.EthUInt64) string { + return fmt.Sprintf("%v:%v", from, nonce) +} + +func newNonceTracker(clock utils.Clock, networkConfig sdnmessage.BlockchainNetworks, cleanInterval time.Duration) nonceTracker { + nt := nonceTracker{ + clock: clock, + networkConfig: networkConfig, + addressNonceToTx: cmap.New(), + cleanInterval: cleanInterval, + quit: make(chan bool), + } + go nt.cleanLoop() + return nt +} + +func (nt *nonceTracker) getTransaction(from types.EthAddress, nonce types.EthUInt64) (*trackedTx, bool) { + k := fromNonceKey(from, nonce) + utx, ok := nt.addressNonceToTx.Get(k) + if !ok { + return nil, ok + } + tx := utx.(trackedTx) + return &tx, ok +} + +func (nt *nonceTracker) setTransaction(tx *types.EthTransaction, network types.NetworkNum) { + reuseNonceGasChange := new(big.Float).SetFloat64(nt.networkConfig[network].AllowGasPriceChangeReuseSenderNonce) + reuseNonceDelay := time.Duration(nt.networkConfig[network].AllowTimeReuseSenderNonce) * time.Second + + intGasFeeCap := new(big.Int) + gasFeeCap := new(big.Float).SetInt(tx.EffectiveGasFeeCap().Int) + gasFeeCap.Mul(gasFeeCap, reuseNonceGasChange).Int(intGasFeeCap) + + intGasTipCap := new(big.Int) + gasTipCap := new(big.Float).SetInt(tx.EffectiveGasTipCap().Int) + gasTipCap.Mul(gasTipCap, reuseNonceGasChange).Int(intGasTipCap) + + tracked := trackedTx{ + tx: tx, + expireTime: nt.clock.Now().Add(reuseNonceDelay), + gasFeeCap: types.EthBigInt{Int: intGasFeeCap}, + gasTipCap: types.EthBigInt{Int: intGasTipCap}, + } + nt.addressNonceToTx.Set(fromNonceKey(tx.From, tx.Nonce), tracked) +} + +// isReuseNonceActive returns whether reuse nonce tracking is active +func (nt nonceTracker) isReuseNonceActive(networkNum types.NetworkNum) bool { + config := nt.networkConfig[networkNum] + return config != nil && config.EnableCheckSenderNonce +} + +// track returns whether the tx is the newest from its address, and if it should be considered a duplicate +func (nt *nonceTracker) track(tx *types.EthTransaction, network types.NetworkNum) (bool, *types.SHA256Hash) { + oldTx, ok := nt.getTransaction(tx.From, tx.Nonce) + if !ok { + nt.setTransaction(tx, network) + return false, nil + } + + if (tx.EffectiveGasFeeCap().GreaterThan(oldTx.gasFeeCap) && tx.EffectiveGasTipCap().GreaterThan(oldTx.gasTipCap)) || nt.clock.Now().After(oldTx.expireTime) { + nt.setTransaction(tx, network) + return false, nil + } + return true, &oldTx.tx.Hash.SHA256Hash +} + +func (nt *nonceTracker) cleanLoop() { + ticker := time.NewTicker(nt.cleanInterval) + for { + select { + case <-ticker.C: + nt.clean() + case <-nt.quit: + ticker.Stop() + return + } + } +} + +func (nt *nonceTracker) clean() { + currentTime := nt.clock.Now() + sizeBefore := nt.addressNonceToTx.Count() + removed := 0 + for item := range nt.addressNonceToTx.IterBuffered() { + tracked := item.Val.(trackedTx) + if currentTime.After(tracked.expireTime) { + nt.addressNonceToTx.Remove(item.Key) + removed++ + } + } + log.Tracef("nonceTracker Cleanup done. Size at start %v, cleaned %v", sizeBefore, removed) +} diff --git a/services/ethtxstore_test.go b/services/ethtxstore_test.go new file mode 100644 index 0000000..0425f75 --- /dev/null +++ b/services/ethtxstore_test.go @@ -0,0 +1,291 @@ +package services + +import ( + "github.com/bloXroute-Labs/gateway/sdnmessage" + "github.com/bloXroute-Labs/gateway/test/bxmock" + "github.com/bloXroute-Labs/gateway/types" + "github.com/ethereum/go-ethereum/common" + ethtypes "github.com/ethereum/go-ethereum/core/types" + "github.com/ethereum/go-ethereum/crypto" + "github.com/ethereum/go-ethereum/rlp" + "github.com/stretchr/testify/assert" + "math/big" + "math/rand" + "testing" + "time" +) + +var privateKey, _ = crypto.GenerateKey() +var blockchainNetwork = sdnmessage.BlockchainNetwork{ + AllowTimeReuseSenderNonce: 2, + AllowGasPriceChangeReuseSenderNonce: 1.1, + EnableCheckSenderNonce: true, +} + +func TestEthTxStore_InvalidChainID(t *testing.T) { + store := NewEthTxStore(&bxmock.MockClock{}, 30*time.Second, 30*time.Second, 30*time.Second, + NewEmptyShortIDAssigner(), NewHashHistory("seenTxs", 30*time.Minute), nil, sdnmessage.BlockchainNetworks{testNetworkNum: &blockchainNetwork}) + hash := types.SHA256Hash{1} + tx := bxmock.NewSignedEthTx(ethtypes.LegacyTxType, 1, privateKey) + content, _ := rlp.EncodeToBytes(&tx) + + // first time seeing valid tx + result1 := store.Add(hash, content, types.ShortIDEmpty, testNetworkNum, true, types.TFPaidTx, time.Now(), tx.ChainId().Int64()) + assert.True(t, result1.NewTx) + assert.True(t, result1.NewContent) + assert.False(t, result1.NewSID) + assert.False(t, result1.FailedValidation) + assert.False(t, result1.ReuseSenderNonce) + assert.Equal(t, 1, store.Count()) + +} + +func TestEthTxStore_Add(t *testing.T) { + store := NewEthTxStore(&bxmock.MockClock{}, 30*time.Second, 30*time.Second, 30*time.Second, + NewEmptyShortIDAssigner(), NewHashHistory("seenTxs", 30*time.Minute), nil, sdnmessage.BlockchainNetworks{testNetworkNum: &blockchainNetwork}) + hash := types.SHA256Hash{1} + tx := bxmock.NewSignedEthTx(ethtypes.LegacyTxType, 1, privateKey) + content, _ := rlp.EncodeToBytes(&tx) + + // ignore, transaction is a duplicate + result2 := store.Add(hash, content, types.ShortIDEmpty, testNetworkNum, true, types.TFPaidTx, time.Now(), tx.ChainId().Int64()) + assert.True(t, result2.NewTx) + assert.True(t, result2.NewContent) + assert.False(t, result2.NewSID) + assert.False(t, result2.FailedValidation) + assert.False(t, result2.ReuseSenderNonce) + assert.Equal(t, 1, store.Count()) + + // add short ID for already seen tx + result3 := store.Add(hash, content, 1, testNetworkNum, true, types.TFPaidTx, time.Now(), tx.ChainId().Int64()) + assert.False(t, result3.NewTx) + assert.False(t, result3.NewContent) + assert.True(t, result3.NewSID) + assert.False(t, result3.FailedValidation) + assert.False(t, result3.ReuseSenderNonce) + assert.Equal(t, 1, store.Count()) + + // add invalid transaction - should not be added but should get to History + tx = bxmock.NewSignedEthTx(ethtypes.LegacyTxType, 2, privateKey) + content, _ = rlp.EncodeToBytes(&tx) + hash = types.SHA256Hash{2} + + result1 := store.Add(hash, content, types.ShortIDEmpty, testNetworkNum, true, types.TFPaidTx, time.Now(), testChainID) + assert.True(t, result1.FailedValidation) + assert.Equal(t, 1, store.Count()) + + // add it again. This time should be IgnoreSeen + result3 = store.Add(hash, content, types.ShortIDEmpty, testNetworkNum, true, types.TFPaidTx, time.Now(), testChainID) + assert.False(t, result3.NewTx) + assert.False(t, result3.NewContent) + assert.False(t, result3.NewSID) + assert.False(t, result3.FailedValidation) + assert.False(t, result3.ReuseSenderNonce) + + assert.Equal(t, 1, store.Count()) + +} + +func TestEthTxStore_AddReuseSenderNonce(t *testing.T) { + mc := bxmock.MockClock{} + nc := blockchainNetwork + nc.AllowTimeReuseSenderNonce = 10 + store := NewEthTxStore(&mc, 30*time.Second, 30*time.Second, 20*time.Second, NewEmptyShortIDAssigner(), NewHashHistory("seenTxs", 30*time.Minute), nil, sdnmessage.BlockchainNetworks{testNetworkNum: &nc}) + + // original transaction + hash1 := types.SHA256Hash{1} + tx1 := bxmock.NewSignedEthTx(ethtypes.LegacyTxType, 1, privateKey) + content1, _ := rlp.EncodeToBytes(&tx1) + + // transaction that reuses nonce + hash2 := types.SHA256Hash{2} + tx2 := bxmock.NewSignedEthTx(ethtypes.LegacyTxType, 1, privateKey) + content2, _ := rlp.EncodeToBytes(&tx2) + + // incremented nonce + hash3 := types.SHA256Hash{3} + tx3 := bxmock.NewSignedEthTx(ethtypes.LegacyTxType, 2, privateKey) + content3, _ := rlp.EncodeToBytes(&tx3) + + // different sender + privateKey2, _ := crypto.GenerateKey() + hash4 := types.SHA256Hash{4} + tx4 := bxmock.NewSignedEthTx(ethtypes.LegacyTxType, 1, privateKey2) + content4, _ := rlp.EncodeToBytes(&tx4) + + // add original transaction + result0 := store.Add(hash1, content1, types.ShortIDEmpty, testNetworkNum, true, types.TFPaidTx, mc.Now(), tx1.ChainId().Int64()) + assert.Equal(t, 1, store.Count()) + result0.Transaction.SetAddTime(mc.Now()) + //result0.Transaction.MarkProcessed() + + // add transaction with same nonce/sender, should be notified and not added to tx store + result1 := store.Add(hash2, content2, types.ShortIDEmpty, testNetworkNum, true, types.TFPaidTx, mc.Now(), tx2.ChainId().Int64()) + assert.True(t, result1.NewTx) + assert.True(t, result1.NewContent) + assert.False(t, result1.NewSID) + assert.False(t, result1.FailedValidation) + assert.True(t, result1.ReuseSenderNonce) + assert.Equal(t, 1, store.Count()) + result1.Transaction.SetAddTime(mc.Now()) + + // add transaction with incremented nonce + result2 := store.Add(hash3, content3, types.ShortIDEmpty, testNetworkNum, true, types.TFPaidTx, mc.Now(), tx3.ChainId().Int64()) + assert.Equal(t, 2, store.Count()) + result2.Transaction.SetAddTime(mc.Now()) + + // add transaction with different sender + result3 := store.Add(hash4, content4, types.ShortIDEmpty, testNetworkNum, true, types.TFPaidTx, mc.Now(), tx4.ChainId().Int64()) + assert.Equal(t, 3, store.Count()) + result3.Transaction.SetAddTime(mc.Now()) + + // time has elapsed, ok now + mc.IncTime(11 * time.Second) + result2 = store.Add(hash2, content2, types.ShortIDEmpty, testNetworkNum, true, types.TFPaidTx, mc.Now(), tx2.ChainId().Int64()) + assert.True(t, result2.NewTx) + assert.True(t, result2.NewContent) + assert.False(t, result2.NewSID) + assert.False(t, result2.FailedValidation) + assert.False(t, result2.ReuseSenderNonce) + assert.Equal(t, 4, store.Count()) + + // clean tx without shortID - we clean all tx excluding hash2 that was added 11 seconds ago + mc.IncTime(11 * time.Second) + cleaned, cleanedShortIDs := store.BxTxStore.clean() + assert.Equal(t, 0, len(cleanedShortIDs[testNetworkNum])) + assert.Equal(t, 3, cleaned) + // clean tx without shortID - now we should clean hash2 + mc.IncTime(11 * time.Second) + cleaned, cleanedShortIDs = store.BxTxStore.clean() + assert.Equal(t, 0, len(cleanedShortIDs[testNetworkNum])) + assert.Equal(t, 1, cleaned) + + // now, should be able to add it back + mc.IncTime(11 * time.Second) + result2 = store.Add(hash2, content2, types.ShortIDEmpty, testNetworkNum, true, types.TFPaidTx, mc.Now(), tx2.ChainId().Int64()) + assert.False(t, result2.NewTx) + assert.False(t, result2.NewContent) + assert.False(t, result2.NewSID) + assert.False(t, result2.FailedValidation) + assert.False(t, result2.ReuseSenderNonce) + assert.True(t, result2.AlreadySeen) + assert.Equal(t, 0, store.Count()) + +} + +func TestEthTxStore_AddInvalidTx(t *testing.T) { + store := NewEthTxStore(&bxmock.MockClock{}, 30*time.Second, 30*time.Second, 10*time.Second, + NewEmptyShortIDAssigner(), NewHashHistory("seenTxs", 30*time.Minute), nil, sdnmessage.BlockchainNetworks{blockchainNetwork.NetworkNum: &blockchainNetwork}) + hash := types.SHA256Hash{1} + content := types.TxContent{1, 2, 3} + + // invalid tx + result := store.Add(hash, content, types.ShortIDEmpty, testNetworkNum, true, types.TFPaidTx, time.Now(), testChainID) + assert.True(t, result.NewTx) + assert.True(t, result.NewContent) + assert.False(t, result.NewSID) + assert.True(t, result.FailedValidation) + assert.False(t, result.ReuseSenderNonce) + assert.Equal(t, 1, store.Count()) +} + +func TestNonceTracker_track(t *testing.T) { + c := bxmock.MockClock{} + nc := blockchainNetwork + nc.AllowTimeReuseSenderNonce = 1 + nc.NetworkNum = testNetworkNum + n := newNonceTracker(&c, sdnmessage.BlockchainNetworks{nc.NetworkNum: &nc}, 10) + var fromBytes common.Address + rand.Read(fromBytes[:]) + address := types.EthAddress{Address: &fromBytes} + + tx := types.EthTransaction{ + From: address, + GasPrice: types.EthBigInt{Int: big.NewInt(100)}, + } + txSame := types.EthTransaction{ + From: address, + GasPrice: types.EthBigInt{Int: big.NewInt(100)}, + } + txLowerGas := types.EthTransaction{ + From: address, + GasPrice: types.EthBigInt{Int: big.NewInt(5)}, + } + txSlightlyHigherGas := types.EthTransaction{ + From: address, + GasPrice: types.EthBigInt{Int: big.NewInt(101)}, + } + txHigherGas := types.EthTransaction{ + From: address, + GasPrice: types.EthBigInt{Int: big.NewInt(111)}, + } + + duplicate, _ := n.track(&tx, testNetworkNum) + assert.False(t, duplicate) + + duplicate, _ = n.track(&txSame, testNetworkNum) + assert.True(t, duplicate) + + duplicate, _ = n.track(&txLowerGas, testNetworkNum) + assert.True(t, duplicate) + + duplicate, _ = n.track(&txSlightlyHigherGas, testNetworkNum) + assert.True(t, duplicate) + + duplicate, _ = n.track(&txHigherGas, testNetworkNum) + assert.False(t, duplicate) + + c.IncTime(5 * time.Second) + duplicate, _ = n.track(&txLowerGas, testNetworkNum) + assert.False(t, duplicate) +} + +func TestNonceTracker_clean(t *testing.T) { + c := bxmock.MockClock{} + nc := blockchainNetwork + nc.AllowTimeReuseSenderNonce = 1 + nc.NetworkNum = testNetworkNum + n := newNonceTracker(&c, sdnmessage.BlockchainNetworks{nc.NetworkNum: &nc}, 10) + var fromBytes common.Address + rand.Read(fromBytes[:]) + address := types.EthAddress{Address: &fromBytes} + + tx := types.EthTransaction{ + From: address, + GasPrice: types.EthBigInt{Int: big.NewInt(100)}, + Nonce: types.EthUInt64{UInt64: 1}, + } + tx2 := types.EthTransaction{ + From: address, + GasPrice: types.EthBigInt{Int: big.NewInt(100)}, + Nonce: types.EthUInt64{UInt64: 2}, + } + tx3 := types.EthTransaction{ + From: address, + GasPrice: types.EthBigInt{Int: big.NewInt(100)}, + Nonce: types.EthUInt64{UInt64: 3}, + } + + n.track(&tx, testNetworkNum) + c.IncTime(500 * time.Millisecond) + + n.track(&tx2, testNetworkNum) + c.IncTime(500 * time.Millisecond) + + n.track(&tx3, testNetworkNum) + c.IncTime(100 * time.Millisecond) + + n.clean() + + rtx, ok := n.getTransaction(address, types.EthUInt64{UInt64: 1}) + assert.Nil(t, rtx) + assert.False(t, ok) + + rtx, ok = n.getTransaction(address, types.EthUInt64{UInt64: 2}) + assert.Equal(t, &tx2, rtx.tx) + assert.True(t, ok) + + rtx, ok = n.getTransaction(address, types.EthUInt64{UInt64: 3}) + assert.Equal(t, &tx3, rtx.tx) + assert.True(t, ok) +} diff --git a/services/firewall.go b/services/firewall.go new file mode 100644 index 0000000..7d666c5 --- /dev/null +++ b/services/firewall.go @@ -0,0 +1,87 @@ +package services + +import ( + "fmt" + "github.com/bloXroute-Labs/gateway/sdnmessage" + "github.com/bloXroute-Labs/gateway/types" + "github.com/bloXroute-Labs/gateway/utils" + log "github.com/sirupsen/logrus" + "sync" + "time" +) + +// FirewallRulesCleanupInterval - is cleanup interval for firewall rules +const FirewallRulesCleanupInterval = 15 * time.Minute + +// Firewall - is manager for FirewallRules messages +type Firewall struct { + rules []sdnmessage.FirewallRule + lock sync.RWMutex + clock utils.Clock +} + +// NewFirewall returns new manager for FirewallRules +func NewFirewall(cleanupInterval time.Duration) *Firewall { + return newFirewall(utils.RealClock{}, cleanupInterval) +} + +func newFirewall(clock utils.Clock, cleanupInterval time.Duration) *Firewall { + firewall := &Firewall{ + rules: []sdnmessage.FirewallRule{}, + clock: clock, + } + log.Tracef("starting new firewall") + go firewall.cleanup(cleanupInterval) + return firewall +} + +// AddRule - add a new firewallRule +func (firewall *Firewall) AddRule(firewallRule sdnmessage.FirewallRule) { + firewallRule.SetExpirationTime(firewall.clock.Now().Add(time.Duration(firewallRule.Duration) * time.Second)) + firewall.lock.Lock() + defer firewall.lock.Unlock() + firewall.rules = append(firewall.rules, firewallRule) + log.Debugf("firewall: new rule %v added", firewallRule) +} + +func (firewall *Firewall) cleanup(cleanupInterval time.Duration) { + log.Debugf("starting firewall cleanup routine") + ticker := time.NewTicker(cleanupInterval) + for { + select { + case <-ticker.C: + firewall.clean() + } + } +} + +func (firewall *Firewall) clean() int { + timeNow := firewall.clock.Now() + remainedRules := make([]sdnmessage.FirewallRule, 0) + firewall.lock.Lock() + defer firewall.lock.Unlock() + for _, rule := range firewall.rules { + if timeNow.Before(rule.GetExpirationTime()) { + remainedRules = append(remainedRules, rule) + } + } + cleaned := len(firewall.rules) - len(remainedRules) + firewall.rules = remainedRules + log.Debugf("firewall: %v rules has been cleaned", cleaned) + return cleaned +} + +// Validate - return error message if connection should be rejected +func (firewall *Firewall) Validate(accountID types.AccountID, nodeID types.NodeID) error { + firewall.lock.RLock() + defer firewall.lock.RUnlock() + for _, rule := range firewall.rules { + if firewall.clock.Now().Before(rule.GetExpirationTime()) && + (rule.AccountID == "*" || rule.AccountID == accountID) && + (rule.PeerID == "*" || rule.PeerID == nodeID) { + return fmt.Errorf("connection with accountID %v peerID %v forbidden by firewall", + rule.AccountID, rule.PeerID) + } + } + return nil +} diff --git a/services/firewall_test.go b/services/firewall_test.go new file mode 100644 index 0000000..18b00b1 --- /dev/null +++ b/services/firewall_test.go @@ -0,0 +1,153 @@ +package services + +import ( + "github.com/bloXroute-Labs/gateway/sdnmessage" + "github.com/bloXroute-Labs/gateway/test/bxmock" + "github.com/bloXroute-Labs/gateway/types" + uuid "github.com/satori/go.uuid" + "github.com/stretchr/testify/assert" + "testing" + "time" +) + +func TestFirewall_Add(t *testing.T) { + clock := &bxmock.MockClock{} + firewall := newFirewall(clock, 30*time.Minute) + + firewall.AddRule(sdnmessage.FirewallRule{AccountID: generateRandAccountID(), PeerID: generateRandNodeID(), Duration: 10}) + firewall.AddRule(sdnmessage.FirewallRule{AccountID: generateRandAccountID(), PeerID: generateRandNodeID(), Duration: 20}) + firewall.AddRule(sdnmessage.FirewallRule{AccountID: generateRandAccountID(), PeerID: generateRandNodeID(), Duration: 30}) + + assert.Equal(t, 3, len(firewall.rules)) + clock.IncTime(25 * time.Second) + assert.Equal(t, 2, firewall.clean()) + clock.IncTime(35 * time.Second) + assert.Equal(t, 1, firewall.clean()) + + firewall.AddRule(sdnmessage.FirewallRule{AccountID: generateRandAccountID(), PeerID: generateRandNodeID(), Duration: 20}) + firewall.AddRule(sdnmessage.FirewallRule{AccountID: generateRandAccountID(), PeerID: generateRandNodeID(), Duration: 10}) + firewall.AddRule(sdnmessage.FirewallRule{AccountID: generateRandAccountID(), PeerID: generateRandNodeID(), Duration: 50}) + firewall.AddRule(sdnmessage.FirewallRule{AccountID: generateRandAccountID(), PeerID: generateRandNodeID(), Duration: 40}) + firewall.AddRule(sdnmessage.FirewallRule{AccountID: generateRandAccountID(), PeerID: generateRandNodeID(), Duration: 30}) + + assert.Equal(t, 5, len(firewall.rules)) + clock.IncTime(15 * time.Second) + assert.Equal(t, 1, firewall.clean()) + + assert.Equal(t, 4, len(firewall.rules)) + clock.IncTime(10 * time.Second) + assert.Equal(t, 1, firewall.clean()) + + assert.Equal(t, 3, len(firewall.rules)) + clock.IncTime(10 * time.Second) + assert.Equal(t, 1, firewall.clean()) +} + +func TestFirewall_ConnectionAllowed(t *testing.T) { + clock := &bxmock.MockClock{} + firewall := newFirewall(clock, 30*time.Minute) + + accountID1 := types.AccountID("") + nodeID1 := types.NodeID("") + firewall.AddRule(sdnmessage.FirewallRule{AccountID: accountID1, PeerID: nodeID1, Duration: 20}) + + rejectConnection := firewall.Validate(accountID1, nodeID1) + assert.NotNil(t, rejectConnection) + rejectConnection = firewall.Validate(generateRandAccountID(), "") + assert.Nil(t, rejectConnection) + rejectConnection = firewall.Validate("", generateRandNodeID()) + assert.Nil(t, rejectConnection) + rejectConnection = firewall.Validate(generateRandAccountID(), generateRandNodeID()) + assert.Nil(t, rejectConnection) + + accountID2 := generateRandAccountID() + nodeID2 := generateRandNodeID() + firewall.AddRule(sdnmessage.FirewallRule{AccountID: accountID2, PeerID: nodeID2, Duration: 20}) + + rejectConnection = firewall.Validate(accountID2, nodeID2) + assert.NotNil(t, rejectConnection) + rejectConnection = firewall.Validate("", "") + assert.NotNil(t, rejectConnection) + rejectConnection = firewall.Validate(generateRandAccountID(), "") + assert.Nil(t, rejectConnection) + rejectConnection = firewall.Validate("", generateRandNodeID()) + assert.Nil(t, rejectConnection) + rejectConnection = firewall.Validate(generateRandAccountID(), generateRandNodeID()) + assert.Nil(t, rejectConnection) + + accountID3 := types.AccountID("*") + nodeID3 := generateRandNodeID() + firewall.AddRule(sdnmessage.FirewallRule{AccountID: accountID3, PeerID: nodeID3, Duration: 20}) + + rejectConnection = firewall.Validate("", nodeID3) + assert.NotNil(t, rejectConnection) + rejectConnection = firewall.Validate("", "") + assert.NotNil(t, rejectConnection) + rejectConnection = firewall.Validate(generateRandAccountID(), "") + assert.Nil(t, rejectConnection) + rejectConnection = firewall.Validate("", generateRandNodeID()) + assert.Nil(t, rejectConnection) + rejectConnection = firewall.Validate(generateRandAccountID(), generateRandNodeID()) + assert.Nil(t, rejectConnection) + + accountID4 := generateRandAccountID() + nodeID4 := types.NodeID("*") + firewall.AddRule(sdnmessage.FirewallRule{AccountID: accountID4, PeerID: nodeID4, Duration: 20}) + + rejectConnection = firewall.Validate("", "") + assert.NotNil(t, rejectConnection) + rejectConnection = firewall.Validate("", nodeID4) + assert.Nil(t, rejectConnection) + rejectConnection = firewall.Validate(generateRandAccountID(), "") + assert.Nil(t, rejectConnection) + rejectConnection = firewall.Validate("", generateRandNodeID()) + assert.Nil(t, rejectConnection) + rejectConnection = firewall.Validate(generateRandAccountID(), generateRandNodeID()) + assert.Nil(t, rejectConnection) + + accountID5 := types.AccountID("*") + nodeID5 := types.NodeID("*") + firewall.AddRule(sdnmessage.FirewallRule{AccountID: accountID5, PeerID: nodeID5, Duration: 20}) + + rejectConnection = firewall.Validate("", "") + assert.NotNil(t, rejectConnection) + rejectConnection = firewall.Validate(accountID5, nodeID5) + assert.NotNil(t, rejectConnection) + rejectConnection = firewall.Validate(generateRandAccountID(), "") + assert.NotNil(t, rejectConnection) + rejectConnection = firewall.Validate("", generateRandNodeID()) + assert.NotNil(t, rejectConnection) + rejectConnection = firewall.Validate(generateRandAccountID(), generateRandNodeID()) + assert.NotNil(t, rejectConnection) + + clock.IncTime(35 * time.Second) + + rejectConnection = firewall.Validate("", "") + assert.Nil(t, rejectConnection) + rejectConnection = firewall.Validate(accountID2, nodeID2) + assert.Nil(t, rejectConnection) + rejectConnection = firewall.Validate("", nodeID3) + assert.Nil(t, rejectConnection) + rejectConnection = firewall.Validate(accountID5, nodeID5) + assert.Nil(t, rejectConnection) + rejectConnection = firewall.Validate(generateRandAccountID(), "") + assert.Nil(t, rejectConnection) + rejectConnection = firewall.Validate("", generateRandNodeID()) + assert.Nil(t, rejectConnection) + rejectConnection = firewall.Validate(generateRandAccountID(), generateRandNodeID()) + assert.Nil(t, rejectConnection) + + assert.Equal(t, 5, firewall.clean()) +} + +func generateRandAccountID() types.AccountID { + id := uuid.NewV1() + accountID := types.AccountID(id.String()) + return accountID +} + +func generateRandNodeID() types.NodeID { + id := uuid.NewV1() + nodeID := types.NodeID(id.String()) + return nodeID +} diff --git a/services/hashhistory.go b/services/hashhistory.go new file mode 100644 index 0000000..604f93c --- /dev/null +++ b/services/hashhistory.go @@ -0,0 +1,87 @@ +package services + +import ( + "github.com/bloXroute-Labs/gateway/utils" + cmap "github.com/orcaman/concurrent-map" + log "github.com/sirupsen/logrus" + "time" +) + +// HashHistory holds hashes that we have seen in the past +type HashHistory struct { + name string // for logging + clock utils.Clock + cleanupFreq time.Duration + data cmap.ConcurrentMap +} + +// NewHashHistory creates a new object +func NewHashHistory(name string, cleanupFreq time.Duration) HashHistory { + return newHashHistory(name, utils.RealClock{}, cleanupFreq) +} + +func newHashHistory(name string, clock utils.Clock, cleanupFreq time.Duration) HashHistory { + hh := HashHistory{ + name: name, + clock: clock, + cleanupFreq: cleanupFreq, + data: cmap.New(), + } + go hh.cleanup() + return hh +} + +// Add adds the hash for the duration +func (hh HashHistory) Add(hash string, expiration time.Duration) { + hh.data.Set(hash, hh.clock.Now().Add(expiration)) +} + +// Remove removes the hash from the data +func (hh HashHistory) Remove(hash string) { + hh.data.Remove(hash) +} + +// SetIfAbsent Sets the given value under the specified key if no value was associated with it. +func (hh HashHistory) SetIfAbsent(hash string, expiration time.Duration) bool { + return hh.data.SetIfAbsent(hash, hh.clock.Now().Add(expiration)) +} + +// Exists checks if hash is in history +func (hh HashHistory) Exists(hash string) bool { + if val, ok := hh.data.Get(hash); ok { + expiration := val.(time.Time) + if hh.clock.Now().Before(expiration) { + return true + } + } + return false +} + +// Count provides the size of the history +func (hh HashHistory) Count() int { + return hh.data.Count() +} + +func (hh HashHistory) cleanup() { + ticker := time.NewTicker(hh.cleanupFreq) + for { + select { + case <-ticker.C: + itemsCleaned := hh.clean() + log.Debugf("cleaned %v entries in HashHistory[%v]", itemsCleaned, hh.name) + } + } +} + +func (hh HashHistory) clean() int { + historyCleaned := 0 + timeNow := hh.clock.Now() + for item := range hh.data.IterBuffered() { + expiration := item.Val.(time.Time) + if timeNow.After(expiration) { + hh.data.Remove(item.Key) + historyCleaned++ + } + } + return historyCleaned +} diff --git a/services/hashhistory_test.go b/services/hashhistory_test.go new file mode 100644 index 0000000..c173061 --- /dev/null +++ b/services/hashhistory_test.go @@ -0,0 +1,41 @@ +package services + +import ( + "github.com/bloXroute-Labs/gateway/test/bxmock" + "github.com/bloXroute-Labs/gateway/types" + "github.com/stretchr/testify/assert" + "testing" + "time" +) + +func TestHashHistory_Set_Get(t *testing.T) { + clock := &bxmock.MockClock{} + history := newHashHistory("", clock, 30*time.Minute) + + hash1 := types.SHA256Hash{1} + hash2 := types.SHA256Hash{2} + hash3 := types.SHA256Hash{3} + hash4 := types.SHA256Hash{4} + history.Add(string(hash1[:]), 10*time.Minute) + history.Add(string(hash2[:]), 25*time.Minute) + history.Add(string(hash3[:]), 45*time.Minute) + assert.Equal(t, 3, history.Count()) + ok := history.Exists(string(hash3[:])) + assert.True(t, ok) + ok = history.Exists(string(hash4[:])) + assert.False(t, ok) + + clock.IncTime(20 * time.Minute) + ok = history.Exists(string(hash1[:])) + assert.False(t, ok) + assert.Equal(t, 3, history.Count()) + clock.IncTime(20 * time.Minute) + ok = history.Exists(string(hash1[:])) + assert.False(t, ok) + ok = history.Exists(string(hash2[:])) + assert.False(t, ok) + + assert.Equal(t, 3, history.Count()) + assert.Equal(t, 2, history.clean()) + assert.Equal(t, 1, history.Count()) +} diff --git a/services/loggers/txtracelogger.go b/services/loggers/txtracelogger.go new file mode 100644 index 0000000..af3fc83 --- /dev/null +++ b/services/loggers/txtracelogger.go @@ -0,0 +1,41 @@ +package loggers + +import ( + "github.com/bloXroute-Labs/gateway/connections" + "github.com/bloXroute-Labs/gateway/types" + "github.com/bloXroute-Labs/gateway/utils" + log "github.com/sirupsen/logrus" +) + +// TxTrace is used to generate log records for TxTrace +type TxTrace interface { + Log(hash types.SHA256Hash, source connections.Conn) +} + +type noStats struct { +} + +func (noStats) Log(hash types.SHA256Hash, source connections.Conn) { +} + +type txTrace struct { + logger *log.Logger +} + +// NewTxTrace is used to create TxTrace logger +func NewTxTrace(txTraceLogger *log.Logger) TxTrace { + if txTraceLogger == nil { + return noStats{} + } + return txTrace{logger: txTraceLogger} +} + +func (tt txTrace) Log(hash types.SHA256Hash, source connections.Conn) { + var sourceName string + if source.Info().ConnectionType == utils.Blockchain { + sourceName = "Blockchain" + } else { + sourceName = "BDN" + } + tt.logger.Tracef("%v - %v %v", hash, sourceName, source.Info().PeerIP) +} diff --git a/services/shortidassigner.go b/services/shortidassigner.go new file mode 100644 index 0000000..53f50cf --- /dev/null +++ b/services/shortidassigner.go @@ -0,0 +1,20 @@ +package services + +import "github.com/bloXroute-Labs/gateway/types" + +// ShortIDAssigner - an interface for short ID assigner struct +type ShortIDAssigner interface { + Next() types.ShortID +} + +type emptyShortIDAssigner struct { +} + +func (empty *emptyShortIDAssigner) Next() types.ShortID { + return types.ShortIDEmpty +} + +// NewEmptyShortIDAssigner - create an assigner that never assign +func NewEmptyShortIDAssigner() ShortIDAssigner { + return &emptyShortIDAssigner{} +} diff --git a/services/shortidassigner_test.go b/services/shortidassigner_test.go new file mode 100644 index 0000000..ef462c7 --- /dev/null +++ b/services/shortidassigner_test.go @@ -0,0 +1,15 @@ +package services + +import ( + "github.com/bmizerany/assert" + "testing" +) + +func TestShortIDAssigner(t *testing.T) { + assigner := NewEmptyShortIDAssigner() + sum := 0 + for i := 0; i < 1000; i++ { + sum += int(assigner.Next()) + } + assert.Equal(t, sum, 0) +} diff --git a/services/statistics/eventlogic.go b/services/statistics/eventlogic.go new file mode 100644 index 0000000..64ef97d --- /dev/null +++ b/services/statistics/eventlogic.go @@ -0,0 +1,24 @@ +package statistics + +// EventLogic that instruct the log_agent how to treat this stat record +type EventLogic int + +const ( + // EventLogicBlockInfo - represent block info + EventLogicBlockInfo EventLogic = 1 << iota + + // EventLogicMatch - represent a stat records that have several hashes and used to match between them + EventLogicMatch + + // EventLogicSummary - represent a summary record + EventLogicSummary + + // EventLogicPropagationStart - represent start propagation + EventLogicPropagationStart + + // EventLogicPropagationEnd - represent end propagation + EventLogicPropagationEnd + + // EventLogicNone - represent no special logic + EventLogicNone EventLogic = 0 +) diff --git a/services/statistics/fluentdstats.go b/services/statistics/fluentdstats.go new file mode 100644 index 0000000..33899bc --- /dev/null +++ b/services/statistics/fluentdstats.go @@ -0,0 +1,284 @@ +package statistics + +import ( + "encoding/base64" + "encoding/binary" + "fmt" + "github.com/bloXroute-Labs/gateway/bxmessage" + "github.com/bloXroute-Labs/gateway/connections" + "github.com/bloXroute-Labs/gateway/sdnmessage" + "github.com/bloXroute-Labs/gateway/types" + "github.com/fluent/fluent-logger-golang/fluent" + log "github.com/sirupsen/logrus" + "math" + "sync" + "time" + "unsafe" +) + +const ( + // MaxTailByteValue is the highest number of bytes based on TailByByteCount + MaxTailByteValue = uint16(math.MaxUint16) + + // TailByteCount is the number of bytes to consider in tx hash for writing events + TailByteCount = unsafe.Sizeof(MaxTailByteValue) + + // DateFormat is an example to date time string format + DateFormat = "2006-01-02T15:04:05.000000" +) + +// Stats is used to generate STATS record for transactions +type Stats interface { + AddBlockEvent(name string, source connections.Conn, blockHash types.SHA256Hash, networkNum types.NetworkNum, + sentPeers int, startTime time.Time, sentGatewayPeers int) + AddTxsByShortIDsEvent(name string, source connections.Conn, txInfo *types.BxTransaction, + shortID types.ShortID, sourceID types.NodeID, sentPeers int, sentGatewayPeers int, + startTime time.Time, priority bxmessage.SendPriority, debugData interface{}) + AddGatewayBlockEvent(name string, source connections.Conn, blockHash types.SHA256Hash, networkNum types.NetworkNum, + sentPeers int, startTime time.Time, sentGatewayPeers int, originalSize int, compressSize int, shortIDsCount int, txsCount int, recoveredTxsCount int, block *types.BxBlock) +} + +// NoStats is used to generate empty stats +type NoStats struct { +} + +// AddBlockEvent does nothing +func (NoStats) AddBlockEvent(name string, source connections.Conn, blockHash types.SHA256Hash, networkNum types.NetworkNum, + sentPeers int, startTime time.Time, sentGatewayPeers int) { +} + +// AddGatewayBlockEvent does nothing +func (NoStats) AddGatewayBlockEvent(name string, source connections.Conn, blockHash types.SHA256Hash, networkNum types.NetworkNum, + sentPeers int, startTime time.Time, sentGatewayPeers int, originalSize int, compressSize int, shortIDsCount int, txsCount int, recoveredTxsCount int, block *types.BxBlock) { +} + +// AddTxsByShortIDsEvent does nothing +func (NoStats) AddTxsByShortIDsEvent(name string, source connections.Conn, txInfo *types.BxTransaction, + shortID types.ShortID, sourceID types.NodeID, sentPeers int, sentGatewayPeers int, + startTime time.Time, priority bxmessage.SendPriority, debugData interface{}) { +} + +// FluentdStats struct that represents fluentd stats info +type FluentdStats struct { + NodeID types.NodeID + FluentD *fluent.Fluent + Networks map[types.NetworkNum]sdnmessage.BlockchainNetwork + Lock *sync.RWMutex + logNetworkContent bool +} + +// AddTxsByShortIDsEvent generates a fluentd STATS event +func (s FluentdStats) AddTxsByShortIDsEvent(name string, source connections.Conn, txInfo *types.BxTransaction, + shortID types.ShortID, sourceID types.NodeID, sentPeers int, sentGatewayPeers int, + startTime time.Time, priority bxmessage.SendPriority, debugData interface{}) { + if txInfo == nil || !s.shouldLogEventForTx(txInfo) { + return + } + + now := time.Now() + + logic := EventLogicNone + notFromBDN := source.Info().IsGateway() || source.Info().IsCloudAPI() + switch { + // if from external source + case name == "TxProcessedByRelayProxyFromPeer" && notFromBDN: + logic = EventLogicPropagationStart | EventLogicSummary + // if from bdn + case name == "TxProcessedByRelayProxyFromPeer": + logic = EventLogicPropagationEnd | EventLogicSummary + } + + record := Record{ + Type: "BxTransaction", + Data: txRecord{ + EventSubjectID: txInfo.Hash().Format(false), + EventLogic: logic, + NodeID: s.NodeID, + EventName: name, + NetworkNum: txInfo.NetworkNum(), + StartDateTime: startTime.Format(DateFormat), + SentGatewayPeers: sentGatewayPeers, + ExtraData: txExtraData{ + MoreInfo: fmt.Sprintf("source: %v - %v, priority %v, sent: %v (%v), duration: %v, debug data: %v", source, source.Info().ConnectionType.FormatShortNodeType(), priority, sentPeers, sentGatewayPeers, now.Sub(startTime), debugData), + ShortID: shortID, + NetworkNum: txInfo.NetworkNum(), + SourceID: sourceID, + IsCompactTransaction: len(txInfo.Content()) == 0, + AlreadySeenContent: false, + ExistingShortIds: txInfo.ShortIDs(), + }, + EndDateTime: now.Format(DateFormat), + }, + } + s.LogToFluentD(record, now, "stats.transactions.events.p") +} + +// AddBlockEvent generates a fluentd STATS event +func (s FluentdStats) AddBlockEvent(name string, source connections.Conn, blockHash types.SHA256Hash, networkNum types.NetworkNum, + sentPeers int, startTime time.Time, sentGatewayPeers int) { + now := time.Now() + + record := Record{ + Type: "BlockInfo", + Data: blockRecord{ + EventSubjectID: blockHash.String(), + EventLogic: EventLogicNone, + NodeID: s.NodeID, + EventName: name, + NetworkNum: networkNum, + SourceID: source.Info().NodeID, + StartDateTime: startTime.Format(DateFormat), + EndDateTime: now.Format(DateFormat), + SentGatewayPeers: sentGatewayPeers, + ExtraData: blockExtraData{ + MoreInfo: fmt.Sprintf("source: %v - %v, sent: %v", source, source.Info().ConnectionType.FormatShortNodeType(), sentPeers), + }, + }, + } + s.LogToFluentD(record, now, "stats.blocks.events.p") +} + +// AddGatewayBlockEvent add block event for the gateway +func (s FluentdStats) AddGatewayBlockEvent(name string, source connections.Conn, blockHash types.SHA256Hash, networkNum types.NetworkNum, + sentPeers int, startTime time.Time, sentGatewayPeers int, originalSize int, compressSize int, shortIDsCount int, txsCount int, recoveredTxsCount int, block *types.BxBlock) { + now := time.Now() + + record := Record{ + Type: "GatewayBlockInfo", + Data: gatewayBlockRecord{ + EventSubjectID: blockHash.String(), + EventLogic: EventLogicNone, + NodeID: s.NodeID, + EventName: name, + NetworkNum: networkNum, + SourceID: source.Info().NodeID, + StartDateTime: startTime.Format(DateFormat), + EndDateTime: now.Format(DateFormat), + SentGatewayPeers: sentGatewayPeers, + ExtraData: blockExtraData{ + MoreInfo: fmt.Sprintf("source: %v - %v, sent: %v", source, source.Info().ConnectionType.FormatShortNodeType(), sentPeers), + }, + OriginalSize: originalSize, + CompressSize: compressSize, + ShortIDsCount: shortIDsCount, + TxsCount: txsCount, + RecoveredTxsCount: recoveredTxsCount, + }, + } + s.LogToFluentD(record, now, "stats.gateway.blocks.events.p") + + s.addBlockContent(name, networkNum, blockHash, block) +} + +func (s FluentdStats) addBlockContent(name string, networkNum types.NetworkNum, blockHash types.SHA256Hash, blockContentInfo *types.BxBlock) { + if !s.logNetworkContent { + return + } + if name != "GatewayReceivedBlockFromBlockchainNode" && name != "GatewayProcessBlockFromBDN" { + return + } + if blockContentInfo == nil { + log.Errorf("can't addBlockContent for %v, networkNum %v, event %v - block is nil", blockHash, networkNum, name) + return + } + + now := time.Now() + + txs := make([]byte, 0, len(blockContentInfo.Txs)) + for _, tx := range blockContentInfo.Txs { + txs = append(txs, tx.Content()...) + } + + record := Record{ + Type: "NetworkEthContentBlock", + Data: ethBlockContent{ + BlockHash: blockHash.String(), + NetworkNum: networkNum, + Header: base64.StdEncoding.EncodeToString(blockContentInfo.Header), + Txs: base64.StdEncoding.EncodeToString(txs), + Trailer: base64.StdEncoding.EncodeToString(blockContentInfo.Trailer), + }, + } + + s.LogToFluentD(record, now, "network_content.block.stats") +} + +// NewStats is used to create transaction STATS logger +func NewStats(fluentDEnabled bool, fluentDHost string, nodeID types.NodeID, networks *sdnmessage.BlockchainNetworks, logNetworkContent bool) Stats { + if !fluentDEnabled { + return NoStats{} + } + + return newStats(fluentDHost, nodeID, networks, logNetworkContent) +} + +// LogToFluentD log info to the fluentd +func (s FluentdStats) LogToFluentD(record interface{}, ts time.Time, logName string) { + d := LogRecord{ + Level: "STATS", + Name: logName, + Instance: s.NodeID, + Msg: record, + Timestamp: ts.Format(DateFormat), + } + + err := s.FluentD.EncodeAndPostData("bx.go.log", ts, d) + if err != nil { + log.Errorf("Failed to send STATS to fluentd - %v", err) + } +} + +func newStats(fluentdHost string, nodeID types.NodeID, sdnHTTPNetworks *sdnmessage.BlockchainNetworks, logNetworkContent bool) Stats { + fluentlogger, err := fluent.New(fluent.Config{ + FluentHost: fluentdHost, + FluentPort: 24224, + MarshalAsJSON: true, + Async: true, + }) + + if err != nil { + log.Panic() + } + + t := FluentdStats{ + NodeID: nodeID, + FluentD: fluentlogger, + Networks: make(map[types.NetworkNum]sdnmessage.BlockchainNetwork), + Lock: &sync.RWMutex{}, + logNetworkContent: logNetworkContent, + } + + for _, network := range *sdnHTTPNetworks { + t.UpdateBlockchainNetwork(*network) + } + + return t +} + +// UpdateBlockchainNetwork - updates the blockchainNetwork object +func (s FluentdStats) UpdateBlockchainNetwork(network sdnmessage.BlockchainNetwork) { + s.Lock.Lock() + s.Networks[network.NetworkNum] = network + s.Lock.Unlock() +} + +func (s FluentdStats) shouldLogEventForTx(txInfo *types.BxTransaction) bool { + txHashPercentage := s.getLogPercentageByHash(txInfo.NetworkNum()) + if txHashPercentage <= 0 { + return false + } + txHash := txInfo.Hash() + lastBytesValue := binary.BigEndian.Uint16(txHash[types.SHA256HashLen-TailByteCount:]) + probabilityValue := float64(lastBytesValue) * float64(100) / float64(MaxTailByteValue) + return probabilityValue <= txHashPercentage +} + +func (s FluentdStats) getLogPercentageByHash(networkNum types.NetworkNum) float64 { + s.Lock.RLock() + defer s.Lock.RUnlock() + blockchainNetwork, ok := s.Networks[networkNum] + if !ok { + return 0 + } + return blockchainNetwork.TxPercentToLogByHash +} diff --git a/services/statistics/fluentdstats_test.go b/services/statistics/fluentdstats_test.go new file mode 100644 index 0000000..37b44c8 --- /dev/null +++ b/services/statistics/fluentdstats_test.go @@ -0,0 +1,107 @@ +package statistics + +import ( + "encoding/binary" + "github.com/bloXroute-Labs/gateway/sdnmessage" + "github.com/bloXroute-Labs/gateway/test/bxmock" + "github.com/bloXroute-Labs/gateway/types" + "github.com/stretchr/testify/assert" + "math/rand" + "testing" + "time" +) + +func TestShouldLog(t *testing.T) { + networks := sdnmessage.BlockchainNetworks{ + 1: bxmock.MockNetwork(1, "Ethereum", "1", 0.5), + 2: bxmock.MockNetwork(2, "Ethereum", "2", 50), + 3: bxmock.MockNetwork(3, "Ethereum", "3", 0.001), + 4: bxmock.MockNetwork(4, "Ethereum", "4", 0.025), + } + + stats := newStats("localhost", "node", &networks, false) + ft := stats.(FluentdStats) + assert.False( + t, ft.shouldLogEventForTx( + types.NewBxTransaction(generateRandTxHash(0xffff), 1, 0, time.Now()), + ), + ) + assert.False( + t, ft.shouldLogEventForTx( + types.NewBxTransaction(generateRandTxHash(0x7d00), 1, 0, time.Now()), + ), + ) + assert.False( + t, ft.shouldLogEventForTx( + types.NewBxTransaction(generateRandTxHash(0x3e80), 1, 0, time.Now()), + ), + ) + assert.False( + t, ft.shouldLogEventForTx( + types.NewBxTransaction(generateRandTxHash(0x05dc), 1, 0, time.Now()), + ), + ) + assert.True( + t, ft.shouldLogEventForTx( + types.NewBxTransaction(generateRandTxHash(0x0012), 1, 0, time.Now()), + ), + ) + + assert.False( + t, ft.shouldLogEventForTx( + types.NewBxTransaction(generateRandTxHash(0x000a), 3, 0, time.Now()), + ), + ) + assert.True( + t, ft.shouldLogEventForTx( + types.NewBxTransaction(generateRandTxHash(0x0000), 3, 0, time.Now()), + ), + ) + + assert.True( + t, ft.shouldLogEventForTx( + types.NewBxTransaction(generateRandTxHash(0x0000), 4, 0, time.Now()), + ), + ) + + assert.True( + t, ft.shouldLogEventForTx( + types.NewBxTransaction(generateRandTxHash(0x0010), 4, 0, time.Now()), + ), + ) + + assert.True( + t, ft.shouldLogEventForTx( + types.NewBxTransaction(generateRandTxHash(0x000f), 4, 0, time.Now()), + ), + ) + + assert.False( + t, ft.shouldLogEventForTx( + types.NewBxTransaction(generateRandTxHash(0x0011), 4, 0, time.Now()), + ), + ) + + hash, _ := types.NewSHA256HashFromString("fb0d50a5731201b9265c66444ce2d20973b4e16a540716d2d3be7f091d13b900") + assert.False( + t, ft.shouldLogEventForTx( + types.NewBxTransaction(hash, 4, 0, time.Now()), + ), + ) + hash, _ = types.NewSHA256HashFromString("fb0d50a5731201b9265c66444ce2d20973b4e16a540716d2d3be7f091d130010") + assert.True( + t, ft.shouldLogEventForTx( + types.NewBxTransaction(hash, 4, 0, time.Now()), + ), + ) +} + +func generateRandTxHash(tail uint16) types.SHA256Hash { + var hash types.SHA256Hash + if _, err := rand.Read(hash[:]); err != nil { + panic(err) + } + binary.BigEndian.PutUint16((hash)[types.SHA256HashLen-TailByteCount:], tail) + + return hash +} diff --git a/services/statistics/record.go b/services/statistics/record.go new file mode 100644 index 0000000..6b9bd13 --- /dev/null +++ b/services/statistics/record.go @@ -0,0 +1,83 @@ +package statistics + +import "github.com/bloXroute-Labs/gateway/types" + +// Record represents a bloxroute style stat type record +type Record struct { + Type string `json:"type"` + Data interface{} `json:"data"` +} + +type blockRecord struct { + EventSubjectID string `json:"event_subject_id"` + EventLogic EventLogic `json:"event_logic"` + NodeID types.NodeID `json:"node_id"` + EventName string `json:"event_name"` + NetworkNum types.NetworkNum `json:"network_num"` + SourceID types.NodeID `json:"source_id"` + StartDateTime string `json:"start_date_time"` + ExtraData blockExtraData `json:"extra_data,omitempty"` + EndDateTime string `json:"end_date_time"` + SentGatewayPeers int `json:"gateway_peers"` +} + +type gatewayBlockRecord struct { + EventSubjectID string `json:"event_subject_id"` + EventLogic EventLogic `json:"event_logic"` + NodeID types.NodeID `json:"node_id"` + EventName string `json:"event_name"` + NetworkNum types.NetworkNum `json:"network_num"` + SourceID types.NodeID `json:"source_id"` + StartDateTime string `json:"start_date_time"` + ExtraData blockExtraData `json:"extra_data,omitempty"` + EndDateTime string `json:"end_date_time"` + SentGatewayPeers int `json:"gateway_peers"` + OriginalSize int `json:"original_size"` + CompressSize int `json:"compress_size"` + ShortIDsCount int `json:"short_ids_count"` + TxsCount int `json:"txs_count"` + RecoveredTxsCount int `json:"recovered_txs_count"` +} + +type ethBlockContent struct { + BlockHash string `json:"block_hash"` + NetworkNum types.NetworkNum `json:"network_num"` + Header string `json:"header"` + Txs string `json:"txs"` + Trailer string `json:"trailer"` +} + +type blockExtraData struct { + MoreInfo string `json:"more_info,omitempty"` +} + +// LogRecord represents a log message to be sent to FluentD +type LogRecord struct { + Level string `json:"level"` + Name string `json:"name"` + Instance types.NodeID `json:"instance"` + Msg interface{} `json:"msg"` + Timestamp string `json:"timestamp"` +} + +type txRecord struct { + EventSubjectID string `json:"event_subject_id"` + EventLogic EventLogic `json:"event_logic"` + NodeID types.NodeID `json:"node_id"` + EventName string `json:"event_name"` + NetworkNum types.NetworkNum `json:"network_num"` + StartDateTime string `json:"start_date_time"` + ExtraData txExtraData `json:"extra_data,omitempty"` + EndDateTime string `json:"end_date_time"` + SentGatewayPeers int `json:"gateway_peers"` +} + +type txExtraData struct { + MoreInfo string `json:"more_info,omitempty"` + ShortID types.ShortID `json:"short_id"` + NetworkNum types.NetworkNum `json:"network_num"` + SourceID types.NodeID `json:"source_id"` + IsCompactTransaction bool `json:"is_compact_transaction"` + AlreadySeenContent bool `json:"already_seen_content"` + ExistingShortIds types.ShortIDList `json:"existing_short_ids"` +} diff --git a/services/txstore.go b/services/txstore.go new file mode 100644 index 0000000..ca239ce --- /dev/null +++ b/services/txstore.go @@ -0,0 +1,49 @@ +package services + +import ( + pbbase "github.com/bloXroute-Labs/gateway/protobuf" + "github.com/bloXroute-Labs/gateway/types" + "time" +) + +// ReEntryProtection - protect against hash re-entrance +const ReEntryProtection = true + +// NoReEntryProtection - no re-entrance protection needed +const NoReEntryProtection = false + +// TxStore is the service interface for transaction storage and processing +type TxStore interface { + Start() error + Stop() + + Add(hash types.SHA256Hash, content types.TxContent, shortID types.ShortID, network types.NetworkNum, + validate bool, flags types.TxFlags, timestamp time.Time, networkChainID int64) TransactionResult + Get(hash types.SHA256Hash) (*types.BxTransaction, bool) + HasContent(hash types.SHA256Hash) bool + + RemoveShortIDs(*types.ShortIDList, bool, string) + RemoveHashes(*types.SHA256HashList, bool, string) + GetTxByShortID(types.ShortID) (*types.BxTransaction, error) + + Clear() + + Iter() (iter <-chan *types.BxTransaction) + Count() int + Summarize() *pbbase.TxStoreReply + CleanNow() +} + +// TransactionResult is returned after the transaction service processes a new tx message, deciding whether to process it +type TransactionResult struct { + NewTx bool + NewContent bool + NewSID bool + Reprocess bool + FailedValidation bool + ReuseSenderNonce bool + Transaction *types.BxTransaction + AssignedShortID types.ShortID + DebugData interface{} + AlreadySeen bool +} diff --git a/test/bxmock/bxlistener.go b/test/bxmock/bxlistener.go new file mode 100644 index 0000000..da6ca8f --- /dev/null +++ b/test/bxmock/bxlistener.go @@ -0,0 +1,29 @@ +package bxmock + +import ( + "github.com/bloXroute-Labs/gateway/bxmessage" + "github.com/bloXroute-Labs/gateway/connections" +) + +// MockBxListener is a flexible struct that implements connections.BxListener +type MockBxListener struct{} + +// NodeStatus returns an empty status +func (m MockBxListener) NodeStatus() connections.NodeStatus { + return connections.NodeStatus{} +} + +// HandleMsg does nothing +func (m MockBxListener) HandleMsg(msg bxmessage.Message, conn connections.Conn, background connections.MsgHandlingOptions) error { + return nil +} + +// OnConnEstablished does nothing +func (m MockBxListener) OnConnEstablished(conn connections.Conn) error { + return nil +} + +// OnConnClosed does nothing +func (m MockBxListener) OnConnClosed(conn connections.Conn) error { + return nil +} diff --git a/test/bxmock/clock.go b/test/bxmock/clock.go new file mode 100644 index 0000000..efc9f8a --- /dev/null +++ b/test/bxmock/clock.go @@ -0,0 +1,95 @@ +package bxmock + +import ( + "github.com/bloXroute-Labs/gateway/utils" + "sync" + "time" +) + +// MockClock represents a fake time service for testing purposes +type MockClock struct { + m sync.Mutex + + currentTime time.Time + timers []*mockTimer +} + +// SetTime sets the time that this mock clock will always return +func (mc *MockClock) SetTime(t time.Time) { + mc.m.Lock() + defer mc.m.Unlock() + + mc.currentTime = t + + unfiredTimers := make([]*mockTimer, 0) + for _, timer := range mc.timers { + if timer.fireTime.Before(t) || timer.fireTime.Equal(t) { + timer.Fire(timer.fireTime) + } else { + unfiredTimers = append(unfiredTimers, timer) + } + } + + mc.timers = unfiredTimers +} + +// IncTime advances the clock by the duration +func (mc *MockClock) IncTime(d time.Duration) { + mc.SetTime(mc.currentTime.Add(d)) +} + +// Now returns the desired time for tests +func (mc MockClock) Now() time.Time { + return mc.currentTime +} + +// Sleep blocks the current goroutine until the timer has been incremented +func (mc *MockClock) Sleep(d time.Duration) { + t := mc.Timer(d) + <-t.Alert() +} + +// Timer returns a mock timer that will fire when mock clock ticks past its fire time +func (mc *MockClock) Timer(d time.Duration) utils.Timer { + mc.m.Lock() + defer mc.m.Unlock() + + timer := &mockTimer{ + clock: mc, + fireTime: mc.Now().Add(d), + alertCh: make(chan time.Time, 1), + } + mc.timers = append(mc.timers, timer) + return timer +} + +type mockTimer struct { + clock *MockClock + fireTime time.Time + active bool + alertCh chan time.Time +} + +func (m *mockTimer) Fire(t time.Time) { + m.alertCh <- t +} + +func (m *mockTimer) Alert() <-chan time.Time { + defer func() { + m.active = false + }() + return m.alertCh +} + +func (m *mockTimer) Reset(d time.Duration) bool { + m.fireTime = m.clock.Now().Add(d) + active := m.active + m.active = true + return active +} + +func (m *mockTimer) Stop() bool { + active := m.active + m.active = false + return active +} diff --git a/test/bxmock/ethblock.go b/test/bxmock/ethblock.go new file mode 100644 index 0000000..15a56d3 --- /dev/null +++ b/test/bxmock/ethblock.go @@ -0,0 +1,61 @@ +package bxmock + +import ( + "github.com/bloXroute-Labs/gateway/types" + "github.com/ethereum/go-ethereum/common" + ethtypes "github.com/ethereum/go-ethereum/core/types" + "math/big" + "time" +) + +// NewEthBlockHeader generates a test header. Note that tx hash, uncle hash, receipt hash, and bloom will be overridden by when actually constructing a blocks +func NewEthBlockHeader(height uint64, parentHash common.Hash) *ethtypes.Header { + if parentHash == (common.Hash{}) { + parentHash = common.BytesToHash(types.GenerateSHA256Hash().Bytes()) + } + header := ethtypes.Header{ + ParentHash: parentHash, + UncleHash: common.BytesToHash(types.GenerateSHA256Hash().Bytes()), + Coinbase: GenerateAddress(), + Root: common.BytesToHash(types.GenerateSHA256Hash().Bytes()), + TxHash: common.BytesToHash(types.GenerateSHA256Hash().Bytes()), + ReceiptHash: common.BytesToHash(types.GenerateSHA256Hash().Bytes()), + Bloom: GenerateBloom(), + Difficulty: big.NewInt(1), + Number: big.NewInt(int64(height)), + GasLimit: uint64(1), + GasUsed: uint64(1), + Time: uint64(time.Now().Unix()), + Extra: []byte{}, + MixDigest: common.BytesToHash(types.GenerateSHA256Hash().Bytes()), + Nonce: GenerateBlockNonce(), + BaseFee: big.NewInt(1), + } + return &header +} + +// NewEthBlockWithHeader generates an Ethereum block testing purposes from a header +func NewEthBlockWithHeader(header *ethtypes.Header) *ethtypes.Block { + txs := []*ethtypes.Transaction{ + NewSignedEthTx(ethtypes.LegacyTxType, 1, nil), + NewSignedEthTx(ethtypes.AccessListTxType, 2, nil), + NewSignedEthTx(ethtypes.DynamicFeeTxType, 3, nil), + } + uncles := []*ethtypes.Header{ + NewEthBlockHeader(header.Number.Uint64(), common.Hash{}), + NewEthBlockHeader(header.Number.Uint64(), common.Hash{}), + } + + block := ethtypes.NewBlock(header, txs, uncles, nil, NewTestHasher()) + return block +} + +// NewEthBlock generates an Ethereum block for testing purposes +func NewEthBlock(height uint64, parentHash common.Hash) *ethtypes.Block { + if parentHash == (common.Hash{}) { + parentHash = common.BytesToHash(types.GenerateSHA256Hash().Bytes()) + } + + initialHeader := NewEthBlockHeader(height, parentHash) + return NewEthBlockWithHeader(initialHeader) +} diff --git a/test/bxmock/ethtx.go b/test/bxmock/ethtx.go new file mode 100644 index 0000000..3f6ce41 --- /dev/null +++ b/test/bxmock/ethtx.go @@ -0,0 +1,114 @@ +package bxmock + +import ( + "crypto/ecdsa" + "github.com/bloXroute-Labs/gateway/bxmessage" + "github.com/bloXroute-Labs/gateway/types" + ethtypes "github.com/ethereum/go-ethereum/core/types" + "github.com/ethereum/go-ethereum/crypto" + "github.com/ethereum/go-ethereum/rlp" + "math/big" +) + +var chainID = big.NewInt(10) +var pKey, _ = crypto.HexToECDSA("dae2cb3b03f8a1bbaedae4d43e159360c8d07ffab119d5d7311a81a9d4f53bd1") + +// NewSignedEthTx generates a valid signed Ethereum transaction from a provided private key. nil can be specified to use a hardcoded key. +func NewSignedEthTx(txType uint8, nonce uint64, privateKey *ecdsa.PrivateKey) *ethtypes.Transaction { + if privateKey == nil { + privateKey = pKey + } + + var unsignedTx *ethtypes.Transaction + + switch txType { + case ethtypes.LegacyTxType: + unsignedTx = newEthLegacyTx(nonce, privateKey) + case ethtypes.AccessListTxType: + unsignedTx = newEthAccessListTx(nonce, privateKey) + case ethtypes.DynamicFeeTxType: + unsignedTx = newEthDynamicFeeTx(nonce, privateKey) + default: + panic("provided tx type does not exist") + } + + signer := ethtypes.NewLondonSigner(chainID) + hash := signer.Hash(unsignedTx) + signature, _ := crypto.Sign(hash.Bytes(), privateKey) + + signedTx, _ := unsignedTx.WithSignature(signer, signature) + return signedTx +} + +// NewSignedEthTxBytes generates a valid Ethereum transaction, and packs it into RLP encoded bytes +func NewSignedEthTxBytes(txType uint8, nonce uint64, privateKey *ecdsa.PrivateKey) (*ethtypes.Transaction, []byte) { + tx := NewSignedEthTx(txType, nonce, privateKey) + b, err := rlp.EncodeToBytes(tx) + if err != nil { + panic(err) + } + return tx, b +} + +// NewSignedEthTxMessage generates a valid Ethereum transaction, and packs it into a bloxroute tx message +func NewSignedEthTxMessage(txType uint8, nonce uint64, privateKey *ecdsa.PrivateKey, networkNum types.NetworkNum) (*ethtypes.Transaction, *bxmessage.Tx) { + ethTx, ethTxBytes := NewSignedEthTxBytes(txType, nonce, privateKey) + var hash types.SHA256Hash + copy(hash[:], ethTx.Hash().Bytes()) + return ethTx, bxmessage.NewTx(hash, ethTxBytes, networkNum, 0, "") +} + +// newEthLegacyTx generates a valid signed Ethereum transaction from a provided private key. nil can be specified to use a hardcoded private key. +func newEthLegacyTx(nonce uint64, privateKey *ecdsa.PrivateKey) *ethtypes.Transaction { + address := crypto.PubkeyToAddress(privateKey.PublicKey) + unsignedTx := ethtypes.NewTx(ðtypes.LegacyTx{ + Nonce: nonce, + GasPrice: big.NewInt(1), + Gas: 0, + To: &address, + Value: big.NewInt(1), + Data: []byte{}, + V: nil, + R: nil, + S: nil, + }) + return unsignedTx +} + +func newEthAccessListTx(nonce uint64, privateKey *ecdsa.PrivateKey) *ethtypes.Transaction { + address := crypto.PubkeyToAddress(privateKey.PublicKey) + unsignedTx := ethtypes.NewTx(ðtypes.AccessListTx{ + ChainID: chainID, + Nonce: nonce, + GasPrice: big.NewInt(1), + Gas: 0, + To: &address, + Value: big.NewInt(1), + Data: []byte{}, + AccessList: nil, + V: nil, + R: nil, + S: nil, + }) + return unsignedTx +} + +// newEthDynamicFeeTx generates a valid signed Ethereum transaction from a provided private key. nil can be specified to use a hardcoded private key. +func newEthDynamicFeeTx(nonce uint64, privateKey *ecdsa.PrivateKey) *ethtypes.Transaction { + address := crypto.PubkeyToAddress(privateKey.PublicKey) + unsignedTx := ethtypes.NewTx(ðtypes.DynamicFeeTx{ + ChainID: chainID, + Nonce: nonce, + GasTipCap: big.NewInt(1), + GasFeeCap: big.NewInt(1), + Gas: 0, + To: &address, + Value: big.NewInt(1), + Data: []byte{}, + AccessList: nil, + V: nil, + R: nil, + S: nil, + }) + return unsignedTx +} diff --git a/test/bxmock/ethutil.go b/test/bxmock/ethutil.go new file mode 100644 index 0000000..86add3b --- /dev/null +++ b/test/bxmock/ethutil.go @@ -0,0 +1,56 @@ +package bxmock + +import ( + "github.com/ethereum/go-ethereum/common" + ethtypes "github.com/ethereum/go-ethereum/core/types" + "golang.org/x/crypto/sha3" + "hash" + "math/rand" +) + +// GenerateBloom randomly generates a bloom +func GenerateBloom() ethtypes.Bloom { + var bloom ethtypes.Bloom + _, _ = rand.Read(bloom[:]) + return bloom +} + +// GenerateBlockNonce randomly generates a block nonce +func GenerateBlockNonce() ethtypes.BlockNonce { + var blockNonce ethtypes.BlockNonce + _, _ = rand.Read(blockNonce[:]) + return blockNonce +} + +// GenerateAddress randomly generates an address +func GenerateAddress() common.Address { + var address common.Address + _, _ = rand.Read(address[:]) + return address +} + +// TestHasher is the helper tool for transaction/receipt list hashing (taken from Geth test cases) +type TestHasher struct { + hasher hash.Hash +} + +// NewTestHasher creates a new TestHasher +func NewTestHasher() *TestHasher { + return &TestHasher{hasher: sha3.NewLegacyKeccak256()} +} + +// Reset resets the test hasher to its initial state +func (h *TestHasher) Reset() { + h.hasher.Reset() +} + +// Update updates test hasher values +func (h *TestHasher) Update(key, val []byte) { + h.hasher.Write(key) + h.hasher.Write(val) +} + +// Hash returns an Ethereum common hash +func (h *TestHasher) Hash() common.Hash { + return common.BytesToHash(h.hasher.Sum(nil)) +} diff --git a/test/bxmock/network.go b/test/bxmock/network.go new file mode 100644 index 0000000..2cf0bec --- /dev/null +++ b/test/bxmock/network.go @@ -0,0 +1,17 @@ +package bxmock + +import ( + "github.com/bloXroute-Labs/gateway/sdnmessage" + "github.com/bloXroute-Labs/gateway/types" +) + +// MockNetwork creates a mock network +func MockNetwork(networkNum types.NetworkNum, protocol string, network string, + percentToLogByHash float64) *sdnmessage.BlockchainNetwork { + return &sdnmessage.BlockchainNetwork{ + Network: network, + NetworkNum: networkNum, + Protocol: protocol, + TxPercentToLogByHash: percentToLogByHash, + } +} diff --git a/test/bxmock/tls.go b/test/bxmock/tls.go new file mode 100644 index 0000000..23a4b95 --- /dev/null +++ b/test/bxmock/tls.go @@ -0,0 +1,136 @@ +package bxmock + +import ( + "bytes" + "crypto/tls" + "encoding/binary" + "errors" + "github.com/bloXroute-Labs/gateway/bxmessage" + "github.com/bloXroute-Labs/gateway/types" + "github.com/bloXroute-Labs/gateway/utils" + "net" + "strconv" + "strings" + "time" +) + +const connectionTimeout = 200 * time.Millisecond + +// MockBytes represents a struct for passing messages to MockTLS to queue up a message or close the connection +type MockBytes struct { + b []byte + close bool +} + +// MockTLS is a tls.Conn that is easily manipulated for test cases +type MockTLS struct { + *tls.Conn + ip string + netIP net.IP + port int + nodeID types.NodeID + nodeType utils.NodeType + accountID types.AccountID + queuedBytes chan MockBytes + sendingBytes chan []byte + buf bytes.Buffer + Timeout time.Duration +} + +// NewMockTLS constructs a new mock for testing +func NewMockTLS(ip string, port int64, nodeID types.NodeID, nodeType utils.NodeType, accountID types.AccountID) MockTLS { + split := strings.Split(ip, ".") + ipBytes := make([]byte, len(split)) + for i, part := range split { + intRep, _ := strconv.Atoi(part) + ipBytes[i] = byte(intRep) + } + return MockTLS{ + ip: ip, + netIP: ipBytes, + port: int(port), + nodeID: nodeID, + nodeType: nodeType, + accountID: accountID, + queuedBytes: make(chan MockBytes, 100), + sendingBytes: make(chan []byte), + Timeout: connectionTimeout, + } +} + +// Read pulls messages queued onto the MockTLS connection +func (m MockTLS) Read(b []byte) (int, error) { + msg := <-m.queuedBytes + if msg.close { + return 0, errors.New("closing connection") + } + copy(b, msg.b) + bytesRead := len(msg.b) + return bytesRead, nil +} + +// SetReadDeadline currently does nothing +func (m MockTLS) SetReadDeadline(_ time.Time) error { + return nil +} + +// Write currently does nothing. An expected implementation in the future would track bytes written for comparison for tests. +func (m MockTLS) Write(b []byte) (int, error) { + var header [bxmessage.HeaderLen]byte + m.buf.Write(b) + for { + if m.buf.Len() < bxmessage.HeaderLen { + return len(b), nil + } + + _, _ = m.buf.Read(header[:]) + payloadLen := int(binary.LittleEndian.Uint32(header[bxmessage.PayloadSizeOffset:])) + if m.buf.Len() < payloadLen { + panic("unhandled case (need to create a test that Writes a message in chunks") + } + payload := make([]byte, payloadLen) + _, _ = m.buf.Read(payload) + m.sendingBytes <- append(header[:], payload...) + } +} + +// RemoteAddr is a filler implementation that returns the data this mock was constructed with +func (m MockTLS) RemoteAddr() net.Addr { + addr := net.TCPAddr{ + IP: m.netIP, + Port: m.port, + Zone: "", + } + return &addr +} + +// Properties is a filler implementation that returns the data this mock was constructed with +func (m MockTLS) Properties() (utils.BxSSLProperties, error) { + return utils.BxSSLProperties{ + NodeType: m.nodeType, + NodeID: m.nodeID, + AccountID: m.accountID, + }, nil +} + +// Close simulates an EOF from the remote connection +func (m MockTLS) Close(string) error { + m.queuedBytes <- MockBytes{close: true} + return nil +} + +// MockQueue is a mock only method to queue up bytes to be read by the connection +func (m MockTLS) MockQueue(b []byte) { + m.queuedBytes <- MockBytes{b: b} +} + +// MockAdvanceSent is a mock only method that processes the sent bytes so the next message can be sent on the socket. MockTLS only allows a single message to be queued up at a time. +func (m MockTLS) MockAdvanceSent() ([]byte, error) { + t := time.NewTimer(m.Timeout) + select { + case sentBytes := <-m.sendingBytes: + return sentBytes, nil + case <-t.C: + return nil, errors.New("no bytes were sent on the expected connection") + } +} diff --git a/test/bxmock/wsprovider.go b/test/bxmock/wsprovider.go new file mode 100644 index 0000000..e976938 --- /dev/null +++ b/test/bxmock/wsprovider.go @@ -0,0 +1,81 @@ +package bxmock + +import ( + "github.com/bloXroute-Labs/gateway/blockchain" + "github.com/ethereum/go-ethereum/rpc" + log "github.com/sirupsen/logrus" +) + +// MockWSProvider is a dummy struct that implements blockchain.WSProvider +type MockWSProvider struct { + syncStatusCh chan blockchain.NodeSyncStatus +} + +// NewMockWSProvider returns a new MockWSProvider +func NewMockWSProvider() blockchain.WSProvider { + return &MockWSProvider{ + syncStatusCh: make(chan blockchain.NodeSyncStatus, 1), + } +} + +// Subscribe returns a dummy subscription +func (m *MockWSProvider) Subscribe(responseChannel interface{}, feedName string) (*blockchain.Subscription, error) { + return &blockchain.Subscription{&rpc.ClientSubscription{}}, nil +} + +// CallRPC returns a fake response with no error +func (m *MockWSProvider) CallRPC(method string, payload []interface{}, options blockchain.RPCOptions) (interface{}, error) { + return "response", nil +} + +// FetchTransactionReceipt returns a fake response with no error +func (m *MockWSProvider) FetchTransactionReceipt(payload []interface{}, options blockchain.RPCOptions) (interface{}, error) { + return "response", nil +} + +// Connect is a no-op +func (m *MockWSProvider) Connect() { + return +} + +// Close is a no-op +func (m *MockWSProvider) Close() { +} + +// Log returns a fake log entry +func (m *MockWSProvider) Log() *log.Entry { + return log.WithFields(log.Fields{ + "connType": "WS", + "remoteAddr": "123.45.67.8", + }) +} + +// GetValidRPCCallMethods returns an empty list +func (m *MockWSProvider) GetValidRPCCallMethods() []string { + return []string{} +} + +// GetValidRPCCallPayloadFields returns an empty list +func (m *MockWSProvider) GetValidRPCCallPayloadFields() []string { + return []string{} +} + +// GetRequiredPayloadFieldsForRPCMethod returns an empty list with no error +func (m *MockWSProvider) GetRequiredPayloadFieldsForRPCMethod(method string) ([]string, bool) { + return []string{}, true +} + +// ConstructRPCCallPayload is a no-op +func (m *MockWSProvider) ConstructRPCCallPayload(method string, callParams map[string]string, tag string) ([]interface{}, error) { + return nil, nil +} + +// UpdateNodeSyncStatus pushes sync update to syncStatusCh +func (m *MockWSProvider) UpdateNodeSyncStatus(syncStatus blockchain.NodeSyncStatus) { + m.syncStatusCh <- syncStatus +} + +// ReceiveNodeSyncStatusUpdate returns the syncStatusCh +func (m *MockWSProvider) ReceiveNodeSyncStatusUpdate() chan blockchain.NodeSyncStatus { + return m.syncStatusCh +} diff --git a/test/constants.go b/test/constants.go new file mode 100644 index 0000000..0c9165c --- /dev/null +++ b/test/constants.go @@ -0,0 +1,25 @@ +package test + +// SSLTestPath is the path intermediary SSL certificates are created in for test cases +const SSLTestPath = "ssl" + +// CACertFolder is the folder at which intermediary CA certificates are created in for test cases +const CACertFolder = "ssl/ca" + +// CACertPath is the path of the intermediary CA certificate +const CACertPath = "ssl/ca/ca_cert.pem" + +// RegistrationCert is a sample transaction relay registration cert +const RegistrationCert = "-----BEGIN CERTIFICATE-----\nMIICtjCCAjygAwIBAgIUW8vj+TuK43zqfd7Fj9xNqkf0Fa8wCgYIKoZIzj0EAwIw\nbzELMAkGA1UEBhMCVVMxETAPBgNVBAgMCElsbGlub2lzMREwDwYDVQQHDAhFdmFu\nc3RvbjEbMBkGA1UECgwSYmxvWHJvdXRlIExBQlMgSW5jMR0wGwYDVQQDDBRibG9Y\ncm91dGUudGVzdG5ldC5DQTAeFw0yMTAyMDgxNzUzMDNaFw0yMjAyMDgwMDAwMDBa\nMGwxCzAJBgNVBAYTAiAgMQswCQYDVQQIDAIgIDELMAkGA1UEBwwCICAxGzAZBgNV\nBAoMEmJsb1hyb3V0ZSBMQUJTIEluYzEmMCQGA1UEAwwdYmxvWHJvdXRlLnRlc3Ru\nZXQucmVsYXlfcHJveHkwdjAQBgcqhkjOPQIBBgUrgQQAIgNiAAQ9HXj7YMkHI+7P\nxh5uarJoEmOF2s2/PT/+9Vu4rAdMUZQ0e5jELdTEuo/JGT1AlFwquhiSPAsVroEX\nseZyfViOSajhUBmTYz+LljKilltGzK8Lkbw06jQa08NWX1bMwx2jgZswgZgwHwYD\nVR0jBBgwFoAUkN1jVx3ISR8ATM29MLuIZ0K0dJEwKAYDVR0RBCEwH4IdYmxvWHJv\ndXRlLnRlc3RuZXQucmVsYXlfcHJveHkwDAYDVR0TAQH/BAIwADAOBgNVHQ8BAf8E\nBAMCBeAwFAYFPoJNolwEC1JFTEFZX1BST1hZMBcGBT6CTaJeBA5ibG9Ycm91dGUg\nTEFCUzAKBggqhkjOPQQDAgNoADBlAjAUrqyAz8gun5mI02E0MtiqcH3aVNayIvyH\nc7/c1+qyKLobJ0DwKqaocX4iYZrs7e0CMQC3wRz/P2bIxVeViE8/nnRQKLekBQqp\nBTwJ9D/G46Tg7r6kxj9QGQD8onp/i5AUCOI=\n-----END CERTIFICATE-----\n" + +// RegistrationKey is a sample transaction relay registration key +const RegistrationKey = "-----BEGIN EC PRIVATE KEY-----\nMIGkAgEBBDBX1DtibI/7qGv2jkw0/uSm5nF2OfsGWuXiNcz5Sy3RlsvvKohqb0U2\nVjdqSG9KnESgBwYFK4EEACKhZANiAAQ9HXj7YMkHI+7Pxh5uarJoEmOF2s2/PT/+\n9Vu4rAdMUZQ0e5jELdTEuo/JGT1AlFwquhiSPAsVroEXseZyfViOSajhUBmTYz+L\nljKilltGzK8Lkbw06jQa08NWX1bMwx0=\n-----END EC PRIVATE KEY-----\n" + +// PrivateCert is a sample transaction relay private cert +const PrivateCert = "-----BEGIN CERTIFICATE-----\nMIIDCTCCAo+gAwIBAgIUBPo2XvfCGvlFy6Cl0LPXej+Q5wQwCgYIKoZIzj0EAwIw\nbzELMAkGA1UEBhMCVVMxETAPBgNVBAgMCElsbGlub2lzMREwDwYDVQQHDAhFdmFu\nc3RvbjEbMBkGA1UECgwSYmxvWHJvdXRlIExBQlMgSW5jMR0wGwYDVQQDDBRibG9Y\ncm91dGUudGVzdG5ldC5DQTAeFw0yMTAyMDgyMDAyMDNaFw0yMjAyMDgwMDAwMDBa\nMHIxCzAJBgNVBAYTAiAgMQswCQYDVQQIDAIgIDELMAkGA1UEBwwCICAxGzAZBgNV\nBAoMEmJsb1hyb3V0ZSBMQUJTIEluYzEsMCoGA1UEAwwjYmxvWHJvdXRlLnRlc3Ru\nZXQucmVsYXlfdHJhbnNhY3Rpb24wdjAQBgcqhkjOPQIBBgUrgQQAIgNiAASMommd\nDnH8xgfXKM0HRln9vTk7O0Y1bZp+oRdq3Mv0xEY2lQJnWNTL8QqpfcFlUgJDiCRB\nP9SlS2H/DIbGah5yWv1XIknGkHXaS9o36PIYDAWG7UOwMinSpZ77sdOzq9Wjgegw\ngeUwHwYDVR0jBBgwFoAUkN1jVx3ISR8ATM29MLuIZ0K0dJEwLgYDVR0RBCcwJYIj\nYmxvWHJvdXRlLnRlc3RuZXQucmVsYXlfdHJhbnNhY3Rpb24wDAYDVR0TAQH/BAIw\nADAOBgNVHQ8BAf8EBAMCBeAwGgYFPoJNolwEEVJFTEFZX1RSQU5TQUNUSU9OMC0G\nBT6CTaJdBCQwZjU0YzUwOS0wNmYwLTRiZGQtOGZjMC0zYmRmMWFjMTE5ZWQwFwYF\nPoJNol4EDmJsb1hyb3V0ZSBMQUJTMBAGBT6CTaJfBAdnZW5lcmFsMAoGCCqGSM49\nBAMCA2gAMGUCMHbPKeumEf46NN+FSE9rKCD6bHH9pcm1asaF5XfSdiSzaiOseus+\njIqGOY0L8tKnPwIxAO9XUyoOzutvjrqIJ+APi6imUNKp3GaQeRMdXDXJH4vHoJ6L\nxpn1wK0JgqdzdA2z8w==\n-----END CERTIFICATE-----\n" + +// PrivateKey is a sample transaction relay private key +const PrivateKey = "-----BEGIN EC PRIVATE KEY-----\nMIGkAgEBBDBEU5Aui2ua3hSaWwo1YCH+DQAdu0aQl5N67GS8ZeQr0XnucDasQH0f\n84FLiQ6m7SqgBwYFK4EEACKhZANiAASMommdDnH8xgfXKM0HRln9vTk7O0Y1bZp+\noRdq3Mv0xEY2lQJnWNTL8QqpfcFlUgJDiCRBP9SlS2H/DIbGah5yWv1XIknGkHXa\nS9o36PIYDAWG7UOwMinSpZ77sdOzq9U=\n-----END EC PRIVATE KEY-----\n" + +// CACert is a sample CA certificate +const CACert = "-----BEGIN CERTIFICATE-----\nMIICkDCCAhWgAwIBAgIUQGhfIhpMxaSE0r/jjB35VclkTYgwCgYIKoZIzj0EAwIw\nazELMAkGA1UEBhMCVVMxETAPBgNVBAgMCElsbGlub2lzMREwDwYDVQQHDAhFdmFu\nc3RvbjEbMBkGA1UECgwSYmxvWHJvdXRlIExBQlMgSW5jMRkwFwYDVQQDDBBibG9Y\ncm91dGUuZGV2LkNBMB4XDTIwMTEwOTIyMzY0NloXDTQ4MDMyNzAwMDAwMFowazEL\nMAkGA1UEBhMCVVMxETAPBgNVBAgMCElsbGlub2lzMREwDwYDVQQHDAhFdmFuc3Rv\nbjEbMBkGA1UECgwSYmxvWHJvdXRlIExBQlMgSW5jMRkwFwYDVQQDDBBibG9Ycm91\ndGUuZGV2LkNBMHYwEAYHKoZIzj0CAQYFK4EEACIDYgAEd5IkD4wqWVGbq0jCehjr\nyvOEkbD5vCYIes4UwRH9Z7YSfeKQrOSaW1LUzHCYvqOTLfgHoAC0lS1v9+OlT/LZ\nboey8h4TZwLZ9zLbHzyVcTww01ZFNeBndQ2EmdaSYdqKo3oweDAfBgNVHSMEGDAW\ngBQHnCDGbOz7WL0VB9Kd0YdQSpsnQzAdBgNVHQ4EFgQUB5wgxmzs+1i9FQfSndGH\nUEqbJ0MwGwYDVR0RBBQwEoIQYmxvWHJvdXRlLmRldi5DQTAMBgNVHRMEBTADAQH/\nMAsGA1UdDwQEAwIB/jAKBggqhkjOPQQDAgNpADBmAjEAo6kPPChOytP961lFjKFb\n+zfEPm6sHtBxmgeDMhQwqb1erIIsYfU6zVaA82g9REHvAjEAoLfzcjEq91/Jlcmn\nCSgJY3JUPIocBek+o9cKczwz1ZDuzGscMOF0J4fpTwAyJOUP\n-----END CERTIFICATE-----" diff --git a/test/fixtures/broadcast.go b/test/fixtures/broadcast.go new file mode 100644 index 0000000..177d172 --- /dev/null +++ b/test/fixtures/broadcast.go @@ -0,0 +1,63 @@ +package fixtures + +import "math/big" + +// fixtures for broadcast message compression/decompression +const ( + BroadcastMessageWithShortIDs = "fffefdfc62726f61646361737400000014050000" + + "4c097654d1b901b3b9d616237792557fe1b466b7159856c1662eff78b8df2f7d05000000162a20cfb9874d6c80edde990abecd30626c636b00" + + "ce04000000000000" + + "f904c3" + + "f90252" + + "a02b4e6364a57c6da1212d75ba55bda8df143c69f6489eadc2d4da7253b4d271eca0b083ee4c1200f6e05b74680a94a4443eb0e2fd2ddffdf2dbd8d57bd94b4100e09473c55a98ab4ea955d7b259c27f7461d0ff804d14a05edf8fdd8d59e347e533c684baa90d72d39308dd57bca2c4526bb33381c5c751a0803a52f22a98ddf9bf8ec410bb99be47d0c8fd1c4122300b1e86f0dbb4575981a0012ce881d0ec4d7832c22f538a9555e6b119f404fc40068c97764099d64ccbe9b90100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000640102030405b8647e473df94da5e4df44e4aa128a2064ef5ac279d41883bbe3e7b3d1d66e46e4194d6648465bc88cec17aba82353a665aa9fb5cc75e2f6add0433d3781cf8297926da2a9427d8dfd9521ceace684750facd84eff20da49e522b82069d5e7593679a8304a9aa0b2b5ee018654b61c12517de834decaa1188f363268bd34d451add4d9af258b42880000000000000001" + + // 2 empty txs + "c6c28080c28080" + + // uncle + "f90255" + + "f90252a0170bbfd78c54c880261332460dd02efde3638eb0a93997ece3989bd160a127ada0f68dc7af15c9ee7e3985886970e98714666f8e4ed4845d2ab49dcaaa54e345519418ce00129125db31a98a3b3be294ac796e93fa09a0b5e1e8dcc071eda7a5635725e1b815f9b8014ad290ae5192be85db5235513946a0c0b36742bc64e95e6e0c7e2032bee26983980eaa380deac5ca48d4c04e9fb2f0a099fbc494497a933c29c237c064f9eb9e6fb7905798249f5660cb43dc9d281a65b90100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000640102030405b864c3ce8edb1839afc4dd243e4d8e32ebd70a885af1b553b6c3b9361259fb612aeb42af077ebccf54e2c3a959a7ab2470ed80a7ab7e457e2cdf66bd364896cb89d43842526a8929c4f34ea3da300d7bdd9e2f1623bd33d487360be6e2d05acb66d75af29758a04a1bf2ea4629d79eeb27d7767cede6e2e7f61b660d4afbec810f94c882d8a93f880000000000000001" + + // difficulty + "8a066a2b92db5ce24287cd" + + // block number + "83c93960" + + // short IDs + "02000000010000000200000001" + + BroadcastShortIDsMessageHash = "4c097654d1b901b3b9d616237792557fe1b466b7159856c1662eff78b8df2f7d" + BroadcastTransactionHash1 = "301ccd3a6ba2e8177c2faef495a2e422d58710c2b9f31b3b6e74ebf5fad52c12" + BroadcastTransactionContent1 = "b001ee08010203949f9ac55931a9d9d5da00ce7fb9515af412e1e5df048f0cf7035510dcab2aede3670c939d77c01b0607" + BroadcastTransactionHash2 = "f7bf12d80264fcb5b65798735b2c577feb7dad57be1ab1492016dcdb6ae3e76c" + BroadcastTransactionContent2 = "b84001f83d100204069432feed2ae4007dbf2dc795db1baa8e2b618b68a4089ea03d4dd0187905bb654f00339b7d98bdb17980d6d5cee12d49bc2fd8f3b3c01b0c0e" + BroadcastUncleParentHash = "170bbfd78c54c880261332460dd02efde3638eb0a93997ece3989bd160a127ad" + + BroadcastMessageFullTxs = "fffefdfc62726f616463617374000000c0080000" + + "4c097654d1b901b3b9d616237792557fe1b466b7159856c1662eff78b8df2f7d05000000162a20cfb9874d6c80edde990abecd30626c636b00" + + // sids offset + "7e08000000000000" + + // block size + "f90873" + + // block header + "f90252" + "a0a1c81d7611df43f6c74472dedb3bf7001acea9b6ef11c6558540b8ba711e74c2a0975ef2609c1b0afe080dee8bae8be74079b3e2b16dd76827ee571d655c7abbf294245dc8ce7111a12c138a66f0081d9a19701c39f3a0539a099800b596e0bf9944a4ab1bdadf5499dfcbc70a2ffb0e79113c56db3f84a00bbcff54c37fb20c51f2e1b5f892b786bce0fffc9fd1b7bd3a417db1a78efce4a0ec6208bc5cb0db44ce083a4254ff19c6b184738a483d76f707d715078766ec60b90100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000640102030405b864057d36c96be3c06bd0084bc5430400a525738c9343bd951b9a65d3d830ccf21d1d29f39a10a8e365ee1b9b5f4b1820a6f454703379355fa216143378d4b588099efb77514c2a905158c775d25115877e6f1aac2c8ff6321c5a35fab052dc13ca701bb2eda05279dcf55e54619e7284890b7c0e504d3a241850bfca28dc20a30d184046842b880000000000000001" + + "f4" + + // first tx + "f3" + + "01b1b001ee08010203944a25e9aecde5de30877803e0157a6fc6f485b665048f342b7416693b497233781f67a91e1bc01b0607" + + // uncle + "f905d7" + + "f902b6a0737e2b611159bdb9ae4e1eebf20d528579d78054d8037aa96e9c7d2debd980e7a09307a842cc82d10a26ef5e7a86d8cc33f0465c1e66be73232861ddbea119f9f7942c7a48310ac05b2e6dd0c84bbb7c9d8ddc484d31a0763dfe7f6696890e7b704e6bbddfb433c80c45f374584ea0e525dd0a66beb405a08e921fc7f093b1089027ff40e1c87aab2d12ca740da1d3260d52dac104d845e2a0b7f8b57a4f2b764eca834c519bc6451eaf1533e79ae2a3c297b9119df3267673b90100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000c8020406080ab8c80b4a04e94f01f245a414ee6649fb1bc7490395ea95ac79312301563ef7d899cfc32c91723e0cb55d0d161f1629841eba669b9bb3d4a117f39a1d46206bf1266173f4d6b54d233af463cd45878f565c8139cb22ddd76ae810c895573f655e11fe36c1862c418523c7ef742b97054dbe546be2138eae4235eeee828263e8f8a86173b69433462d9449d562196bf8cb309abed94158e5f504fefdca8dc3275bb32bb6b551623e11525bb3ed45282a7cadf6e29cd4af954cd899fc27d6033fdaa4ab67661fb339023459a0b3a03c31b6ad83ee78354cdd3bac91f1b7f246b21c215e9c34db8a28531d6450880000000000000002f9031ba05f68abeaccf7564fc0b6d650612bc8b175f0984c2902b9066a24328b899ab344a08fdcdecba03f607a826fe344e335259b5b67a8780bab2719f1c47e8ce9a5c944949ca68e9a14574500608b54324c8b9e678b6f9eeda07bd8bce1ca743fd26d958eda95136b6bd980a90b9ace35a95d03d36b6276e6f5a0fd5f89c99ce0609ad0f2acda46a8f286fb6b8c88151a7cc229424c64933883e8a06f0e1b686eb4cfe9b6fed6bf3e174dbe9e195f01006d584388c8b1c13815c015b901000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000012c0306090c0fb9012c315735a53ee94f839f714a9aa155a0cba757dd8fd31f60bddb216c852a543df7e5eb54c9544f2ad79ab1a87d8a0f0f156c0168c4bb68c23139f6c67ed132cf2bae2c477291473f67fbc06def08c0740174a3de016a869282367999ff1c6745e450208de3ca8bf39544b50b56e02d3617d3b8c3ee2be3f5b2ae1d63109d2663ccc4a860082543f4835b967b6afc866cc87d1c44f63d6625c0cb812f90fad436fa695d170c9f8082a5137bcaabca344928c0287696cdc0b7290331f7bb4974d58ff78351402ac8198bead88141261fbf25511c5497bf022d227bf18319d57129225e61c672716cb12df9ce09c1fb21d431373f9a026ba1efe250858154f097f2025b7e87b8e8b851a1d3a7f0e6f4237a38608e05df6082b18754cff4dbf7fc9b5dc80d22eaf7b002de36b15999a03637e6a05d7c59d77764f05996912b72f087ec465370a62a9c2384bd6820611c880000000000000003" + + // difficulty + "8a066a2b92db5ce24287cd" + + // block number + "83c9396" + + "0010000000100000001" + + BroadcastMessageFullTxsBlockHash = "d1929a5545cc896a4ab9ecde6de25b3ddf0e7dfcd8f4d65c3bf160bc6014ff36" + + BroadcastEmptyBlock = "fffefdfc62726f6164636173740000005d020000412c5a542a66a560e5808469db7d39a56aedb6df48693cc812c11992e9df3ded0500000000000000000000000000000000000000626c636b001f02000000000000f90214f9020aa01a484ad0f685f953b0336160bd6eb8bffeb5bbb02661c71a0ad004f1dd6b3056a01dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d493479452bc44d5378309ee2abf1539bf71de1b7d7be3b5a0c8f377d4106ed2852b6e866165c741408987b6a9d5890cd228c1bd10d20294cfa056e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421a056e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421b901000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000087173dc50043062c83c322c483e5542e808460e7485c8c6e616e6f706f6f6c2e6f7267a03a8c369c5873cf81262a3ded4ac03f01afb9f4420e1a31c7a9cc05d81a6ed180883523b0f62c0e6ff0c0c08083c322c40000000001" + BroadcastEmptyBlockHash = "412c5a542a66a560e5808469db7d39a56aedb6df48693cc812c11992e9df3ded" +) + +// some constants encoded in the bx blocks of the above broadcast messages +var ( + BroadcastDifficulty, _ = new(big.Int).SetString("066a2b92db5ce24287cd", 16) + BroadcastBlockNumber, _ = new(big.Int).SetString("c93960", 16) +) diff --git a/test/fixtures/eth.go b/test/fixtures/eth.go new file mode 100644 index 0000000..2553478 --- /dev/null +++ b/test/fixtures/eth.go @@ -0,0 +1,67 @@ +package fixtures + +// ContractCreationTxHash is the hash of ContractCreationTx +const ContractCreationTxHash = "7df870e552898df04761d6ea87ac848e3c60bfa35a9036b2b4d53ac64730a5b6" + +// ContractCreationTx is sample encoded transaction that creates a new smart contract +const ContractCreationTx = "f91610820be08509c7652400832625a08080b915bb60806040523480156200001157600080fd5b50604051620014db380380620014db833981810160405260408110156200003757600080fd5b81019080805160405193929190846401000000008211156200005857600080fd5b9083019060208201858111156200006e57600080fd5b82516401000000008111828201881017156200008957600080fd5b82525081516020918201929091019080838360005b83811015620000b85781810151838201526020016200009e565b50505050905090810190601f168015620000e65780820380516001836020036101000a031916815260200191505b50604052602001805160405193929190846401000000008211156200010a57600080fd5b9083019060208201858111156200012057600080fd5b82516401000000008111828201881017156200013b57600080fd5b82525081516020918201929091019080838360005b838110156200016a57818101518382015260200162000150565b50505050905090810190601f168015620001985780820380516001836020036101000a031916815260200191505b50604052505082518391508290620001b8906003906020850190620001fa565b508051620001ce906004906020840190620001fa565b505060058054601260ff1990911617610100600160a81b031916610100330217905550620002a6915050565b828054600181600116156101000203166002900490600052602060002090601f0160209004810192826200023257600085556200027d565b82601f106200024d57805160ff19168380011785556200027d565b828001600101855582156200027d579182015b828111156200027d57825182559160200191906001019062000260565b506200028b9291506200028f565b5090565b5b808211156200028b576000815560010162000290565b61122580620002b66000396000f3fe608060405234801561001057600080fd5b50600436106100f55760003560e01c806340c10f19116100975780639dc29fac116100665780639dc29fac14610352578063a457c2d71461038b578063a9059cbb146103c4578063dd62ed3e146103fd576100f5565b806340c10f19146102ad5780634755abbb146102e657806370a082311461031757806395d89b411461034a576100f5565b806318160ddd116100d357806318160ddd146101f957806323b872dd14610213578063313ce567146102565780633950935114610274576100f5565b806306fdde03146100fa578063070313fa14610177578063095ea7b3146101ac575b600080fd5b610102610438565b6040805160208082528351818301528351919283929083019185019080838360005b8381101561013c578181015183820152602001610124565b50505050905090810190601f1680156101695780820380516001836020036101000a031916815260200191505b509250505060405180910390f35b6101aa6004803603602081101561018d57600080fd5b503573ffffffffffffffffffffffffffffffffffffffff166104ec565b005b6101e5600480360360408110156101c257600080fd5b5073ffffffffffffffffffffffffffffffffffffffff81351690602001356105c3565b604080519115158252519081900360200190f35b6102016105e0565b60408051918252519081900360200190f35b6101e56004803603606081101561022957600080fd5b5073ffffffffffffffffffffffffffffffffffffffff8135811691602081013590911690604001356105e6565b61025e610687565b6040805160ff9092168252519081900360200190f35b6101e56004803603604081101561028a57600080fd5b5073ffffffffffffffffffffffffffffffffffffffff8135169060200135610690565b6101aa600480360360408110156102c357600080fd5b5073ffffffffffffffffffffffffffffffffffffffff81351690602001356106eb565b6102ee610784565b6040805173ffffffffffffffffffffffffffffffffffffffff9092168252519081900360200190f35b6102016004803603602081101561032d57600080fd5b503573ffffffffffffffffffffffffffffffffffffffff166107a5565b6101026107cd565b6101aa6004803603604081101561036857600080fd5b5073ffffffffffffffffffffffffffffffffffffffff813516906020013561084c565b6101e5600480360360408110156103a157600080fd5b5073ffffffffffffffffffffffffffffffffffffffff81351690602001356108e1565b6101e5600480360360408110156103da57600080fd5b5073ffffffffffffffffffffffffffffffffffffffff8135169060200135610956565b6102016004803603604081101561041357600080fd5b5073ffffffffffffffffffffffffffffffffffffffff8135811691602001351661096a565b60038054604080516020601f60027fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff6101006001881615020190951694909404938401819004810282018101909252828152606093909290918301828280156104e25780601f106104b7576101008083540402835291602001916104e2565b820191906000526020600020905b8154815290600101906020018083116104c557829003601f168201915b5050505050905090565b600554610100900473ffffffffffffffffffffffffffffffffffffffff16331461057757604080517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152600c60248201527f6d75737420626520706f6f6c0000000000000000000000000000000000000000604482015290519081900360640190fd5b6005805473ffffffffffffffffffffffffffffffffffffffff909216610100027fffffffffffffffffffffff0000000000000000000000000000000000000000ff909216919091179055565b60006105d76105d06109a2565b84846109a6565b50600192915050565b60025490565b60006105f3848484610aed565b61067d846105ff6109a2565b610678856040518060600160405280602881526020016111396028913973ffffffffffffffffffffffffffffffffffffffff8a1660009081526001602052604081209061064a6109a2565b73ffffffffffffffffffffffffffffffffffffffff1681526020810191909152604001600020549190610cbd565b6109a6565b5060019392505050565b60055460ff1690565b60006105d761069d6109a2565b8461067885600160006106ae6109a2565b73ffffffffffffffffffffffffffffffffffffffff908116825260208083019390935260409182016000908120918c168152925290205490610d6e565b600554610100900473ffffffffffffffffffffffffffffffffffffffff16331461077657604080517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152600c60248201527f6d75737420626520706f6f6c0000000000000000000000000000000000000000604482015290519081900360640190fd5b6107808282610de9565b5050565b600554610100900473ffffffffffffffffffffffffffffffffffffffff1681565b73ffffffffffffffffffffffffffffffffffffffff1660009081526020819052604090205490565b60048054604080516020601f60027fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff6101006001881615020190951694909404938401819004810282018101909252828152606093909290918301828280156104e25780601f106104b7576101008083540402835291602001916104e2565b600554610100900473ffffffffffffffffffffffffffffffffffffffff1633146108d757604080517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152600c60248201527f6d75737420626520706f6f6c0000000000000000000000000000000000000000604482015290519081900360640190fd5b6107808282610f1a565b60006105d76108ee6109a2565b84610678856040518060600160405280602581526020016111cb60259139600160006109186109a2565b73ffffffffffffffffffffffffffffffffffffffff908116825260208083019390935260409182016000908120918d16815292529020549190610cbd565b60006105d76109636109a2565b8484610aed565b73ffffffffffffffffffffffffffffffffffffffff918216600090815260016020908152604080832093909416825291909152205490565b3390565b73ffffffffffffffffffffffffffffffffffffffff8316610a12576040517f08c379a00000000000000000000000000000000000000000000000000000000081526004018080602001828103825260248152602001806111a76024913960400191505060405180910390fd5b73ffffffffffffffffffffffffffffffffffffffff8216610a7e576040517f08c379a00000000000000000000000000000000000000000000000000000000081526004018080602001828103825260228152602001806110f16022913960400191505060405180910390fd5b73ffffffffffffffffffffffffffffffffffffffff808416600081815260016020908152604080832094871680845294825291829020859055815185815291517f8c5be1e5ebec7d5bd14f71427d1e84f3dd0314c0f7b2291e5b200ac8c7c3b9259281900390910190a3505050565b73ffffffffffffffffffffffffffffffffffffffff8316610b59576040517f08c379a00000000000000000000000000000000000000000000000000000000081526004018080602001828103825260258152602001806111826025913960400191505060405180910390fd5b73ffffffffffffffffffffffffffffffffffffffff8216610bc5576040517f08c379a00000000000000000000000000000000000000000000000000000000081526004018080602001828103825260238152602001806110ac6023913960400191505060405180910390fd5b610bd0838383611064565b610c1a816040518060600160405280602681526020016111136026913973ffffffffffffffffffffffffffffffffffffffff86166000908152602081905260409020549190610cbd565b73ffffffffffffffffffffffffffffffffffffffff8085166000908152602081905260408082209390935590841681522054610c569082610d6e565b73ffffffffffffffffffffffffffffffffffffffff8084166000818152602081815260409182902094909455805185815290519193928716927fddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef92918290030190a3505050565b60008184841115610d66576040517f08c379a00000000000000000000000000000000000000000000000000000000081526004018080602001828103825283818151815260200191508051906020019080838360005b83811015610d2b578181015183820152602001610d13565b50505050905090810190601f168015610d585780820380516001836020036101000a031916815260200191505b509250505060405180910390fd5b505050900390565b600082820183811015610de257604080517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152601b60248201527f536166654d6174683a206164646974696f6e206f766572666c6f770000000000604482015290519081900360640190fd5b9392505050565b73ffffffffffffffffffffffffffffffffffffffff8216610e6b57604080517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152601f60248201527f45524332303a206d696e7420746f20746865207a65726f206164647265737300604482015290519081900360640190fd5b610e7760008383611064565b600254610e849082610d6e565b60025573ffffffffffffffffffffffffffffffffffffffff8216600090815260208190526040902054610eb79082610d6e565b73ffffffffffffffffffffffffffffffffffffffff83166000818152602081815260408083209490945583518581529351929391927fddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef9281900390910190a35050565b73ffffffffffffffffffffffffffffffffffffffff8216610f86576040517f08c379a00000000000000000000000000000000000000000000000000000000081526004018080602001828103825260218152602001806111616021913960400191505060405180910390fd5b610f9282600083611064565b610fdc816040518060600160405280602281526020016110cf6022913973ffffffffffffffffffffffffffffffffffffffff85166000908152602081905260409020549190610cbd565b73ffffffffffffffffffffffffffffffffffffffff831660009081526020819052604090205560025461100f9082611069565b60025560408051828152905160009173ffffffffffffffffffffffffffffffffffffffff8516917fddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef9181900360200190a35050565b505050565b6000610de283836040518060400160405280601e81526020017f536166654d6174683a207375627472616374696f6e206f766572666c6f770000815250610cbd56fe45524332303a207472616e7366657220746f20746865207a65726f206164647265737345524332303a206275726e20616d6f756e7420657863656564732062616c616e636545524332303a20617070726f766520746f20746865207a65726f206164647265737345524332303a207472616e7366657220616d6f756e7420657863656564732062616c616e636545524332303a207472616e7366657220616d6f756e74206578636565647320616c6c6f77616e636545524332303a206275726e2066726f6d20746865207a65726f206164647265737345524332303a207472616e736665722066726f6d20746865207a65726f206164647265737345524332303a20617070726f76652066726f6d20746865207a65726f206164647265737345524332303a2064656372656173656420616c6c6f77616e63652062656c6f77207a65726fa2646970667358221220987173ae06a00aaf418e92fd1f57b81c2c1341970dc2d243d753c1adfa0346ca64736f6c63430007040033000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000a0000000000000000000000000000000000000000000000000000000000000002453616666726f6e204c502065706f63682031332041412052617269204441492064736563000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000007534146462d4c500000000000000000000000000000000000000000000000000025a079a8b8a7af98e152e35b8f9b0f1ab83e02eefd70b664d403f7faac0b198bef4ba01c06f51696c4b7a1c51f6e580ac47e48bb04ae633e64d8c88ad8f61ee2e8f2ca" + +// LegacyTransactionHash is the hash of LegacyTransaction +const LegacyTransactionHash = "0x00278cf7120dbbbee72eb7bdaaa2eac8ec41ef931c30fd6d218bdad1b2b2324e" + +// LegacyFromAddress is the from address of LegacyTransaction +const LegacyFromAddress = "0x622961e7f76b5e573df44afdeb712749bbee398d" + +// LegacyGasPrice is the gas price of LegacyTransaction +const LegacyGasPrice = 5.3e+10 + +// LegacyChainID is the chain ID of LegacyTransaction +const LegacyChainID = 1 + +// LegacyTransaction is a sample encoded transaction of the legacy format +const LegacyTransaction = "f90155820126850c570bd20083028851947a250d5630b4cf539739df2c5dacb4c659f2488d8901158e460913d00000b8e47ff36ab50000000000000000000000000000000000000000000000f63ad7b170466de7d80000000000000000000000000000000000000000000000000000000000000080000000000000000000000000622961e7f76b5e573df44afdeb712749bbee398d000000000000000000000000000000000000000000000000000000005ee26a790000000000000000000000000000000000000000000000000000000000000002000000000000000000000000c02aaa39b223fe8d0a0e5c4f27ead9083c756cc20000000000000000000000006b175474e89094c44da98b954eedeac495271d0f25a0cf5e7717042a53b761dad24b9e6873f2da6bb381ab0bec1a1ba7e15bc924b0b2a05a6d627f0345f84ac6fbf708d30a3ddac8e12105d4430ab6768d81a7a8db7191" + +// AccessListTransactionHash is the hash of AccessListTransaction +const AccessListTransactionHash = "0x9310dc4f07748222d37f43c7296826cf4bf6693fa207968bd7500659ee2cc04d" + +// AccessListFromAddress is the from address of AccessListTransaction +const AccessListFromAddress = "0x0087c5900b9bbc051b5f6299f5bce92383273b28" + +// AccessListGasPrice is the gas price of AccessListTransaction +const AccessListGasPrice = 2.256e+11 + +// AccessListChainID is the chainID of AccessListTransaction +const AccessListChainID = 1 + +// AccessListLength is the length of the access list in AccessListTransaction +const AccessListLength = 3 + +// AccessListTransaction is a sample encoded transaction of type 1 with an access list +const AccessListTransaction = "b9022501f9022101829237853486ced000830285ee94653911da49db4cdb0b7c3e4d929cfb56024cd4e680b8a48201aa3f000000000000000000000000c02aaa39b223fe8d0a0e5c4f27ead9083c756cc200000000000000000000000000000000000000000000000083a297567e20f8000000000000000000000000000d8775f648430679a709e98d2b0cb6250d2887ef000000000000000000000000000000000000000000000358c5ee87d374000000fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff90111f859940d8775f648430679a709e98d2b0cb6250d2887eff842a01d76467e21923adb4ee07bcae017030c6208bbccde21ff0a61518956ad9b152aa0ec5bfdd140da829800c64d740e802727fca06fadec8b5d82a7b406c811851b55f85994653911da49db4cdb0b7c3e4d929cfb56024cd4e6f842a02a9a57a342e03a2b55a8bef24e9c777df22a7442475b1641875a66dba65855f0a0d0bcf4df132c65dad73803c5e5e1c826f151a3342680034a8a4c8e5f8eb0c13ff85994c02aaa39b223fe8d0a0e5c4f27ead9083c756cc2f842a01fc85b67921559ce4fef22a331ff00c886678cf8b163d395e45fe0543f8750bda047a365b3ae9dbfa1c60a2cd30347765e914a89b7dac828db3ac3bd3e775b1a9980a0a340fc367050387a1b295514210a48de3836ee7923d9739bf0104e6d79c37997a06e63c7801da3c72f1a53f9b809ae04a45637da673c2a9c47065a1b1cdeafef7d" + +// AccessListTransactionForRPCInterface is the raw bytes of AccessListTransaction used for RPC submission, compatible with `UnmarshalBinary` +const AccessListTransactionForRPCInterface = "01f9022101829237853486ced000830285ee94653911da49db4cdb0b7c3e4d929cfb56024cd4e680b8a48201aa3f000000000000000000000000c02aaa39b223fe8d0a0e5c4f27ead9083c756cc200000000000000000000000000000000000000000000000083a297567e20f8000000000000000000000000000d8775f648430679a709e98d2b0cb6250d2887ef000000000000000000000000000000000000000000000358c5ee87d374000000fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff90111f859940d8775f648430679a709e98d2b0cb6250d2887eff842a01d76467e21923adb4ee07bcae017030c6208bbccde21ff0a61518956ad9b152aa0ec5bfdd140da829800c64d740e802727fca06fadec8b5d82a7b406c811851b55f85994653911da49db4cdb0b7c3e4d929cfb56024cd4e6f842a02a9a57a342e03a2b55a8bef24e9c777df22a7442475b1641875a66dba65855f0a0d0bcf4df132c65dad73803c5e5e1c826f151a3342680034a8a4c8e5f8eb0c13ff85994c02aaa39b223fe8d0a0e5c4f27ead9083c756cc2f842a01fc85b67921559ce4fef22a331ff00c886678cf8b163d395e45fe0543f8750bda047a365b3ae9dbfa1c60a2cd30347765e914a89b7dac828db3ac3bd3e775b1a9980a0a340fc367050387a1b295514210a48de3836ee7923d9739bf0104e6d79c37997a06e63c7801da3c72f1a53f9b809ae04a45637da673c2a9c47065a1b1cdeafef7d" + +// DynamicFeeTransactionHash is the hash of DynamicFeeTransaction +const DynamicFeeTransactionHash = "0x5421534dccf306cd34755e51d65e32fb0e179f76f467dbf3e94c8e809708788f" + +// DynamicFeeFromAddress is the from address of DynamicFeeTransaction +const DynamicFeeFromAddress = "0x4fbbf0b8a9b8753ffab30fb60af5860104071b8e" + +// DynamicFeeChainID is the chainID of DynamicFeeTransaction +const DynamicFeeChainID = 0 + +// DynamicFeeAccessListLength is the length of the access list in DynamicFeeTransaction +const DynamicFeeAccessListLength = 0 + +// DynamicFeeFeePerGas is the max fee per gas of DynamicFeeTransaction +const DynamicFeeFeePerGas = 31 + +// DynamicFeeTipPerGas is the max tip per gas of DynamicFeeTransaction +const DynamicFeeTipPerGas = 15 + +// DynamicFeeTransaction is a sample encoded transaction of type 2 with the EIP 1559 dynamic fee pricing +const DynamicFeeTransaction = "b86302f86080800f1f649400000000000000000000000000000000000000006480c001a0c9519f4f2b30335884581971573fadf60c6204f59a911df35ee8a540456b2660a032f1e8e2c5dd761f9e4f88f41c8310aeaba26a8bfcdacfedfa12ec3862d37521" + +// DynamicFeeTransactionForRPCInterface is the raw bytes of DynamicFeeTransaction used for RPC submission, compatible with `UnmarshalBinary` +const DynamicFeeTransactionForRPCInterface = "02f86080800f1f649400000000000000000000000000000000000000006480c001a0c9519f4f2b30335884581971573fadf60c6204f59a911df35ee8a540456b2660a032f1e8e2c5dd761f9e4f88f41c8310aeaba26a8bfcdacfedfa12ec3862d37521" diff --git a/test/fixtures/mev.go b/test/fixtures/mev.go new file mode 100644 index 0000000..d018544 --- /dev/null +++ b/test/fixtures/mev.go @@ -0,0 +1,47 @@ +package fixtures + +// MEVBundlePayload valid payload for mev bundle +var MEVBundlePayload = +// Header +"fffefdfc6d657662756e646c650000005b000000fbed5e7084704a92ca28bb682843100a374707c5b25f83a15a674e3b0aa39e130000000000000000000000000000000000000000" + + // mev miner method length + "0E00" + + // mev miner method + "6574685f73656e6442756e646c65" + + // number of mev miners + "02" + + // mev miner name length + "0b00" + + // mev miner name + "74657374206d696e657231" + + // mev miner name 2 length + "0b00" + + // mev miner name 2 + "74657374206d696e657232" + + // Params + "7465737420706172616d73" + + // Control digit + "01" + +// MEVSearcherPayload valid payload for MevSearcher message +var MEVSearcherPayload = +// Header +"fffefdfc6d65767365617263686572005800000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000" + + // mev miner method length + "1200" + + // mev miner method + "6574685f73656e644d65676162756e646c65" + + // Mev builders + "01" + + // Miner name length + "0900" + + // Miner name + "6e616d652074657374" + + // Miner auth length + "0900" + + // Miner auth + "617574682074657374" + + // Params + "636f6e74656e742074657374" + + // Control digit + "01" diff --git a/test/fixtures/txs.go b/test/fixtures/txs.go new file mode 100644 index 0000000..33b09af --- /dev/null +++ b/test/fixtures/txs.go @@ -0,0 +1,14 @@ +package fixtures + +// reference bxmessage.Txs from Python code for testing +const ( + TxsMessage = "fffefdfc7478730000000000000000001d0100000200000001000000ce98ca70fdc775a9ad5fa2cf81a4393d32abaec487f7e5be730c002f79b47d6b6400000020d30151479dc3e912a7280960f0937d66245186ec0d5fea33882b317bb8f4d320cb7ada85caff54406efae80920a05f9b521286b8db36fb1ea69d33000b4d585190fc9e7cdc5697556a2ef688071712485eb7ab6a8fda84dbf56fd809f00b85e6158cb702000000567445299eddec68ed45d8c57169fa88d2ef901bc4468680946be341e2572ca164000000edd9d8fbe01e295743ac53a52473745c3fe2ec6c972e737c4e814c86709faedf6da4683c96ad0e1c7572a4257ee8dcc7d711631f422cce0ecd70a5be70fb682234f70ef835494f6e393f9184c930c4cd8ddddc615db05146ad2c8d64e0ba8516adb9421d01" + + TxsHash1 = "ce98ca70fdc775a9ad5fa2cf81a4393d32abaec487f7e5be730c002f79b47d6b" + TxsContent1 = "20d30151479dc3e912a7280960f0937d66245186ec0d5fea33882b317bb8f4d320cb7ada85caff54406efae80920a05f9b521286b8db36fb1ea69d33000b4d585190fc9e7cdc5697556a2ef688071712485eb7ab6a8fda84dbf56fd809f00b85e6158cb7" + TxsShortID1 = 1 + + TxsHash2 = "567445299eddec68ed45d8c57169fa88d2ef901bc4468680946be341e2572ca1" + TxsContent2 = "edd9d8fbe01e295743ac53a52473745c3fe2ec6c972e737c4e814c86709faedf6da4683c96ad0e1c7572a4257ee8dcc7d711631f422cce0ecd70a5be70fb682234f70ef835494f6e393f9184c930c4cd8ddddc615db05146ad2c8d64e0ba8516adb9421d" + TxsShortID2 = 2 +) diff --git a/test/utils.go b/test/utils.go new file mode 100644 index 0000000..58ce82c --- /dev/null +++ b/test/utils.go @@ -0,0 +1,80 @@ +package test + +import ( + "encoding/json" + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/common/hexutil" + log "github.com/sirupsen/logrus" + "math/rand" + "os" + "regexp" + "strings" + "sync" +) + +// MarshallJSONToMap is a test utility for serializing a struct to JSON while easily being able to assert on its contents +func MarshallJSONToMap(v interface{}) (map[string]interface{}, error) { + var result map[string]interface{} + + b, err := json.Marshal(v) + if err != nil { + return result, err + } + + err = json.Unmarshal(b, &result) + if err != nil { + return result, err + } + + return result, nil +} + +// Contains is a one-line utility for check key existence in maps for tests +func Contains(m map[string]interface{}, k string) bool { + _, ok := m[k] + return ok +} + +// NewEthAddress is a one-line utility for deserializing strings into Ethereum addresses +func NewEthAddress(s string) common.Address { + var address common.Address + addressBytes, _ := hexutil.Decode(s) + copy(address[:], addressBytes) + return address +} + +var currentTestPort = 9700 +var testPortLock = &sync.Mutex{} + +// NextTestPort allocates ports 1 by 1 to allow tests to run concurrently +func NextTestPort() int { + testPortLock.Lock() + defer testPortLock.Unlock() + + current := currentTestPort + currentTestPort++ + return current +} + +// GenerateBytes return a random generated byte slice of the specified length +func GenerateBytes(count int) []byte { + b := make([]byte, count) + _, _ = rand.Read(b) + return b +} + +var matchFirstCap = regexp.MustCompile("(.)([A-Z][a-z]+)") +var matchAllCap = regexp.MustCompile("([a-z0-9])([A-Z])") + +// ToSnakeCase takes a camel case string and returns it in snake case +func ToSnakeCase(str string) string { + snake := matchFirstCap.ReplaceAllString(str, "${1}_${2}") + snake = matchAllCap.ReplaceAllString(snake, "${1}_${2}") + return strings.ToLower(snake) +} + +// ConfigureLogger sets the log level for tests. Mainly useful while debugging tests. +func ConfigureLogger(level log.Level) { + log.SetLevel(level) + log.SetOutput(os.Stdout) +} diff --git a/types/blockchaintransaction.go b/types/blockchaintransaction.go new file mode 100644 index 0000000..7bc406a --- /dev/null +++ b/types/blockchaintransaction.go @@ -0,0 +1,7 @@ +package types + +// BlockchainTransaction represents a generic blockchain transaction that allows filtering its fields +type BlockchainTransaction interface { + WithFields(fields []string) BlockchainTransaction + Filters(filters []string) map[string]interface{} +} diff --git a/types/blocknotification.go b/types/blocknotification.go new file mode 100644 index 0000000..6d078f7 --- /dev/null +++ b/types/blocknotification.go @@ -0,0 +1,104 @@ +package types + +import ( + ethcommon "github.com/ethereum/go-ethereum/common" + ethtypes "github.com/ethereum/go-ethereum/core/types" +) + +// BlockNotification - represents a single block +type BlockNotification struct { + BlockHash ethcommon.Hash `json:"hash,omitempty"` + Header *Header `json:"header,omitempty"` + Transactions []EthTransaction `json:"transactions,omitempty"` + Uncles []Header `json:"uncles,omitempty"` + notificationType FeedType +} + +// Header - represents Ethereum block header +type Header struct { + ParentHash ethcommon.Hash `json:"parentHash"` + Sha3Uncles ethcommon.Hash `json:"sha3Uncles"` + Miner EthAddress `json:"miner"` + StateRoot ethcommon.Hash `json:"stateRoot"` + TransactionsRoot ethcommon.Hash `json:"transactionsRoot"` + ReceiptsRoot ethcommon.Hash `json:"receiptsRoot"` + LogsBloom EthBigInt `json:"logsBloom"` + Difficulty EthBigInt `json:"difficulty"` + Number EthBigInt `json:"number"` + GasLimit EthUInt64 `json:"gasLimit"` + GasUsed EthUInt64 `json:"gasUsed"` + Timestamp EthUInt64 `json:"timestamp"` + ExtraData EthBytes `json:"extraData"` + MixHash ethcommon.Hash `json:"mixHash"` + Nonce EthUInt64 `json:"nonce"` + BaseFee *int `json:"baseFeePerGas,omitempty"` +} + +// ConvertEthHeaderToBlockNotificationHeader converts Ethereum header to bloxroute Ethereum Header +func ConvertEthHeaderToBlockNotificationHeader(ethHeader *ethtypes.Header) *Header { + newHeader := Header{ + ParentHash: ethHeader.ParentHash, + Sha3Uncles: ethHeader.UncleHash, + Miner: EthAddress{Address: ðHeader.Coinbase}, + StateRoot: ethHeader.Root, + TransactionsRoot: ethHeader.TxHash, + ReceiptsRoot: ethHeader.ReceiptHash, + LogsBloom: EthBigInt{Int: ethHeader.Bloom.Big()}, + Difficulty: EthBigInt{Int: ethHeader.Difficulty}, + Number: EthBigInt{Int: ethHeader.Number}, + GasLimit: EthUInt64{UInt64: ethHeader.GasLimit}, + GasUsed: EthUInt64{UInt64: ethHeader.GasUsed}, + Timestamp: EthUInt64{UInt64: ethHeader.Time}, + ExtraData: EthBytes{B: ethHeader.Extra}, + MixHash: ethHeader.MixDigest, + Nonce: EthUInt64{UInt64: ethHeader.Nonce.Uint64()}, + } + if ethHeader.BaseFee != nil { + baseFee := int(ethHeader.BaseFee.Int64()) + newHeader.BaseFee = &baseFee + } + return &newHeader +} + +// WithFields - +func (ethBlockNotification *BlockNotification) WithFields(fields []string) Notification { + block := BlockNotification{} + for _, param := range fields { + switch param { + case "hash": + block.BlockHash = ethBlockNotification.BlockHash + case "header": + block.Header = ethBlockNotification.Header + case "transactions": + block.Transactions = ethBlockNotification.Transactions + case "uncles": + block.Uncles = ethBlockNotification.Uncles + } + } + return &block +} + +// Filters - +func (ethBlockNotification *BlockNotification) Filters(filters []string) map[string]interface{} { + return nil +} + +// LocalRegion - +func (ethBlockNotification *BlockNotification) LocalRegion() bool { + return false +} + +// GetHash - +func (ethBlockNotification *BlockNotification) GetHash() string { + return ethBlockNotification.BlockHash.Hex() +} + +// SetNotificationType - set feed name +func (ethBlockNotification *BlockNotification) SetNotificationType(feedName FeedType) { + ethBlockNotification.notificationType = feedName +} + +// NotificationType - feed name +func (ethBlockNotification *BlockNotification) NotificationType() FeedType { + return ethBlockNotification.notificationType +} diff --git a/types/blocknotification_test.go b/types/blocknotification_test.go new file mode 100644 index 0000000..bd966f3 --- /dev/null +++ b/types/blocknotification_test.go @@ -0,0 +1,19 @@ +package types + +import ( + "testing" +) + +const ( + broadcastMsg = "fffefdfc62726f6164636173740000004c8100004c097654d1b901b3b9d616237792557fe1b466b7159856c1662eff78b8df2f7d05000000162a20cfb9874d6c80edde990abecd30626c636b001a7e000000000000f97e0ff9021aa0fe10db4928f37e357d69067cfb19be6b28bb5ed598975ba42e8502e24dde1ae7a01dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d493479400192fb10df37c9fb26829eb2cc623cd1bf599e8a002f8afec6e5e16c48fe183eb6278970563cfe793ed620a71f357343a89f4d1f5a0da30c743d828504443f63de5ffda9e6d6ed6a174891711eb3d43e24e4b573e09a06ac0e1c804ab3bea73c0cf85ae41301f3c633c6ac83988da10d1eff0733e0829b901002a6152ef3a82c04818c80621e282c802184832a40ca810286133e8ca00e597c11817f1c3a4749010f9045b2b8b0035c00b9f87062b0f6a8fb5c19b206028ac783a404006081ac02b3aae829b50fc5723649f4186237344882a2287e2984d759899160348aa36856023201946000219e902811414057825b0a2051d91480d1b004c20c272170c2f01c1dc970885c20117400886c55d34383801751048285e2fb62bf1824511832988e96142818868b88b03000419d2818804a321ba0a1889d99994b0410a0a8b39c20e40002a025e0d1009d05680d5b621fcf860031a487c64228431645d29b98205cc409601c10ca4088dba9234f9518852015ae804d27b9421871a73e7e1f8600583bf73f883e4a88983e4a44a8460b5d90e99457468657265756d50504c4e532f326d696e6572735f455536a010cdf4c75a623f1657602e1c52b0d543caca128b39b11e5865af564711406c208806614b8025bb3060f97bdff87201b86ff86d8326d535843b9aca0082d6d894e0c79764cba407a2fcceb0084d4d3a1c41ed8fff87b23781b024b0008025a0036f2fa7fbf428fb29122dcf59f607ac9f13e2ea58be0510287e4b736b34f068a0334d2b491279469c072942aab7d7d5f58943277e628a5e5435e6221dabbe7dd8f87201b86ff86d8326d536843b9aca0082d6d8946df807720137d43183d94db8f06f40cdb05df5bd87b2d6229ca83c008025a0a1bfe19a0f3d0ca4454c9f6dd297a907af1322dc07aee4c623f023f6e1aa407da064ba5721881e72406e8f0dcbda53622c3f339a09d42cb7f2f56d1a30af26c737f87201b86ff86d8326d537843b9aca0082d6d8948dfe03f8f05b6154ab980583c951d2736a2af8da87c68feb07d202008025a02e2de364288dc1cceb0819670a58367636b5f19a02cab4adc51665ea2e58418da06d240648c639ff75bf7750155ed1b2febc3e059f83a4d1d85239073c1b5579a4f87201b86ff86d8326d538843b9aca0082d6d894aeb7c48848d81ffe3c217cec147a5d06f3a386c887b25d7f0627d4008026a0e87477d65806b6aacf26a2605d60e5446bd2e6000085d1e8626360914768420fa075915220ba8a5876a6bb541a77cf2eab77bfd8c8c5e0d5c1c0f726bd590e2af4f87201b86ff86d8326d539843b9aca0082d6d894aa54e7c81f8acef96895ab22fc20ea336f37a03987b334f45ee5f2008025a0f296859789f6395edc507ac4ac32b918cae2af46ff7cd709052320771ddb3e74a004d9b8884fae013d83187cc4302811f14f595216f4e7ee911d5417bbdd0feb6ff87301b870f86e8326d53a843b9aca0082d6d8941305a09ff60606e4357ad21984816234314dcbd38810d5ea17a9cabc008026a0798331f4777a9baf4bf13ebadcfc59ce95a54b6f5593ee48a9720c5e08c198a1a06de5482ec5648fd33890ef5893d4b12db38355c3d3bd398d0b837af642fc55fdf87201b86ff86d8326d53b843b9aca0082d6d8943adb56486178254360091b44d9bb47d6befaea7087b1c1990ec90c008026a09f13983df763bf64d7354fa1d5f81c27404d96e179bef879b77feee038a34fc8a0676dd109f1ca21e6143cc2eeaddf8a5b27e890e73e302ce5c5f43ca11456741cf87201b86ff86d8326d53c843b9aca0082d6d894244c12d0c1148f59d3f1684e562d3d5f9d1df8c687b57fa521eadc008025a0e17e4d9968c6fd4f6607a20c0613508a6091965197c460a575014056a1e917ada046780f0b0b52307b0ba986a4ebeb78724bc38992a0f0ba73ed3e76ac17e7ef33f87201b86ff86d8326d53d843b9aca0082d6d8949aae3afa901db7bc12e8c576f51f2c623cd8e80287b4c8917cf286008026a011a5698363ad04012e554286cbb1c4d54b4bce1cc0a4fd1c7af34fe7d6b1e300a056701e73d22a0abdeb549ab1ba8f0ead1d982e203ed28019ea1db43e5aa767cbf87201b86ff86d8326d53e843b9aca0082d6d894bd744b8d450c7b88a867ffcf63f482e06d8bcb7a87b4f5c00c7d92008025a0ba25bca7b4d1209b2939cd0e4a2e52dc96269fefad15c8279ca95471d6e21254a02deb3e43d8cd2e0cbdd158d63e67c0f9c664b6e99b1fc935177e4b0b27eef12ff87201b86ff86d8326d53f843b9aca0082d6d894fa19ccf9d4dbc5e430d384490e0bae5594aa2f6487d3034932756a008025a02bb65bd8d03fe2285145254b46f879542abf3c4c63b7edcdc7454489971876c3a0653e7ec14d2caed88a73d695fd024d1f3650fd3ff17e13a518ee7137f2eed6faf87201b86ff86d8326d540843b9aca0082d6d894848e907ef63880a3f1cc84fa36e1a2ab72c15a0a87bae2e544edb8008026a0ce6f36f79f3baf93eb43d49f230cf4411209becd741453b33a2742bde4ddacc9a032205a009c97d91bd969c7e42aa7e6d95d154c4ecad00e259692e36df708d7caf87201b86ff86d8326d541843b9aca0082d6d894b476a66650ee3dab999f14bf801a0ed0a3f6739887b62c9beed79c008025a05af4780bceaef118ad42cb693bd206d1169bc0e873a390e56d65f0a73e788524a02ae24b0a0dcba1f5f3c1464e771a6b24284d113bb35c2c81322a3f41549dfd4ef87201b86ff86d8326d542843b9aca0082d6d894ea9593c092091c91b7d633976f4be229008cfa3487b2d3d0a93556008026a02391fc3e8eefdadbdd09a5bb765a839caf6bf7efbd98a26a1efb0770e9191610a06983584bc9a64c4daf3495d06cac8e6910f0ccb64b2aa3ce5e82a04b1fe179fef87201b86ff86d8326d543843b9aca0082d6d8948f55c0e2434749ae9ff5d59519227c99a2a4663387b1daa801f54c008025a060e391affd800595602bbca6fb327705c8aae63e00cacf7a9a4df0d55374b961a06043f42edaa51b23c4ac6cc8cfedd0577b8c230c1ca469d1f67f04970a30efa4f87201b86ff86d8326d544843b9aca0082d6d89491647b53b32551fbd0e5846a0de5534a400620f387bb8dc1c3ea28008025a048e8fde2d6c0c514cbe9d8c5d656d61f2f214d73b701dcde1f7d0d980307ee87a00ad6e649a3a72cff5afd1ac23eb6be19f16cb5c745ec3df458fc0a70537a9be1f87201b86ff86d8326d545843b9aca0082d6d894895180f71438ffcf7b194b5063776b9c76fdd4a687b2c29b815446008026a061d51eb92aaa2301ab09c15865831f288faee16520d3f7034abdb1ec50572d57a068801c66cf550b295cdd59ed05f204a982325507b3cee05d66aab218fe59d15bf87201b86ff86d8326d546843b9aca0082d6d8944782dbbb2a70f0a6ab9f59aa68c9fd24033ac61587c0564542afde008026a0e96f2c4090f0b8f4b99ef7b4e2534332f7db4dd849e4d8956ed3a90896b6ea9da00dd9142b635d6e88ca22746f1d7ea0575ae7f2e5d2c72b5fcfdc1ab7524ead00f87201b86ff86d8326d547843b9aca0082d6d8946b86ad848a1177decb2a02b77f7fede01892cae887ddd841ce611e008025a042386c2e3ef68981673e3c23b8161cad95819cfd4c7a4a73c08d232d47ce76eaa02accf735a16482665cd64862a51f8909d19d5210e05f5f7402f8fc0a87617bf6f87201b86ff86d8326d548843b9aca0082d6d894487f78dd13b56907930f8c12fe24c20ceb86b9e487b22284a7c17e008026a0ddffa1d5940fed2f0280d6c5339334cfe8d4f9c442a57a017af12e4ad6ed8f5fa03099c537f4269d648871484687303e6b202271c210397b7c4bfc0653faf863fcf87201b86ff86d8326d549843b9aca0082d6d8944af9a4a950b3b0ee3d3086759a6ac2615661df7a87b32faaed1920008025a0237ad648dedba7190209124c01860491f3300ebcdea35c497043ea2fd795f3caa01183a76cf0fe8a50019d068f156c74ae2db3ebcc9606cab2699c7951553fbe06f87201b86ff86d8326d54a843b9aca0082d6d89418e79d340046fade9c04016cef88ebac6275adba87f435106e14b6008025a02d8b636a2c0371d3fa1e01a27721b838b337fa7e7e4cd8a507a46e420142b300a02a7cbcd1f125ca879bafbc8f0776956f93dbf910c4d0f836a564b4e581e22106f87201b86ff86d8326d54b843b9aca0082d6d894d7196a575a89b9e4e3508ee001aaeee4226d6df087b283e309d0f2008026a021ca90a6c271115ee0ecb499bcb432338058f884265575fa6fc67df58c5389bda0368bae6b5516bae82d6f18958cf39d8c600a8130fc208fab8a875ed7534c4f14f87201b86ff86d8326d54c843b9aca0082d6d8949a06d0ac99641bb3b083afecca1d0187872d75d787b4bb43a4341e008025a01431bc969b2a82482b0dd2d56312ba56b8765665f05c65f02d00e77c4b5c965ca00cd77b1b43b4ae7d2f14cbb73c5899cdb3dd30a225912484915e8bb63a133fe1f87201b86ff86d8326d54d843b9aca0082d6d8947207329756b82958f63edb16e0ec98b9a685d3ea87b2223e572732008026a034fe9504c2fec3eeee4ca4f57a07ae6b01ea24697dbea81e393788d228e52006a00a4e29ec91e4ebabc0b69ab7fae2f06796f62962fd8a0e1fab9062cdd8cc12a2f87301b870f86e8326d54e843b9aca0082d6d894e0c46a44008c1f275f4e6e53b2c626c4f6e414d6880135e3e9c6747a008025a09cfa0bfe219287de63ca02d1ee7589419226ca4c6697cb20a03d14454c473e4fa0611acfd02d5543190067e5675c8fdaaf97747858550245da4473af559575804ff87301b870f86e8326d54f843b9aca0082d6d89482355cbf8d2990d33734def63241a2fc55153580880109c48c2398b6008026a03552e4cdd3a67eeb5f867e5fbff5333803203e84dfea1b516fa08e7c05d246a9a054a64c3509cbf0b65322fa3ee53bb495a0a1bff065f3d5e0992ef7a745513a12f87201b86ff86d8326d550843b9aca0082d6d894aa3e3ba38e4ff5cd1f36410c6060ec7dd6469f3187c9684f45b470008025a0413053df3f499396424efccc32f8eedd2164fba8b752fcfef4f5fde897a836cea01b8b64f684b5d80dc74ac1de6b915bd4b64aef2e2d1820b14a598df4eddca0ecf87201b86ff86d8326d551843b9aca0082d6d8944923e3ef15820ff17267e20013b7c133037b765487b4836f197aba008025a09ec9ce6d3a842fa672bb55e460c3fe055659d48eb78921d816fea0b1bffc8f5ea02084f9493508a593437f39e59237024fa6cabbcb2994f159b36751e694bde1d3f87201b86ff86d8326d552843b9aca0082d6d894b21a4f3ebb8d0571a4882eb462159629c13649c687b499397d29e0008025a0f5df875aa45a1f32144f139c7aa7e2895eae53aac06487ed4f460b50c274375ca065c7fbfa59a2fcc4894adcf91769a42d1dd76cd5fb5c964e9df617c982a2bd2cf87201b86ff86d8326d553843b9aca0082d6d894ab084821167eadef9c60d9ae2883de13cf82a51c87dc774d1f7088008025a06f67777140a4aba2907b4fb8d2330ad87634a124b97d5309297eb90a6e9967b9a02e2a19526c27cd8e77cda145e2ac3d33a6038f249236ebefd96e611631d03354f87201b86ff86d8326d554843b9aca0082d6d89495a5c28e26d3c45bd15ae28538b7ff67aec2833e87b1e01a6e2cfe008026a0d7959edfa8b7ad6a2a49c8ccb8a92bd59f87d42e8fef63108f566761677e1d17a0794af0071d3de2c03d306eddb0554037517345146ca9a66b8ef39fd13d0378dcf87201b86ff86d8326d555843b9aca0082d6d89411e47239efc6d844d0eee03280a177be6240a0e387b3e83ac87400008026a0885471b7af9b397b025f4e68dab50e43b159780fe25dc76dc0dba4dd56f103c5a03a01eb8e0bc44bb5a27059cb37df2230c517d9af66580edadd6050d32c2ec225f87201b86ff86d8326d556843b9aca0082d6d8943e3a1a4322bb1a1b2edaf2f83e0970e0f51f829787b77b6f23303c008025a09748dd75bd3af5e53c170f9b9309336123bc025e33fbb64113d1ce6d5cf04445a039850df5ef4400ae36bf7e02923b6a4389e5a542bc5ba15572248e6d2d277f0ef87201b86ff86d8326d557843b9aca0082d6d8945a1e2709277d767cfb280aa3cea5cd8522d3355087b7df05dc2fca008026a0f111aceca83d1771c4e4677d36ab899aa47ec5a8e4af6394343f1ec8220feb5ea024178fa297a4726dfae71a810d55bce427ac48560c2b45f690bbd0fe03f96a54f87201b86ff86d8326d558843b9aca0082d6d894b6c3419950a8c36dde14c4ba12756a865837361387b3a7f2c75ea4008026a057498a8a2e96e8bc03fbf0929d20a96af739c4e946a22359f8d496514ffab399a01f2796c3064e408d256f19f887fc4f57a06f302cfe37cf13e9f7880f9f7bfa35f87201b86ff86d8326d559843b9aca0082d6d8941ff3deaaad3178abe07fb2748c55b30d6ce63eb687b8428245dc2e008026a0b759c501a84898da3721ec8dabef4aacb70064aeae2494f7c9c1505c2a305ef5a0667ccdaa164ea18bca586294ec260d0a499d4a4bd46d99302791924f3e6cecdef87201b86ff86d8326d55a843b9aca0082d6d894d64e290436ee3d67769696a0c0e32c86caed4b2587baf1ae65a382008026a0d0c4da8d3650850819fdb7acfe9dc6f1d56aa1b7baf8122356ec64a501324a23a0377708dad2025081eb08cb1575a11acbda1b2707ee0f6395a4a51bd46df5b415f87201b86ff86d8326d55b843b9aca0082d6d894dc62ba1ab9fef7f65274374857ff801e030210cf87b25cf695d570008026a051f182707daa5a4edc63798bc28d3d7a59e3ffd66eb1d9630db402ffac73acaaa056fbc89c4fd7553a612e291b35a697a9128681176bf59f5624da3a166ff270e9f87201b86ff86d8326d55c843b9aca0082d6d8940dbf51e8f0cdfbdcdc60e929088900d06a0b18ef87b528ede50fb2008026a00c87a0636ca6b103fb6d3e2ab8ee05c4daf9a57cbb368fe0df98891ee93c5106a030c2623dda057c00c301d7d614c64130136ab2cd7f5950fb9be2858e20782dbef87201b86ff86d8326d55d843b9aca0082d6d894ab93d919aa5622d6136d0cbc9ff4c69a166ef16487b7156bb0350a008026a02606ac01c5a07077a7186351ac9142bb6176e7b629e611d47a6b90937a8c3c54a0266b5097e9344f40673612364120031f4f43e8984f64d0eb71cc47e518471375f87201b86ff86d8326d55e843b9aca0082d6d894cc271f7a99b1855cf6c9d30904032a9057f9b67887b2abe2b27d20008026a03dfd2e570e39005f6e22606efa9cebcae7b98a6aec580e7512513328f3ca4648a07d970d205b987e106e1d8c587ff202009298ba7ce6ea7f7de1cb83612613da34f87201b86ff86d8326d55f843b9aca0082d6d894a853cb01d62a8c78e4e62be93c957fd604977e3187b6905735c50c008026a0dfc09d7582d1ff26ffb57af8ffe6f2c0c62de28ca88f4f86bf1a7f3c11646d2aa010be608f1101f759fb1d044ab06bb9d5f3a3af88f6796d59dbad3ab12519232af87201b86ff86d8326d560843b9aca0082d6d894eeb3c66c3c26dd786c9d8b26e99c90f97dae8bc487b3b70ecb5556008026a0c2a26939b17733e35456c63be616e17cccb1f740354fee4cd4a6659983a0c0bea047cacc443d55218fdf4def8c5a6a58347bd904c09772059a6a0a85897938b44bf87201b86ff86d8326d561843b9aca0082d6d89497dea707765133af914155fd0423d1a4ef2e90c487dea8c2b26746008026a0ae6050e3d59d59061cf6a7bf22fe460c043d55b83097ce09c0a705dbae757781a0657ac64dfd48b58f86366f9e38672edd646924b6c5cbdf94de2bf362c5218efcf87201b86ff86d8326d562843b9aca0082d6d894733d7d699987feb32154b1f843d5b8b962475dd387b1cfadf9801a008026a03fa58001cb293353308ef357886773b28fbd6f7f19f516b5c4b97c859eddc624a066455d5e748a40a52cbb96a545cda0d0d29861bbe76bebf7df44243b38894cfbf87201b86ff86d8326d563843b9aca0082d6d894a2fb74853a5b75b0f3d7064a03d74ea30a8234d287ba0765ebf560008026a0aaffeab466a877a47c4815933688f5bf72578edb310bf585d081aac42c7da015a05e865bd1fb45d0bbb990e592b97dc7a75896a908f033f7899b95949d4b3fa9cbf87301b870f86e8326d564843b9aca0082d6d894cf40a874394c2d2b38ecd5903836ee23b1fc9666880925f15e1d461e008025a0d13e4b3e05849fe77f7db3b01b1579efdfe9844a296ba646064873b44cfd51d0a03b2f5e00514707be888964dc5417f3a95ae780d62e2b83613d93715dff093947f87201b86ff86d8326d565843b9aca0082d6d894742c2ac819885e0dbe27570b83f762a9071b655987b1e19c3b1278008026a085e09726e1da9a6636b372a1dc74e69cef055b8ad1e86372fc7f268b8e928f34a06b6d3d527b13f983c8c19e5f8458622dcb3b6579917c2f9326f66e51cd4d36ebf87201b86ff86d8326d566843b9aca0082d6d894eb0b2606fbe853c3ee680ec7a819332235e91a1587b31675cac378008025a03aaeac44198a178682b25b32cf808983697c2f909a615db729d72af874772586a026f599477ea89ca9c819d09bd62372ee3dc693239118c87064fa1e3ab969db47f87201b86ff86d8326d567843b9aca0082d6d8948cd55c226132934ade15b3269dc9cffe2b1889ad87b1dfbc998100008026a086add5e0f0cfda24802003ca430ba893d1ec0da07eb7c38c0ac1bae502b6f02fa0723c2c4ffaad42dde21fd6824ae5fcb04d51777fda30207b8765b75cb708a526f87201b86ff86d8326d568843b9aca0082d6d89470a51443f3e8ec06b207039a95cd5b5514daa49d87b20f63874324008026a0e9b70c68c1593db8cf9a652785a1eaade87aa1c06dae11ee7c174dc8857bd87da004ef880336aae984389c86072e0b6ee0ec11d24588190da2cb44971805bc5e3ef87201b86ff86d8326d569843b9aca0082d6d8944b67ac3de8ed8051672a9ce5150b3b4d12a7919587b2806d955a24008025a08bb51f72fcc0e6f7a93e04dc4af879dc22b1543928c1d9bb416d59c11abb3ab0a0034726e9eb5af12d075d0c826ee71faec08ed6f9bbb2900d6449a97d15241636f87201b86ff86d8326d56a843b9aca0082d6d8948cd32b6ce5e277794ae01c0ef598e55378fdd7cd87b43ec7a5438e008026a0c963f94199926d8da3e2191040309292b4a47c3cf49462be3139769acddd9b3ca008f5be9e005e21c99910ec482ac1862e0862f3ff1a9b26eb24c4cc4202989f29f87201b86ff86d8326d56b843b9aca0082d6d894848e5c9fc72f7114db62e08bd94b1b88cbb9e9f987b62468088d0e008025a008e1e4d0d061fe960648de69fed5150b27464a131522cb6bb2e0980ffbf509d9a03e5c69c29430588278aea7c4c8b8bce6d98152d86642f5976e29ccb181e82fe3f87201b86ff86d8326d56c843b9aca0082d6d894cbff5801237017c302a55ab38a14482ea7d21e5d87b5b8b3fee97c008026a05b566a986f925c1e63324ddd0f3ddb54d364d53e0e9cc2c71d67239f6b04b7d0a005a551f1455146e5cc231c1bef826bc4b736d34e245304f39dcca72a019059fff87101b86ef86c8326d56d843b9aca0082d6d8946bd64d990b165984539704599c060d3aa0ed417d87c4bf36adc81a0080259fddf04ff67dd5dca06edc221fb3a6e66476584ccb2f54f082685d9104e3ed5fa06281bd10e8d6f80906e361f0db532d817932390144309fa16591860cfd649d7bf87201b86ff86d8326d56e843b9aca0082d6d894644cf8af083251ed4024afd2ea4269595d70255587b27b35d5814a008026a0217477c3e6213520ad57f217228faac61ff6892965bd40b0f1dc8416e2697453a031059b390f92b258f1ed5c994cc267f6a22856f1befb45e504c3ca2385acdc74f87201b86ff86d8326d56f843b9aca0082d6d8941f19a5f6eaaaa30293ae796fc47f9bbed9d1f1b787ba9d0af1ca90008026a0d19e3d69bcd2fb6522c861e26a2fe52d31852b09f778b254c188486a15d8be35a03352173e54e4227485321c5893a2c5bbbfb689a6e943ef04795ae35095a536fef87201b86ff86d8326d570843b9aca0082d6d8946e2ed1592923bb21d1e9f92c7823dfe26702800a87b3617ff330a6008025a0054950e0058ac51185a0e62127d80fbf16445a0d0be2c1810ae35e8a7b7b5a96a05d5465d8980bdfbef962db849a41891d7a27a32f12e5bad1b060a83a10a3c8fbf87201b86ff86d8326d571843b9aca0082d6d8946c71ed4c82157b2cc96ad7a165b69305f7cbe9a487b223bebe6bf0008026a095c92e13119d58190fca43cab8625093f1952dba135f6b780ac019036930909ba044f9f1c8d0d63b08f4a8118b620b32b049cd8c9b8cd5614df6c838c3fb9c24acf87301b870f86e8326d572843b9aca0082d6d89423b8e248995d9c8727457ec82eca797b2d4a9a9d88010a797e07477c008026a06322d096d7004a8f1b7ef4b170a3943df393bb2a7e69e7b87f437bffb2130cd3a026741b4f96c8648bfaa6ab8ba77c741cd17e3ec0f62b74924d83c6ccf7e36c7ef87201b86ff86d8326d573843b9aca0082d6d894a851379d3b7e04b7c555b95fb8fa4e03d360a06c87b1feb923c05c008026a06bf9a3568c50cdb245e8b9bd7923c34139171173ebf403b2df94c70e7d025376a0225165b3e64656ba1eb8b9fa7f786bd363a04f877bc703535a68df39a5ae8262f87201b86ff86d8326d574843b9aca0082d6d894f10b0a0a29490643f10ab022be1965622b75a7d687b280d749dc60008026a0d69917f7bf2d8697f5820bcad1a7276e562378ceba4657d27ac7c5e18e52cedda0144909e8b6cc53e98efe7de5e39ac040208339c3d53d5342fc255445f07f744af87201b86ff86d8326d575843b9aca0082d6d8940d4f94d85ee0d5325301724b0ff4dd94693fb1b087b421316e9090008025a006d94a30de036c14d160e5e712b8450809a431e3cc25875ed571c938721e34eda01af8209066151cfbaa0301869e06879c1641ba923191b003ecedfc354d499f89f87201b86ff86d8326d576843b9aca0082d6d894bf1435b9a911b3d05503b6f24a0dc497974cbd1287b1f9269617c6008025a03aee8c27919506d20a22f748af43f84d5292e25fda0d8822da9686d0bedf6796a02a3854c39f9b47a23a22f5f1bef0343673ad3e2dced333d4edc2cb8ee7fa22a2f87201b86ff86d8326d577843b9aca0082d6d89418483a34b75284a3bd6d2d696d4c77e5d4bc7f2787b4999c358870008025a0cf60f0becb16a2d832db9c51446f5fb95228648ad52f00c865e2a3a6c0d8d4aaa0028ae92748cd8513a87640fc49fc02a61bd361b2f9eb21db26252f10dc9f6109f87201b86ff86d8326d578843b9aca0082d6d894a61c1a9fb469cbcb0248368157f6d11eccfd0b7187b1a6dd00408e008026a04da3b351d8a3b5b57f1acb9c4c0d738e6f9e982b24008dd1cf3cd7e076773552a047462710c72534244807bf7d28f459d3f48cfff5124f52085f780819ea98b37df87201b86ff86d8326d579843b9aca0082d6d894edbb8198f471d788f62f1f45ecb11f89c162df5d87b249315010cc008025a09f766fd59a51262226ddfe129b6d5e1f1504ddd79b2ee2750ce57c76022ef137a06c1bf5cb10fdf3fc44537408c1a0e4b596f37114ffd73a827d9d8d64e22569b6f87201b86ff86d8326d57a843b9aca0082d6d894b5c9a7dd015e9cfb284448360732e6a3a43c005687b2dc8197319e008025a0938fab3b1949b9647a1143e51edede32ec753756a84e43ab922423b07c025b4aa057263b74f2b216c2e1cd839a8a636969ff6a4157434055b2c04c74e245d58f42f87201b86ff86d8326d57b843b9aca0082d6d89416e2013e1074957a71ae9bc273714ac6b4b1dd2387e5e1f6270b5a008025a04db74760fcc9106813ff5ef5611decc0319eeb992321bbebf4f8ee925376276ba05fda9a517f14a96946fef5503d8b76a8466dc8c392fd44eadf666ad86ebb00cbf87201b86ff86d8326d57c843b9aca0082d6d89416b7ef9171583912f58958c56748f3b0af3e24b687b3be0fd2b3e8008025a03849e5b56ad3030327fee184b2ba38fd617dc4d392970f0f3e0804e73ec7f2d7a01cff596a359d13bc8bb8b8090168fd3be5c5d204b972271fd42b2e3e868d4120f87201b86ff86d8326d57d843b9aca0082d6d894af9ec2684bb9c32ce8bdcd67339c3612f3d04b7d87b6e2f11f367e008026a041eb3b57a43658e8eb08e021d227d5d04b941ed8992308a383c9f35aaebeb388a06d959161c2d38ae0c1c5c200d4eb96c6fec3a141d004dfcb00a9a2573bb47f73f87201b86ff86d8326d57e843b9aca0082d6d894905d9043b61505390fa48b66f99e3240b8cb095987b68ffebc0134008025a09e7cd1b2e6185a37cc825be58323dcb8ed1391ba4e3af6c5a68dd2d16adec891a0197147a8f896d02c0c14e03cc823450de62e0bf179bbd077c632aea40869ee5ef87201b86ff86d8326d57f843b9aca0082d6d8943fd8ac64488058716f9b4f6edcb3de1e1801857987b1bb68a52d7a008026a0dd20e3c2511a3c1e4b040394d3168f30225911dc580047c0f8d5e5ae8701a2eca05ee2a53d0e5775fe0a3b1793086aa0203b4461c89875e7c860c4690842085366f87201b86ff86d8326d580843b9aca0082d6d8942db46a11739835f32d33cc9333868744034e8cfa87c587ebbeca6a008026a0cb2bdf1e36e0d56c92b51d58df94dd28ffe10c0049d04762080ed71ea4d4c1dca02bd40b2870aaee180648c8bfe085534547b39d45579636578d4940bc550a2345f87201b86ff86d8326d581843b9aca0082d6d894e4231feaf0de677fc0fc909630185cc8a5a5792087b360fef63782008025a0e12a3037cb57e6665e197b3730875820d350c4adf5fc7e0b3812f689a3d074b5a06bf60124a49064b31b4c7a81540705f43992c185af1a65f2d58b64c4e91c4ae0f87201b86ff86d8326d582843b9aca0082d6d894e5a4f33ce23780ec66ba4dc52643fcb3c422e28687b1d21ee45df2008025a0038293a167763a04b05ffd4f7dd37ecac5e9639d8a4be5b5886fdb1c9bda7076a0252af1a5bbb64b96b0fe5975c1a68081811f16a631e8e033084b228b6305a236f87301b870f86e8326d583843b9aca0082d6d894ecb73869a9c69445e4dd5d4068a626db1c98ea2788048ffb366d2ff2008025a0d1c7e4db6290b004b02e651cc7da93215ccef7f187dade733decad29a0918468a07f439817aec1c5d5508f9064363cb1fd0a88323cc44f9ac25aa6831f7842ec46f87101b86ef86c8326d584843b9aca0082d6d8942f7550200345f23bc0d5e6aa4cd123828d388e6887b72fa30817c4008026a0d74a501291614e4713f39100157a8595fe446a310840d332e903165abb00ce129f04f4024704f0863a598f60e8c20cc2bb5be01b7b99e86534a6b2571eada896f87201b86ff86d8326d585843b9aca0082d6d8945aad3828e27ba68afc1aca62846e90111f45898787b1a9a8f488ec008026a0f2a484bbb6e23896e2eaa44dc0dc303ace982c1e8d308386f31729f2bbdaf2e8a03b41b1834da7cf5a1384794abaa9286939e79d63f7689cfca6e663cdb772f992f87201b86ff86d8326d586843b9aca0082d6d8944200f6f33da38e1cca67eac6e8eb6c27a05af23d87b38e63c5a468008026a0069b680c4e2260ddf00a48c259fe6ad6c32cb05f20eebb440185ea292b6a5e16a05295e979bcfaff6192eba45659f512c97960649669415f9017ba3bbd5850c6b6f87201b86ff86d8326d587843b9aca0082d6d894ffd07530b6a344da3909bcfd3f599383fbe4b06c87b344be13ed48008025a02b1cff4f20fc434252b1b7db12d8475791e1838015a0cccd2286b006789a8a2aa031c842bdb610fd5f543d60a5c9e32838bb46c52b2c3b441ef57cb5cbb9c9538ef87201b86ff86d8326d588843b9aca0082d6d894dc8aa014c2c643281122f9889c558a80fe64126587b2dc441f914e008026a0117fa5e428ee44c464dae17548ef3dc4b8646a9da91b4341c169578aab2a9206a023766dfb216ad6a102125992bc3ce96e0f0a01ca25d69f134ea45edd685cb2f9f87201b86ff86d8326d589843b9aca0082d6d894c7ca6aed87f0b36f6ac945dd85b8bd97c42eea3c87b250297e7562008026a04f090b7350bf0f7e0566b3487ee43e0ada47b7b07a6cdfaf39445455b754da2fa006665e852fde7e59453593ebfe99c99f183355afbc07508cde3da182d7dd3e34f87201b86ff86d8326d58a843b9aca0082d6d8941b6d4497b3636c8fa7e46d0809f8a9ea6fa4df6187b1ba089af46a008025a0d7d080eecddaf864c35046715563e9c5cc9569b1b70a9919ddc8c04ca5b643d1a04ac9edfc4a9ca331fe3f1397467062940e8907666b651b7df02ea3a9dcfc3a0af87201b86ff86d8326d58b843b9aca0082d6d8941f86bc09df422f39ce772f3bffa65b68365f86e687b2a75eb16d6e008026a0f847c53f0ef5e922da9ff52b4064c64639d3560d2c27d50856e4fddf44facdc2a033ba3102985bb0b6f910e58a0975a080212bce0e4497468f84a454253e0d782ef87201b86ff86d8326d58c843b9aca0082d6d894d80db5a4b9c75a65c2425809cb5b62b95432d2ed87b70296ee0980008026a0d4dfca2704469d76446c946d1db4fdb4781c77e854348f988bd534f4533bb38ca032a4afc4abe3498633cd3ff184be5128b4803b75657dd5939f882469a0e6de43f87201b86ff86d8326d58d843b9aca0082d6d8949c98b002eae103d430b31ab855f6a357a411796f87cae13e4d7fde008026a0e01dbd9905886928c26c450fbf12c92dd1695cf106fed545af22a4841cd3d872a032ec689cda233757b97844010be3beaa885966c8608a2c4418c8faa76b016c77f87201b86ff86d8326d58e843b9aca0082d6d894a3652127c547e2bf8fd61a84df08d670f3d01f8e87c6afc8f35d2e008026a00d82f1936a9fa46f4f0f71c5ff145f20f4f1d1cfa53f7c804110772aa907d815a00830f4a22a47eb4134d2961caf26c262957c843ef911dbd1d9beda2f2a534375f87301b870f86e8326d58f843b9aca0082d6d89460504af991ecb199b0a5bd3df79bcb340d1d3eb9880145492890556c008025a08eb6bfe6cf22b26be732043e506609e107459acb03459acbcfd98a53f0912957a04dd898d378d2ebb299d284cb3bf832ddddb669deed844309595a4e8c45fdc290f87201b86ff86d8326d590843b9aca0082d6d894c6df021f2afba1dbd3d78f46b844e123232fe20987b1e0a70f6196008025a0389127cffb0b616734eecdd812531247dc6758a8f3daf9efd1abd351f5568acca05f8d11a5b3dda435f5f3551c8463c7f0b5223fc24710727e27159b159a6aa539f87201b86ff86d8326d591843b9aca0082d6d894f2bdbbae0c3e1aa4f96097f666c2e1fb3448ef4787b6a9eed4de7a008026a0bc5173c57153b2f118c1748c33e0c57712432000ba20d51d9f2a67ab9f56b316a075432d649140382ec4854d0066b490bab63ddf81ef5bf0e36b7af52d5a7a6796f87201b86ff86d8326d592843b9aca0082d6d8948d89d6fa95da2e570502dbea62dddb4c7bff5df087ba35563290ec008025a0bcefcd0101b12a7faaf12138a5ba40165f514a2cc74ca27fbfe079e2785afa88a07c52587485aa154f15adf47a36aae19687958f43112b2ef7cc9160114d9f20ddf87201b86ff86d8326d593843b9aca0082d6d89413826972c957d26da9f75454aa498da68d65091087e3ae81fb1832008025a001816ea6bc529cbab6d61bc7fe2fdbccb83a00a8bddeb1ea1deed6ccb8c4a7d4a06a5e7743d59fb91d87b3026e097e9152f3389cc7d9ac35b67f1b8ab4a1fc2236f87201b86ff86d8326d594843b9aca0082d6d8949f5db7f647ac8be542edbbd6af8687d24aa5a8e687d079522cb2a2008025a06c261f07bafc3aa8585ff5e6548baeb4ea587c2921bec958addd6b1c75afe0d5a04cc6b9fcfc0dc3ca945a3befb8df0bab51546246329572ec4080153221c6eae7f87201b86ff86d8326d595843b9aca0082d6d894880811dbe318b922c40bd77e1c831797d5f185b387b901c080ef82008026a0ec10b48ec716eed469157b987b3e2add53c9fe47ada2d3fa9a1e253b57e2672ea06f494f84160ff15958becbb173b226c47db389c1c5da318046327e9b9c9e5f42f87301b870f86e8326d596843b9aca0082d6d894dc9606795656c3c266631b0ff7d4361c380413ca8801d83c63f3f862008026a06116b9ce164b6124b2fc9b8c519faa2b7ec8e9000a843126009bdbd197ae4e8aa046cb8081197c2531e8bf41e42b8c44da7b2f62acc325202a7e53e32a15ff6906f87301b870f86e8326d597843b9aca0082d6d8947fdf914260e8947a26119226c2911a135d8ac4ab880212e81f598276008026a04cf88c33b195fa57224742290257416e9eba58826a47be3c523b66176b4fe058a0545919e08aa975360024130577e7c0c33496fd5f0daba3777d1f0b139411baf6f87201b86ff86d8326d598843b9aca0082d6d89456949c22fd7844502c6eecbe66ed5e59544b303887b320e3320412008026a0667c9447390ca9d0be1ef453827f89cdd1f4e0f612e1a804785362e357470885a04d1ef3dd6b92ccd13cc076f12116a3606b357befd7720e809a24ac39225bc58df87201b86ff86d8326d599843b9aca0082d6d894b90030f3dcd35baf3617c7da86670a5df161180787b5adc8a18c00008025a01874b021183af4596fbb2515922bca022c82697517bd920b1780bf7a80f1152da047a898701b629ad0cbaac9856bd16c1860cf4337553cb22ad38cc75ebda7e29ef87201b86ff86d8326d59a843b9aca0082d6d89454e1f42560e3d4280318726f830ab13934d42db887b3d87508b414008025a0230f0155d0c39e02f129f8150620148f16d8dca0cfbba5eadf69e783cfef3dcba03979342e07f92bb1eb5d5373ca789cebf2d3a322577e28f4869b43bac25e95b8f87301b870f86e8326d59b843b9aca0082d6d894ddf8e844a203359096c0326173d2cd7233ef9d5d88013a2442cc3ae8008026a0c421bf1bcd739c26e7956e637eff97898758545c296cbf798bd5032debe81e13a03dddc83b5a778c8314a34428ac874b331b8d23e49c2e6e65139245a1f094c8e0f87201b86ff86d8326d59c843b9aca0082d6d8949f81ec6dfeba3f8468d7f4dfd56369625426431987bbaac0df330c008025a07fa31824add489651fa9a9095753f9645b72bfbc41306d61b2d5192b5808a89aa07432426f65427404b797d24947ffea5157dd0819601a81444a686d5c9a473dd5f87201b86ff86d8326d59d843b9aca0082d6d8944eacdd3344345bb01a4ca885c0a079d9ee1c26da87b749d416a730008026a09e86e54533a25226dfe44339584059a6b5b55ee2711e8c2b7362ce0eb642d66ba01f53248a885abc11f74bd8122b602c752e7b7f6d63471d3e0fc6c3820f970620f87201b86ff86d8326d59e843b9aca0082d6d89475f1def38c16aad0f27af57f37a3eb64cf8ccfd287b34c3eee3f0c008026a003deb1b9addb6ad9468ac957c5e45b3d07c303bdddfc97dc0cb6258373cb289ba01a46208d4b6a02333e9447ee9801176450a054934108b9002713700718e545d2f87201b86ff86d8326d59f843b9aca0082d6d894dd00c863ad6618a1856eb0e57eadd6812d7f106287b221e6cbce82008025a08496d134d56255ad4fb2e56b1ff7fbe86a09bb931c5617cc8b55851b59e213c0a037c74b079ec5a15034dff1658f808db0cfd7cc683818fa867787ee513c3e350df87201b86ff86d8326d5a0843b9aca0082d6d89446160e9449865b2991d51e985b567f2587ebc02d87b57298f1aef8008025a080e30b919cac8094cd726f0f6339fb1d7eb472d97e81f1f2a455fb32f73f912ba0545145bcc439cb848cc0c1311d20a70e6b69a993faaac4e18c604a26de439f2af87201b86ff86d8326d5a1843b9aca0082d6d89464d1110332fb2d0e999d9aae6ac629e0b9bc9fae87b1e3fd8c6d72008026a01c1e62e59ae9ec6fa583b148d933e4b5fde95bb14e3639b20c307de14971017ca001b128c54032461288e7ffe66958cc65b9ae3a8e03c90bcc70076ff7815dbca7f87201b86ff86d8326d5a2843b9aca0082d6d894340aed3a0e79033bc2b6ad29755c9edfb44f8f5987b8a1a4d2e9b2008026a0163f2845cac9eca99d475e03abd27526b55099496a6c6bf223e4f04a83bb1903a030717ab6d88c870e242a936ccff837b194f6a7f17770cfa827ef33f2135e60cef87201b86ff86d8326d5a3843b9aca0082d6d89494311526c00f87f9790ed2b6e3a7bcb7705a39d587bcae6a40e32e008026a0284ad344f562abb40f83304d6b6857a35cd433cf80bda78c091a8c3cf8341cd3a07687e9f01b2cb7921dd942224f14642d2e016551fb2b4a4b33a461df6914ba04f87201b86ff86d8326d5a4843b9aca0082d6d8942bcab4f647ed9384891cf7e867bf43c650f7be7087b71a3c4b3256008026a0fbff8a9114e5863861242407ac4088a967a71bfdfc660281760d77e4b1ec9dc2a02425825f0d3b18586a50c6774579cbfd7566676464b7541666f9e81c33e0ab4df87201b86ff86d8326d5a5843b9aca0082d6d894dffb3a65b39d84bf6c4faf0461a3d3c9533a831a87b32e27ba92ea008026a042f90e3d7728c622cb0b77e28cdad4d981666e4c754e64f334390dd00ead5862a006d145229d0e1a755749709d8b15ecfc1c389767efd990b4cb6ee89e8ca2ec9cf87201b86ff86d8326d5a6843b9aca0082d6d894a1dc3d610c94c41366c949f50efa1cf9b4b0b72387b34f2fac1016008025a0044fa632dcc6507620a14b5f925ecc2f811692ff1f6aac29d3334a29ae0ee9e0a027f52d524a1d22b2a1de65b3cedc0b71656f78c8f976a2e63f0a8a6a005e590cf87201b86ff86d8326d5a7843b9aca0082d6d8943df4db7a64f96b92028c220849f9ca67255efdd587b3069c409e7a008026a04c18f67fbbaabac042561d99923f6f615408b6a004bfcdf728e18ab17604149ca039d5a310cc8729321d6081acb0a312a8c5e7f9ef393eab50c459786aa919f58df87201b86ff86d8326d5a8843b9aca0082d6d8940822f226b019e83114eb170cfc0dcd49b21e268287b21e213f5844008026a0ebce886aa2d9378e989183ce0350c305fd0025df8f22bf6b44f9e3b1695d1baaa016ca0d783c519c5daff0310d4dc24c72eb0cf51bf480a5bb4a2769bd429e9c4bf87201b86ff86d8326d5a9843b9aca0082d6d894725465f7758361d357f5b45f9cb2e7c41e47ca1087b2e1f7817b26008025a081f0b39c74ffdc349c71c9735acc8271eb025b82388228a25f6db8b0971bf356a0692b24f6caca9d11327043a358d602281403e29b42d779592a1dcd52820e0375f87201b86ff86d8326d5aa843b9aca0082d6d8943ca5fa0aa8d69667412be0b816bca90dc1a220f487c1ff459f900a008026a0707a9c15c9000ccc310ceae7ea8e93dbf93919e0460769ac9ba954ad2e84acfba07859a95a2bad33a08c83bf5b9e240f95c26e7e72aee447ef415e0b7382b16646f87201b86ff86d8326d5ab843b9aca0082d6d8949bc50faabbbc74bcf78378b24f061824b37d41dd87b1cb2d76823e008025a0abbb557b5f78c8696c60ebe6e1ffce04bf4c69dce519c653e6b88e40a190f610a05290839f2e60c3ddb374dd45735cb251e27b95328ed63ce48d4a560a20e5c28df87201b86ff86d8326d5ac843b9aca0082d6d894d6754878ff68dae7e227a36706a1c51f1ab4a6e187ef76f3d1a182008026a06322250a191de131763f215becf62c4d2b1e70df568c6e34d2e927cfa633a073a05984670fcb84d87b85a4a63d7d4c5349b32c3b8a1729b4b688526bc98d790016f87201b86ff86d8326d5ad843b9aca0082d6d8948d034537143662a667dbde56b3233f853d9c1f5f87b2702ce65998008025a0edfbd1a30915a279f74c68b3d77ffc5b213f6ba33c48d533220677bef5c5a86da05d753f39cd3ff4a154bab38851df602a9124896fae972e377f3db85ff661f490f87201b86ff86d8326d5ae843b9aca0082d6d89493f0694cf7460ba3d4752ed0b1c53ad1f621538287ca3758d70aa0008025a0099fdbfd2184f694621ce3a2e17d2e2f0ba0b4fdf9f5bdbb30352748119f10c8a03711f5db226d93406396403ecb0eb27baf9debf1228cb34553dbd141116cdd67f87201b86ff86d8326d5af843b9aca0082d6d8944c3d56665b918f5b994a66d6b0b3d18968b5050c87b356043af1f2008025a03e7cb8fa0b814feb74cc5e185543cf6e5e9aa8db27918e6642504a16b25d7ed6a0220251f3791a6fb861df3d588f871586d78d2e938bd221c55312f2ee2cc6dbb0f87201b86ff86d8326d5b0843b9aca0082d6d894e7291598d13e4ece15e501bce2ea1dde0aa27f1f87b4d412547e96008026a0422af8729f36c3d24ae49d563f0f8569fd8474e8d109673bd977bd4a75a2e8aaa07f7394448fd73d8b41920d3bbb229d96d022247965428a22f6e35de2902938f4f87201b86ff86d8326d5b1843b9aca0082d6d894f52b6b4e2b23a1f34fc3665aec0a254928b9ad2087b1c3b9e1a774008026a087b0c4bb2774d66aa933a798011c326d4b6c0176c922776dbd43541a1bb49efaa073bd8150e69a7bac99e0149dfe9a9a1783fbdc62e0b3065e1f1df839de9e1833f87201b86ff86d8326d5b2843b9aca0082d6d894841ce5329217d2b7256a552d49a2387df0bd441a87bf43141015b4008026a08da563a09afda8723fe0b41e0f3b266dbaae0574c6f420a474b8d199142a0684a0012e33f59f40d944a693128fb86644110fd7411af8ad73dee1153798e057765af87201b86ff86d8326d5b3843b9aca0082d6d894eb0811a855d06801ffe26ef0effe40c84fbb75b187b3c1ece33bd8008025a046fd7c5250aa4666b5368b7c2b7735aa81ca45dc8f203c0147a5dded334755a0a01757295f9aadda0dca2aad9f6869c25da496b0069e49dc8b05c0fe6fa1cfe3aaf87201b86ff86d8326d5b4843b9aca0082d6d89443d138b8f1daba13f15ee8832c22a6e657bceed087bc6b9afae43a008025a08925163dd2f964fed53f66fb1cba94d13f6c22403b32fe13e1d75ad6905c2b30a05f8860bb138df043fd66b7b651f3f2a43ca92fcd7783dbdf1cf46467ef5b33daf87201b86ff86d8326d5b5843b9aca0082d6d89452b534698b34802e275e0c725aa7aa426dc9ce9b87b2f083662b6a008025a033e107452db3c5bff34306e24d06c78b698c6a516d2affbced5a3d9a15c718b5a07743b50d9552d058349d784e90fc74b7073ad626786745dab92a3b8898266872f87201b86ff86d8326d5b6843b9aca0082d6d8945d9dcc2f0a80c51e633b9b84b9744a6ccdd7917c87b478644d7cb8008025a0c26f731cc11968818dd64cf81a138e5dbc1dd615f237bbfafb2ee84aeb95e434a02f8d94f9b39e29761fc92d71ec77f9c57826c37fa84d6820054f7bd4d5f4d49df87201b86ff86d8326d5b7843b9aca0082d6d894ddd369aa2151f956c18093c8336e402a54396a1387b2b12994a344008025a00e565a28c1f08557866266f87c64e7624b99ebdd6b4a6d4f1922c1bf9f0c7deaa02ca90ee5c44071f6037ff85bc316ecb0b27685c64f1ea2dde05ef104df4888edf87201b86ff86d8326d5b8843b9aca0082d6d8947f054e74d21def960ba1e5cbb1bcdb0b52d6acfd87b4874ac461ee008026a086e7e3c31af02f832238f5848908ce1038d0c2fad0a5c460521a1b456f4d91bba04233e94d043ac9870403e8aad5e77b5395f2b453a866e81a4c44c6c7b3d33044f87201b86ff86d8326d5b9843b9aca0082d6d894a4d85f0a64b4f0969baf961fafb60da83776678d87b6a4ddbb649c008025a070d2232def667a2f6294d6128a6b28c32783695edab3ebf39611cd8836b97895a0642b2e961cd49b815702931b62327932c55c915ea352127be29b422659c91913f87201b86ff86d8326d5ba843b9aca0082d6d894c30eb1798b9479c4feea87cf70996e8379ff355487b24b4d3f3ca2008025a0b54c6997fabe167fcdff38ce44d44a1e8ac0d5e88bec85f495cef380a3925313a04287d73cea41531be7ad21029e9a2dc003fbb8bdf35010127dc94d3e8c6be3edf87201b86ff86d8326d5bb843b9aca0082d6d8945869ff9132ae640ec263cb2e9a3e00f8647354bc87b1ac8446b95e008025a0dc16cb4af527e5d23af13713e02bf2a6b4ac8c5442ea4e1a4daf7026213a7355a0530d91f048b28d9387b88f993e5db3f79f4fc6e80d83434b09c2e123c547ec08f87201b86ff86d8326d5bc843b9aca0082d6d8948fd618da4a503dcbe528e57bbefee0d6685e0bba87b1ff07d61f10008025a0aae62063c9c954c7f719e803ee1a0547fe719d6c577bb7f3e3d86b599af22a2ca04b5996bb2fcbe944b89f6c74bd655b56c235aae1e2ca638aadbffdf5ee01f704f87201b86ff86d8326d5bd843b9aca0082d6d894934a911d70655100743dfcf5c76df12009e9b1f287b2a22d3ae7e2008026a050150bfacfd8bd0e47e34a772c0bf534788f2154c7ce149ac09f2b4d5fa95654a077d1d171591ca3ac41ac16c0ffbc20286ef556b4eaf8d9be84e860fd8af98200f87201b86ff86d8326d5be843b9aca0082d6d894a4f7a43e5b86db32c7515f4828a606a82498d0a687b346c207d1d8008026a0ff61bae680a704de167810cceaa879193fe994afa52d4230104a152b9f7a98daa06822a503260b39d35713634b9275714c563f5fd02f5c4e692703ed825c45b703f87201b86ff86d8326d5bf843b9aca0082d6d894e812172b586e5ce0cc1107851d8e23277210cc1b87b232a74dc240008025a0679e283dc4c3ea4d789d05580241165a17b2a670546cec7f4a6da6569d215c5aa03f5eabe6ee7120c701662d24e06ce9f7aa3749ad08728d52b84d941bb24ad3daf87201b86ff86d8326d5c0843b9aca0082d6d8941e912722b01cf323628295c946a06bf41847300e87b1ce4db3ac40008026a06cb8c1b8ad0e923461c6a3af0a8a01ad414df22347f339a1f3923cad0a7543c6a025efe42f1375e838a73bd032db558a0d210c7ca027448491c175dd546996c2b0f87201b86ff86d8326d5c1843b9aca0082d6d894b3187b0453f1859b0a6c24405827347c6308e1b187e03aa0505346008025a0d564d7a1fd5250cc5d79309c5e7d12e60b9bc47390c7cb79ec9bea88c562d543a069bfc4efba6d4e5ec550b300fe118474df3e43f481116fed2de304481230c496f87201b86ff86d8326d5c2843b9aca0082d6d894b09700b38351aca924de52fc0a476bef0057c76e87b5954fdb5eb2008026a0c0422082d1a5a9e21db40f81bdbe0de97c59e13b0b58a1178da76af27a86adb1a033b1f87a07486bab417bf134da5dc158c362da2c3e3cd3475252de2ee76bfefdf87201b86ff86d8326d5c3843b9aca0082d6d8949adaefd0a6e364c71c639e4f0ca71d1355a3767587b246b2a88666008026a0cc8355c0c7bf62f623e982ebe6b9dcc2a9f4bc2442ce686a3fb8500b2262e385a05a397ab3bb89a13c49e49d9821a308c30c56bb484a6f00e6b9f7ce646a0af233f87201b86ff86d8326d5c4843b9aca0082d6d894f83b6fe4facb28096fe2091a09ba0043f7f2c97087b3e7046b762e008025a02155c9bf524c67df0efcbba3524515700cd6563ff97650db761e886079247eb9a006402a593cc2f435f9d85f60a6081a0121a7cf16e022c6803db2fcd8e1358377f87201b86ff86d8326d5c5843b9aca0082d6d8940b4f4038128f0f58aa447d1e42e604dc1037f7ab87b253472bf8b6008025a0894f5e89da4bdcc70b49088aa87656cfe1e91e589fcd99c5caf9380dce7d647aa048819507cbcd50de84917f7d74876f0e7753b65e18503b58cbf5ffb33aa38906f87201b86ff86d8326d5c6843b9aca0082d6d894d8300da70ce1fb441c263a9208d874426f76548f87f78ae4e82d6e008025a0f1339e936a8898866487398029ec08d0a59e2bd0f664bfe91e69dee5c61b005ea0326884cf4bf5f82419775a3a35fe19a04c49665ac0afb7f10aa6167fe1c8e174f87201b86ff86d8326d5c7843b9aca0082d6d894a2f21b1114d645ac1c2bc837b6f6c07aa38281bd87b1f1283cdeb4008025a0e754f923203b25f6ccaa5a0f4abd744f3364d0d6e7465d5c28c8d44f6debc151a07bccdc4a22b697e5423445d353f07340f8d633b7ec334058a8952ae3dcff8a50f87201b86ff86d8326d5c8843b9aca0082d6d89421631b347fe01302fce4dc93893da3940c0c974f87b1d559354054008026a00d32d768562ddbff34c376b84ad37a7e74f468d15213e65cb10f7e68dbc6305ea0087d0c2ee133d542522d34e2a380a731fdbfb4d418638b10d292deca0249a7dcf87201b86ff86d8326d5c9843b9aca0082d6d8941ce186e5578404b7c63b39569e29223de0a0385f87cd334336bf0c008025a092437985f3f3fb16c1f740cd595030318bf6a6fde16dce31e046adba2cbd1a92a05ef5ea54189f687776d741b1c54c8fa1697ca53ab08de3309a0054e7582f9144f87201b86ff86d8326d5ca843b9aca0082d6d8948c32ba555fb006bd607b71f74bbb14961993e31b87b6e4009a3a8a008026a0f333304f84583a1e93d80d42533bd6e800485b0ca4f90de00f004127ce01749da0341b371a0dede780ee92f67d0333ac824be7873f4b583d2892cde4d0f367c86ef87201b86ff86d8326d5cb843b9aca0082d6d89446913c892102221be1d0db4d409786c7d732e59e87bad3f8fbeac8008026a0a46a746d0b41c9bd67a5bdcaddb1d7eec7986a7c66a8c12b081ccb21f520de71a07275c91052e14f8e2552f6429f467b4bcda95cb04bb89374fc8869cb03b52023f87201b86ff86d8326d5cc843b9aca0082d6d8942ace964770c5cf71fe92f232d01ff22222a34bd087b4861e6a640a008026a0a85669c9c12d4e25695c03ff17429596ece6c0687df60fc08c34a794ed255dfaa03fe3570de5d628c3da2e41e95d8dd973943dbdd86cc6cb21218fdb3bd20c35f1f87201b86ff86d8326d5cd843b9aca0082d6d894915b024deaee7a6b6ec126e1c34dec063aefb8dd87b2771c002968008025a0b9fe85796d8ea158142ffce85001496b2a63918eac1c0ae1e00f8762500cb8c7a05263fc0cf92e4942fb3ac0e03ec268d72535d449f1819e8892650befa3c08287f87201b86ff86d8326d5ce843b9aca0082d6d894534992c44e76b93819df8680f376eefb7f0526e287b22b725a8db8008025a071ff094929ecdfa1f36378f93430937fc966370f97c429c2873d0e5d7d256191a025bba650f071630150aa7bbf020f3112f3694bb20a96481952480f678069e223f87201b86ff86d8326d5cf843b9aca0082d6d894c37a15f3c73ad5868f50d4decbce591a9597daf587b8935b0df186008026a0e3960105ec7c31454505755b6c7f0756358a0d0a28dda7a92ff1ec61faadd49aa009df6de2c3ef1c8e777a311c10b4ec637a75ea30d8cc36fc08f1aec389321baaf87201b86ff86d8326d5d0843b9aca0082d6d894cc6f2dda50048206f16560707a54eacae3cc2cc887c6af8620d4b8008026a0a6a85dd5a863a3731bcb8e87909cf75718cad3ad7c195dec2d394c8180ed78a8a0443768c149ed80c03b26778f697ec7b43a4126116c7e974a7699589a34c48e2bf87301b870f86e8326d5d1843b9aca0082d6d894e3fe3fd1913a06041605774d51a52e5279837053880118940d75279e008026a0cfb7c2c1fd7a2ec5ef450083e78a3d31d2ae88a038d8630378dd1c6a81b115c4a0467d1efe3ec2172cfe579d54df5d9830257744ed57f5a752c781f326cd76642df87101b86ef86c8326d5d2843b9aca0082d6d8949c263d4f406862bc2660fc0097bb2b1ccb5c7d9587b20ee5912642008025a025cb7155ac1b2d05ca1a6850f8fd2f43ac0b69b5df0d95d09e55889e04b7e21d9f2a4602cac30950f0598efd61971bf8f81e5843720583de532375cbc9a9a8e7f87201b86ff86d8326d5d3843b9aca0082d6d894559813f58cd333afaa57c19e1de60405f2acab9187b1f954afd002008026a0b13642943a43f0329894325c69d28b9e058abce65ffc2972a0b6943e5d79ca55a064e92c0c03c4d1b6cc31adc8592b07b35c2541faa1acb80f15d9cd988ae107dff87201b86ff86d8326d5d4843b9aca0082d6d894ef4acaf3216e470f5b02accce580e49b989ff2c787b3caa66e9752008026a0cb10f73c3bd9dd32d68b89ac4c3f25c71e4b881d61082900727624727934b522a027b6f6f44c5ba6d0924f9b7dea5ba6698438703eec9b0b04e84417c71ac64837f87201b86ff86d8326d5d5843b9aca0082d6d8947f7b5b50ca633d2222c2ac8d52f7c9109215e0e387cdd861d99c22008026a02f2d50f400e10614c2b0863081d8117e078c1b6d1a497f15f9c274c04c94f4c5a0668df10ba752a8079d704a05edfa2081bd6ef501b51c79deb06a4442d77f4779f87201b86ff86d8326d5d6843b9aca0082d6d89491fc515133876892ea6b2299d5275265134de27987b2303ce7d280008026a01395ef53ba340180616270926db1224e9f0a7f45a507aac7c93f4bb7187d6c8aa026ee012480e29d6df06acf9169750a57ecee1ba95c9ab060afc0b9a7ae109d34f87201b86ff86d8326d5d7843b9aca0082d6d894d1af51eb67684afacb4b5effe63399a15031e8bf87b2598c4e6482008025a0cb7b3480a02ba38f37e11d1e88abc3310119be959707a48f10d59c6da54f663ca024c4b02deb5c4fe9c2e1dcbf0b3bf4983f028af44dedb771b213b35be3c093baf87301b870f86e8326d5d8843b9aca0082d6d894a739fffa0c36945c4f66c91e50efeb96a1003fc488011f28791f76ee008026a06ff356eb1ecf08b6e0a2d2ccad12884b610c99fcad7a76e2d3ed1cf9da90ed2ba04b5c1639edd9403106ee1b254ed9606747c936378022ce52e959d6794c66e859f87201b86ff86d8326d5d9843b9aca0082d6d8949253e1d539524f20fa98452b1afe2071bc85438d87b51b54d8046c008026a07c9bb2f93a94c6acc4f24dc6be7bc79f9b6bad4f8301e855a1286b18185ff08ba07549943f7380d05f9c21eff97acd49a292b436a3482db5ed72d344735693f9abf87201b86ff86d8326d5da843b9aca0082d6d894364195fda2e41dbe54c2d4b529906bc475c63bef87b69d31929c14008026a079d5e73cecfee5b6b8760a5531cabb7d7e8978f96dcbc416bab1b7adcfd0d39fa01ca2203a2dcd85983c457406acc7f24a508b81d3e1010fae0ba84f1febbf561cf87201b86ff86d8326d5db843b9aca0082d6d8946304783ae18b053652d28e8bef8acf53358d3f2d87b1e02c2020f6008026a0b716d47ed164115ed207312420557eb22cb74a1b157d3ccd14c3bd2c51510fada00f860a0fed877aabb932d0ea79ca854cc775a793e8089d024654703bd092e4b1f87201b86ff86d8326d5dc843b9aca0082d6d8947c8d51f7e46b7bfb440488719096187360d2fca487b5aa05e0573a008025a00ce85549d4a4910d14f494af245dc083b5354c6ebf0ebad71109e9aad450d6b0a019577ca78a84b438c4a2a2e530ae90b192dd646b683e04495bc6137314e80dddf87201b86ff86d8326d5dd843b9aca0082d6d894ea82dd326151aadf99ef95680fadf0158ead48d387b3264604b8e6008025a0a4b5269af5a19d4a46cd50d7ddb070fb0f2ebfa77446fa8f91b439698210fefea052353298af81fda6df5974405bddcc184b5e9de4d31e49149f78d628f1e70b54f87201b86ff86d8326d5de843b9aca0082d6d894e472a3d8df9de6056d6a887598df1c2ea9f1f80c87b8ace61a6458008025a09be27ecf9ef04532972fba3990f8c9a6b70c9a07fd7350e37d395c2a6460f973a066f1e27dfbfc0a4f12c5a926f8e96db12cc73e2557ad408f49d08866dbf88336f87201b86ff86d8326d5df843b9aca0082d6d894073c6fbf6a035b353e428017e8efb031ac6e31f887c891a228f4ea008026a0848901cdc33904fac81a095c3cc6f30e7eb3d3256efc6273a57344499e699bcfa010d179dcbdc6ae0d561f90645f23f48c75641aecc10277522d1eb3b0fc74fefef87201b86ff86d8326d5e0843b9aca0082d6d894e275305c31aa08a594b707af799eb66c7264c67487b5a3b3787474008026a03291a6574a5eca54c090ce7f4df8e6fcdf1d4c6c6706a750e8fc80abfe81e076a076ac5d108318dfc149578386d4de3be96f995a365d0ac1881ca983d8fde403fef87201b86ff86d8326d5e1843b9aca0082d6d894464dfe2edbf793394d4b93cfd35249a8e5e360ea87b527a73bbea4008025a06b0b851cefd6dfd6787fafc2405787b2f7902095757cdb3e12199441a04dec84a0785403747d53f4deb9f427780cb11828afeaab48a60b738f3582bb80856b4ee6f87201b86ff86d8326d5e2843b9aca0082d6d89487d06e7d59ab9b02641377f400a7e3f3e962329687bbac27a9f4fe008025a0f16e5dfef30b775d36279a0169fb8f4cc85d2a6b357365f56ace390013ce2b90a0116be64a258fb91d3d17e790ee3584f2897ff95650b5695121ffd095a848e938f87201b86ff86d8326d5e3843b9aca0082d6d894ce8e04ed0b6d9b6031e17688b2f0662773c78d9b87c10f9dd13eea008026a09a921af4ce11f37664a8aca8de6aa5385d697dd4503b578f6cef4f266d2965e4a049569ef304ea0160cdcde30e434feedba0c51f389cc956fd89e6b70904c21b38f87201b86ff86d8326d5e4843b9aca0082d6d894df403ff1b7a9dbe8f1b4f0362361eb3d1dfc777387b1a9e4181d58008026a0aa855193c84e8ff04ae3e09402209dee85f9bdd5fbe14f497f3a5367770b4216a02dced09dfc5d3e22ae1fcd25a28429248ba2bcf5c699260c51ee064e74a9390ef87201b86ff86d8326d5e5843b9aca0082d6d894c80a8ac5f0f93cec7acfb86709b80a1521df186687f3e233b21ace008026a0dc66071d409199f4afc7d5b1a19437280efb7568b8bc082a4c254e2ae55f72cea04f3513f6d776219d14a0d9399df604085d3c99a386da66a0d999c5da094bcd79f87201b86ff86d8326d5e6843b9aca0082d6d8946b55d053a6ec4c7a0ee538a3e3c776a814ba506887b535486ef388008025a08f972075c4119729b93da36bace971833ab84c6241a0e15d1c464097ef37c5bca05706207f0c8edf358d72b297810d96d894a2e7be55f34e0da51b271e8cefa166f87201b86ff86d8326d5e7843b9aca0082d6d894047d4de6332c0ee448f8ad1d39b80d3c7bed466887b377492cd9da008026a0a7b29d00b174bf81589fb9dce257b0ee0bf576e5f50c25eab5c94018ae43b8faa028ae64e94fc0d3edb60c6bbf73eee41c1a139c7d847ffda7bee0ed695e50fcdbf87201b86ff86d8326d5e8843b9aca0082d6d89474bf0b8f9dde2881722ad804f3e990943ab4d24a87b2aad3377914008026a0c60fd6f97b4725ae367748b75be35502df6330ac0119494beae965734b15e9dda052035553b5126705f2b7547aa585c4f74570120883db18baea9c90705dcbf559f87201b86ff86d8326d5e9843b9aca0082d6d894cb2c6ec0b1633f6f120e9c25d202c92f5e01d73e87b3270ac2a5a8008026a0c3e163466ae12fe93e432d085bea0dd84527e9c56ca20928670d401a86f7d4dca020dc959528df0851e8031a4ff2c2ea446a3aa51c6dd84c2df5435caa2d275022f87201b86ff86d8326d5ea843b9aca0082d6d894cf7d016cee2c17a278d6b37eba13ba7298a1fd4e87b4817044e386008025a04a4e50556cd06cb38eaa6ca7ea282b8d7d2f2178629dbdb73f7cf3b2be277790a0526e4483126e0af832e10b39f78501f6a98d5961e4c739c7be81e19db10db613f87201b86ff86d8326d5eb843b9aca0082d6d894e01b1a297712b493fb0482a4db1ff66c7d8b780587cbe0984cc6a2008026a0fbeb7d369d56049bb1cd66d233d92906ab04e85c096cdf78c21f5ca713ef9c61a00b9c6c43f86073601d960b0b3d6a075a111541b510ae6c50cc81f3dccb441031f87201b86ff86d8326d5ec843b9aca0082d6d8941d9f700dae0d61ff26d57f865537cc908e3a10cb87b639a7e378b6008026a0820cbaabd041ef611d6e3487d4d9115579a031c3151eb39b4a4daf703a1787e6a002faa3b4187998299137b5ccb3eba8f795c7cb5d0a01f34dc220250175d1dc98f87201b86ff86d8326d5ed843b9aca0082d6d8940199746c003d69354d8d026b77b0172ce1b709f787b20945f9a17c008026a07fa02a5e126e5d82ab181b261bea351994ea03b53bd4f05150f549b0d8327486a02d5e8366f5a5cd8636a7a70abd0b560f718f126a17bceacccb1af15bd31d90eff87201b86ff86d8326d5ee843b9aca0082d6d894c0069d92e82bdbffdbe924ff9f55cd6489fc7e7787b223a6c324aa008026a062d9b995ce2759a06626d1ff8e6b3575a7364b8174f08aba07fd442887f37c1da05fd7c38bc753f3a2779064f7edc50666be47b7c35ecb86a2b8f7be78c375968ff87201b86ff86d8326d5ef843b9aca0082d6d894eccbaefa8ab1397d5df4b43f0ccb9fb6bdd59c2c87b269d7b3355a008025a025b1d912f1475451d392c9c28a928dae1646d2d780efdcdb5be2dd46defc7b04a02d2111c2cf2e7173990a32771ff06d0313d62d40a27ce49f4833614a049c0881f87201b86ff86d8326d5f0843b9aca0082d6d89454b810fecd51b93bcebea4f6573bca49988c161387b2ea85be5fdc008025a0362cefdbc6e08889ee92f5fb8e507858f47ff3acc8fe9b706cde1ed6dfc51a1ba02d965d190d3772d1bbba1a87218d76cd014899add771606fbfa3bd740d4b4887f87201b86ff86d8326d5f1843b9aca0082d6d894edc6acb1f5ad2ad37dcdb394ebb9fc4cf464ebb587b1a9eff7f396008025a07d10203c5777b6cc46055539ad2bade8b99765e49f22577de09315291ac6544da0639b4dab720890a791910ac0f3e5628269a4e1e787f9bd098456290a1ed3202bf87201b86ff86d8326d5f2843b9aca0082d6d89423e90af807122e60d9c8ededdcfe9761a91c049087f4f60945960c008026a0d84ca1e9b6f74ba371811a0652d320995ab1f856767b99d5642384357db52b6ba0742b5833ac7d4a2c2165ad564636512c65c5cbbaf0c4e18a9a4e380d26161ffef87201b86ff86d8326d5f3843b9aca0082d6d894a60decd56245d914b1763c6384bb0c7aacfa971387b260a857e69c008025a0a90c5fcc9c27d0713c499d65f244adeaa7e86a60ed4fe7a4f59c3a81c123c87fa03915d83e0023d60ba5d8b77c900ee2bf17f3621b15168d4d4df350a6b3b8f216f87201b86ff86d8326d5f4843b9aca0082d6d894f59c7ad4c3c8b5abee6ac09e74bef61b8c036e1b87b4b7cee28dae008025a097b36394161220d1ab94610606adeac3672c5862e560eeb8c36d005935aa2e2fa021cee35e9aedda7e082d06471f723d72169606562de347fc2eaa061ca5c66b90f87201b86ff86d8326d5f5843b9aca0082d6d8948cf444ac46714a7227f4f8ad5cc5c400ffbef30187c64fc48dce46008025a041a872ce06b7c4c9f92ac30598ae221df28cd772bf0470bb6eb57a981a9395e5a05ba66e20b7768f21a4bbb1721245f596730d7cad6110170b499f1533ae2c966ff87201b86ff86d8326d5f6843b9aca0082d6d89436e60fe5558a9eb63bf46509c8c79879f0b864c987b484ab489646008026a019896113e8acc80473e093f93c22e086554f3aa746d86053408b670d2cfeb2d0a058253563d7671386426151e145a154bc1e9cb5fee5e048c28c1fafb4608fd049f87201b86ff86d8326d5f7843b9aca0082d6d894f0e1ed8c1f1883b4476169cadcd6fb5b91ccb60187b22b768b6fec008026a0570025b330607f52f91ce87071daa4780d13419c659b16748a1a501b17ddf9aca07330d2884ebbc4a0937faf1001a62cf9a25d949cf2a323fe3a51f76ebef31f4ff87301b870f86e8326d5f8843b9aca0082d6d8946516b01ba3a6c81e7fe54a9c03554d3aa2c219438801270f1a1f662e008026a0c3484221e570accc2764ecfcc13f5d7d043c7b0971dc4029682365a9c8b1f615a03e15dd9b9dd01997ca7d5043bcb568ed0d113056d6fdda7ca4c128b4e3ef77a0f87201b86ff86d8326d5f9843b9aca0082d6d89498a17d59a1f8a64a39a859df73966f23a71db7de87b24d5fa29e1e008026a087fc7fdaf8acb9ee6e96e887fbea8bf972be5dca2b4d520b9b60469977109eada048619ff72a2d66f4464c5392389c8444f71818786948b250b8dc781ecc2c3c9af87201b86ff86d8326d5fa843b9aca0082d6d894574ca4f7968d5dd3c4d0be82e418bdcbbb70501487b24a9b98e494008026a046491d3350e4a0c2fd4688dee51ea95da796147f3e91a7a8b268b78490e9a976a03b3c5c68d59424994af3c2280fcb0e19e48412151e1c58ccb0b7962dc2710b0bf87201b86ff86d8326d5fb843b9aca0082d6d8946248d6827950af1e12b8c5307896f717765c560187b2c4b85eeb44008025a0f5ca2f26cafbf4fa636b3779e67d3f72a2985993c93798b7f996f8381c6de792a0760b9644b0fc9c059706c52f581f632e5bfdead45bd2d1d5fa11c8dccccea3aef87101b86ef86c8326d5fc843b9aca0082d6d894c515204e177ac0ca63517e7054f658f64703714e87b9c074e60fb60080259f6b05837ce4b68d661ffdeff543c63c9ac4ca75de1412ea344cca93411cb667a02167c1ba5fb694d54ed2da9ecc99c046a7341cecda492de4d18bd38bde89218df87201b86ff86d8326d5fd843b9aca0082d6d89488fd21cac2d61b5dd558cba7e96847a7c834855787c2df0662aa68008026a0b73f60dee813179fc37e6f613e8960796a6b8dcd3039d9ebf6d4b44a8cbd3908a02444c077ef7234a12b51df1c1f4b5b3c4462555ce2796a010acd53507716302df87201b86ff86d8326d5fe843b9aca0082d6d89482cea4b4a9738b626d11fcbfd35dd6c1ce34189b87b5c131010fce008026a05afee5d41ed4068d34e3acc01ea81030c88b0e6fb598b48c06f7f6af267b8a5ea0589cdaca4fe7f800a1be0253f406f7d20d1724318abffda0ddb2657a1ec0281cf87301b870f86e8326d5ff843b9aca0082d6d8943989d8a563ce40362187481435102c82ce9d98828801195928bf6008008025a05af68fbf76cb369d72a3c7cae53f3488352472d71c3e033864571ded902b1475a05ddbe72397855b5eeb2053a243108efaaaf63ffe32d75e7972f80bb3cbe98376f87201b86ff86d8326d600843b9aca0082d6d8943ada35700dd750ad3672876f68fb184bef75cabe87b3df66768fc8008026a0ca72f4a0b164edbec49906c8c1edbce1c1b69675b087d6fef1a1a77688cea286a079ceddbf54135705531af74abf05a3f161407d9b2ffa8bd1c917e03365715074f87201b86ff86d8326d601843b9aca0082d6d894393dfb0c6e9cf0378e8fd02a7e8880aa571dff2787b372b28b6b08008025a032487afa2c9f90ad39d11ad36b1237f2d019bc5d449cfb1c58949d208b2c8913a04228729efce533d0d5dc4cbf872187767cbdd10983d4c711a7c34724f9c4f936f87201b86ff86d8326d602843b9aca0082d6d89487280f0cdf0a005c07778c8d6022dcb554ff383f87ba05dd9a21ce008026a03e0ae8f4ebe137ecff9f2c4c3e1d896bd1bdcb752967c4fdbcc7c7b5e28fa7a7a0714745147417f7707b8bc9b3473a1faba25d5ca9f64e2eb007afac97fc400764f87201b86ff86d8326d603843b9aca0082d6d89463d114df37ffad489cad8d3371b5f1dc8c64462787b23bfef0ab80008026a06f6c5332e3e7d20fa2e20afab127f27ac55f04994ad17fd7d12640c227dc71f5a00e9b5d843dd44c171cff370b1a23a20c7ca423754ef189a2466199af8683de84f87201b86ff86d8326d604843b9aca0082d6d8948babc460731cc581bf7cc2d819069cb747077d5587b748abb1f0b6008026a0d5e155a9a0977830a6bb448a903a8dc0e9c5b21f390dac1fec51dbd912300bf1a0337f88c4b14f592eab51ba7ec102d5597da54847ec6fba452a085c8bcca964def87201b86ff86d8326d605843b9aca0082d6d89497cef61a58e4e65c6a580b3c53185da00f9c0dbf87b4ac5f45c002008025a0c4c9752a503bdd5fb2544270360c5fdd6867aff33244b096784c3cee0335c8dda06cf98b80ef96010616fd8470beea0f47d4a2005d61f0b961fa01ff7ce3b64c1bf87201b86ff86d8326d606843b9aca0082d6d894a2a4abf822f5f573aaf6674889b2a2101506a73187c6808d96f396008025a03c5590e497ca013a120286e599985c652c5523481be1036f1bdf5ca7a5f0433aa05de48c43b329027457773d69b7644ab27b6b61d9d11e051f465258bbfebf7183f87201b86ff86d8326d607843b9aca0082d6d8948fc2bc46767bf867e4da644330be8cf84f85fc7887e0dc4246fb18008025a04b55ca7a8b029482a1cfa2088ccb1c3e8a74127ec80604f3905766902da065fea064fdf1f2a2b022fca2269cdde3042341947514b8c76fecaf8db9b247293734e5f87201b86ff86d8326d608843b9aca0082d6d8947d1230e858b8120c71aef78554aafed7b58e06b787b33b6d6d27b4008025a053e9abcf9741cf7762e6feb5da14ae701e416207c389e405d4b0dec37ae20ecba00cc41e8e1d1329731ab1a3b88ad6b38fbb774eac9aaae89b783050af351f32c7f87201b86ff86d8326d609843b9aca0082d6d8946c86638ed2c4151ae4a43ba162b9fd720d5be50287b74ac729e6f8008025a02efa9c704887476ff0826d83248022b5d75a80061e1b8d9fdd62c97d9c172cc4a0135dd5f1924f8b465e5ddb7d8e3c422c73852e10faeaad8e8b621d75a5359793f87301b870f86e8326d60a843b9aca0082d6d894cd2dcbf1383da77124f5c4d12e14671600eb42e588027cdc312fb6e2008026a061c42aeb0046f61cbe3266b27e6d83a117170e4cee10c64ff1d798f0fd7393e2a0321ca9e2853d618aa71639bd4638657f869b469a878367405e2c567bb79617b0f87201b86ff86d8326d60b843b9aca0082d6d8941e348b2185e0018c8cdd4b003b55437e8f64443187b2467a503372008026a08511cca8072fd7984842f1479fd29535a018260713ba4366e87592d561407603a011fa59069eabc78f83259e11a26612e4ea5da1282e61ab0f2ca53d365b43b87df87201b86ff86d8326d60c843b9aca0082d6d8943c2f193d0973656eb3a1390545c4eb8f68df629e87b8073a23824c008026a036932646f5c94125cad5040af9da3da292ad6797082c5c26590eef90ae96564ca069432a8675cb0e407e7c41b426e4b40eb7b05752c3deb94be2f113d9ea39defaf87201b86ff86d8326d60d843b9aca0082d6d89457181c7dfdcc31fa45240fa03645cd21bb44ac7487ed0d4ab53f70008026a0ccaf3aa58f0c7b46f240c368234c7f9d7cd01f574238049365d3932efb351fada04d77ef557bd123a017f270e575c6afb66c6c5cab6e46931a29e82e1435452239f87201b86ff86d8326d60e843b9aca0082d6d89474949b031457f2209d98dbaf5f17116a3566711e87b347049ebf84008026a0b12cf1d389b867519ac6ecba025f8f811470cdfcba19ef412a77643004be83cea02a4ac99bffe8995085add5835ba9247022aeed111b63054450130ba22dffc76bf87201b86ff86d8326d60f843b9aca0082d6d8948b85cbcd1678860bec299761fb141263a97a30ff87ba40db3aff30008025a0cf983e55aa23db979ef91cd3e7dbc5da4b2c476d00f475fb80c9c6e7d338d4f3a0536321b678477b66c0b9febc2e7215c17b18afab3fdbc817be9420a0ed41f28df87201b86ff86d8326d610843b9aca0082d6d894a8a617ae1020ed32ab6fc814782cdf6b4ea217a787b88fb29c0fea008026a0231b02b58923fc39ebde3f56f702c0bb20be58d34c3b6e203e1c9df73cfeb00ea0086243ca7dc44c7a958b85a29df4dd04a514d6428f8a79c805ff3ecea2c199fcf87201b86ff86d8326d611843b9aca0082d6d894b8e46d96ab16b0587fae7147b75d36a05186197c87b31c7c8723cc008025a0e2b36ebdc405f9ae6710b1d73eaaa87488d7f252cc2e90f3c0b5bea21cae30b8a07db1b8af8fbffe4807327a412d87ecf06e45d810569816ba69a62542c6256369f87201b86ff86d8326d612843b9aca0082d6d89435ca2c52bb6beb3396f35e94e38087d8a6416e4887b42830d4b39c008026a0339f5eb25a199160ef90e098f5be784b467797f9d90b63b9234cdbd43814092ea0519a5b327c4f2768ca54c4c7a9616b1d2d623a1de55d7945d4c3197553fddc23f87201b86ff86d8326d613843b9aca0082d6d894540c97c6af0e81787b582a4290706af1da44dabb87b24c5f49e75c008026a054b08aabcc92f494b15083eef4daa424b69062c08e0edf8fb764b982a6d0e688a014d0f70a16957c687ab3791fb891c8f7246170011a5b68b25d762cd2880c23eef87201b86ff86d8326d614843b9aca0082d6d89496e83ba15c7d62deafb3ca658a2fca092535414b87b5db2bcfbd60008026a0a95ddd857761fd995eb9dd0bbf6a1834af4e8917de252d26b0c4df79f5e0eb4ba01dbcc6aba23e5889434f205e00d10926115520ccb758661c5c5461345a70d01cf87201b86ff86d8326d615843b9aca0082d6d89410eb7d24d3eebd58d9080d4b4e48e708ba876b2887b59f825aa5aa008026a0a6054f7a6f06997ce03892bc9afeecd3f86bb3d8291d02e936adf4caa71f493ea032c2282fc408616d4cc5a8eb17029ca8233c8206cc5bcb90e7e7122d1ddaa92ff87201b86ff86d8326d616843b9aca0082d6d8942b465d78966bb3631cce54c1654ec5f3bccfd8de87b5bcb7f16b32008026a03c40274be971681d58772a76d3b90c1a49d8e96ee11de7c1743dbca9dc9457c2a0617d13c22e8cabc2e21b0336e3c961f6e0e0122b5f882dc584c3410bd53b4f0ef87201b86ff86d8326d617843b9aca0082d6d8948ac6ab4177e84fd8086e7a18bfe5ed8b33a3d1f087b2819a668d9c008026a0e462b8cb162ac44892aae4150544efca70bd851d2d956dffdd8c00a1353ada84a01e956bb942ea283831885d59bc9a6b066764fa7ab4f71fc8bae911a5c2033d52f87201b86ff86d8326d618843b9aca0082d6d89400061796c82d74ea8c0479d26751586e1b696fa787b96d9257f814008026a05ddf883a57ea923dc360a12a5efdcbdbe02dfb559b970082aaba844ab31dbb1ba05b46cca276ab1b5947daa70269389678d2df8f8f42faa9a4fc6d0b7f9c394849f87201b86ff86d8326d619843b9aca0082d6d89403478f32953dad286e2e047c97ece138d004219287b291eaeaabd0008025a078e2ed8db226b8021dcd8c8a4ecde7dcb0aabb592397d359d517035e5b5182bba0398fb05c7f006ce8ee2ea30e998845ba7172d2788a5e5a03ea3bcd3101adcbcef87201b86ff86d8326d61a843b9aca0082d6d894ad2e9c664519b9098daaee62b7b60c6adbc9082087c26cab310cd8008026a0bbefd1d201dddffa9294bd33b77676d3c014e158734700e3ae75ad33434c1227a06806ced27eb4d0b3de01735cfa7eb0a385fb62d4ddf652283a58cd4d84905ee9f87201b86ff86d8326d61b843b9aca0082d6d894c439467737fb023520c57844c47e2cfa4a46fbf487b9378c6fac8e008025a06ef24f4a0b133163579471401bbaa82e38b9d1f763d16f964f339f787194e484a066c89df50f3db3f3504bf037a922f0fc8d55b653d841f715922f1e83b33dbd4cf87201b86ff86d8326d61c843b9aca0082d6d894a7b1d8a9c71251318b47fbbc2ef6e45b7040d78d87b63d5b46c568008025a0df6e89404fef716940bce4a3bd010bb1fe3e3b23ce8feeebb65a6ce5cda32f11a0693c1ff4103a6b126475e72fe28e3f349b672a8f27b45c0ab80203265759e1b4f87201b86ff86d8326d61d843b9aca0082d6d894256277274aa78e0c311872d95ffe830e6d663bcd87b535553d34ee008025a0a3cd71ffb86e2b432f4176598823d093c08b39f804278429cbc3f60e08621ebba0441168abdcc808986ca82e250f52affd4a8144a17ea2378fe2a09b9d575e8602f87201b86ff86d8326d61e843b9aca0082d6d894a930a473caa41ecec1396c53d82486e287e2aa4487b6e2db77fb1c008025a052150b2c3b2527f77bf0614718a4d0842510c0e29c126e4d53d0ee721478dc3fa05c0fed67b5ecd4ea8daa33bf3bda55fe19f5d8c15f05d9af95f0f96cdf95ab1af87201b86ff86d8326d61f843b9aca0082d6d8946c359771de273714b4410cccbc08a91e55792dd687bf4983ce27e6008025a00bb42661a0bb3938ce3a4e44b01dbc15c6198638d8c64665218e600df1076785a011bb288121cd03266240693e7c66cbb2a9857ca134215af10df6049b0d6f54fdf87201b86ff86d8326d620843b9aca0082d6d894eaa41c8115c4c40420eae295ac699af83fdf145a87b1d32f8967f0008025a03b0f1c6cfbc8594001d6023c2c3571b8db8b78b0587d81ab2c3d1bbeb5f24217a01deb1e6e5e23adefde7f15d8e31479a9c05f98b9963bc86abd03706139076dcef87201b86ff86d8326d621843b9aca0082d6d894986c6fa3909d066fddb5a8c5b315be7a139d5f5687b21151211bf4008025a070bdf4f37d8ef596628c1290c4a1df9819b8230b649fb6a329d2fe10b174d203a00de7fcc1468f890beab81a56e071a29cd8ad464a148a7a92d40d72e891bee02cf87201b86ff86d8326d622843b9aca0082d6d894ff24607de564e9bc0310a298a0fc7030251bd30287b2598bd72eee008026a0730203b963f0c340544d2ca2f0659c9f2b7328987760d6bf07b9f692ae09f07fa052b050f60d42e7b45b1447a8207655e4a236ce3f84299ed9fb95980131acfe13f87101b86ef86c8326d623843b9aca0082d6d894ff787ff914bbbbfd9b2e0ea8c7f2f44397af12bc87b253ac73fdf4008025a0160d0a04c530b6cb95c1a9586693596bdb248544a1df4c2aa336643853ecb57b9f1b4a6f8a55664d5a028cd99f881a33ef2a34e8bcec458567276110eb5db9d1f87201b86ff86d8326d624843b9aca0082d6d89411475cb4cfdfbb368ba73b33297087097181693187b4e7afc9de4c008026a07cfec25298a3e8b2a61259ef4fbf7eeeb745cac2738f7677b3c500f67b1d1d34a06a17dd575935ddeef535e097dcba43bc5f1a3bb1459f316c3732a7bea8006796f87301b870f86e8326d625843b9aca0082d6d8942ec6762464facc666471ac1133c9ebec4fe4311188038d328cffe516008025a027e84ab40ec529b0b56edd29d7ec37d06ce1bb1d96f30a093de82b7e6e66329da02476ca7a2673f0e5ce32d338e87c92358a38dd12d1bc2b02734ae1ee16c8d8a5f87201b86ff86d8326d626843b9aca0082d6d89493d8bb63b44b22a53a766207b9bdf533c358deaa87cb2c853abcfe008025a02029b0a2ead42188bd5685179244273e5aa3c57afb3c81b2f7f207e25eba11c6a040afa6819c8ca2e5448d0781bec9eb212b3893eb51b9de210d937fb0938f88c6f87201b86ff86d8326d627843b9aca0082d6d894e4c418524e2cb968048faa077cc13844e9e530d587b1d2afb674be008025a0a2c999d8f1d14c5f9a1dab13317f8d7fbca672827760f112220fd5b9d5be225aa047766f283fdff687921999daf1a53c2d295a98bad428dd9a359ea9ec6bca9c02f87201b86ff86d8326d628843b9aca0082d6d89412d613d8bee41fd81ae0013f856e09195b35cdd187b441248a218a008025a064b64c1080195d97101a5d14c088df699c72fbc81a6b3be08e06ed35fdcb4c83a04219c54acabc7b7c3276a6b511431077dd19f1bd6c7a21bcd7e19558cb3f6bbbf87201b86ff86d8326d629843b9aca0082d6d894528fdb044c299150f9bd13eba915009dbd5ad46b87d22659be8b90008026a07c573344050e4f92c4fd39b13550f7ed08fe07a7afcaebaa23d488face6a1a2ea0216e86e06295908f5b17655a9d19ccc2c0e092124e8855cf724f4aaf6e138b07f87201b86ff86d8326d62a843b9aca0082d6d89468f9981f038db5938fb1e6d9d3f3570c4188283f87b68e858c7aec008025a0e0e4eb600626678e2b1ed90d7caa2686130e187c50e1f4b660a2fea13e4932caa040ea3d498e62f0e7b78a93cb28375f5687667c035a80b5b8a9b355adfe2e4c3af87201b86ff86d8326d62b843b9aca0082d6d89497b6930fdd8521890f4aa1f120894d06b78323e587b1b4a3eb6946008026a0b25e14053e7be2e029a7645a055a9357be37700e92bca6f3ff769ae96d8ae67ca05d09e5d70d1e7222fce3d546838a4bbef3630fd896c201aa5fccc66f79b74b68f87201b86ff86d8326d62c843b9aca0082d6d894e51843a32e731d2ada96e851e7fe3d5930959a9287c1616cef0b16008026a0299a902acc8debfbde5914b4dc547b55b13eb71c8d7fe3baf6d412fadf3b304da02b6ed43219182e87475666d51691c5fec8fd3a530678afe2958cc9d69307269df87201b86ff86d8326d62d843b9aca0082d6d89450b83ff168d46d467f74965018019860517305e087b1af8946250e008025a08076b03fdd8820bc9641f0e87d6aad5c8cad86475b615feb0afcddfe5b02b236a0666a5db454cc05c5bbb5c82f91d76b3a8d8fa1416140b990b09da051eb9f4df7f87201b86ff86d8326d62e843b9aca0082d6d8943051e40cf5bf175ffb5e1ee786ff7cfdaef2d3c887b2e22f9e3350008026a036ebb790e474558264641ad6009c23c70f9fef123f95d163a25dc82cf5a8862da07a7b9285cd90934988f20f4ceb655ff74d4d1c1aba9f6357ca14a6c8e73c521bf87201b86ff86d8326d62f843b9aca0082d6d89444873b01fc9bda50ecc2c213a3941f5d4fe3605687b45f9baaeac4008025a04e9bdcd3a9b755b3999cc4c15fb9888c353411571fdaf4621c555682768c9ee3a06257fa72a3488544916a4b855132fdb2dfcdad7432525408a5ce3c544864d1b3f87201b86ff86d8326d630843b9aca0082d6d894170e2ee57832697ef73576f85d2464cf42ac9b8d87b2a31d474b68008026a003fe3dd2436e1973f7882f69436ce407a126a26f2b889ba94ae0490ab6d1c2c9a03d8fcc7a99b40131306456fdb6fe293c3d2686d4147efb3d938cd989607f0591f87201b86ff86d8326d631843b9aca0082d6d894eacd509f8fb951074f36ec2ae60b1344f6f6917d87b29f561999a4008025a0fd46f5f05f80eb87ca791bbc710883e2df40db90d3bbcd041da3301931909f97a0432d79412bcb0dd4589610a2a4655f93d3109246a95d805b916510ec4b8dc70df87201b86ff86d8326d632843b9aca0082d6d8946e45412f9cb5f2390decc8c1aac548383e8ac69787b1c8803e0a08008026a0d53739c6b28119ed9f41bd4f247b462c8ee19912b8554d7325388588dbc8312aa01d7927551300cf5e787792af4994e5e52aab5b895c091a1725b738dc2655c722f87201b86ff86d8326d633843b9aca0082d6d89407ff45d1eca3722e58f252482acb5ae5760e8bed87b7215854b470008025a0d36862d80246a34dc3ea09d690e1e80697664556fd3efd3e36dec71fa03c0f2ea06e5b6492083abcade71fd1c350cca4548a6c20546eb71626d98ca2a47abdbcccf87201b86ff86d8326d634843b9aca0082d6d894af73512855e5fc6f6e2f4ea3bd7c73553e7c22f187b39a5ae45950008026a0bf4ec1a0716490a8a47d69c6ebfb29b1cfa0433b356803189ac634d6a3f74988a071f8242dc19fb09c744a48f984277754361d4af8f387c161a3f77fa3ead91ac6f87201b86ff86d8326d635843b9aca0082d6d89418cb8b10ec1b4009d1a60447792b33a0330fe07a87b43f00b066e0008025a08b85033c16364503644359d9aba9ce434ad866ce35396cdc54287eb222a85d0ca020a00fae027309db728909bc12af4b08a3278cfb91a9d6d824629d454c0d2c00f87201b86ff86d8326d636843b9aca0082d6d894444a8fa84b6f31e65492db8d61398121edbaaa0587bdb3cac0ef7c008026a0ce46e68b43438a178b398ef875758b8ab1920d44c594090adaf040ad0e9c7604a0132aaabb36fd94a45d157f9ec3d2482611f31a6d4ba2b0ed9631b4775e0f81c5f87201b86ff86d8326d637843b9aca0082d6d8948279addf81064c2c97d3bcb2b903813db59850d987b23c9ee50f96008026a029420083e7157e875ea15d5cef5923f4bce5eb920c105a8bc3eae29c6918fe74a02efc673876995413462cb11d50458975b80d94b9f34c7fd7595e7e6b833d472ff87201b86ff86d8326d638843b9aca0082d6d894d5d04ab4a1e7183bb15d4e8b7f276fb92493da1b87bf226f1291e2008026a0dd804e26ed9244f2cbe73ddd8080fb58afaf68d7db471446b132871cccd23466a01b79790fd60fd1c00074fed0111a987aa262b0cd454faf5497fe2709409049f9f87201b86ff86d8326d639843b9aca0082d6d89482ecd292cfab8aad4113c3343332e222d0482b4387b8de23524166008026a0454d9cc0b85f245e9ff5c81419e5e5d1fe072e598067a0dc89e8b04a73a7e371a0791c85f96d3dea7ee2a5bca3ce07f601ee8298ced510bf03088807449888ba96f87201b86ff86d8326d63a843b9aca0082d6d8946348d209b35ca7681eb0a68236957c7825be8df487b6de5b6c32d4008025a049c36811ba9e645ac35156f3fde231643e5eb57cadb42fa41173426a3a122b4ba04a4ef6f485f799f3cb4c8bc856f239ecc4ee83b7e3489a38cccdd543453439e6f87201b86ff86d8326d63b843b9aca0082d6d894a6226997367257ffb4fe3bb5464b876cca6bec8a87b48efc48129c008026a02121f3a60ce29dbd2a5a8bbf6932cafb9f7fca07f3f132eaa3bf3aee2d45d1a3a0433cb6e565ffc179a441e685f1e3cf5074ffb6ac720d3cde48ff0fd21997ac12f87201b86ff86d8326d63c843b9aca0082d6d8948c84427ce288ca9e18d309d7624184840ca660ce87b24e4e0dc61e008026a0b914904890f9a0607f9c74188dae49557a91bc2be149ee93e65e54c20427e47ba06efbd3f9085148a6a098abb198171d50a334e2f581cc880267231de8cfb3474ec28080c28080c28080c28080c28080c28080c28080c28080c28080c28080c28080c28080c28080c28080c28080c28080c28080c28080c28080c28080c28080c28080c28080c28080c28080c28080c28080c28080c28080c28080c28080c28080c28080c28080c28080c28080c28080c28080c28080c28080c28080c28080c28080c28080c28080c28080c28080c28080c28080c28080c28080c28080c28080c28080c28080c28080c28080c28080c28080c28080c28080c28080c28080c28080c28080c28080c28080c28080c28080c28080c28080c28080c28080c28080c28080c28080c28080c28080c28080c28080c28080c28080c28080c28080c28080c28080c28080c28080c28080c28080c28080c28080c28080c28080c28080c28080c28080c28080c28080c28080c28080c28080c28080c28080c28080c28080c28080c28080c28080c28080c28080c28080c28080c28080c28080c28080c28080c28080c28080c28080c28080c28080c28080c28080c28080c28080c28080c28080c28080c28080c28080c28080c28080c28080c28080c28080c28080c28080c28080c28080c28080c28080c28080c28080c28080c28080c28080c28080c28080f901fa01b901f6b901f301f901ef01820b1085055ae8260083124f8094b534f99a38cba55696a6bb910626cafddcacd0df80b90184c5d404940000000000000000000000000000000000000000000000000747a767c4dea5340000000000000000000000000000000000000000000000000011fa0b0e9040000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001b00000000000000000000000000000000000000000000000000000000000000a00000000000000000000000000000000000000000000000000000000000000002000000000000000000000000328dfd0139e26cb0fef7b0742b49b0fe4325f82100000000000000000000000041d5d79431a913c4ae7d69a668ecdfe5ff9dfb68000000000000000000000000000000000000000000000000000000000000000000000000000000000000000073e02eaab68a41ea63bdae9dbd4b7678827b2352000000000000000000000000c02aaa39b223fe8d0a0e5c4f27ead9083c756cc20000000000000000000000000000000000000000000000000000000000000000c080a02205645b2edfaeb0205d74adec1038b66c57f02886c9a918c78961b1e028f98ba01f6f3bbe3097fac98acf7e6e3ab570be5585c1dcbc7c886b2bd30caf12e9b3aec28080c28080c28080c28080c28080c28080c28080c28080c28080c28080c28080c28080c28080c28080c28080c28080c28080c28080c28080c28080c28080c28080c28080c28080c28080c28080c28080c28080c28080c28080c28080c28080c28080c28080c28080c28080c28080c28080c28080c28080c08a056a6bbc71af3c6b65e483bf73f8bd0000006f7b4a4e217b4a4e5a7a4a4e167b4a4e417c4a4e857a4a4e4d7c4a4e3d7b4a4e4a7b4a4eb8794a4eee794a4efd7a4a4eab7b4a4e197c4a4e8d7c4a4e477c4a4e3b7c4a4eeb794a4eaa794a4e5c7a4a4e7a7a4a4e897a4a4ebd7a4a4e1c7b4a4e4e7b4a4e647b4a4e7c7b4a4e917b4a4e9d7b4a4eaf7b4a4ecb7b4a4eeb7b4a4efc7b4a4e097c4a4e167c4a4e237c4a4e3f7c4a4e507c4a4e637c4a4e737c4a4e8f7c4a4e9e794a4ee4794a4e0a7a4a4e1b7a4a4e227a4a4e277a4a4e347a4a4e4d7a4a4e837a4a4ea37a4a4ea87b4a4e877c4a4e387c4a4ee2794a4e247a4a4e267a4a4e707a4a4e937b4a4e4f7a4a4eda794a4e847a4a4e747c4a4e767c4a4e797c4a4e7b7c4a4e7e7c4a4ec2794a4e8b7a4a4e307b4a4e147b4a4ebf7b4a4ee1794a4e097a4a4e0d7a4a4e217a4a4e3c7a4a4e727b4a4e8c7b4a4e697c4a4e847c4a4e117c4a4e577b4a4e687b4a4ebe794a4eae7b4a4e2e7c4a4e207a4a4e7f7b4a4eb2794a4ee6794a4e037a4a4e447a4a4e2a7b4a4e4f7b4a4e507b4a4e6e7b4a4e5c7c4a4eba794a4ebf794a4ee8794a4ef0794a4e2a7a4a4e2b7a4a4e317a4a4e4a7a4a4e4e7a4a4e627a4a4e657a4a4eee7a4a4e1f7b4a4e2e7b4a4e467b4a4e547b4a4e597b4a4e8e7b4a4ea97b4a4eca7b4a4ece7b4a4ecd7b4a4ed07b4a4ecf7b4a4ed87b4a4eda7b4a4ed97b4a4ede7b4a4ee07b4a4ee57b4a4ee77b4a4ee67b4a4ee87b4a4eea7b4a4ef17b4a4ef57b4a4e017c4a4e127c4a4e1a7c4a4e487c4a4e597c4a4e5f7c4a4e7d7c4a4eec7b4a4e787c4a4ef3794a4e6a7a4a4e727a4a4e777a4a4e5a7b4a4eb17b4a4efa7b4a4e777b4a4e8a7b4a4ec87b4a4edc7b4a4e067c4a4e1f7c4a4e927a4a4e787a4a4e287c4a4ea3794a4edd794a4eed794a4e237a4a4e3e7a4a4e487a4a4e587a4a4e877a4a4ea57a4a4ef47a4a4e6d7b4a4ec07b4a4e137c4a4e217c4a4e7c7c4a4e617a4a4e9f7b4a4eee7b4a4ea47a4a4e5c7b4a4e967b4a4ed57b4a4e687a4a4ea4794a4eae794a4eb4794a4e147a4a4e6f7a4a4e717a4a4e817a4a4e01" + blockHash = "4c097654d1b901b3b9d616237792557fe1b466b7159856c1662eff78b8df2f7d" + messageType = "blck" + networkNum = NetworkNum(5) +) + +func TestEthBlock(t *testing.T) { + block := BlockNotification{} + _ = block.WithFields([]string{"hash", "header", "transactions", "uncles"}) + //unpack + +} diff --git a/types/broadcastresults.go b/types/broadcastresults.go new file mode 100644 index 0000000..f9cf086 --- /dev/null +++ b/types/broadcastresults.go @@ -0,0 +1,14 @@ +package types + +import "fmt" + +// BroadcastResults represents broadcast msg summery results +type BroadcastResults struct { + RelevantPeers, NotOpenPeers, ExcludedPeers, ErrorPeers, SentPeers, SentGatewayPeers int +} + +// String returns string of broadcast result +func (br BroadcastResults) String() string { + return fmt.Sprintf("relevant %v, excluded %v, sent %v, gateways %v, errored %v", + br.RelevantPeers, br.ExcludedPeers, br.SentPeers, br.SentGatewayPeers, br.NotOpenPeers+br.ErrorPeers) +} diff --git a/types/bxblock.go b/types/bxblock.go new file mode 100644 index 0000000..88cbdd9 --- /dev/null +++ b/types/bxblock.go @@ -0,0 +1,120 @@ +package types + +import ( + "bytes" + "encoding/hex" + "encoding/json" + "errors" + "math/big" + "time" +) + +// BxBlockTransaction represents a tx in the BxBlock. +type BxBlockTransaction struct { + hash SHA256Hash + content []byte +} + +// NewBxBlockTransaction creates a new tx in the BxBlock. This transaction is usable for compression. +func NewBxBlockTransaction(hash SHA256Hash, content []byte) *BxBlockTransaction { + return &BxBlockTransaction{ + hash: hash, + content: content, + } +} + +// NewRawBxBlockTransaction creates a new transaction that's not ready for compression. This should only be used when parsing the result of an existing BxBlock. +func NewRawBxBlockTransaction(content []byte) *BxBlockTransaction { + return &BxBlockTransaction{ + content: content, + } +} + +// Hash returns the transaction hash +func (b BxBlockTransaction) Hash() SHA256Hash { + return b.hash +} + +// Content returns the transaction bytes +func (b BxBlockTransaction) Content() []byte { + return b.content +} + +// BxBlock represents an encoded block ready for compression or decompression +type BxBlock struct { + hash SHA256Hash + Header []byte + Txs []*BxBlockTransaction + Trailer []byte + TotalDifficulty *big.Int + Number *big.Int + timestamp time.Time +} + +// NewBxBlock creates a new BxBlock that's ready for compression. This means that all transaction hashes must be included. +func NewBxBlock(hash SHA256Hash, header []byte, txs []*BxBlockTransaction, trailer []byte, totalDifficulty *big.Int, number *big.Int) (*BxBlock, error) { + for _, tx := range txs { + if tx.Hash() == (SHA256Hash{}) { + return nil, errors.New("all transactions must contain hashes") + } + } + return NewRawBxBlock(hash, header, txs, trailer, totalDifficulty, number), nil +} + +// NewRawBxBlock create a new BxBlock without compression restrictions. This should only be used when parsing the result of an existing BxBlock. +func NewRawBxBlock(hash SHA256Hash, header []byte, txs []*BxBlockTransaction, trailer []byte, totalDifficulty *big.Int, number *big.Int) *BxBlock { + bxBlock := &BxBlock{ + hash: hash, + Header: header, + Txs: txs, + Trailer: trailer, + TotalDifficulty: totalDifficulty, + Number: number, + timestamp: time.Now(), + } + return bxBlock +} + +// Serialize returns an expensive string representation of the BxBlock +func (b BxBlock) Serialize() string { + m := make(map[string]interface{}) + m["header"] = hex.EncodeToString(b.Header) + m["trailer"] = hex.EncodeToString(b.Trailer) + m["totalDifficulty"] = b.TotalDifficulty.String() + m["number"] = b.Number.String() + + txs := make([]string, 0, len(b.Txs)) + for _, tx := range b.Txs { + txs = append(txs, hex.EncodeToString(tx.content)) + } + m["txs"] = txs + + jsonBytes, _ := json.Marshal(m) + return string(jsonBytes) +} + +// Hash returns block hash +func (b BxBlock) Hash() SHA256Hash { + return b.hash +} + +// Timestamp returns block add time +func (b BxBlock) Timestamp() time.Time { + return b.timestamp +} + +// Equals checks the byte contents of each part of the provided BxBlock. Note that some fields are set throughout the object's lifecycle (bx block hash, transaction hash), so these fields are not checked for equality. +func (b *BxBlock) Equals(other *BxBlock) bool { + if !bytes.Equal(b.Header, other.Header) || !bytes.Equal(b.Trailer, other.Trailer) { + return false + } + + for i, tx := range b.Txs { + otherTx := other.Txs[i] + if !bytes.Equal(tx.content, otherTx.content) { + return false + } + } + + return b.TotalDifficulty.Cmp(other.TotalDifficulty) == 0 && b.Number.Cmp(other.Number) == 0 +} diff --git a/types/bxtransaction.go b/types/bxtransaction.go new file mode 100644 index 0000000..aa7461b --- /dev/null +++ b/types/bxtransaction.go @@ -0,0 +1,156 @@ +package types + +import ( + pbbase "github.com/bloXroute-Labs/gateway/protobuf" + "google.golang.org/protobuf/types/known/timestamppb" + "sync" + "time" +) + +// TxContent represents a byte array containing full transaction bytes +type TxContent []byte + +// BxTransaction represents a single bloXroute transaction +type BxTransaction struct { + m sync.Mutex + hash SHA256Hash + content TxContent + shortIDs ShortIDList + addTime time.Time + flags TxFlags + networkNum NetworkNum +} + +// NewBxTransaction creates a new transaction to be stored. Transactions are not expected to be initialized with content or shortIDs; they should be added via AddShortID and SetContent. +func NewBxTransaction(hash SHA256Hash, networkNum NetworkNum, flags TxFlags, timestamp time.Time) *BxTransaction { + return &BxTransaction{ + hash: hash, + addTime: timestamp, + networkNum: networkNum, + flags: flags, + } +} + +// NewRawBxTransaction creates a new transaction directly from the hash and content. In general, NewRawBxTransaction should not be added directly to TxStore, and should only be validated further before storing. +func NewRawBxTransaction(hash SHA256Hash, content TxContent) *BxTransaction { + return &BxTransaction{ + hash: hash, + content: content, + } +} + +// Hash returns the transaction hash +func (bt *BxTransaction) Hash() SHA256Hash { + return bt.hash +} + +// Flags returns the transaction flags for routing +func (bt *BxTransaction) Flags() TxFlags { + return bt.flags +} + +// AddFlags adds the provided flag to the transaction flag set +func (bt *BxTransaction) AddFlags(flags TxFlags) { + bt.flags |= flags +} + +// SetFlags sets the message flags +func (bt *BxTransaction) SetFlags(flags TxFlags) { + bt.flags = flags +} + +// Content returns the transaction contents (usually the blockchain transaction bytes) +func (bt *BxTransaction) Content() TxContent { + return bt.content +} + +// ShortIDs returns the (possibly multiple) short IDs assigned to a transaction +func (bt *BxTransaction) ShortIDs() ShortIDList { + return bt.shortIDs +} + +// NetworkNum provides the network number of the transaction +func (bt *BxTransaction) NetworkNum() NetworkNum { + return bt.networkNum +} + +// AddTime returns the time the transaction was added +func (bt *BxTransaction) AddTime() time.Time { + return bt.addTime +} + +// SetAddTime sets the time the transaction was added. Should be called with Lock() +func (bt *BxTransaction) SetAddTime(t time.Time) { + bt.addTime = t +} + +// Lock locks the transaction so changes can be made +func (bt *BxTransaction) Lock() { + bt.m.Lock() +} + +// Unlock unlocks the transaction +func (bt *BxTransaction) Unlock() { + bt.m.Unlock() +} + +// AddShortID adds an assigned shortID, indicating whether it was actually new. Should be called with Lock() +func (bt *BxTransaction) AddShortID(shortID ShortID) bool { + if shortID == ShortIDEmpty { + return false + } + + for _, existingShortID := range bt.shortIDs { + if shortID == existingShortID { + return false + } + } + bt.shortIDs = append(bt.shortIDs, shortID) + return true +} + +// SetContent sets the blockchain transaction contents only if the contents are new and has never been set before. SetContent returns whether the content was updated. Should be called with Lock() +func (bt *BxTransaction) SetContent(content TxContent) bool { + if len(bt.content) == 0 && len(content) > 0 { + bt.content = make(TxContent, len(content)) + copy(bt.content, content) + return true + } + return false +} + +// BlockchainTransaction parses and returns a transaction for the given network number's spec +func (bt *BxTransaction) BlockchainTransaction(extractSender bool) (BlockchainTransaction, error) { + return bt.parseTransaction(extractSender) +} + +func (bt *BxTransaction) parseTransaction(extractSender bool) (BlockchainTransaction, error) { + // TODO - add support for additional networks + + // for now, since we only support Ethereum based transaction + // we are not checking but parsing as if the transaction is Ethereum based. + return EthTransactionFromBytes(bt.hash, bt.content, extractSender) + /* + switch bt.networkNum { + case EthereumNetworkNum: + return NewEthTransaction(bt.hash, bt.content) + default: + return nil, fmt.Errorf("no message converter found for network num %v", bt.networkNum) + } + */ +} + +// Protobuf formats transaction info as a protobuf response struct +func (bt *BxTransaction) Protobuf() *pbbase.BxTransaction { + shortIDs := make([]uint64, 0) + for _, shortID := range bt.shortIDs { + shortIDs = append(shortIDs, uint64(shortID)) + } + ts := timestamppb.New(bt.addTime) + + return &pbbase.BxTransaction{ + Hash: bt.hash.Format(false), + ShortIds: shortIDs, + AddTime: ts, + } +} diff --git a/types/bxtransaction_test.go b/types/bxtransaction_test.go new file mode 100644 index 0000000..5578520 --- /dev/null +++ b/types/bxtransaction_test.go @@ -0,0 +1,94 @@ +package types + +import ( + "encoding/hex" + "github.com/ethereum/go-ethereum/common/hexutil" + "github.com/stretchr/testify/assert" + "strings" + "testing" + "time" +) + +//example of real tx +//{"jsonrpc":"2.0","id":null,"method":"subscribe","params":{"subscription":"65770230-8550-46cd-8059-4f6e13484c83", +//"result":{"txContents":{"from":"0x832f166799a407275500430b61b622f0058f15d6","gas":"0x13880","gasPrice":"0x1bf08eb000", +//"hash":"0xed2b4580a766bc9d81c73c35a8496f0461e9c261621cb9f4565ae52ade56056d","input":"0x","nonce":"0x1b7f8", +//"value":"0x50b32f902486000","v":"0x1c","r":"0xaa803263146bda76a58ebf9f54be589280e920616bc57e7bd68248821f46fd0c", +//"s":"0x40266f84a2ecd4719057b0633cc80e3e0b3666f6f6ec1890a920239634ec6531","to":"0xb877c7e556d50b0027053336b90f36becf67b3dd"}}}} +var testNetworkNum = NetworkNum(5) + +func TestValidContentParsing(t *testing.T) { + var hash SHA256Hash + hashRes, _ := hex.DecodeString("ed2b4580a766bc9d81c73c35a8496f0461e9c261621cb9f4565ae52ade56056d") + copy(hash[:], hashRes) + + content, _ := hex.DecodeString("f8708301b7f8851bf08eb0008301388094b877c7e556d50b0027053336b90f36becf67b3dd88050b32f902486000801ca0aa803263146bda76a58ebf9f54be589280e920616bc57e7bd68248821f46fd0ca040266f84a2ecd4719057b0633cc80e3e0b3666f6f6ec1890a920239634ec6531") + + tx := NewBxTransaction(hash, testNetworkNum, TFPaidTx, time.Now()) + tx.SetContent(content) + blockchainTx, err := tx.BlockchainTransaction(true) + assert.Nil(t, err) + + ethTx, ok := blockchainTx.(*EthTransaction) + assert.True(t, ok) + + assert.Equal(t, uint8(0), ethTx.TxType.UInt8) + assert.Equal(t, 0, ethTx.AccessList.StorageKeys()) + assert.Equal(t, "0xb877c7e556d50b0027053336b90f36becf67b3dd", strings.ToLower(ethTx.To.String())) + assert.Equal(t, "0x40266f84a2ecd4719057b0633cc80e3e0b3666f6f6ec1890a920239634ec6531", hexutil.Encode(ethTx.S.Bytes())) + assert.Equal(t, "0xaa803263146bda76a58ebf9f54be589280e920616bc57e7bd68248821f46fd0c", hexutil.Encode(ethTx.R.Bytes())) + assert.Equal(t, int64(0x1c), ethTx.V.Int64()) + assert.Equal(t, int64(0x50b32f902486000), ethTx.Value.Int64()) + assert.Equal(t, uint64(0x1b7f8), ethTx.Nonce.UInt64) + assert.Equal(t, []byte{}, ethTx.Input.B) + assert.Equal(t, "ed2b4580a766bc9d81c73c35a8496f0461e9c261621cb9f4565ae52ade56056d", ethTx.Hash.String()) + assert.Equal(t, "0x832f166799a407275500430b61b622f0058f15d6", strings.ToLower(ethTx.From.String())) + assert.Equal(t, int64(0x1bf08eb000), ethTx.GasPrice.Int64()) + assert.Equal(t, uint64(0x13880), ethTx.GasLimit.UInt64) + +} + +//func TestValidContentParsingType1Tx(t *testing.T) { +// var hash SHA256Hash +// hashRes, _ := hex.DecodeString("9310dc4f07748222d37f43c7296826cf4bf6693fa207968bd7500659ee2cc04d") +// copy(hash[:], hashRes) +// +// content, _ := hex.DecodeString("01f9022101829237853486ced000830285ee94653911da49db4cdb0b7c3e4d929cfb56024cd4e680b8a48201aa3f000000000000000000000000c02aaa39b223fe8d0a0e5c4f27ead9083c756cc200000000000000000000000000000000000000000000000083a297567e20f8000000000000000000000000000d8775f648430679a709e98d2b0cb6250d2887ef000000000000000000000000000000000000000000000358c5ee87d374000000fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff90111f859940d8775f648430679a709e98d2b0cb6250d2887eff842a01d76467e21923adb4ee07bcae017030c6208bbccde21ff0a61518956ad9b152aa0ec5bfdd140da829800c64d740e802727fca06fadec8b5d82a7b406c811851b55f85994653911da49db4cdb0b7c3e4d929cfb56024cd4e6f842a02a9a57a342e03a2b55a8bef24e9c777df22a7442475b1641875a66dba65855f0a0d0bcf4df132c65dad73803c5e5e1c826f151a3342680034a8a4c8e5f8eb0c13ff85994c02aaa39b223fe8d0a0e5c4f27ead9083c756cc2f842a01fc85b67921559ce4fef22a331ff00c886678cf8b163d395e45fe0543f8750bda047a365b3ae9dbfa1c60a2cd30347765e914a89b7dac828db3ac3bd3e775b1a9980a0a340fc367050387a1b295514210a48de3836ee7923d9739bf0104e6d79c37997a06e63c7801da3c72f1a53f9b809ae04a45637da673c2a9c47065a1b1cdeafef7d") +// tx := NewBxTransaction(hash, 5) +// tx.SetContent(content) +// blockchainTx, err := tx.BlockchainTransaction() +// assert.Nil(t, err) +// +// ethTx, ok := blockchainTx.(*Transaction) +// assert.True(t, ok) +// +// assert.Equal(t, uint8(1), ethTx.TxType.UInt8) +// assert.Equal(t, int64(1), ethTx.ChainID.Int64()) +// assert.Equal(t, 6, ethTx.AccessList.StorageKeys()) +// assert.Equal(t, "0x653911da49db4cdb0b7c3e4d929cfb56024cd4e6", strings.ToLower(ethTx.To.String())) +// assert.Equal(t, uint64(37431), ethTx.Nonce.UInt64) +// assert.Equal(t, "9310dc4f07748222d37f43c7296826cf4bf6693fa207968bd7500659ee2cc04d", ethTx.Hash.String()) +// assert.Equal(t, uint64(225600000000), ethTx.GasPrice.Uint64()) +// assert.Equal(t, uint64(165358), ethTx.GasLimit.UInt64) +// assert.Equal(t, "0x0087c5900b9bbc051b5f6299f5bce92383273b28", strings.ToLower(ethTx.From.String())) +//} + +func TestNotValidContentParsing(t *testing.T) { + var hash SHA256Hash + hashRes, _ := hex.DecodeString("aaaa2050da578b41d47fd664974bb1bda379be0a3949976a19d91a6cb7ca2079") + copy(hash[:], hashRes) + + content, _ := hex.DecodeString("aaaa510e8516d1415400830283f9947a250d5630b4cf539739df2c5dacb4c659f2488d87b1a2bc2ec50000b8e47ff36ab5000000000000000000000000000000000000000000000010ee3c5d3728912a5d0000000000000000000000000000000000000000000000000000000000000080000000000000000000000000fd4d885c79fe72447239f50372940926b88017f5000000000000000000000000000000000000000000000000000000006024dfbb0000000000000000000000000000000000000000000000000000000000000002000000000000000000000000c02aaa39b223fe8d0a0e5c4f27ead9083c756cc20000000000000000000000009ed8e7c9604790f7ec589f99b94361d8aab64e5e26a076c06c21ac0d27d2866af4f344538f695b1729b54b764a32a758b5849df3b418a0229f5f935a60d4ef051c074ab49de8270f6ce949ba5e758d8de08923ff087cad") + + tx := &BxTransaction{ + hash: hash, + content: content, + shortIDs: make(ShortIDList, 0), + addTime: time.Now(), + networkNum: testNetworkNum, + } + + blockchainTx, err := tx.BlockchainTransaction(true) + assert.NotNil(t, err) + assert.Nil(t, blockchainTx) +} diff --git a/types/capabilityflags.go b/types/capabilityflags.go new file mode 100644 index 0000000..29de30a --- /dev/null +++ b/types/capabilityflags.go @@ -0,0 +1,11 @@ +package types + +// CapabilityFlags represents various flags for capabilities in hello msg +type CapabilityFlags uint16 + +// flag constant values +const ( + CapabilityFastSync CapabilityFlags = 1 << iota + CapabilityMevBuilder + CapabilityMevMiner +) diff --git a/types/common.go b/types/common.go new file mode 100644 index 0000000..ff319ca --- /dev/null +++ b/types/common.go @@ -0,0 +1,98 @@ +package types + +import ( + "bytes" + "fmt" +) + +// UInt32Len is the byte length of unsigned 32bit integers +const UInt32Len = 4 + +// UInt64Len is the byte length of unsigned 64bit integers +const UInt64Len = 8 + +// UInt16Len is the byte length of unsigned 16bit integers +const UInt16Len = 2 + +// UInt8Len is the byte length of unsigned 8bit integers +const UInt8Len = 1 + +// TxFlagsLen represents the byte length of transaction flag +const TxFlagsLen = 2 + +// NodeEndpoint - represent the node endpoint struct sent in BdnPerformanceStats +type NodeEndpoint struct { + IP string + Port int + PublicKey string +} + +// String returns string representation of NodeEndpoint +func (e *NodeEndpoint) String() string { + return fmt.Sprintf("%v %v %v", e.IP, e.Port, e.PublicKey) +} + +// IPPort returns string of IP and Port +func (e *NodeEndpoint) IPPort() string { + return fmt.Sprintf("%v %v", e.IP, e.Port) +} + +// ShortID represents the compressed transaction ID +type ShortID uint32 + +// ShortIDList represents short id list +type ShortIDList []ShortID + +// ShortIDsByNetwork represents map of shortIDs by network +type ShortIDsByNetwork map[NetworkNum]ShortIDList + +// NodeID represents a node's assigned ID. This field is a UUID. +type NodeID string + +// AccountID represents a user's BDN account. This field is a UUID. +type AccountID string + +// EmptyAccountID represent no Account ID set +const EmptyAccountID AccountID = "" + +// NewAccountID constructs an accountID from bytes, stripping off null bytes. +func NewAccountID(b []byte) AccountID { + trimmed := bytes.Trim(b, "\x00") + return AccountID(trimmed) +} + +// NodeIDLen is the number of characters in a NodeID +const NodeIDLen = 36 + +// ShortIDEmpty is the default value indicating no assigned short ID +const ShortIDEmpty = 0 + +// ShortIDLen is the byte length of packed short IDs +const ShortIDLen = UInt32Len + +// NetworkNum represents the network that a message is being routed in (Ethereum Mainnet, Ethereum Ropsten, etc.) +type NetworkNum uint32 + +// NetworkNumLen is the byte length of packed network numbers +const NetworkNumLen = UInt32Len + +// BloxrouteAccountID marks an internally generated certificate (e.g. for relays / internal gateways) +const BloxrouteAccountID = "bloXroute LABS" + +// BloxrouteGoGateway is initiated in gateway node model for the field: name +const BloxrouteGoGateway = "bloxroute go gateway" + +// GoGatewayVersion is version of gateway +const GoGatewayVersion = "2.0.1" + +// AllNetworkNum is the network number for relays that facilitate transactions from all networks +const AllNetworkNum NetworkNum = 0 + +// ErrorNotificationCode len of code +type ErrorNotificationCode uint32 + +// ErrorTypeLen represents len of error type +const ErrorTypeLen = 2 + +// ErrorNotificationCodeLen represents len of code +const ErrorNotificationCodeLen = 4 diff --git a/types/crypto_test.go b/types/crypto_test.go new file mode 100644 index 0000000..2c38a78 --- /dev/null +++ b/types/crypto_test.go @@ -0,0 +1,18 @@ +package types + +import ( + "crypto/sha256" + "encoding/hex" + utils2 "github.com/bloXroute-Labs/gateway/bxmessage/utils" + "github.com/stretchr/testify/assert" + "testing" +) + +func TestSHA256(t *testing.T) { + buff := []byte{255, 254, 253, 252, 103, 101, 116, 116, 120, 115, 0, 0, 0, 0, 0, 0, 5, 0, 0, 0, 0, 0, 0, 0, 1} + hash := sha256.Sum256(buff) + res := sha256.Sum256(hash[:]) + assert.Equal(t, "79ba4333350dfddd68ad06c44ff195ee991450d35cfa2b961f72d117ffd6fc95", hex.EncodeToString(res[:])) + res2 := utils2.DoubleSHA256([]byte{255, 254, 253, 252, 103, 101, 116, 116, 120, 115, 0, 0, 0, 0, 0, 0, 5, 0, 0, 0, 0, 0, 0, 0, 1}) + assert.Equal(t, "79ba4333350dfddd68ad06c44ff195ee991450d35cfa2b961f72d117ffd6fc95", hex.EncodeToString(res2[:])) +} diff --git a/types/errortype.go b/types/errortype.go new file mode 100644 index 0000000..92789c9 --- /dev/null +++ b/types/errortype.go @@ -0,0 +1,10 @@ +package types + +// ErrorType represents error type +type ErrorType uint16 + +// flag constant values +const ( + ErrorTypeTemporary ErrorType = 0 + ErrorTypePermanent ErrorType = 1 +) diff --git a/types/ethtransaction.go b/types/ethtransaction.go new file mode 100644 index 0000000..8a092c8 --- /dev/null +++ b/types/ethtransaction.go @@ -0,0 +1,436 @@ +package types + +import ( + "bytes" + "encoding/json" + "fmt" + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/common/hexutil" + ethtypes "github.com/ethereum/go-ethereum/core/types" + "github.com/ethereum/go-ethereum/rlp" + "math" + "math/big" + "strconv" + "strings" +) + +var nullEthAddress = common.BigToAddress(big.NewInt(0)) + +// EthTransaction represents the JSON encoding of an Ethereum transaction +type EthTransaction struct { + TxType EthTransactionType + From EthAddress + GasPrice EthBigInt + GasLimit EthUInt64 + Hash EthHash + Input EthBytes + Nonce EthUInt64 + Value EthBigInt + V EthBigInt + R EthBigInt + S EthBigInt + To EthAddress + ChainID EthBigInt + AccessList ethtypes.AccessList + GasFeeCap EthBigInt + GasTipCap EthBigInt +} + +// ethTxJSON is the JSON representation of transactions, with pointer types for omission +type ethTxJSON struct { + TxType *EthTransactionType `json:"type,omitempty"` + Nonce *EthUInt64 `json:"nonce,omitempty"` + Gas *EthUInt64 `json:"gas,omitempty"` + Value *EthBigInt `json:"value,omitempty"` + Input *EthBytes `json:"input,omitempty"` + V *EthBigInt `json:"v,omitempty"` + R *EthBigInt `json:"r,omitempty"` + S *EthBigInt `json:"s,omitempty"` + To *EthAddress `json:"to,omitempty"` + From *EthAddress `json:"from,omitempty"` + + // Legacy and access list transactions only, never omitted + GasPrice *EthBigInt `json:"gasPrice"` + + // JSON encoding only + Hash *EthHash `json:"hash,omitempty"` + + // Access list and dynamic fee transactions only + ChainID *EthBigInt `json:"chainId,omitempty"` + AccessList *ethtypes.AccessList `json:"accessList,omitempty"` + + // Dynamic fee transaction only + GasFeeCap *EthBigInt `json:"maxFeePerGas,omitempty"` + GasTipCap *EthBigInt `json:"maxPriorityFeePerGas,omitempty"` +} + +// EmptyFilteredTransactionMap - is an empty filtered transaction map use for evaluating filters +var EmptyFilteredTransactionMap = map[string]interface{}{ + "from": "0x0", + "gas_price": float64(0), + "gas": float64(0), + "tx_hash": "0x0", + "input": "0x0", + "method_id": "0x0", + "value": float64(0), + "to": "0x0", + "type": "0", + "chain_id": float64(0), + "max_fee_per_gas": float64(0), + "max_priority_fee_per_gas": float64(0), +} + +// NewEthTransaction converts a canonic Ethereum transaction to EthTransaction +func NewEthTransaction(h SHA256Hash, rawEthTx *ethtypes.Transaction, extractSender bool) (*EthTransaction, error) { + var ( + sender common.Address + err error + ) + v, r, s := rawEthTx.RawSignatureValues() + if extractSender { + sender, err = ethtypes.Sender(ethtypes.NewLondonSigner(rawEthTx.ChainId()), rawEthTx) + if err != nil { + return nil, fmt.Errorf("could not parse Ethereum transaction sender: %v", err) + } + } + ethTx := &EthTransaction{ + TxType: EthTransactionType{rawEthTx.Type()}, + From: EthAddress{Address: &sender}, + GasLimit: EthUInt64{UInt64: rawEthTx.Gas()}, + Hash: EthHash{SHA256Hash: h}, + Input: EthBytes{B: rawEthTx.Data()}, + Nonce: EthUInt64{UInt64: rawEthTx.Nonce()}, + Value: EthBigInt{Int: rawEthTx.Value()}, + V: EthBigInt{Int: v}, + R: EthBigInt{Int: r}, + S: EthBigInt{Int: s}, + To: EthAddress{Address: rawEthTx.To()}, + } + ethTx.ChainID = EthBigInt{rawEthTx.ChainId()} + ethTx.GasPrice = EthBigInt{Int: rawEthTx.GasPrice()} + ethTx.AccessList = rawEthTx.AccessList() + ethTx.GasFeeCap = EthBigInt{rawEthTx.GasFeeCap()} + ethTx.GasTipCap = EthBigInt{rawEthTx.GasTipCap()} + return ethTx, nil +} + +// EthTransactionFromBytes parses and constructs an Ethereum transaction from bytes +func EthTransactionFromBytes(h SHA256Hash, tc TxContent, extractSender bool) (*EthTransaction, error) { + var rawEthTx ethtypes.Transaction + + err := rlp.DecodeBytes(tc, &rawEthTx) + if err != nil { + return nil, fmt.Errorf("could not decode Ethereum transaction: %v", err) + } + + return NewEthTransaction(h, &rawEthTx, extractSender) +} + +// EffectiveGasFeeCap returns a common "gas fee cap" that can be used for all types of transactions +func (et EthTransaction) EffectiveGasFeeCap() EthBigInt { + switch et.TxType { + case DynamicFeeTransactionType: + return et.GasFeeCap + } + return et.GasPrice +} + +// EffectiveGasTipCap returns a common "gas tip cap" that can be used for all types of transactions +func (et EthTransaction) EffectiveGasTipCap() EthBigInt { + switch et.TxType { + case DynamicFeeTransactionType: + return et.GasTipCap + } + return et.GasPrice +} + +// Filters returns the same transaction, with only the specified fields included in a map data format for running through a conditional filter. -1 values are provided for fields like gas_price, max_fee_per_gas, etc. since these are typically expected to be unsigned integers. +func (et EthTransaction) Filters(filters []string) map[string]interface{} { + var transactionFilters = make(map[string]interface{}) + for _, param := range filters { + switch param { + case "value": + floatValue, _ := new(big.Float).SetInt(et.Value.Int).Float64() + transactionFilters[param] = floatValue + case "gas": + transactionFilters[param] = float64(et.GasLimit.UInt64) + case "gas_price": + if et.GasPrice.Int != nil && et.TxType != DynamicFeeTransactionType { + transactionFilters[param], _ = new(big.Float).SetInt(et.GasPrice.Int).Float64() + } else { + transactionFilters[param] = -1 + } + case "to": + if et.To.Address != nil { + transactionFilters[param] = strings.ToLower(et.To.Address.String()) + } else { + transactionFilters[param] = "" + } + case "from": + if et.From.Address != nil { + transactionFilters[param] = strings.ToLower(et.From.Address.String()) + } else { + transactionFilters[param] = "" + } + case "method_id": + methodID := strings.ToLower(hexutil.Encode(et.Input.B)) + if len(methodID) >= 10 { + transactionFilters[param] = "0x" + methodID[2:10] + } else { + transactionFilters[param] = methodID + } + case "type": + transactionFilters[param] = strconv.Itoa(int(et.TxType.UInt8)) + case "chain_id": + if et.ChainID.Int != nil { + transactionFilters[param] = int(et.ChainID.Int64()) + } + case "max_fee_per_gas": + if et.GasFeeCap.Int != nil && et.TxType == DynamicFeeTransactionType { + transactionFilters[param] = int(et.GasFeeCap.Int64()) + } else { + transactionFilters[param] = -1 + } + case "max_priority_fee_per_gas": + if et.GasTipCap.Int != nil && et.TxType == DynamicFeeTransactionType { + transactionFilters[param] = int(et.GasTipCap.Int64()) + } else { + transactionFilters[param] = -1 + } + } + } + return transactionFilters +} + +// WithFields returns the same transaction, with only the specified fields included for serialization purposes +func (et EthTransaction) WithFields(fields []string) BlockchainTransaction { + transactionContent := EthTransaction{} + transactionContent.TxType = NilTransactionType + transactionContent.Nonce = nilNonceValue + + for _, param := range fields { + switch param { + case "tx_contents.tx_hash": + transactionContent.Hash = et.Hash + case "tx_contents.nonce": + transactionContent.Nonce = et.Nonce + case "tx_contents.gas_price": + if et.TxType != DynamicFeeTransactionType { + transactionContent.GasPrice = et.GasPrice + } + case "tx_contents.gas": + transactionContent.GasLimit = et.GasLimit + case "tx_contents.to": + transactionContent.To = et.To + if et.To.Address == nil { + transactionContent.To.Address = &nullEthAddress + } + case "tx_contents.value": + transactionContent.Value = et.Value + case "tx_contents.input": + transactionContent.Input = et.Input + case "tx_contents.v": + transactionContent.V = et.V + case "tx_contents.r": + transactionContent.R = et.R + case "tx_contents.s": + transactionContent.S = et.S + case "tx_contents.from": + transactionContent.From = et.From + case "tx_contents.type": + transactionContent.TxType = et.TxType + case "tx_contents.access_list": + transactionContent.AccessList = et.AccessList + case "tx_contents.chain_id": + transactionContent.ChainID = et.ChainID + case "tx_contents.max_fee_per_gas": + if et.TxType == DynamicFeeTransactionType { + transactionContent.GasFeeCap = et.GasFeeCap + } + case "tx_contents.max_priority_fee_per_gas": + if et.TxType == DynamicFeeTransactionType { + transactionContent.GasTipCap = et.GasTipCap + } + } + } + return transactionContent +} + +// EthTransactionType represents different types of encoded Ethereum transactions +type EthTransactionType EthUInt8 + +// Constants for identifying each Ethereum transaction types +var ( + NilTransactionType = EthTransactionType{math.MaxUint8} + LegacyTransactionType = EthTransactionType{0} + AccessListTransactionType = EthTransactionType{1} + DynamicFeeTransactionType = EthTransactionType{2} +) + +var nilNonceValue = EthUInt64{math.MaxUint64} + +// EthAddress wraps an Ethereum address +type EthAddress struct { + *common.Address +} + +// MarshalJSON formats an EthAddress according to the bloxroute spec +func (a EthAddress) MarshalJSON() ([]byte, error) { + if a.Address == nil { + return json.Marshal("0x") + } + return json.Marshal(strings.ToLower(a.Address.Hex())) +} + +// EthBigInt wraps an big.Int value +type EthBigInt struct { + *big.Int +} + +// LessThan compares the wrapped big.Int with another +func (bi EthBigInt) LessThan(other EthBigInt) bool { + return bi.Int.Cmp(other.Int) == -1 +} + +// GreaterThan compares the wrapped big.Int with another +func (bi EthBigInt) GreaterThan(other EthBigInt) bool { + return bi.Int.Cmp(other.Int) == 1 +} + +// MarshalJSON formats an big.Int according to the bloxroute spec +func (bi EthBigInt) MarshalJSON() ([]byte, error) { + if bi.Int != nil { + return json.Marshal(strings.ToLower(hexutil.EncodeBig(bi.Int))) + } + return json.Marshal("") +} + +// EthUInt64 wraps a uint64 +type EthUInt64 struct { + UInt64 uint64 +} + +// MarshalJSON formats an uint64 according to the bloxroute spec +func (ui EthUInt64) MarshalJSON() ([]byte, error) { + return json.Marshal(strings.ToLower(hexutil.EncodeUint64(ui.UInt64))) +} + +// LessThan compares the wrapped uint64 with another +func (ui EthUInt64) LessThan(other EthUInt64) bool { + return ui.UInt64 < other.UInt64 +} + +// GreaterThan compares the wrapped uint64 with another +func (ui EthUInt64) GreaterThan(other EthUInt64) bool { + return ui.UInt64 > other.UInt64 +} + +// EthUInt8 wraps an uint8 +type EthUInt8 struct { + UInt8 uint8 +} + +// MarshalJSON formats an uint8 according to the bloxroute spec +func (ui EthUInt8) MarshalJSON() ([]byte, error) { + return json.Marshal(strings.ToLower(hexutil.EncodeUint64(uint64(ui.UInt8)))) +} + +// LessThan compares the wrapped uint8 with another +func (ui EthUInt8) LessThan(other EthUInt8) bool { + return ui.UInt8 < other.UInt8 +} + +// GreaterThan compares the wrapped uint8 with another +func (ui EthUInt8) GreaterThan(other EthUInt8) bool { + return ui.UInt8 > other.UInt8 +} + +// MarshalJSON formats an EthTransactionType according to the bloxroute spec +func (ui EthTransactionType) MarshalJSON() ([]byte, error) { + return json.Marshal(strings.ToLower(hexutil.EncodeUint64(uint64(ui.UInt8)))) +} + +// EthBytes wraps an Ethereum bytearray +type EthBytes struct { + B []byte +} + +// MarshalJSON formats an wrapped []byte according to the bloxroute spec +func (eb EthBytes) MarshalJSON() ([]byte, error) { + return json.Marshal(strings.ToLower(hexutil.Encode(eb.B))) +} + +// EthHash wraps an SHA256Hash +type EthHash struct { + SHA256Hash +} + +// MarshalJSON formats an wrapped SHA256Hash according to the bloxroute spec +func (eh EthHash) MarshalJSON() ([]byte, error) { + return json.Marshal(strings.ToLower(eh.SHA256Hash.Format(true))) +} + +// MarshalJSON formats an ethTxJSON according to the bloxroute spec via EthTransaction +func (et EthTransaction) MarshalJSON() ([]byte, error) { + var enc ethTxJSON + + if et.TxType != NilTransactionType { + enc.TxType = &et.TxType + } + if et.AccessList != nil { + enc.AccessList = &et.AccessList + } + emptyByteVar := make([]byte, 32) + if !bytes.Equal(et.Hash.SHA256Hash[:], emptyByteVar) { + enc.Hash = &et.Hash + } + if et.Nonce != nilNonceValue { + enc.Nonce = &et.Nonce + } + if et.GasLimit.UInt64 != 0 { + enc.Gas = &et.GasLimit + } + if et.Input.B != nil { + enc.Input = &et.Input + } + if et.GasPrice.Int != nil { + enc.GasPrice = &et.GasPrice + } + if et.Value.Int != nil { + enc.Value = &et.Value + } + if et.V.Int != nil { + enc.V = &et.V + } + if et.R.Int != nil { + enc.R = &et.R + } + if et.S.Int != nil { + enc.S = &et.S + } + if et.To.Address != nil { + enc.To = &et.To + } + if et.From.Address != nil { + enc.From = &et.From + } + if et.TxType != LegacyTransactionType { + enc.ChainID = &et.ChainID + } + if et.GasFeeCap.Int != nil { + enc.GasFeeCap = &et.GasFeeCap + } + if et.GasTipCap.Int != nil { + enc.GasTipCap = &et.GasTipCap + } + + // include nil "to" field if requested + marshalled, err := json.Marshal(&enc) + if enc.To != nil && enc.To.Address != &nullEthAddress { + return marshalled, err + } + var mapWithNilToField map[string]interface{} + json.Unmarshal(marshalled, &mapWithNilToField) + mapWithNilToField["to"] = nil + return json.Marshal(mapWithNilToField) +} diff --git a/types/ethtransaction_test.go b/types/ethtransaction_test.go new file mode 100644 index 0000000..0914965 --- /dev/null +++ b/types/ethtransaction_test.go @@ -0,0 +1,272 @@ +package types + +import ( + "encoding/hex" + "github.com/bloXroute-Labs/gateway/test" + "github.com/bloXroute-Labs/gateway/test/fixtures" + "github.com/ethereum/go-ethereum/common/hexutil" + "github.com/stretchr/testify/assert" + "math/big" + "testing" + "time" +) + +func ethTransaction(hashString string, txString string) (SHA256Hash, EthTransaction, *BxTransaction, error) { + hash, err := NewSHA256HashFromString(hashString) + if err != nil { + return SHA256Hash{}, EthTransaction{}, nil, err + } + + content, err := hex.DecodeString(txString) + if err != nil { + return hash, EthTransaction{}, nil, err + } + + tx := NewBxTransaction(hash, testNetworkNum, TFPaidTx, time.Now()) + tx.SetContent(content) + + blockchainTx, err := tx.BlockchainTransaction(true) + if err != nil { + return hash, EthTransaction{}, tx, err + } + + return hash, *blockchainTx.(*EthTransaction), tx, nil +} + +func TestLegacyTransaction(t *testing.T) { + expectedGasPrice := new(big.Int).SetInt64(fixtures.LegacyGasPrice) + expectedFromAddress := test.NewEthAddress(fixtures.LegacyFromAddress) + expectedChainID := new(big.Int).SetInt64(fixtures.LegacyChainID) + + hash, ethTx, _, err := ethTransaction(fixtures.LegacyTransactionHash, fixtures.LegacyTransaction) + assert.Nil(t, err) + + // check decoding transaction structure + assert.Equal(t, LegacyTransactionType, ethTx.TxType) + assert.Equal(t, hash, ethTx.Hash.SHA256Hash) + assert.Equal(t, EthBigInt{Int: expectedGasPrice}, ethTx.GasPrice) + assert.Equal(t, EthBigInt{Int: expectedGasPrice}, ethTx.GasFeeCap) + assert.Equal(t, EthBigInt{Int: expectedGasPrice}, ethTx.GasTipCap) + assert.Equal(t, EthBigInt{Int: expectedChainID}, ethTx.ChainID) + assert.Equal(t, expectedFromAddress.Bytes(), ethTx.From.Bytes()) + + // check WithFields + fieldsTx := ethTx.WithFields([]string{ + "tx_contents.from", + "tx_contents.tx_hash", + "tx_contents.gas_price", + "tx_contents.chain_id", + "tx_contents.max_fee_per_gas", + "tx_contents.max_priority_fee_per_gas", + "tx_contents.type", + }) + ethFieldsTx := fieldsTx.(EthTransaction) + assert.Equal(t, &expectedFromAddress, ethFieldsTx.From.Address) + assert.Equal(t, hash, ethFieldsTx.Hash.SHA256Hash) + assert.Equal(t, expectedGasPrice, ethFieldsTx.GasPrice.Int) + // when chain ID is explicitly asked for it's included + assert.Equal(t, EthBigInt{big.NewInt(fixtures.LegacyChainID)}, ethFieldsTx.ChainID) + assert.Equal(t, EthBigInt{}, ethFieldsTx.GasFeeCap) + assert.Equal(t, EthBigInt{}, ethFieldsTx.GasTipCap) + + // check JSON serialization on WithFields tx + jsonMap, err := test.MarshallJSONToMap(fieldsTx) + assert.Nil(t, err) + assert.Equal(t, "0x0", jsonMap["type"]) + assert.Equal(t, fixtures.LegacyFromAddress, jsonMap["from"]) + assert.Equal(t, fixtures.LegacyTransactionHash, jsonMap["hash"]) + assert.Equal(t, hexutil.EncodeBig(expectedGasPrice), jsonMap["gasPrice"]) + + assert.False(t, test.Contains(jsonMap, "maxFeePerGas")) + assert.False(t, test.Contains(jsonMap, "maxPriorityFeePerGas")) + assert.False(t, test.Contains(jsonMap, "accessList")) + // chainID not included during serialization of LegacyTransaction + assert.False(t, test.Contains(jsonMap, "chainID")) + + // check Filters + filteredTx := ethTx.Filters([]string{ + "from", + "chain_id", + "gas_price", + "max_fee_per_gas", + "max_priority_fee_per_gas", + }) + assert.Nil(t, err) + assert.False(t, test.Contains(filteredTx, "type")) + assert.Equal(t, fixtures.LegacyFromAddress, filteredTx["from"]) + assert.Equal(t, fixtures.LegacyGasPrice, filteredTx["gas_price"]) + // when chain ID is explicitly asked for it's included + assert.Equal(t, 1, filteredTx["chain_id"]) + assert.Equal(t, -1, filteredTx["max_fee_per_gas"]) + assert.Equal(t, -1, filteredTx["max_priority_fee_per_gas"]) +} + +func TestAccessListTransaction(t *testing.T) { + expectedGasPrice := new(big.Int).SetInt64(fixtures.AccessListGasPrice) + expectedFromAddress := test.NewEthAddress(fixtures.AccessListFromAddress) + + hash, ethTx, _, err := ethTransaction(fixtures.AccessListTransactionHash, fixtures.AccessListTransaction) + assert.Nil(t, err) + + // check decoding transaction structure + assert.Equal(t, AccessListTransactionType, ethTx.TxType) + assert.Equal(t, hash, ethTx.Hash.SHA256Hash) + assert.Equal(t, EthBigInt{Int: expectedGasPrice}, ethTx.GasPrice) + assert.Equal(t, EthBigInt{Int: expectedGasPrice}, ethTx.GasFeeCap) + assert.Equal(t, EthBigInt{Int: expectedGasPrice}, ethTx.GasTipCap) + assert.Equal(t, expectedFromAddress.Bytes(), ethTx.From.Bytes()) + + // check WithFields + fieldsTx := ethTx.WithFields([]string{ + "tx_contents.from", + "tx_contents.tx_hash", + "tx_contents.gas_price", + "tx_contents.chain_id", + "tx_contents.max_fee_per_gas", + "tx_contents.max_priority_fee_per_gas", + "tx_contents.access_list", + }) + assert.Nil(t, err) + + ethFieldsTx := fieldsTx.(EthTransaction) + assert.Equal(t, &expectedFromAddress, ethFieldsTx.From.Address) + assert.Equal(t, hash, ethFieldsTx.Hash.SHA256Hash) + assert.Equal(t, expectedGasPrice, ethFieldsTx.GasPrice.Int) + assert.Equal(t, int64(fixtures.AccessListChainID), ethFieldsTx.ChainID.Int64()) + assert.Equal(t, EthBigInt{}, ethFieldsTx.GasFeeCap) + assert.Equal(t, EthBigInt{}, ethFieldsTx.GasTipCap) + + // check JSON serialization on WithFields tx + jsonMap, err := test.MarshallJSONToMap(fieldsTx) + assert.Nil(t, err) + assert.Equal(t, fixtures.AccessListFromAddress, jsonMap["from"]) + assert.Equal(t, fixtures.AccessListTransactionHash, jsonMap["hash"]) + assert.Equal(t, hexutil.EncodeBig(expectedGasPrice), jsonMap["gasPrice"]) + assert.Equal(t, hexutil.EncodeUint64(fixtures.AccessListChainID), jsonMap["chainId"]) + assert.Equal(t, fixtures.AccessListLength, len(jsonMap["accessList"].([]interface{}))) + + assert.False(t, test.Contains(jsonMap, "maxFeePerGas")) + assert.False(t, test.Contains(jsonMap, "maxPriorityFeePerGas")) + + // check Filters + filteredTx := ethTx.Filters([]string{ + "from", + "chain_id", + "gas_price", + "max_fee_per_gas", + "max_priority_fee_per_gas", + "type", + }) + assert.Nil(t, err) + assert.Equal(t, "1", filteredTx["type"]) + assert.Equal(t, fixtures.AccessListFromAddress, filteredTx["from"]) + assert.Equal(t, fixtures.AccessListGasPrice, filteredTx["gas_price"]) + assert.Equal(t, fixtures.AccessListChainID, filteredTx["chain_id"]) + assert.Equal(t, -1, filteredTx["max_fee_per_gas"]) + assert.Equal(t, -1, filteredTx["max_priority_fee_per_gas"]) +} + +func TestDynamicFeeTransaction(t *testing.T) { + expectedFromAddress := test.NewEthAddress(fixtures.DynamicFeeFromAddress) + + hash, ethTx, _, err := ethTransaction(fixtures.DynamicFeeTransactionHash, fixtures.DynamicFeeTransaction) + assert.Nil(t, err) + + // check decoding transaction structure + assert.Equal(t, DynamicFeeTransactionType, ethTx.TxType) + assert.Equal(t, hash, ethTx.Hash.SHA256Hash) + assert.Equal(t, int64(fixtures.DynamicFeeFeePerGas), ethTx.GasPrice.Int64()) + assert.Equal(t, int64(fixtures.DynamicFeeFeePerGas), ethTx.GasFeeCap.Int64()) + assert.Equal(t, int64(fixtures.DynamicFeeTipPerGas), ethTx.GasTipCap.Int64()) + assert.Equal(t, expectedFromAddress.Bytes(), ethTx.From.Bytes()) + + // check WithFields + fieldsTx := ethTx.WithFields([]string{ + "tx_contents.from", + "tx_contents.tx_hash", + "tx_contents.gas_price", + "tx_contents.chain_id", + "tx_contents.max_fee_per_gas", + "tx_contents.max_priority_fee_per_gas", + "tx_contents.access_list", + "tx_contents.type", + }) + assert.Nil(t, err) + + ethFieldsTx := fieldsTx.(EthTransaction) + + assert.Equal(t, &expectedFromAddress, ethFieldsTx.From.Address) + assert.Equal(t, hash, ethFieldsTx.Hash.SHA256Hash) + assert.Equal(t, uint8(2), ethFieldsTx.TxType.UInt8) + assert.Equal(t, int64(fixtures.DynamicFeeChainID), ethFieldsTx.ChainID.Int64()) + assert.Equal(t, EthBigInt{}, ethFieldsTx.GasPrice) + assert.Equal(t, int64(fixtures.DynamicFeeFeePerGas), ethFieldsTx.GasFeeCap.Int64()) + assert.Equal(t, int64(fixtures.DynamicFeeTipPerGas), ethFieldsTx.GasTipCap.Int64()) + + // check JSON serialization on WithFields tx + jsonMap, err := test.MarshallJSONToMap(fieldsTx) + assert.Nil(t, err) + assert.Equal(t, fixtures.DynamicFeeFromAddress, jsonMap["from"]) + assert.Equal(t, fixtures.DynamicFeeTransactionHash, jsonMap["hash"]) + assert.Equal(t, hexutil.EncodeUint64(fixtures.DynamicFeeChainID), jsonMap["chainId"]) + assert.Equal(t, fixtures.DynamicFeeAccessListLength, len(jsonMap["accessList"].([]interface{}))) + assert.Equal(t, hexutil.EncodeUint64(fixtures.DynamicFeeFeePerGas), jsonMap["maxFeePerGas"]) + assert.Equal(t, hexutil.EncodeUint64(fixtures.DynamicFeeTipPerGas), jsonMap["maxPriorityFeePerGas"]) + assert.Equal(t, "0x2", jsonMap["type"]) + assert.Equal(t, nil, jsonMap["gasPrice"]) + + // check WithFields without type + fieldsTxWithoutType := ethTx.WithFields([]string{ + "tx_contents.gas_price", + "tx_contents.max_fee_per_gas", + "tx_contents.max_priority_fee_per_gas", + }) + assert.Nil(t, err) + + ethFieldsTxWithoutType := fieldsTxWithoutType.(EthTransaction) + assert.Equal(t, EthBigInt{}, ethFieldsTx.GasPrice) + assert.Equal(t, int64(fixtures.DynamicFeeFeePerGas), ethFieldsTxWithoutType.GasFeeCap.Int64()) + assert.Equal(t, int64(fixtures.DynamicFeeTipPerGas), ethFieldsTxWithoutType.GasTipCap.Int64()) + + // check JSON serialization without type field + jsonMapWithoutType, err := test.MarshallJSONToMap(fieldsTxWithoutType) + assert.Nil(t, err) + assert.Equal(t, hexutil.EncodeUint64(fixtures.DynamicFeeFeePerGas), jsonMapWithoutType["maxFeePerGas"]) + assert.Equal(t, hexutil.EncodeUint64(fixtures.DynamicFeeTipPerGas), jsonMapWithoutType["maxPriorityFeePerGas"]) + assert.Equal(t, nil, jsonMapWithoutType["gasPrice"]) + + // check Filters + filteredTx := ethTx.Filters([]string{ + "from", + "chain_id", + "gas_price", + "max_fee_per_gas", + "max_priority_fee_per_gas", + }) + assert.Nil(t, err) + assert.Equal(t, fixtures.DynamicFeeFromAddress, filteredTx["from"]) + assert.Equal(t, fixtures.DynamicFeeChainID, filteredTx["chain_id"]) + assert.Equal(t, -1, filteredTx["gas_price"]) + assert.Equal(t, fixtures.DynamicFeeFeePerGas, filteredTx["max_fee_per_gas"]) + assert.Equal(t, fixtures.DynamicFeeTipPerGas, filteredTx["max_priority_fee_per_gas"]) +} + +func TestContractCreationTx(t *testing.T) { + hash, ethTx, _, err := ethTransaction(fixtures.ContractCreationTxHash, fixtures.ContractCreationTx) + assert.Nil(t, err) + assert.Equal(t, hash, ethTx.Hash.SHA256Hash) + + txWithFields := ethTx.WithFields([]string{"tx_contents.to", "tx_contents.from"}) + assert.Nil(t, err) + + ethTxWithFields, ok := txWithFields.(EthTransaction) + assert.True(t, ok) + + ethJSON, err := test.MarshallJSONToMap(ethTxWithFields) + assert.Nil(t, err) + + to, ok := ethJSON["to"] + assert.Equal(t, true, ok) + assert.Equal(t, nil, to) + assert.Equal(t, "0x09e9ff67d9d5a25fa465db6f0bede5560581f8cb", ethJSON["from"]) +} diff --git a/types/feedtype.go b/types/feedtype.go new file mode 100644 index 0000000..2715e32 --- /dev/null +++ b/types/feedtype.go @@ -0,0 +1,24 @@ +package types + +// FeedType types of feeds +type FeedType string + +// FeedType enumeration +const ( + NewTxsFeed FeedType = "newTxs" + PendingTxsFeed FeedType = "pendingTxs" + BDNBlocksFeed FeedType = "bdnBlocks" + NewBlocksFeed FeedType = "newBlocks" + OnBlockFeed FeedType = "ethOnBlock" + TxReceiptsFeed FeedType = "txReceipts" +) + +// Exists - checks if a field exists in feedType list +func Exists(field FeedType, slice []FeedType) bool { + for _, valid := range slice { + if field == valid { + return true + } + } + return false +} diff --git a/types/hash.go b/types/hash.go new file mode 100644 index 0000000..531c42f --- /dev/null +++ b/types/hash.go @@ -0,0 +1,76 @@ +package types + +import ( + "encoding/hex" + "errors" + "fmt" + "github.com/ethereum/go-ethereum/crypto" + "math/rand" +) + +// SHA256HashLen is the byte length of SHA256 hashes +const SHA256HashLen = 32 + +// SHA256Hash represents a byte array containing SHA256 hash (e.g. transaction hash, block hash) +type SHA256Hash [SHA256HashLen]byte + +// SHA256HashList represents hash list +type SHA256HashList []SHA256Hash + +// NewSHA256Hash converts an existing byte array to a SHA256Hash +func NewSHA256Hash(b []byte) (SHA256Hash, error) { + var hash SHA256Hash + if len(b) != SHA256HashLen { + return hash, errors.New("provided hash string is an incorrect length") + } + copy(hash[:], b) + return hash, nil +} + +// NewSHA256HashFromString parses a SHA256Hash from a serialized string format +func NewSHA256HashFromString(hashStr string) (SHA256Hash, error) { + if hashStr[:2] == "0x" { + hashStr = hashStr[2:] + } + + var hash SHA256Hash + hashBytes, err := hex.DecodeString(hashStr) + if err != nil { + return hash, fmt.Errorf("could not decode hash string: %v %v", hashStr, err) + } + + return NewSHA256Hash(hashBytes) +} + +// NewSHA256FromKeccak derives an SHA256Hash object using keccak hash on the provided byte array +func NewSHA256FromKeccak(b []byte) SHA256Hash { + keccakHash := crypto.Keccak256(b) + var hash SHA256Hash + copy(hash[:], keccakHash) + return hash +} + +// GenerateSHA256Hash randomly generates a new SHA256Hash object +func GenerateSHA256Hash() SHA256Hash { + var hash SHA256Hash + _, _ = rand.Read(hash[:]) + return hash +} + +// Bytes returns the underlying byte representation +func (s SHA256Hash) Bytes() []byte { + return s[:] +} + +// String dumps the SHA256Hash to a readable format +func (s SHA256Hash) String() string { + return hex.EncodeToString(s[:]) +} + +// Format dumps the SHA256Hash to a readable format, optionally with a 0x prefix +func (s SHA256Hash) Format(prefix bool) string { + if prefix { + return fmt.Sprintf("%v%v", "0x", s) + } + return s.String() +} diff --git a/types/newtxnotification.go b/types/newtxnotification.go new file mode 100644 index 0000000..bace780 --- /dev/null +++ b/types/newtxnotification.go @@ -0,0 +1,94 @@ +package types + +import ( + "fmt" + ethtypes "github.com/ethereum/go-ethereum/core/types" + "github.com/ethereum/go-ethereum/rlp" + log "github.com/sirupsen/logrus" +) + +// NewTransactionNotification - contains BxTransaction which contains the local region of the ethereum transaction and all its fields. +type NewTransactionNotification struct { + *BxTransaction + BlockchainTransaction +} + +// CreateNewTransactionNotification - creates NewTransactionNotification object which contains bxTransaction and local region +func CreateNewTransactionNotification(bxTx *BxTransaction) Notification { + return &NewTransactionNotification{ + bxTx, + nil, + } +} + +func (newTransactionNotification *NewTransactionNotification) makeBlockchainTransaction() error { + var err error + newTransactionNotification.BxTransaction.m.Lock() + defer newTransactionNotification.BxTransaction.m.Unlock() + if newTransactionNotification.BlockchainTransaction == nil { + newTransactionNotification.BlockchainTransaction, err = newTransactionNotification.BxTransaction.BlockchainTransaction(true) + if err != nil { + err = fmt.Errorf("invalid tx with hash %v. error %v", newTransactionNotification.BxTransaction.Hash(), err) + log.Errorf("failed in makeBlockchainTransaction - %v", err) + return err + } + } + return nil +} + +//Filters - creates BlockchainTransaction if needs and returns a map of requested fields and their value for evaluation +func (newTransactionNotification *NewTransactionNotification) Filters(filters []string) map[string]interface{} { + err := newTransactionNotification.makeBlockchainTransaction() + if err != nil { + return nil + } + return newTransactionNotification.BlockchainTransaction.Filters(filters) +} + +// WithFields - creates BlockchainTransaction if needs and returns the value of requested fields of the transaction +func (newTransactionNotification *NewTransactionNotification) WithFields(fields []string) Notification { + err := newTransactionNotification.makeBlockchainTransaction() + if err != nil { + return &NewTransactionNotification{ + nil, + nil, + } + } + + newBlockchainTransaction := newTransactionNotification.BlockchainTransaction.WithFields(fields) + return &NewTransactionNotification{ + nil, + newBlockchainTransaction, + } +} + +// LocalRegion - returns the local region of the ethereum transaction +func (newTransactionNotification *NewTransactionNotification) LocalRegion() bool { + return TFLocalRegion&newTransactionNotification.BxTransaction.Flags() != 0 +} + +// GetHash - returns tha hash of BlockchainTransaction +func (newTransactionNotification *NewTransactionNotification) GetHash() string { + return newTransactionNotification.BxTransaction.hash.Format(true) +} + +// RawTx - returns the tx raw content +// the tx bytes returned can be used directly to submit to RPC endpoint +// rlp.DecodeBytes is used for the wire protocol, while `MarshalBinary`/`UnmarshalBinary` is used for RPC interface +func (newTransactionNotification *NewTransactionNotification) RawTx() []byte { + var rawTx ethtypes.Transaction + err := rlp.DecodeBytes(newTransactionNotification.BxTransaction.content, &rawTx) + if err != nil { + log.Infof("invalid tx content %v with hash %v. error %v", newTransactionNotification.BxTransaction.content, newTransactionNotification.BxTransaction.Hash(), err) + } + marshalledTxBytes, err := rawTx.MarshalBinary() + if err != nil { + log.Infof("invalid raw eth tx %v error %v", newTransactionNotification.BxTransaction.Hash(), err) + } + return marshalledTxBytes +} + +// NotificationType - returns the feed name notification +func (newTransactionNotification *NewTransactionNotification) NotificationType() FeedType { + return NewTxsFeed +} diff --git a/types/notification.go b/types/notification.go new file mode 100644 index 0000000..b17d407 --- /dev/null +++ b/types/notification.go @@ -0,0 +1,10 @@ +package types + +// Notification represents a generic notification that allows filtering its fields +type Notification interface { + WithFields(fields []string) Notification + Filters(filters []string) map[string]interface{} + LocalRegion() bool + GetHash() string + NotificationType() FeedType +} diff --git a/types/onblocknotification.go b/types/onblocknotification.go new file mode 100644 index 0000000..f09aee9 --- /dev/null +++ b/types/onblocknotification.go @@ -0,0 +1,59 @@ +package types + +// OnBlockNotification - represents the result of an RPC call on published block +type OnBlockNotification struct { + Name string `json:"name,omitempty"` + Response string `json:"response,omitempty"` + BlockHeight string `json:"block_height,omitempty"` + Tag string `json:"tag,omitempty"` + hash string +} + +// NewOnBlockNotification returns a new OnBlockNotification +func NewOnBlockNotification(name string, response string, blockHeight string, tag string, hash string) *OnBlockNotification { + return &OnBlockNotification{ + Name: name, + Response: response, + BlockHeight: blockHeight, + Tag: tag, + hash: hash, + } +} + +// WithFields - +func (n *OnBlockNotification) WithFields(fields []string) Notification { + onBlockNotification := OnBlockNotification{} + for _, param := range fields { + switch param { + case "name": + onBlockNotification.Name = n.Name + case "response": + onBlockNotification.Response = n.Response + case "block_height": + onBlockNotification.BlockHeight = n.BlockHeight + case "tag": + onBlockNotification.Tag = n.Tag + } + } + return &onBlockNotification +} + +// Filters - +func (n *OnBlockNotification) Filters(filters []string) map[string]interface{} { + return nil +} + +// LocalRegion - +func (n *OnBlockNotification) LocalRegion() bool { + return false +} + +// GetHash - +func (n *OnBlockNotification) GetHash() string { + return n.hash +} + +// NotificationType - feed name +func (n *OnBlockNotification) NotificationType() FeedType { + return OnBlockFeed +} diff --git a/types/pendingtxnotification.go b/types/pendingtxnotification.go new file mode 100644 index 0000000..2a68bd0 --- /dev/null +++ b/types/pendingtxnotification.go @@ -0,0 +1,21 @@ +package types + +// PendingTransactionNotification - contains BxTransaction which contains the local region of the ethereum transaction and all its fields. +type PendingTransactionNotification struct { + NewTransactionNotification +} + +// CreatePendingTransactionNotification - creates PendingTransactionNotification object which contains bxTransaction and local region +func CreatePendingTransactionNotification(bxTx *BxTransaction) Notification { + return &PendingTransactionNotification{ + NewTransactionNotification{ + bxTx, + nil, + }, + } +} + +// NotificationType - returns the feed name notification +func (pendingTransactionNotification *PendingTransactionNotification) NotificationType() FeedType { + return PendingTxsFeed +} diff --git a/types/subscribe.go b/types/subscribe.go new file mode 100644 index 0000000..c1b0bc2 --- /dev/null +++ b/types/subscribe.go @@ -0,0 +1,13 @@ +package types + +// SubscriptionResponse struct that represent subscription response from the node +type SubscriptionResponse struct { + Jsonrpc string `json:"jsonrpc"` + ID int `json:"id"` + Result string `json:"result"` + Method string `json:"method"` + Params struct { + Subscription string `json:"subscription"` + Result interface{} `json:"result"` + } `json:"params"` +} diff --git a/types/txflags.go b/types/txflags.go new file mode 100644 index 0000000..90c3760 --- /dev/null +++ b/types/txflags.go @@ -0,0 +1,30 @@ +package types + +// TxFlags represents various flags controlling routing behavior of transactions +type TxFlags uint16 + +// flag constant values +const ( + TFStatusMonitoring TxFlags = 1 << iota + TFPaidTx + TFNonceMonitoring + TFRePropagate + TFEnterpriseSender + TFLocalRegion + TFPrivateTx + TFEliteSender + TFDeliverToNode + + TFStatusTrack = TFStatusMonitoring | TFPaidTx + TFNonceTrack = TFNonceMonitoring | TFStatusTrack +) + +// IsPaid indicates whether the transaction is considered paid from the user and consumes quota +func (f TxFlags) IsPaid() bool { + return f&TFPaidTx != 0 +} + +// ShouldDeliverToNode indicates whether the transaction should be forwarded to the blockchain node +func (f TxFlags) ShouldDeliverToNode() bool { + return f&TFDeliverToNode != 0 +} diff --git a/types/txreceiptnotification.go b/types/txreceiptnotification.go new file mode 100644 index 0000000..73e3690 --- /dev/null +++ b/types/txreceiptnotification.go @@ -0,0 +1,180 @@ +package types + +import ( + "encoding/json" +) + +const nullAddressStr = "0x" + +// TxReceiptNotification - represents a transaction receipt feed entry +// to avoid deserializing/reserializing the message from Ethereum RPC, no conversion work is done +type TxReceiptNotification struct { + receipt txReceipt +} + +type txReceipt struct { + BlockHash string `json:"block_hash,omitempty"` + BlockNumber string `json:"block_number,omitempty"` + ContractAddress interface{} `json:"contract_address,omitempty"` + CumulativeGasUsed string `json:"cumulative_gas_used,omitempty"` + EffectiveGasPrice string `json:"effective_gas_price,omitempty"` + From interface{} `json:"from,omitempty"` + GasUsed string `json:"gas_used,omitempty"` + Logs []interface{} `json:"logs,omitempty"` + LogsBloom string `json:"logs_bloom,omitempty"` + Status string `json:"status,omitempty"` + To interface{} `json:"to,omitempty"` + TransactionHash string `json:"transaction_hash,omitempty"` + TransactionIndex string `json:"transaction_index,omitempty"` + TxType string `json:"type,omitempty"` +} + +// NewTxReceiptNotification returns a new TxReceiptNotification +func NewTxReceiptNotification(txReceipt map[string]interface{}) *TxReceiptNotification { + txReceiptNotification := TxReceiptNotification{} + + blockHash, ok := txReceipt["blockHash"] + if ok { + txReceiptNotification.receipt.BlockHash = blockHash.(string) + } + + blockNumber, ok := txReceipt["blockNumber"] + if ok { + txReceiptNotification.receipt.BlockNumber = blockNumber.(string) + } + + contractAddress, ok := txReceipt["contractAddress"] + if ok { + txReceiptNotification.receipt.ContractAddress = contractAddress + } + + cumulativeGasUsed, ok := txReceipt["cumulativeGasUsed"] + if ok { + txReceiptNotification.receipt.CumulativeGasUsed = cumulativeGasUsed.(string) + } + + effectiveGasPrice, ok := txReceipt["effectiveGasPrice"] + if ok { + txReceiptNotification.receipt.EffectiveGasPrice = effectiveGasPrice.(string) + } + + from, ok := txReceipt["from"] + if ok { + txReceiptNotification.receipt.From = from + } + + gasUsed, ok := txReceipt["gasUsed"] + if ok { + txReceiptNotification.receipt.GasUsed = gasUsed.(string) + } + + logs, ok := txReceipt["logs"] + if ok { + txReceiptNotification.receipt.Logs = logs.([]interface{}) + } + + logsBloom, ok := txReceipt["logsBloom"] + if ok { + txReceiptNotification.receipt.LogsBloom = logsBloom.(string) + } + + status, ok := txReceipt["status"] + if ok { + txReceiptNotification.receipt.Status = status.(string) + } + + to, ok := txReceipt["to"] + if ok { + txReceiptNotification.receipt.To = to + } + + transactionHash, ok := txReceipt["transactionHash"] + if ok { + txReceiptNotification.receipt.TransactionHash = transactionHash.(string) + } + + transactionIndex, ok := txReceipt["transactionIndex"] + if ok { + txReceiptNotification.receipt.TransactionIndex = transactionIndex.(string) + } + + txType, ok := txReceipt["type"] + if ok { + txReceiptNotification.receipt.TxType = txType.(string) + } + + return &txReceiptNotification +} + +// MarshalJSON formats txReceiptNotification, including nil "to" field if requested +func (r *TxReceiptNotification) MarshalJSON() ([]byte, error) { + marshalled, err := json.Marshal(r.receipt) + if r.receipt.To != nullAddressStr { + return marshalled, err + } + var mapWithNilToField map[string]interface{} + json.Unmarshal(marshalled, &mapWithNilToField) + mapWithNilToField["to"] = nil + return json.Marshal(mapWithNilToField) +} + +// WithFields - +func (r *TxReceiptNotification) WithFields(fields []string) Notification { + txReceiptNotification := TxReceiptNotification{} + for _, param := range fields { + switch param { + case "block_hash": + txReceiptNotification.receipt.BlockHash = r.receipt.BlockHash + case "block_number": + txReceiptNotification.receipt.BlockNumber = r.receipt.BlockNumber + case "contract_address": + txReceiptNotification.receipt.ContractAddress = r.receipt.ContractAddress + case "cumulative_gas_used": + txReceiptNotification.receipt.CumulativeGasUsed = r.receipt.CumulativeGasUsed + case "effective_gas_price": + txReceiptNotification.receipt.EffectiveGasPrice = r.receipt.EffectiveGasPrice + case "from": + txReceiptNotification.receipt.From = r.receipt.From + case "gas_used": + txReceiptNotification.receipt.GasUsed = r.receipt.GasUsed + case "logs": + txReceiptNotification.receipt.Logs = r.receipt.Logs + case "logs_bloom": + txReceiptNotification.receipt.LogsBloom = r.receipt.LogsBloom + case "status": + txReceiptNotification.receipt.Status = r.receipt.Status + case "to": + txReceiptNotification.receipt.To = r.receipt.To + if r.receipt.To == nil { + txReceiptNotification.receipt.To = nullAddressStr + } + case "transaction_hash": + txReceiptNotification.receipt.TransactionHash = r.receipt.TransactionHash + case "transaction_index": + txReceiptNotification.receipt.TransactionIndex = r.receipt.TransactionIndex + case "type": + txReceiptNotification.receipt.TxType = r.receipt.TxType + } + } + return &txReceiptNotification +} + +// Filters - +func (r *TxReceiptNotification) Filters(filters []string) map[string]interface{} { + return nil +} + +// LocalRegion - +func (r *TxReceiptNotification) LocalRegion() bool { + return false +} + +// GetHash - +func (r *TxReceiptNotification) GetHash() string { + return r.receipt.BlockHash +} + +// NotificationType - feed name +func (r *TxReceiptNotification) NotificationType() FeedType { + return TxReceiptsFeed +} diff --git a/types/txreceiptnotification_test.go b/types/txreceiptnotification_test.go new file mode 100644 index 0000000..a4343a0 --- /dev/null +++ b/types/txreceiptnotification_test.go @@ -0,0 +1,87 @@ +package types + +import ( + "github.com/bloXroute-Labs/gateway/test" + "github.com/stretchr/testify/assert" + "testing" +) + +var txReceiptMap = map[string]interface{}{ + "to": "0x18cf158e1766ca6bdbe2719dace440121b4603b3", + "transactionHash": "0x4df870e552898df04761d6ea87ac848e3c60bfa35a9036b2b4d53ac64730a5b6", + "blockHash": "0x5df870e552898df04761d6ea87ac848e3c60bfa35a9036b2b4d53ac64730a5b7", + "blockNumber": "0xd1d827", + "contractAddress": "0x28cf158e1766ca6bdbe2719dace440121b4603b2", + "cumulativeGasUsed": "0xf9389e", + "effectiveGasPrice": "0x1c298e1cb9", + "from": "0x13cf158e1766ca6bdbe2719dace440121b4603b1", + "gasUsed": "0x5208", + "logs": []interface{}{"0x7cf870e552898df04761d6ea87ac848e3c60bfa35a9036b2b4d53ac64730a5b5"}, + "logsBloom": "0x3df870e552898df04761d6ea87ac848e3c60bfa35a9036b2b4d53ac64730a5b4", + "status": "0x1", + "transactionIndex": "0x64", + "type": "0x2", +} + +var validTxReceiptParams = []string{"block_hash", "block_number", "contract_address", + "cumulative_gas_used", "effective_gas_price", "from", "gas_used", "logs", "logs_bloom", + "status", "to", "transaction_hash", "transaction_index", "type"} + +func TestTxReceiptNotification(t *testing.T) { + txReceiptNotification := NewTxReceiptNotification(txReceiptMap) + + txReceiptWithFields := txReceiptNotification.WithFields(validTxReceiptParams) + + ethTxReceiptWithFields, ok := txReceiptWithFields.(*TxReceiptNotification) + assert.True(t, ok) + + receiptJSON, err := test.MarshallJSONToMap(ethTxReceiptWithFields) + assert.Nil(t, err) + + for _, param := range validTxReceiptParams { + _, ok = receiptJSON[param] + assert.Equal(t, true, ok) + } + for k, v := range txReceiptMap { + assert.Equal(t, v, receiptJSON[test.ToSnakeCase(k)]) + } +} + +func TestTxReceiptNotificationWithoutToField(t *testing.T) { + txReceiptNotification := NewTxReceiptNotification(txReceiptMap) + + txReceiptWithFields := txReceiptNotification.WithFields([]string{"transaction_hash"}) + + ethTxReceiptWithFields, ok := txReceiptWithFields.(*TxReceiptNotification) + assert.True(t, ok) + + receiptJSON, err := test.MarshallJSONToMap(ethTxReceiptWithFields) + assert.Nil(t, err) + + _, ok = receiptJSON["to"] + assert.Equal(t, false, ok) + assert.Equal(t, "0x4df870e552898df04761d6ea87ac848e3c60bfa35a9036b2b4d53ac64730a5b6", receiptJSON["transaction_hash"]) +} + +func TestContractCreationTxReceipt(t *testing.T) { + contractCreationReceiptMap := txReceiptMap + for k, v := range txReceiptMap { + contractCreationReceiptMap[k] = v + } + contractCreationReceiptMap["to"] = nil + + txReceiptNotification := NewTxReceiptNotification(contractCreationReceiptMap) + + txReceiptWithFields := txReceiptNotification.WithFields([]string{"to", "from"}) + + ethTxReceiptWithFields, ok := txReceiptWithFields.(*TxReceiptNotification) + assert.True(t, ok) + + receiptJSON, err := test.MarshallJSONToMap(ethTxReceiptWithFields) + assert.Nil(t, err) + + to, ok := receiptJSON["to"] + assert.Equal(t, true, ok) + assert.Equal(t, nil, to) + assert.Equal(t, "0x13cf158e1766ca6bdbe2719dace440121b4603b1", receiptJSON["from"]) +} diff --git a/utils/cache.go b/utils/cache.go new file mode 100644 index 0000000..493b5fc --- /dev/null +++ b/utils/cache.go @@ -0,0 +1,18 @@ +package utils + +import ( + "io/ioutil" + "path" +) + +// UpdateCacheFile - update a cache file +func UpdateCacheFile(dataDir string, fileName string, value []byte) error { + cacheFileName := path.Join(dataDir, fileName) + return ioutil.WriteFile(cacheFileName, value, 0644) +} + +// LoadCacheFile - load a cache file +func LoadCacheFile(dataDir string, fileName string) ([]byte, error) { + cacheFileName := path.Join(dataDir, fileName) + return ioutil.ReadFile(cacheFileName) +} diff --git a/utils/clock.go b/utils/clock.go new file mode 100644 index 0000000..5e46b8a --- /dev/null +++ b/utils/clock.go @@ -0,0 +1,51 @@ +package utils + +import "time" + +// Clock should be injected into any component that requires access to time +type Clock interface { + Now() time.Time + Timer(d time.Duration) Timer + Sleep(d time.Duration) +} + +// Timer wraps the time.Timer object for mockability in test cases +type Timer interface { + Alert() <-chan time.Time + Reset(d time.Duration) bool + Stop() bool +} + +// RealClock represents the typical clock implementation using the built-in time.Time +type RealClock struct{} + +// Now returns the current system time +func (RealClock) Now() time.Time { + return time.Now() +} + +// Timer returns a timer that will fire after the provided duration +func (RealClock) Timer(d time.Duration) Timer { + return realTimer{time.NewTimer(d)} +} + +// Sleep pauses the current goroutine for the specified duration +func (RealClock) Sleep(d time.Duration) { + time.Sleep(d) +} + +type realTimer struct { + *time.Timer +} + +func (r realTimer) Alert() <-chan time.Time { + return r.Timer.C +} + +func (r realTimer) Reset(d time.Duration) bool { + return r.Timer.Reset(d) +} + +func (r realTimer) Stop() bool { + return r.Timer.Stop() +} diff --git a/utils/eth_flags.go b/utils/eth_flags.go new file mode 100644 index 0000000..8ed666e --- /dev/null +++ b/utils/eth_flags.go @@ -0,0 +1,20 @@ +package utils + +import "github.com/urfave/cli/v2" + +// Ethereum specific flags +var ( + EnodesFlag = &cli.StringFlag{ + Name: "enodes", + Usage: "comma separated list of enode peers to connect to (multiple peers is currently not yet supported, so please only specify one)", + } + PrivateKeyFlag = &cli.StringFlag{ + Name: "private-key", + Usage: "private key for encrypted communication with Ethereum node", + Required: false, + } + EthWSUriFlag = &cli.StringFlag{ + Name: "eth-ws-uri", + Usage: "Ethereum websockets endpoint", + } +) diff --git a/utils/flags.go b/utils/flags.go new file mode 100644 index 0000000..ce063ce --- /dev/null +++ b/utils/flags.go @@ -0,0 +1,300 @@ +package utils + +import ( + "github.com/bloXroute-Labs/gateway" + "github.com/urfave/cli/v2" +) + +// CLI flag variable definitions +var ( + HostFlag = &cli.StringFlag{ + Name: "host", + Usage: "listening interface to bind server on (can be omitted to default to 0.0.0.0)", + Value: bxgateway.AllInterfaces, + } + ExternalIPFlag = &cli.StringFlag{ + Name: "external-ip", + Usage: "public IP address to send to bxapi (can be omitted to be derived on startup)", + Aliases: []string{"ip"}, + } + PortFlag = &cli.IntFlag{ + Name: "port", + Usage: "port for accepting gateway connection", + Aliases: []string{"p"}, + Value: 1809, + } + LegacyTxPortFlag = &cli.IntFlag{ + Name: "legacy-tx-port", + Usage: "tx port for accepting 'RELAY_TRANSACTION' gateway connection (legacy flag)", + Value: 1810, + } + RelayBlockPortFlag = &cli.Int64Flag{ + Name: "relay-block-port", + Usage: "port of block relay", + Aliases: []string{"rb"}, + Value: 0, // was 1809 + } + RelayBlockHostFlag = &cli.StringFlag{ + Name: "relay-block-host", + Usage: "host of block relay", + Aliases: []string{"rbh"}, + Value: "localhost", + } + RelayTxPortFlag = &cli.Int64Flag{ + Name: "relay-tx-port", + Usage: "port of transaction relay", + Aliases: []string{"rp"}, + Value: 0, // was 1811 + } + RelayTxHostFlag = &cli.StringFlag{ + Name: "relay-tx-host", + Usage: "host of transaction relay", + Aliases: []string{"rth"}, + Value: "localhost", + } + RelayHostFlag = &cli.StringFlag{ + Name: "relay-ip", + Usage: "host of relay", + } + EnvFlag = &cli.StringFlag{ + Name: "env", + Usage: "development environment (local, localproxy, testnet, mainnet)", + Value: "mainnet", + } + SDNURLFlag = &cli.StringFlag{ + Name: "sdn-url", + Usage: "SDN URL", + } + SDNSocketIPFlag = &cli.StringFlag{ + Name: "sdn-socket-ip", + Usage: "SDN socket broker IP address", + Value: "127.0.0.1", + } + SDNSocketPortFlag = &cli.IntFlag{ + Name: "sdn-socket-port", + Usage: "SDN socket broker port", + Value: 1800, + } + WSFlag = &cli.BoolFlag{ + Name: "ws", + Usage: "starts a websocket RPC server", + Value: false, + } + WSTLSFlag = &cli.BoolFlag{ + Name: "ws-tls", + Usage: "starts the websocket server using TLS", + Value: false, + } + WSHostFlag = &cli.StringFlag{ + Name: "ws-host", + Usage: "host address for RPC server to run on", + Value: "127.0.0.1", + } + WSPortFlag = &cli.IntFlag{ + Name: "ws-port", + Usage: "port for RPC server to run on", + Aliases: []string{"wsp", "rpc-port"}, + Value: 28333, + } + CACertURLFlag = &cli.StringFlag{ + Name: "ca-cert-url", + Usage: "URL for retrieving CA certificates", + } + FluentdHostFlag = &cli.StringFlag{ + Name: "fluentd-host", + Usage: "fluentd host", + Aliases: []string{"fh"}, + Value: "localhost", + } + FluentDFlag = &cli.BoolFlag{ + Name: "fluentd", + Usage: "sends logs records to fluentD", + Value: false, + } + LogNetworkContentFlag = &cli.BoolFlag{ + Name: "log-network-content", + Usage: "sends blockchain content to fluentD", + Value: false, + Hidden: true, + } + // TODO: this currently must be a file path in current code, not a URL + RegistrationCertDirFlag = &cli.StringFlag{ + Name: "registration-cert-dir", + Usage: "base URL for retrieving SSL certificates", + } + DisableProfilingFlag = &cli.BoolFlag{ + Name: "disable-profiling", + Usage: "true to disable the pprof http server (for relays, where profiling is enabled by default)", + Value: false, + } + DataDirFlag = &cli.StringFlag{ + Name: "data-dir", + Usage: "directory for storing various persistent files (e.g. private SSL certs)", + Value: "datadir", + } + // TBD: remove priority queue and priority from code base. Left here for backward competability but hidden + AvoidPrioritySendingFlag = &cli.BoolFlag{ + Name: "avoid-priority-sending", + Usage: "avoid sending via priority queue", + Value: true, + Hidden: true, + } + LogLevelFlag = &cli.StringFlag{ + Name: "log-level", + Usage: "log level for stdout", + Aliases: []string{"l"}, + Value: "info", + } + LogFileLevelFlag = &cli.StringFlag{ + Name: "log-file-level", + Usage: "log level for the log file", + Value: "info", + } + LogMaxSizeFlag = &cli.IntFlag{ + Name: "log-max-size", + Usage: "maximum size in megabytes of the log file before it gets rotated", + Value: 100, + } + LogMaxAgeFlag = &cli.IntFlag{ + Name: "log-max-age", + Usage: "maximum number of days to retain old log files based on the timestamp encoded in their filename", + Value: 10, + } + LogMaxBackupsFlag = &cli.IntFlag{ + Name: "log-max-backups", + Usage: "maximum number of old log files to retain", + Value: 10, + } + RedisFlag = &cli.BoolFlag{ + Name: "redis", + Usage: "optionally enable Redis for extended caching support for tx trace", + Value: false, + } + RedisHostFlag = &cli.StringFlag{ + Name: "redis-host", + Usage: "redis connection host address", + Value: "127.0.0.1", + } + RedisPortFlag = &cli.IntFlag{ + Name: "redis-port", + Usage: "redis connection port", + Value: 6379, + } + ContinentFlag = &cli.StringFlag{ + Name: "continent", + Usage: "override value for continent current node is running in (otherwise autodetected from IP address)", + Required: false, + } + CountryFlag = &cli.StringFlag{ + Name: "country", + Usage: "override value for country current node is running in (otherwise autodetected from IP address)", + Required: false, + } + RegionFlag = &cli.StringFlag{ + Name: "region", + Usage: "override value for datacenter region current node is running in (otherwise autodetected from IP address)", + Required: false, + } + GRPCFlag = &cli.BoolFlag{ + Name: "grpc", + Usage: "starts the GRPC server", + Value: false, + } + GRPCHostFlag = &cli.StringFlag{ + Name: "grpc-host", + Usage: "host address for GRPC server to run on", + Value: "127.0.0.1", + } + GRPCPortFlag = &cli.IntFlag{ + Name: "grpc-port", + Usage: "port for GRPC server to run on", + Value: 5001, + } + GRPCUserFlag = &cli.StringFlag{ + Name: "grpc-user", + Usage: "user for GRPC authentication", + Value: "", + } + GRPCPasswordFlag = &cli.StringFlag{ + Name: "grpc-password", + Usage: "password for GRPC authentication", + Value: "", + } + GRPCAuthFlag = &cli.StringFlag{ + Name: "grpc-auth", + Usage: "raw authentication header for GRPC ", + } + PeerFileFlag = &cli.StringFlag{ + Name: "peer-file", + Usage: "peer file containing the ip:port list of potential peers for the node to connect to", + Value: "proxypeers", + } + BlockchainNetworkFlag = &cli.StringFlag{ + Name: "blockchain-network", + Usage: "determine the blockchain network (Mainnet or BSC-Mainnet)", + Value: "Mainnet", + } + SyncPeerIPFlag = &cli.StringFlag{ + Name: "sync-peer-ip", + Usage: "the ip address of the node that should sync this node. if not provided the ATR will be used", + } + PlatformProviderFlag = &cli.StringFlag{ + Name: "platform-provider", + Usage: "override value for current node platform provider", + Required: false, + } + DisableTxStoreCleanupFlag = &cli.BoolFlag{ + Name: "disable-txstore-cleanup", + Usage: "if true, relay would NOT be responsible for the txs cleanup", + Value: false, + } + BlocksOnlyFlag = &cli.BoolFlag{ + Name: "blocks-only", + Usage: "set this flag to only propagate blocks from the BDN to the connected node", + Aliases: []string{"miner"}, + Value: false, + } + AllTransactionsFlag = &cli.BoolFlag{ + Name: "all-txs", + Usage: "set this flag to propagate all transactions from the BDN to the connected node (warning: may result in worse performance and propagation times)", + Value: false, + } + TxTraceEnabledFlag = &cli.BoolFlag{ + Name: "txtrace", + Usage: "for gateways only, enables transaction trace logging", + Value: false, + } + TxTraceMaxFileSizeFlag = &cli.IntFlag{ + Name: "txtrace-max-file-size", + Usage: "for gateways only, sets max size of individual tx trace log file (megabytes)", + Value: 100, + } + TxTraceMaxBackupFilesFlag = &cli.IntFlag{ + Name: "txtrace-max-backup-files", + Usage: "for gateways only, sets max number of backup tx trace log files retained (0 enables unlimited backups)", + Value: 3, + } + NodeTypeFlag = &cli.StringFlag{ + Name: "node-type", + Usage: "set node type", + Value: "external_gateway", + } + SSLFlag = &cli.BoolFlag{ + Name: "ssl", + Usage: "Opens a http/websocket server with TLS", + Value: false, + } + ManageWSServer = &cli.BoolFlag{ + Name: "manage-ws-server", + Usage: "for gateways only, monitors blockchain node sync status and shuts down/restarts websocket server accordingly", + Value: false, + } + MevBuilderURIFlag = &cli.StringFlag{ + Name: "mev-builder-uri", + Usage: "set mev builder for gateway", + } + MevMinerURIFlag = &cli.StringFlag{ + Name: "mev-miner-uri", + Usage: "set mev miner for gateway", + } +) diff --git a/utils/ipresolver.go b/utils/ipresolver.go new file mode 100644 index 0000000..108fa50 --- /dev/null +++ b/utils/ipresolver.go @@ -0,0 +1,38 @@ +package utils + +import ( + "fmt" + log "github.com/sirupsen/logrus" + "io/ioutil" + "net/http" + "regexp" +) + +const publicIPResolver = "http://checkip.dyndns.org/" + +var ipRegex, _ = regexp.Compile("[0-9]+(?:\\.[0-9]+){3}") + +// GetPublicIP fetches the publicly seen IP address of the currently running process. +func GetPublicIP() (string, error) { + response, err := http.Get(publicIPResolver) + if err != nil { + return "", err + } + + defer func() { + err = response.Body.Close() + if err != nil { + log.Error(fmt.Errorf("unable to close response body %v error %v", response.Body, err)) + } + }() + + body, err := ioutil.ReadAll(response.Body) + if err != nil { + return "", err + } + if response.StatusCode != 200 { + return "", fmt.Errorf(string(body)) + } + + return string(ipRegex.Find(body)), nil +} diff --git a/utils/memory.go b/utils/memory.go new file mode 100644 index 0000000..da95e90 --- /dev/null +++ b/utils/memory.go @@ -0,0 +1,42 @@ +package utils + +import ( + "errors" + "io/ioutil" + "os" + "strconv" + "strings" +) + +// GetAppMemoryUsage retreive the allocated heap bytes +func GetAppMemoryUsage() (int, error) { + bytes, err := getRss() + if err != nil { + return 0, err + } + return int(bToMb(bytes)), err +} + +func getRss() (int64, error) { + buf, err := ioutil.ReadFile("/proc/self/statm") + if err != nil { + return 0, err + } + + fields := strings.Split(string(buf), " ") + if len(fields) < 2 { + return 0, errors.New("Cannot parse statm") + } + + rss, err := strconv.ParseInt(fields[1], 10, 64) + if err != nil { + return 0, err + } + + return rss * int64(os.Getpagesize()), err +} + +// Convert Byte to Mega-Byte +func bToMb(b int64) int64 { + return b / 1024 / 1024 +} diff --git a/utils/nodetype.go b/utils/nodetype.go new file mode 100644 index 0000000..18f5a58 --- /dev/null +++ b/utils/nodetype.go @@ -0,0 +1,141 @@ +package utils + +import ( + "fmt" + "strings" +) + +// NodeType represents flag indicating node type (Gateway, Relay, etc.) +type NodeType int + +// IsGateway indicates if this instance is a gateway +var IsGateway bool = true + +const ( + // InternalGateway is a gateway run by bloxroute + InternalGateway NodeType = 1 << iota + + // ExternalGateway is a gateway run by anyone + ExternalGateway + + // RelayTransaction is a relay routing transaction messages only + RelayTransaction + + // RelayBlock is a relay routing block messages only + RelayBlock + + // API is the bloxroute SDN + API + + // APISocket is the bloxroute SDN socket broker + APISocket + + // CloudAPI is the cloud API instances + CloudAPI + + // Jobs is the jobs instances that proxy feeds + Jobs + + // GatewayGo is a gateway running in Go + GatewayGo + + // RelayProxy is the proxy relay that connects to gateways and sits in front of relays + RelayProxy + + // Websocket is a websocket connection to a node + Websocket + + // GRPC is a gRPC connection + GRPC + + // Blockchain represents a blockchain connection type + Blockchain + + // Gateway collects all the various gateway types + Gateway = InternalGateway | ExternalGateway | GatewayGo + + // GatewayType aliases Gateway + GatewayType = Gateway + + // Relay collects all the relay types + Relay = RelayTransaction | RelayBlock + + // RelayType aliases Relay + RelayType = Relay +) + +var nodeTypeNames = map[NodeType]string{ + InternalGateway: "INTERNAL_GATEWAY", + ExternalGateway: "EXTERNAL_GATEWAY", + RelayTransaction: "RELAY_TRANSACTION", + RelayBlock: "RELAY_BLOCK", + API: "API", + APISocket: "API_SOCKET", + CloudAPI: "BLOXROUTE_CLOUD_API", + Jobs: "JOBS", + GatewayGo: "GATEWAY_GO", + Gateway: "GATEWAY", + Relay: "RELAY", + RelayProxy: "RELAY_PROXY", + Websocket: "WEBSOCKET", + GRPC: "GRPC", + Blockchain: "BLOCKCHAIN", +} +var nodeNameTypes = map[string]NodeType{ + "INTERNAL_GATEWAY": InternalGateway, + "EXTERNAL_GATEWAY": ExternalGateway, + "RELAY_TRANSACTION": RelayTransaction, + "RELAY_BLOCK": RelayBlock, + "API": API, + "API_SOCKET": APISocket, + "BLOXROUTE_CLOUD_API": CloudAPI, + "JOBS": Jobs, + "GATEWAY_GO": GatewayGo, + "GATEWAY": Gateway, + "RELAY": Relay, + "WEBSOCKET": Websocket, + "GRPC": GRPC, + "RELAY_PROXY": RelayProxy, + "BLOCKCHAIN": Blockchain, +} + +// String returns the string representation of a node type for use (e.g. in JSON dumps) +func (n NodeType) String() string { + s, ok := nodeTypeNames[n] + if ok { + return s + } + return "UNKNOWN" +} + +// DeserializeNodeType parses the node type from a serialized form. +// Placeholder function, since this node type is not currently used. +func DeserializeNodeType(b []byte) (NodeType, error) { + s, ok := nodeNameTypes[string(b)] + if ok { + return s, nil + } + return 0, fmt.Errorf("could not deserialize unknown node value %v", string(b)) +} + +// FromStringToNodeType return nodeType of string name +func FromStringToNodeType(s string) (NodeType, error) { + cs := strings.Replace(s, "-", "", -1) + cs = strings.ToUpper(cs) + nt, ok := nodeNameTypes[cs] + if ok { + return nt, nil + } + return 0, fmt.Errorf("could not deserialize unknown node value %v", cs) +} + +// FormatShortNodeType returns the short string representation of a node type +func (n NodeType) FormatShortNodeType() string { + if n&Gateway != 0 { + return "G" + } + if n&RelayTransaction != 0 { + return "R" + } + return n.String() +} diff --git a/utils/priorityqueue.go b/utils/priorityqueue.go new file mode 100644 index 0000000..bf21df7 --- /dev/null +++ b/utils/priorityqueue.go @@ -0,0 +1,143 @@ +package utils + +import ( + "container/heap" + "github.com/bloXroute-Labs/gateway/bxmessage" + "sync" + "time" +) + +// MsgPriorityQueue hold messages by message priority +type MsgPriorityQueue struct { + lock sync.Mutex + callBack func(bxmessage.Message) + callBackInterval time.Duration + storage items + lastPopTime time.Time +} + +// PriorityQueueItem holds message to be sent by priority and time that message is pushed onto queue +type PriorityQueueItem struct { + msg bxmessage.Message + timeReceived time.Time +} + +// NewMsgPriorityQueue creates an empty, initialized priorityQueue heap for messages +func NewMsgPriorityQueue(callBack func(bxmessage.Message), callBackInterval time.Duration) *MsgPriorityQueue { + pq := &MsgPriorityQueue{ + callBack: callBack, + callBackInterval: callBackInterval, + storage: make(items, 0), + } + heap.Init(&pq.storage) + return pq +} + +// Len provides the number of messages in the priority queue +func (pq *MsgPriorityQueue) Len() int { + pq.lock.Lock() + defer pq.lock.Unlock() + return pq.len() +} + +func (pq *MsgPriorityQueue) len() int { + // should be called with lock held + return pq.storage.Len() +} +func (pq *MsgPriorityQueue) push(item PriorityQueueItem) { + // should be called with lock held + heap.Push(&pq.storage, item) +} + +// Push adds new message to the priority queue +func (pq *MsgPriorityQueue) Push(msg bxmessage.Message) { + pq.lock.Lock() + defer pq.lock.Unlock() + + item := PriorityQueueItem{msg, time.Now()} + + // if no callback needed, place on PQ and leave + if pq.callBack == nil || pq.callBackInterval == 0 { + pq.push(item) + return + } + // if pq is empty we might skip the priority queue + if pq.len() == 0 && time.Now().Sub(pq.lastPopTime) > pq.callBackInterval { + go pq.callBack(item.msg) + pq.lastPopTime = time.Now() + return + } + pq.push(item) + // once there is something on pq we need a gorouting to clean it + if pq.len() == 1 { + go func() { + ticker := time.NewTicker(pq.callBackInterval) + for { + select { + case <-ticker.C: + pq.lock.Lock() + msg := pq.pop() + pq.lastPopTime = time.Now() + empty := pq.len() == 0 + pq.lock.Unlock() + go pq.callBack(msg) + // if pq becomes empty we quite + if empty { + ticker.Stop() + return + } + } + } + }() + } +} + +func (pq *MsgPriorityQueue) pop() bxmessage.Message { + // should be called with lock held + if pq.storage.Len() == 0 { + return nil + } + return heap.Pop(&pq.storage).(PriorityQueueItem).msg +} + +// Pop extract the best message from the priority queue +func (pq *MsgPriorityQueue) Pop() bxmessage.Message { + pq.lock.Lock() + defer pq.lock.Unlock() + return pq.pop() +} + +// items holds message to be sent by priority and time received +type items []PriorityQueueItem + +// Len is the size of the queue +func (items items) Len() int { return len(items) } + +// Less provides the lower priority entry +func (items items) Less(i, j int) bool { + // We want Pop to give us the highest priority item (lower number) that was received first + if items[i].msg.GetPriority() != items[j].msg.GetPriority() { + return items[i].msg.GetPriority() < items[j].msg.GetPriority() + } + return items[i].timeReceived.Before(items[j].timeReceived) +} + +// Pop provides the current lowest priority +func (items *items) Pop() interface{} { + old := *items + n := len(old) + item := old[n-1] + *items = old[0 : n-1] + return item +} + +// Push is used to add new entry +func (items *items) Push(x interface{}) { + item := x.(PriorityQueueItem) + *items = append(*items, item) +} + +// Swap is used to swp too entries +func (items items) Swap(i, j int) { + items[i], items[j] = items[j], items[i] +} diff --git a/utils/priorityqueue_test.go b/utils/priorityqueue_test.go new file mode 100644 index 0000000..1f55a0c --- /dev/null +++ b/utils/priorityqueue_test.go @@ -0,0 +1,47 @@ +package utils + +import ( + "github.com/bloXroute-Labs/gateway/bxmessage" + "testing" +) + +func TestPriorityQueue(t *testing.T) { + var listItems []bxmessage.Message + priorities := []bxmessage.SendPriority{10, 20, 15, 100, 1, 0, 15} + // create by priorities + for _, priority := range priorities { + item := &bxmessage.Tx{} + item.SetPriority(priority) + listItems = append(listItems, item) + } + + msgPQ := NewMsgPriorityQueue(nil, 0) + for _, item := range listItems { + msgPQ.Push(item) + } + for msgPQ.len() > 0 { + msg := msgPQ.pop() + t.Logf("Priority: %d", msg.GetPriority()) + } + + //goland:noinspection GoNilness + msgPQ.Push(listItems[0]) + for i := 1; i < len(listItems)-1; i++ { + msg := msgPQ.Pop() + min := msg.GetPriority() + msgPQ.Push(msg) + msgPQ.Push(listItems[i]) + if listItems[i].GetPriority() < min { + min = listItems[i].GetPriority() + } + msg = msgPQ.Pop() + t.Logf("got %v, min %v", msg.GetPriority(), min) + if msg.GetPriority() != min { + t.Errorf("Got wrong entry ") + } + } + msg := msgPQ.Pop() + if msg.GetPriority() != 100 { + t.Errorf("Got wrong last entry") + } +} diff --git a/utils/slices.go b/utils/slices.go new file mode 100644 index 0000000..7274a72 --- /dev/null +++ b/utils/slices.go @@ -0,0 +1,11 @@ +package utils + +// Exists - checks if a field exists in slice +func Exists(field string, slice []string) bool { + for _, valid := range slice { + if field == valid { + return true + } + } + return false +} diff --git a/utils/ssl.go b/utils/ssl.go new file mode 100644 index 0000000..a272d10 --- /dev/null +++ b/utils/ssl.go @@ -0,0 +1,286 @@ +package utils + +import ( + "crypto/ecdsa" + "crypto/elliptic" + "crypto/rand" + "crypto/tls" + "crypto/x509" + "encoding/pem" + "errors" + "fmt" + "github.com/bloXroute-Labs/gateway/types" + "io/ioutil" + "os" + "path" +) + +// SSLCerts represents the required certificate files for interacting with the BDN. +// Private keys/certs are used for TLS socket connections, and registration only keys/certs +// are for creating a CSR for bxapi to return the signed private cert. +type SSLCerts struct { + privateCertFile string + privateKeyFile string + registrationOnlyCertFile string + registrationOnlyKeyFile string + + privateCert *x509.Certificate + privateKey ecdsa.PrivateKey + privateKeyPair *tls.Certificate + registrationOnlyCert x509.Certificate + registrationOnlyCertBlock []byte + registrationOnlyKey ecdsa.PrivateKey + registrationOnlyKeyPair tls.Certificate +} + +// GetCertDir getting cert, key and registration files +func GetCertDir(registrationOnlyBaseURL, privateBaseURL, certName string) (privateCertFile string, privateKeyFile string, registrationOnlyCertFile string, registrationOnlyKeyFile string) { + var ( + privateDir = path.Join(privateBaseURL, certName, "private") + registrationOnlyDir = path.Join(registrationOnlyBaseURL, certName, "registration_only") + ) + + _ = os.MkdirAll(privateDir, 0755) + _ = os.MkdirAll(registrationOnlyDir, 0755) + + privateCertFile = path.Join(privateDir, fmt.Sprintf("%v_cert.pem", certName)) + privateKeyFile = path.Join(privateDir, fmt.Sprintf("%v_key.pem", certName)) + registrationOnlyCertFile = path.Join(registrationOnlyDir, fmt.Sprintf("%v_cert.pem", certName)) + registrationOnlyKeyFile = path.Join(registrationOnlyDir, fmt.Sprintf("%v_key.pem", certName)) + return +} + +// NewSSLCerts returns and initializes new storage of SSL certificates. +// Registration only keys/certs are mandatory. If they cannot be loaded, this function will panic. +// Private keys and certs must match each other. If they do not, a new private key will be generated +// and written, pending loading of a new certificate. +func NewSSLCerts(registrationOnlyBaseURL, privateBaseURL, certName string) SSLCerts { + privateCertFile, privateKeyFile, registrationOnlyCertFile, registrationOnlyKeyFile := GetCertDir(registrationOnlyBaseURL, privateBaseURL, certName) + return NewSSLCertsFromFiles(privateCertFile, privateKeyFile, registrationOnlyCertFile, registrationOnlyKeyFile) +} + +// NewSSLCertsFromFiles receiving cert files info returns and initializes new storage of SSL certificates. +// Registration only keys/certs are mandatory. If they cannot be loaded, this function will panic. +// Private keys and certs must match each other. If they do not, a new private key will be generated +// and written, pending loading of a new certificate. +func NewSSLCertsFromFiles(privateCertFile string, privateKeyFile string, registrationOnlyCertFile string, registrationOnlyKeyFile string) SSLCerts { + registrationOnlyCertBlock, err := ioutil.ReadFile(registrationOnlyCertFile) + if err != nil { + panic(fmt.Errorf("could not read registration only cert from file (%v): %v", registrationOnlyCertFile, err)) + } + registrationOnlyCert, err := parsePEMCert(registrationOnlyCertBlock) + if err != nil { + panic(fmt.Errorf("could not parse PEM data from registration only cert: %v", err)) + } + + registrationOnlyKeyBlock, err := ioutil.ReadFile(registrationOnlyKeyFile) + if err != nil { + panic(fmt.Errorf("could not read registration only key from file (%v): %v", registrationOnlyKeyFile, err)) + } + registrationOnlyKey, err := parsePEMPrivateKey(registrationOnlyKeyBlock) + if err != nil { + panic(fmt.Errorf("could not parse PEM data from registration only key: %v", err)) + } + registrationOnlyKeyPair, err := tls.X509KeyPair(registrationOnlyCertBlock, registrationOnlyKeyBlock) + if err != nil { + panic(fmt.Errorf("could not load registration only key pair: %v", err)) + } + + var privateKey *ecdsa.PrivateKey + var privateCert *x509.Certificate + var privateKeyPair *tls.Certificate + + privateCertBlock, certErr := ioutil.ReadFile(privateCertFile) + if certErr != nil { + privateCertBlock = nil + } + privateKeyBlock, keyErr := ioutil.ReadFile(privateKeyFile) + if keyErr != nil { + privateKeyBlock = nil + } + + if privateKeyBlock == nil && privateCertBlock == nil { + privateKey, err = ecdsa.GenerateKey(elliptic.P384(), rand.Reader) + if err != nil { + panic(fmt.Errorf("could not generate private key: %v", err)) + } + privateKeyBytes, err := x509.MarshalECPrivateKey(privateKey) + if err != nil { + panic(fmt.Errorf("could not marshal generated private key: %v", err)) + } + privateKeyBlock = pem.EncodeToMemory(&pem.Block{Type: "EC PRIVATE KEY", Bytes: privateKeyBytes}) + err = ioutil.WriteFile(privateKeyFile, privateKeyBlock, 0644) + if err != nil { + panic(fmt.Errorf("could not write new private key to file (%v): %v", privateKeyFile, err)) + } + } else if privateKeyBlock != nil && privateCertBlock != nil { + privateKey, err = parsePEMPrivateKey(privateKeyBlock) + if err != nil { + panic(fmt.Errorf("could not parse private key from file (%v): %v", privateKeyFile, err)) + } + privateCert, err = parsePEMCert(privateCertBlock) + if err != nil { + panic(fmt.Errorf("could not parse private cert from file (%v): %v", privateCert, err)) + } + _privateKeyPair, err := tls.X509KeyPair(privateCertBlock, privateKeyBlock) + privateKeyPair = &_privateKeyPair + if err != nil { + panic(fmt.Errorf("could not load private key pair: %v", err)) + } + } else if privateKeyBlock != nil { + privateKey, err = parsePEMPrivateKey(privateKeyBlock) + if err != nil { + panic(fmt.Errorf("could not parse private key from file (%v): %v", privateKeyFile, err)) + } + } else { + panic(fmt.Errorf("found a certificate with no matching private key –– delete the certificate at %v if it's not needed", privateCertFile)) + } + + return SSLCerts{ + privateCertFile: privateCertFile, + privateKeyFile: privateKeyFile, + privateCert: privateCert, + privateKey: *privateKey, + privateKeyPair: privateKeyPair, + + registrationOnlyCertFile: registrationOnlyCertFile, + registrationOnlyKeyFile: registrationOnlyKeyFile, + registrationOnlyCert: *registrationOnlyCert, + registrationOnlyCertBlock: registrationOnlyCertBlock, + registrationOnlyKey: *registrationOnlyKey, + registrationOnlyKeyPair: registrationOnlyKeyPair, + } +} + +func parsePEMPrivateKey(block []byte) (*ecdsa.PrivateKey, error) { + decodedKeyBlock, _ := pem.Decode(block) + keyBytes := decodedKeyBlock.Bytes + return x509.ParseECPrivateKey(keyBytes) +} + +func parsePEMCert(block []byte) (*x509.Certificate, error) { + decodedCertBlock, _ := pem.Decode(block) + certBytes := decodedCertBlock.Bytes + return x509.ParseCertificate(certBytes) +} + +// NeedsPrivateCert indicates if SSL storage has been populated with the private certificate +func (s SSLCerts) NeedsPrivateCert() bool { + return s.privateCert == nil +} + +// CreateCSR returns a PEM encoded x509.CertificateRequest, generated using the registration only cert template +// and signed with the private key +func (s SSLCerts) CreateCSR() ([]byte, error) { + certRequest := x509.CertificateRequest{ + Subject: s.registrationOnlyCert.Subject, + Extensions: s.registrationOnlyCert.Extensions, + } + x509CR, err := x509.CreateCertificateRequest(rand.Reader, &certRequest, &s.privateKey) + if err != nil { + return nil, err + } + serializedCR := pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE REQUEST", Bytes: x509CR}) + return serializedCR, nil +} + +// SerializeRegistrationCert returns the PEM encoded registration x509.Certificate +func (s SSLCerts) SerializeRegistrationCert() ([]byte, error) { + return s.registrationOnlyCertBlock, nil +} + +// SavePrivateCert saves the private certificate and updates the key pair. +func (s *SSLCerts) SavePrivateCert(privateCert string) error { + privateCertBytes := []byte(privateCert) + cert, err := parsePEMCert(privateCertBytes) + if err != nil { + return fmt.Errorf("could not parse private cert: %v", err) + } + s.privateCert = cert + + privateKeyBytes, err := x509.MarshalECPrivateKey(&s.privateKey) + if err != nil { + return fmt.Errorf("stored private key was somehow invalid: %v", err) + } + privateKeyBlock := pem.EncodeToMemory(&pem.Block{Type: "EC PRIVATE KEY", Bytes: privateKeyBytes}) + privateKeyPair, err := tls.X509KeyPair(privateCertBytes, privateKeyBlock) + if err != nil { + return fmt.Errorf("could not parse private key pair: %v", err) + } + s.privateKeyPair = &privateKeyPair + + return ioutil.WriteFile(s.privateCertFile, privateCertBytes, 0644) +} + +// LoadPrivateConfig generates TLS config from the private certificates. +// The resulting config can be used for any bxapi or socket communications. +func (s SSLCerts) LoadPrivateConfig() (*tls.Config, error) { + if s.privateKeyPair == nil { + return nil, errors.New("private key pair has not been loaded") + } + config := &tls.Config{ + Certificates: []tls.Certificate{*s.privateKeyPair}, + InsecureSkipVerify: true, + } + return config, nil +} + +// LoadPrivateConfigWithCA generates TLS config from the private certificate. +// The resulting config can be used to configure a server that allows inbound connections. +func (s SSLCerts) LoadPrivateConfigWithCA(caPath string) (*tls.Config, error) { + if s.privateKeyPair == nil { + return nil, errors.New("private key pair has not been loaded") + } + + caCertPEM, err := ioutil.ReadFile(caPath) + if err != nil { + return nil, err + } + + roots := x509.NewCertPool() + ok := roots.AppendCertsFromPEM(caCertPEM) + if !ok { + panic("failed to parse root certificate") + } + + config := &tls.Config{ + Certificates: []tls.Certificate{*s.privateKeyPair}, + ClientAuth: tls.RequireAndVerifyClientCert, + ClientCAs: roots, + } + return config, nil +} + +// GetNodeID reads the node ID embedded in the private certificate storage +func (s SSLCerts) GetNodeID() (types.NodeID, error) { + if s.privateCert == nil { + return "", errors.New("private certificate has not been loaded") + } + + sslProperties, err := ParseBxCertificate(s.privateCert) + if err != nil { + return "", err + } + + return sslProperties.NodeID, nil +} + +// GetAccountID reads the account ID embedded in the local certificates +func (s SSLCerts) GetAccountID() (types.AccountID, error) { + sslProperties, err := ParseBxCertificate(&s.registrationOnlyCert) + if err != nil { + return "", err + } + return sslProperties.AccountID, nil +} + +// LoadRegistrationConfig generates TLS config from the registration only certificate. +// The resulting config can only be used to register the node with bxapi, which will +// then return a private certificate for future use. +func (s SSLCerts) LoadRegistrationConfig() (*tls.Config, error) { + config := &tls.Config{ + Certificates: []tls.Certificate{s.registrationOnlyKeyPair}, + InsecureSkipVerify: true, + } + return config, nil +} diff --git a/utils/ssl_test.go b/utils/ssl_test.go new file mode 100644 index 0000000..01ea1c3 --- /dev/null +++ b/utils/ssl_test.go @@ -0,0 +1,51 @@ +package utils + +import ( + "github.com/bloXroute-Labs/gateway/test" + "github.com/stretchr/testify/assert" + "os" + "testing" +) + +func TestSSLCerts_NoKeysProvided(t *testing.T) { + setupRegistrationFiles("test") + defer cleanupFiles() + + sslCerts := NewSSLCerts(test.SSLTestPath, test.SSLTestPath, "test") + assert.True(t, sslCerts.NeedsPrivateCert()) + + privateKey, _ := parsePEMPrivateKey([]byte(test.PrivateKey)) + sslCerts.privateKey = *privateKey + err := sslCerts.SavePrivateCert(test.PrivateCert) + + assert.Nil(t, err) + assert.NotNil(t, sslCerts.privateCert) + assert.NotNil(t, sslCerts.privateKeyPair) +} + +func TestSSLCerts_SerializeRegistrationCert(t *testing.T) { + setupRegistrationFiles("test") + defer cleanupFiles() + sslCerts := NewSSLCerts(test.SSLTestPath, test.SSLTestPath, "test") + + registrationCert, err := sslCerts.SerializeRegistrationCert() + assert.Nil(t, err) + assert.Equal(t, test.RegistrationCert, string(registrationCert)) +} + +func TestSSLCerts_LoadCACert(t *testing.T) { + setupRegistrationFiles("test") + setupPrivateFiles("test") + SetupCAFiles() + defer cleanupFiles() + + sslCerts := NewSSLCerts(test.SSLTestPath, test.SSLTestPath, "test") + + tlsConfig, err := sslCerts.LoadPrivateConfigWithCA(test.CACertPath) + assert.Nil(t, err) + assert.NotNil(t, tlsConfig) +} + +func cleanupFiles() { + _ = os.RemoveAll(test.SSLTestPath) +} diff --git a/utils/sslcerts.go b/utils/sslcerts.go new file mode 100644 index 0000000..dfb1155 --- /dev/null +++ b/utils/sslcerts.go @@ -0,0 +1,83 @@ +package utils + +import ( + "fmt" + "github.com/bloXroute-Labs/gateway/test" + "io/ioutil" + "os" + "path" +) + +// TestCerts uses the test certs specified in constants to return an utils.SSLCerts object for connection testing +func TestCerts() SSLCerts { + defer CleanupSSLCerts() + SetupSSLFiles("test") + return TestCertsWithoutSetup() +} + +// TestCertsWithoutSetup uses the test certs specified in constants to return an utils.SSLCerts object for connection testing. This function does not do any setup/teardown of writing said files temporarily to disk. +func TestCertsWithoutSetup() SSLCerts { + return NewSSLCerts(test.SSLTestPath, test.SSLTestPath, "test") +} + +// SetupSSLFiles writes the fixed test certificates to disk for loading into an SSL context. +func SetupSSLFiles(certName string) { + setupRegistrationFiles(certName) + setupPrivateFiles(certName) + SetupCAFiles() +} + +func setupRegistrationFiles(certName string) { + makeFolders(certName) + writeCerts("registration_only", certName, test.RegistrationCert, test.RegistrationKey) +} + +func setupPrivateFiles(certName string) { + makeFolders(certName) + writeCerts("private", certName, test.PrivateCert, test.PrivateKey) +} + +// SetupCAFiles writes the CA files to disk for loading into an SSL context. +func SetupCAFiles() { + err := os.MkdirAll(test.CACertFolder, 0755) + if err != nil { + panic(err) + } + err = ioutil.WriteFile(test.CACertPath, []byte(test.CACert), 0644) + if err != nil { + panic(err) + } +} + +func makeFolders(name string) { + privatePath := path.Join(test.SSLTestPath, name, "private") + registrationPath := path.Join(test.SSLTestPath, name, "registration_only") + err := os.MkdirAll(privatePath, 0755) + if err != nil { + panic(err) + } + err = os.MkdirAll(registrationPath, 0755) + if err != nil { + panic(err) + } +} + +func writeCerts(folder, name, cert, key string) { + p := path.Join(test.SSLTestPath, name, folder) + keyPath := path.Join(p, fmt.Sprintf("%v_cert.pem", name)) + certPath := path.Join(p, fmt.Sprintf("%v_key.pem", name)) + + err := ioutil.WriteFile(keyPath, []byte(cert), 0644) + if err != nil { + panic(err) + } + err = ioutil.WriteFile(certPath, []byte(key), 0644) + if err != nil { + panic(err) + } +} + +// CleanupSSLCerts clears the temporary SSL certs written to disk. +func CleanupSSLCerts() { + _ = os.RemoveAll(test.SSLTestPath) +} diff --git a/utils/sslextension.go b/utils/sslextension.go new file mode 100644 index 0000000..976df2d --- /dev/null +++ b/utils/sslextension.go @@ -0,0 +1,93 @@ +package utils + +import ( + "crypto/x509" + "crypto/x509/pkix" + "github.com/bloXroute-Labs/gateway/types" + log "github.com/sirupsen/logrus" +) + +// BxSSLProperties represents extension data encoded in bloxroute SSL certificates +type BxSSLProperties struct { + NodeType NodeType + NodeID types.NodeID + AccountID types.AccountID + NodePrivileges string // not currently used in Ethereum +} + +// Extension ID types encoded in TLS certificates +const ( + nodeTypeExtensionID = "1.22.333.4444" + nodeIDExtensionID = "1.22.333.4445" + accountIDExtensionID = "1.22.333.4446" + nodePrivilegesExtensionID = "1.22.333.4447" +) + +// general extension IDs +const ( + subjectKeyID = "2.5.29.14" + keyUsageID = "2.5.29.15" + subjectAltNameID = "2.5.29.17" + basicConstraintsID = "2.5.29.19" + authorityKeyID = "2.5.29.35" +) + +// ParseBxCertificate extracts bloXroute specific extension information from the SSL certificates +func ParseBxCertificate(certificate *x509.Certificate) (BxSSLProperties, error) { + var ( + nodeType NodeType + nodeID types.NodeID + accountID types.AccountID + nodePrivileges string + err error + bxSSLExtensions BxSSLProperties + ) + + for _, extension := range certificate.Extensions { + switch extension.Id.String() { + case nodeTypeExtensionID: + nodeType, err = DeserializeNodeType(extension.Value) + if err != nil { + return bxSSLExtensions, err + } + case nodeIDExtensionID: + nodeID = types.NodeID(extension.Value) + case accountIDExtensionID: + accountID = types.AccountID(extension.Value) + case nodePrivilegesExtensionID: + nodePrivileges = string(extension.Value) + case subjectKeyID: + case keyUsageID: + case subjectAltNameID: + case basicConstraintsID: + case authorityKeyID: + default: + log.Debugf("found an unexpected extension in TLS certificate: %v => %v", extension.Id, extension.Value) + } + } + + // TODO maybe need to return proper error? + if nodeID == "" { + return bxSSLExtensions, err + } + bxSSLExtensions = BxSSLProperties{ + NodeType: nodeType, + NodeID: nodeID, + AccountID: accountID, + NodePrivileges: nodePrivileges, + } + return bxSSLExtensions, nil +} + +// GetAccountIDFromBxCertificate get account ID from cert +func GetAccountIDFromBxCertificate(extensions []pkix.Extension) types.AccountID { + var accountID types.AccountID + for _, extension := range extensions { + if extension.Id.String() == accountIDExtensionID { + accountID = types.AccountID(extension.Value) + break + } + } + + return accountID +} diff --git a/utils/util.go b/utils/util.go new file mode 100644 index 0000000..ec9ec13 --- /dev/null +++ b/utils/util.go @@ -0,0 +1,34 @@ +package utils + +import ( + "fmt" + "net/url" + "strconv" + "strings" +) + +// GetWSIPPort parses websocket URI and returns IP and port +func GetWSIPPort(uri string) (string, int, error) { + parsedWSURI, err := url.Parse(uri) + if err != nil { + panic(fmt.Errorf("invalid websocket URI parameter %v provided: %v", uri, err)) + } + wsIPPort := strings.Split(parsedWSURI.Host, ":") + if len(wsIPPort) < 2 { + panic(fmt.Errorf("invalid websocket URI parameter %v provided: %v", uri, err)) + } + IP := wsIPPort[0] + port, err := strconv.Atoi(wsIPPort[1]) + if err != nil { + return "", 0, fmt.Errorf("invalid websocket URI parameter %v provided: %v. Unable to convert port to integer", uri, err) + } + return IP, port, nil +} + +// Abs returns the absolute value of an integer +func Abs(x int) int { + if x < 0 { + return -x + } + return x +} diff --git a/version/version.go b/version/version.go new file mode 100644 index 0000000..0ec0a6b --- /dev/null +++ b/version/version.go @@ -0,0 +1,8 @@ +package version + +var ( + // BuildVersion - github version at build time + BuildVersion string = "2.108.3.0" + // BuildDate - date and time of build + BuildDate string = "12-28-2021 12:59:04" +)