diff options
-rwxr-xr-x | .ci/lint.sh | 8 | ||||
-rw-r--r-- | .github/workflows/ci.yml | 14 | ||||
-rw-r--r-- | .golangci.yml | 48 | ||||
-rw-r--r-- | accounts/keystore/account_cache.go | 2 | ||||
-rw-r--r-- | consensus/dummy/consensus.go | 3 | ||||
-rw-r--r-- | core/state/statedb.go | 1 | ||||
-rw-r--r-- | core/types/block.go | 2 | ||||
-rw-r--r-- | core/vm/instructions.go | 1 | ||||
-rw-r--r-- | coreth.go | 4 | ||||
-rw-r--r-- | eth/backend.go | 25 | ||||
-rw-r--r-- | eth/gasprice/gasprice.go | 2 | ||||
-rw-r--r-- | eth/protocol.go | 221 | ||||
-rw-r--r-- | ethclient/ethclient.go | 14 | ||||
-rw-r--r-- | ethstats/ethstats.go | 785 | ||||
-rw-r--r-- | go.mod | 2 | ||||
-rw-r--r-- | go.sum | 4 | ||||
-rw-r--r-- | miner/worker.go | 15 | ||||
-rw-r--r-- | node/api.go | 1 | ||||
-rw-r--r-- | node/config.go | 2 | ||||
-rw-r--r-- | node/node.go | 1 | ||||
-rw-r--r-- | notes/copied-list.txt | 3 | ||||
-rw-r--r-- | notes/hacked-list.txt | 2 | ||||
-rw-r--r-- | plugin/evm/block.go | 5 | ||||
-rw-r--r-- | plugin/evm/error.go | 2 | ||||
-rw-r--r-- | plugin/evm/export_tx_test.go | 4 | ||||
-rw-r--r-- | plugin/evm/import_tx_test.go | 8 | ||||
-rw-r--r-- | plugin/evm/service.go | 5 | ||||
-rw-r--r-- | plugin/evm/vm.go | 181 | ||||
-rw-r--r-- | plugin/evm/vm_test.go | 400 |
29 files changed, 639 insertions, 1126 deletions
diff --git a/.ci/lint.sh b/.ci/lint.sh new file mode 100755 index 0000000..e5363a8 --- /dev/null +++ b/.ci/lint.sh @@ -0,0 +1,8 @@ +# binary will be $(go env GOPATH)/bin/golangci-lint +curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sh -s -- -b $(go env GOPATH)/bin v1.30.0 + +export PATH=$PATH:$(go env GOPATH)/bin + +golangci-lint --version + +golangci-lint run --max-same-issues 0 diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 31ae101..c5ef50b 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -2,6 +2,20 @@ name: CI on: [pull_request, push] jobs: + lint: + name: Lint Golang v${{ matrix.go }} (${{ matrix.os }}) + runs-on: ${{ matrix.os }} + strategy: + matrix: + go: ['1.15'] + os: [ubuntu-18.04] + steps: + - uses: actions/checkout@v2 + - uses: actions/setup-go@v1 + with: + go-version: ${{ matrix.go }} + - run: .ci/lint.sh + shell: bash test: name: Golang v${{ matrix.go }} (${{ matrix.os }}) runs-on: ${{ matrix.os }} diff --git a/.golangci.yml b/.golangci.yml new file mode 100644 index 0000000..d2a5e38 --- /dev/null +++ b/.golangci.yml @@ -0,0 +1,48 @@ +# This file configures github.com/golangci/golangci-lint. + +run: + timeout: 3m + tests: true + # default is true. Enables skipping of directories: + # vendor$, third_party$, testdata$, examples$, Godeps$, builtin$ + skip-dirs-use-default: true + skip-files: + - core/genesis_alloc.go + +linters: + disable-all: true + enable: + - deadcode + - goconst + - goimports + - gosimple + - govet + - ineffassign + - misspell + - unconvert + - varcheck + +linters-settings: + gofmt: + simplify: true + goconst: + min-len: 3 # minimum length of string constant + min-occurrences: 6 # minimum number of occurrences + +issues: + exclude-rules: + - path: crypto/blake2b/ + linters: + - deadcode + - path: crypto/bn256/cloudflare + linters: + - deadcode + - path: p2p/discv5/ + linters: + - deadcode + - path: core/vm/instructions_test.go + linters: + - goconst + - path: cmd/faucet/ + linters: + - deadcode diff --git a/accounts/keystore/account_cache.go b/accounts/keystore/account_cache.go index 76bd552..7ae4d37 100644 --- a/accounts/keystore/account_cache.go +++ b/accounts/keystore/account_cache.go @@ -28,9 +28,9 @@ import ( "time" "github.com/ava-labs/coreth/accounts" + mapset "github.com/deckarep/golang-set" "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/log" - mapset "github.com/deckarep/golang-set" ) // Minimum amount of time between cache reloads. This limit applies if the platform does diff --git a/consensus/dummy/consensus.go b/consensus/dummy/consensus.go index da63673..866ff97 100644 --- a/consensus/dummy/consensus.go +++ b/consensus/dummy/consensus.go @@ -3,11 +3,12 @@ package dummy import ( "errors" "fmt" - "golang.org/x/crypto/sha3" "math/big" "runtime" "time" + "golang.org/x/crypto/sha3" + "github.com/ava-labs/coreth/consensus" "github.com/ava-labs/coreth/core/state" "github.com/ava-labs/coreth/core/types" diff --git a/core/state/statedb.go b/core/state/statedb.go index c4d926d..f75428b 100644 --- a/core/state/statedb.go +++ b/core/state/statedb.go @@ -43,7 +43,6 @@ type revision struct { var ( // emptyRoot is the known root hash of an empty trie. emptyRoot = common.HexToHash("56e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421") - zeroRoot = common.HexToHash("0000000000000000000000000000000000000000000000000000000000000000") ) type proofList [][]byte diff --git a/core/types/block.go b/core/types/block.go index 8e23488..b293ae7 100644 --- a/core/types/block.go +++ b/core/types/block.go @@ -86,7 +86,7 @@ type Header struct { Extra []byte `json:"extraData" gencodec:"required"` MixDigest common.Hash `json:"mixHash"` Nonce BlockNonce `json:"nonce"` - ExtDataHash common.Hash `json:"extDataHash" gencodec:"required"` + ExtDataHash common.Hash `json:"extDataHash" gencodec:"required"` } // field type overrides for gencodec diff --git a/core/vm/instructions.go b/core/vm/instructions.go index e1eb25e..3e1da26 100644 --- a/core/vm/instructions.go +++ b/core/vm/instructions.go @@ -18,6 +18,7 @@ package vm import ( "errors" + "github.com/ava-labs/coreth/core/types" "github.com/ava-labs/coreth/params" "github.com/ethereum/go-ethereum/common" @@ -40,10 +40,6 @@ type ETHChain struct { bcb *eth.BackendCallbacks } -func isLocalBlock(block *types.Block) bool { - return false -} - // NewETHChain creates an Ethereum blockchain with the given configs. func NewETHChain(config *eth.Config, nodecfg *node.Config, etherBase *common.Address, chainDB ethdb.Database) *ETHChain { if config == nil { diff --git a/eth/backend.go b/eth/backend.go index 728ec4d..9fb8f38 100644 --- a/eth/backend.go +++ b/eth/backend.go @@ -23,7 +23,6 @@ import ( "math/big" "runtime" "sync" - //"sync/atomic" "github.com/ava-labs/coreth/accounts" "github.com/ava-labs/coreth/consensus" @@ -53,6 +52,16 @@ import ( "github.com/ethereum/go-ethereum/rlp" ) +// ProtocolVersions are the supported versions of the eth protocol (first is primary). +var ProtocolVersions = []uint{eth65, eth64, eth63} + +// Constants to match up protocol versions and messages +const ( + eth63 = 63 + eth64 = 64 + eth65 = 65 +) + type BackendCallbacks struct { OnQueryAcceptedBlock func() *types.Block } @@ -498,13 +507,13 @@ func (s *Ethereum) Start() error { s.startBloomHandlers(params.BloomBitsBlocks) // Figure out a max peers count based on the server limits - maxPeers := s.p2pServer.MaxPeers - if s.config.LightServ > 0 { - if s.config.LightPeers >= s.p2pServer.MaxPeers { - return fmt.Errorf("invalid peer config: light peer count (%d) >= total peer count (%d)", s.config.LightPeers, s.p2pServer.MaxPeers) - } - maxPeers -= s.config.LightPeers - } + // maxPeers := s.p2pServer.MaxPeers + // if s.config.LightServ > 0 { + // if s.config.LightPeers >= s.p2pServer.MaxPeers { + // return fmt.Errorf("invalid peer config: light peer count (%d) >= total peer count (%d)", s.config.LightPeers, s.p2pServer.MaxPeers) + // } + // maxPeers -= s.config.LightPeers + // } // Start the networking layer and the light server if requested //s.protocolManager.Start(maxPeers) return nil diff --git a/eth/gasprice/gasprice.go b/eth/gasprice/gasprice.go index 14476ab..33810b2 100644 --- a/eth/gasprice/gasprice.go +++ b/eth/gasprice/gasprice.go @@ -29,8 +29,6 @@ import ( "github.com/ethereum/go-ethereum/log" ) -const sampleNumber = 3 // Number of transactions sampled in a block - var DefaultMaxPrice = big.NewInt(500 * params.GWei) type Config struct { diff --git a/eth/protocol.go b/eth/protocol.go deleted file mode 100644 index ef5dcde..0000000 --- a/eth/protocol.go +++ /dev/null @@ -1,221 +0,0 @@ -// Copyright 2014 The go-ethereum Authors -// This file is part of the go-ethereum library. -// -// The go-ethereum library is free software: you can redistribute it and/or modify -// it under the terms of the GNU Lesser General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// The go-ethereum library is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU Lesser General Public License for more details. -// -// You should have received a copy of the GNU Lesser General Public License -// along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>. - -package eth - -import ( - "fmt" - "io" - "math/big" - - "github.com/ava-labs/coreth/core" - "github.com/ava-labs/coreth/core/forkid" - "github.com/ava-labs/coreth/core/types" - "github.com/ethereum/go-ethereum/common" - "github.com/ethereum/go-ethereum/event" - "github.com/ethereum/go-ethereum/rlp" -) - -// Constants to match up protocol versions and messages -const ( - eth63 = 63 - eth64 = 64 - eth65 = 65 -) - -// protocolName is the official short name of the protocol used during capability negotiation. -const protocolName = "eth" - -// ProtocolVersions are the supported versions of the eth protocol (first is primary). -var ProtocolVersions = []uint{eth65, eth64, eth63} - -// protocolLengths are the number of implemented message corresponding to different protocol versions. -var protocolLengths = map[uint]uint64{eth65: 17, eth64: 17, eth63: 17} - -const protocolMaxMsgSize = 10 * 1024 * 1024 // Maximum cap on the size of a protocol message - -// eth protocol message codes -const ( - StatusMsg = 0x00 - NewBlockHashesMsg = 0x01 - TransactionMsg = 0x02 - GetBlockHeadersMsg = 0x03 - BlockHeadersMsg = 0x04 - GetBlockBodiesMsg = 0x05 - BlockBodiesMsg = 0x06 - NewBlockMsg = 0x07 - GetNodeDataMsg = 0x0d - NodeDataMsg = 0x0e - GetReceiptsMsg = 0x0f - ReceiptsMsg = 0x10 - - // New protocol message codes introduced in eth65 - // - // Previously these message ids were used by some legacy and unsupported - // eth protocols, reown them here. - NewPooledTransactionHashesMsg = 0x08 - GetPooledTransactionsMsg = 0x09 - PooledTransactionsMsg = 0x0a -) - -type errCode int - -const ( - ErrMsgTooLarge = iota - ErrDecode - ErrInvalidMsgCode - ErrProtocolVersionMismatch - ErrNetworkIDMismatch - ErrGenesisMismatch - ErrForkIDRejected - ErrNoStatusMsg - ErrExtraStatusMsg -) - -func (e errCode) String() string { - return errorToString[int(e)] -} - -// XXX change once legacy code is out -var errorToString = map[int]string{ - ErrMsgTooLarge: "Message too long", - ErrDecode: "Invalid message", - ErrInvalidMsgCode: "Invalid message code", - ErrProtocolVersionMismatch: "Protocol version mismatch", - ErrNetworkIDMismatch: "Network ID mismatch", - ErrGenesisMismatch: "Genesis mismatch", - ErrForkIDRejected: "Fork ID rejected", - ErrNoStatusMsg: "No status message", - ErrExtraStatusMsg: "Extra status message", -} - -type txPool interface { - // Has returns an indicator whether txpool has a transaction - // cached with the given hash. - Has(hash common.Hash) bool - - // Get retrieves the transaction from local txpool with given - // tx hash. - Get(hash common.Hash) *types.Transaction - - // AddRemotes should add the given transactions to the pool. - AddRemotes([]*types.Transaction) []error - - // Pending should return pending transactions. - // The slice should be modifiable by the caller. - Pending() (map[common.Address]types.Transactions, error) - - // SubscribeNewTxsEvent should return an event subscription of - // NewTxsEvent and send events to the given channel. - SubscribeNewTxsEvent(chan<- core.NewTxsEvent) event.Subscription -} - -// statusData63 is the network packet for the status message for eth/63. -type statusData63 struct { - ProtocolVersion uint32 - NetworkId uint64 - TD *big.Int - CurrentBlock common.Hash - GenesisBlock common.Hash -} - -// statusData is the network packet for the status message for eth/64 and later. -type statusData struct { - ProtocolVersion uint32 - NetworkID uint64 - TD *big.Int - Head common.Hash - Genesis common.Hash - ForkID forkid.ID -} - -// newBlockHashesData is the network packet for the block announcements. -type newBlockHashesData []struct { - Hash common.Hash // Hash of one particular block being announced - Number uint64 // Number of one particular block being announced -} - -// getBlockHeadersData represents a block header query. -type getBlockHeadersData struct { - Origin hashOrNumber // Block from which to retrieve headers - Amount uint64 // Maximum number of headers to retrieve - Skip uint64 // Blocks to skip between consecutive headers - Reverse bool // Query direction (false = rising towards latest, true = falling towards genesis) -} - -// hashOrNumber is a combined field for specifying an origin block. -type hashOrNumber struct { - Hash common.Hash // Block hash from which to retrieve headers (excludes Number) - Number uint64 // Block hash from which to retrieve headers (excludes Hash) -} - -// EncodeRLP is a specialized encoder for hashOrNumber to encode only one of the -// two contained union fields. -func (hn *hashOrNumber) EncodeRLP(w io.Writer) error { - if hn.Hash == (common.Hash{}) { - return rlp.Encode(w, hn.Number) - } - if hn.Number != 0 { - return fmt.Errorf("both origin hash (%x) and number (%d) provided", hn.Hash, hn.Number) - } - return rlp.Encode(w, hn.Hash) -} - -// DecodeRLP is a specialized decoder for hashOrNumber to decode the contents -// into either a block hash or a block number. -func (hn *hashOrNumber) DecodeRLP(s *rlp.Stream) error { - _, size, _ := s.Kind() - origin, err := s.Raw() - if err == nil { - switch { - case size == 32: - err = rlp.DecodeBytes(origin, &hn.Hash) - case size <= 8: - err = rlp.DecodeBytes(origin, &hn.Number) - default: - err = fmt.Errorf("invalid input size %d for origin", size) - } - } - return err -} - -// newBlockData is the network packet for the block propagation message. -type newBlockData struct { - Block *types.Block - TD *big.Int -} - -// sanityCheck verifies that the values are reasonable, as a DoS protection -func (request *newBlockData) sanityCheck() error { - if err := request.Block.SanityCheck(); err != nil { - return err - } - //TD at mainnet block #7753254 is 76 bits. If it becomes 100 million times - // larger, it will still fit within 100 bits - if tdlen := request.TD.BitLen(); tdlen > 100 { - return fmt.Errorf("too large block TD: bitlen %d", tdlen) - } - return nil -} - -// blockBody represents the data content of a single block. -type blockBody struct { - Transactions []*types.Transaction // Transactions contained within a block - Uncles []*types.Header // Uncles contained within a block -} - -// blockBodiesData is the network packet for block content distribution. -type blockBodiesData []*blockBody diff --git a/ethclient/ethclient.go b/ethclient/ethclient.go index 6dd4df8..8470d64 100644 --- a/ethclient/ethclient.go +++ b/ethclient/ethclient.go @@ -324,13 +324,13 @@ func toBlockNumArg(number *big.Int) string { return hexutil.EncodeBig(number) } -type rpcProgress struct { - StartingBlock hexutil.Uint64 - CurrentBlock hexutil.Uint64 - HighestBlock hexutil.Uint64 - PulledStates hexutil.Uint64 - KnownStates hexutil.Uint64 -} +// type rpcProgress struct { +// StartingBlock hexutil.Uint64 +// CurrentBlock hexutil.Uint64 +// HighestBlock hexutil.Uint64 +// PulledStates hexutil.Uint64 +// KnownStates hexutil.Uint64 +// } // SyncProgress retrieves the current progress of the sync algorithm. If there's // no sync currently running, it returns nil. diff --git a/ethstats/ethstats.go b/ethstats/ethstats.go deleted file mode 100644 index 3dd06e3..0000000 --- a/ethstats/ethstats.go +++ /dev/null @@ -1,785 +0,0 @@ -// Copyright 2016 The go-ethereum Authors -// This file is part of the go-ethereum library. -// -// The go-ethereum library is free software: you can redistribute it and/or modify -// it under the terms of the GNU Lesser General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// The go-ethereum library is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU Lesser General Public License for more details. -// -// You should have received a copy of the GNU Lesser General Public License -// along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>. - -// Package ethstats implements the network stats reporting service. -package ethstats - -import ( - "context" - "encoding/json" - "errors" - "fmt" - "math/big" - "net/http" - "regexp" - "runtime" - "strconv" - "strings" - "sync" - "time" - - "github.com/ava-labs/coreth/consensus" - "github.com/ava-labs/coreth/core" - "github.com/ava-labs/coreth/core/types" - "github.com/ava-labs/coreth/eth" - "github.com/ava-labs/coreth/miner" - "github.com/ava-labs/coreth/node" - "github.com/ava-labs/coreth/rpc" - "github.com/ethereum/go-ethereum/common" - "github.com/ethereum/go-ethereum/common/mclock" - "github.com/ethereum/go-ethereum/eth/downloader" - "github.com/ethereum/go-ethereum/event" - "github.com/ethereum/go-ethereum/les" - "github.com/ethereum/go-ethereum/log" - "github.com/ethereum/go-ethereum/p2p" - "github.com/gorilla/websocket" -) - -const ( - // historyUpdateRange is the number of blocks a node should report upon login or - // history request. - historyUpdateRange = 50 - - // txChanSize is the size of channel listening to NewTxsEvent. - // The number is referenced from the size of tx pool. - txChanSize = 4096 - // chainHeadChanSize is the size of channel listening to ChainHeadEvent. - chainHeadChanSize = 10 -) - -// backend encompasses the bare-minimum functionality needed for ethstats reporting -type backend interface { - SubscribeChainHeadEvent(ch chan<- core.ChainHeadEvent) event.Subscription - SubscribeNewTxsEvent(ch chan<- core.NewTxsEvent) event.Subscription - CurrentHeader() *types.Header - HeaderByNumber(ctx context.Context, number rpc.BlockNumber) (*types.Header, error) - GetTd(ctx context.Context, hash common.Hash) *big.Int - Stats() (pending int, queued int) - Downloader() *downloader.Downloader -} - -// fullNodeBackend encompasses the functionality necessary for a full node -// reporting to ethstats -type fullNodeBackend interface { - backend - Miner() *miner.Miner - BlockByNumber(ctx context.Context, number rpc.BlockNumber) (*types.Block, error) - CurrentBlock() *types.Block - SuggestPrice(ctx context.Context) (*big.Int, error) -} - -// Service implements an Ethereum netstats reporting daemon that pushes local -// chain statistics up to a monitoring server. -type Service struct { - server *p2p.Server // Peer-to-peer server to retrieve networking infos - backend backend - engine consensus.Engine // Consensus engine to retrieve variadic block fields - - node string // Name of the node to display on the monitoring page - pass string // Password to authorize access to the monitoring page - host string // Remote address of the monitoring service - - pongCh chan struct{} // Pong notifications are fed into this channel - histCh chan []uint64 // History request block numbers are fed into this channel - -} - -// connWrapper is a wrapper to prevent concurrent-write or concurrent-read on the -// websocket. -// -// From Gorilla websocket docs: -// Connections support one concurrent reader and one concurrent writer. -// Applications are responsible for ensuring that no more than one goroutine calls the write methods -// - NextWriter, SetWriteDeadline, WriteMessage, WriteJSON, EnableWriteCompression, SetCompressionLevel -// concurrently and that no more than one goroutine calls the read methods -// - NextReader, SetReadDeadline, ReadMessage, ReadJSON, SetPongHandler, SetPingHandler -// concurrently. -// The Close and WriteControl methods can be called concurrently with all other methods. -type connWrapper struct { - conn *websocket.Conn - - rlock sync.Mutex - wlock sync.Mutex -} - -func newConnectionWrapper(conn *websocket.Conn) *connWrapper { - return &connWrapper{conn: conn} -} - -// WriteJSON wraps corresponding method on the websocket but is safe for concurrent calling -func (w *connWrapper) WriteJSON(v interface{}) error { - w.wlock.Lock() - defer w.wlock.Unlock() - - return w.conn.WriteJSON(v) -} - -// ReadJSON wraps corresponding method on the websocket but is safe for concurrent calling -func (w *connWrapper) ReadJSON(v interface{}) error { - w.rlock.Lock() - defer w.rlock.Unlock() - - return w.conn.ReadJSON(v) -} - -// Close wraps corresponding method on the websocket but is safe for concurrent calling -func (w *connWrapper) Close() error { - // The Close and WriteControl methods can be called concurrently with all other methods, - // so the mutex is not used here - return w.conn.Close() -} - -// New returns a monitoring service ready for stats reporting. -func New(node *node.Node, backend backend, engine consensus.Engine, url string) error { - // Parse the netstats connection url - re := regexp.MustCompile("([^:@]*)(:([^@]*))?@(.+)") - parts := re.FindStringSubmatch(url) - if len(parts) != 5 { - return fmt.Errorf("invalid netstats url: \"%s\", should be nodename:secret@host:port", url) - } - ethstats := &Service{ - backend: backend, - engine: engine, - server: node.Server(), - node: parts[1], - pass: parts[3], - host: parts[4], - pongCh: make(chan struct{}), - histCh: make(chan []uint64, 1), - } - - node.RegisterLifecycle(ethstats) - return nil -} - -// Start implements node.Lifecycle, starting up the monitoring and reporting daemon. -func (s *Service) Start() error { - go s.loop() - - log.Info("Stats daemon started") - return nil -} - -// Stop implements node.Lifecycle, terminating the monitoring and reporting daemon. -func (s *Service) Stop() error { - log.Info("Stats daemon stopped") - return nil -} - -// loop keeps trying to connect to the netstats server, reporting chain events -// until termination. -func (s *Service) loop() { - // Subscribe to chain events to execute updates on - chainHeadCh := make(chan core.ChainHeadEvent, chainHeadChanSize) - headSub := s.backend.SubscribeChainHeadEvent(chainHeadCh) - defer headSub.Unsubscribe() - - txEventCh := make(chan core.NewTxsEvent, txChanSize) - txSub := s.backend.SubscribeNewTxsEvent(txEventCh) - defer txSub.Unsubscribe() - - // Start a goroutine that exhausts the subscriptions to avoid events piling up - var ( - quitCh = make(chan struct{}) - headCh = make(chan *types.Block, 1) - txCh = make(chan struct{}, 1) - ) - go func() { - var lastTx mclock.AbsTime - - HandleLoop: - for { - select { - // Notify of chain head events, but drop if too frequent - case head := <-chainHeadCh: - select { - case headCh <- head.Block: - default: - } - - // Notify of new transaction events, but drop if too frequent - case <-txEventCh: - if time.Duration(mclock.Now()-lastTx) < time.Second { - continue - } - lastTx = mclock.Now() - - select { - case txCh <- struct{}{}: - default: - } - - // node stopped - case <-txSub.Err(): - break HandleLoop - case <-headSub.Err(): - break HandleLoop - } - } - close(quitCh) - }() - - // Resolve the URL, defaulting to TLS, but falling back to none too - path := fmt.Sprintf("%s/api", s.host) - urls := []string{path} - - // url.Parse and url.IsAbs is unsuitable (https://github.com/golang/go/issues/19779) - if !strings.Contains(path, "://") { - urls = []string{"wss://" + path, "ws://" + path} - } - - errTimer := time.NewTimer(0) - defer errTimer.Stop() - // Loop reporting until termination - for { - select { - case <-quitCh: - return - case <-errTimer.C: - // Establish a websocket connection to the server on any supported URL - var ( - conn *connWrapper - err error - ) - dialer := websocket.Dialer{HandshakeTimeout: 5 * time.Second} - header := make(http.Header) - header.Set("origin", "http://localhost") - for _, url := range urls { - c, _, e := dialer.Dial(url, header) - err = e - if err == nil { - conn = newConnectionWrapper(c) - break - } - } - if err != nil { - log.Warn("Stats server unreachable", "err", err) - errTimer.Reset(10 * time.Second) - continue - } - // Authenticate the client with the server - if err = s.login(conn); err != nil { - log.Warn("Stats login failed", "err", err) - conn.Close() - errTimer.Reset(10 * time.Second) - continue - } - go s.readLoop(conn) - - // Send the initial stats so our node looks decent from the get go - if err = s.report(conn); err != nil { - log.Warn("Initial stats report failed", "err", err) - conn.Close() - errTimer.Reset(0) - continue - } - // Keep sending status updates until the connection breaks - fullReport := time.NewTicker(15 * time.Second) - - for err == nil { - select { - case <-quitCh: - fullReport.Stop() - // Make sure the connection is closed - conn.Close() - return - - case <-fullReport.C: - if err = s.report(conn); err != nil { - log.Warn("Full stats report failed", "err", err) - } - case list := <-s.histCh: - if err = s.reportHistory(conn, list); err != nil { - log.Warn("Requested history report failed", "err", err) - } - case head := <-headCh: - if err = s.reportBlock(conn, head); err != nil { - log.Warn("Block stats report failed", "err", err) - } - if err = s.reportPending(conn); err != nil { - log.Warn("Post-block transaction stats report failed", "err", err) - } - case <-txCh: - if err = s.reportPending(conn); err != nil { - log.Warn("Transaction stats report failed", "err", err) - } - } - } - fullReport.Stop() - - // Close the current connection and establish a new one - conn.Close() - errTimer.Reset(0) - } - } -} - -// readLoop loops as long as the connection is alive and retrieves data packets -// from the network socket. If any of them match an active request, it forwards -// it, if they themselves are requests it initiates a reply, and lastly it drops -// unknown packets. -func (s *Service) readLoop(conn *connWrapper) { - // If the read loop exists, close the connection - defer conn.Close() - - for { - // Retrieve the next generic network packet and bail out on error - var blob json.RawMessage - if err := conn.ReadJSON(&blob); err != nil { - log.Warn("Failed to retrieve stats server message", "err", err) - return - } - // If the network packet is a system ping, respond to it directly - var ping string - if err := json.Unmarshal(blob, &ping); err == nil && strings.HasPrefix(ping, "primus::ping::") { - if err := conn.WriteJSON(strings.Replace(ping, "ping", "pong", -1)); err != nil { - log.Warn("Failed to respond to system ping message", "err", err) - return - } - continue - } - // Not a system ping, try to decode an actual state message - var msg map[string][]interface{} - if err := json.Unmarshal(blob, &msg); err != nil { - log.Warn("Failed to decode stats server message", "err", err) - return - } - log.Trace("Received message from stats server", "msg", msg) - if len(msg["emit"]) == 0 { - log.Warn("Stats server sent non-broadcast", "msg", msg) - return - } - command, ok := msg["emit"][0].(string) - if !ok { - log.Warn("Invalid stats server message type", "type", msg["emit"][0]) - return - } - // If the message is a ping reply, deliver (someone must be listening!) - if len(msg["emit"]) == 2 && command == "node-pong" { - select { - case s.pongCh <- struct{}{}: - // Pong delivered, continue listening - continue - default: - // Ping routine dead, abort - log.Warn("Stats server pinger seems to have died") - return - } - } - // If the message is a history request, forward to the event processor - if len(msg["emit"]) == 2 && command == "history" { - // Make sure the request is valid and doesn't crash us - request, ok := msg["emit"][1].(map[string]interface{}) - if !ok { - log.Warn("Invalid stats history request", "msg", msg["emit"][1]) - select { - case s.histCh <- nil: // Treat it as an no indexes request - default: - } - continue - } - list, ok := request["list"].([]interface{}) - if !ok { - log.Warn("Invalid stats history block list", "list", request["list"]) - return - } - // Convert the block number list to an integer list - numbers := make([]uint64, len(list)) - for i, num := range list { - n, ok := num.(float64) - if !ok { - log.Warn("Invalid stats history block number", "number", num) - return - } - numbers[i] = uint64(n) - } - select { - case s.histCh <- numbers: - continue - default: - } - } - // Report anything else and continue - log.Info("Unknown stats message", "msg", msg) - } -} - -// nodeInfo is the collection of meta information about a node that is displayed -// on the monitoring page. -type nodeInfo struct { - Name string `json:"name"` - Node string `json:"node"` - Port int `json:"port"` - Network string `json:"net"` - Protocol string `json:"protocol"` - API string `json:"api"` - Os string `json:"os"` - OsVer string `json:"os_v"` - Client string `json:"client"` - History bool `json:"canUpdateHistory"` -} - -// authMsg is the authentication infos needed to login to a monitoring server. -type authMsg struct { - ID string `json:"id"` - Info nodeInfo `json:"info"` - Secret string `json:"secret"` -} - -// login tries to authorize the client at the remote server. -func (s *Service) login(conn *connWrapper) error { - // Construct and send the login authentication - infos := s.server.NodeInfo() - - var network, protocol string - if info := infos.Protocols["eth"]; info != nil { - network = fmt.Sprintf("%d", info.(*eth.NodeInfo).Network) - protocol = fmt.Sprintf("eth/%d", eth.ProtocolVersions[0]) - } else { - network = fmt.Sprintf("%d", infos.Protocols["les"].(*les.NodeInfo).Network) - protocol = fmt.Sprintf("les/%d", les.ClientProtocolVersions[0]) - } - auth := &authMsg{ - ID: s.node, - Info: nodeInfo{ - Name: s.node, - Node: infos.Name, - Port: infos.Ports.Listener, - Network: network, - Protocol: protocol, - API: "No", - Os: runtime.GOOS, - OsVer: runtime.GOARCH, - Client: "0.1.1", - History: true, - }, - Secret: s.pass, - } - login := map[string][]interface{}{ - "emit": {"hello", auth}, - } - if err := conn.WriteJSON(login); err != nil { - return err - } - // Retrieve the remote ack or connection termination - var ack map[string][]string - if err := conn.ReadJSON(&ack); err != nil || len(ack["emit"]) != 1 || ack["emit"][0] != "ready" { - return errors.New("unauthorized") - } - return nil -} - -// report collects all possible data to report and send it to the stats server. -// This should only be used on reconnects or rarely to avoid overloading the -// server. Use the individual methods for reporting subscribed events. -func (s *Service) report(conn *connWrapper) error { - if err := s.reportLatency(conn); err != nil { - return err - } - if err := s.reportBlock(conn, nil); err != nil { - return err - } - if err := s.reportPending(conn); err != nil { - return err - } - if err := s.reportStats(conn); err != nil { - return err - } - return nil -} - -// reportLatency sends a ping request to the server, measures the RTT time and -// finally sends a latency update. -func (s *Service) reportLatency(conn *connWrapper) error { - // Send the current time to the ethstats server - start := time.Now() - - ping := map[string][]interface{}{ - "emit": {"node-ping", map[string]string{ - "id": s.node, - "clientTime": start.String(), - }}, - } - if err := conn.WriteJSON(ping); err != nil { - return err - } - // Wait for the pong request to arrive back - select { - case <-s.pongCh: - // Pong delivered, report the latency - case <-time.After(5 * time.Second): - // Ping timeout, abort - return errors.New("ping timed out") - } - latency := strconv.Itoa(int((time.Since(start) / time.Duration(2)).Nanoseconds() / 1000000)) - - // Send back the measured latency - log.Trace("Sending measured latency to ethstats", "latency", latency) - - stats := map[string][]interface{}{ - "emit": {"latency", map[string]string{ - "id": s.node, - "latency": latency, - }}, - } - return conn.WriteJSON(stats) -} - -// blockStats is the information to report about individual blocks. -type blockStats struct { - Number *big.Int `json:"number"` - Hash common.Hash `json:"hash"` - ParentHash common.Hash `json:"parentHash"` - Timestamp *big.Int `json:"timestamp"` - Miner common.Address `json:"miner"` - GasUsed uint64 `json:"gasUsed"` - GasLimit uint64 `json:"gasLimit"` - Diff string `json:"difficulty"` - TotalDiff string `json:"totalDifficulty"` - Txs []txStats `json:"transactions"` - TxHash common.Hash `json:"transactionsRoot"` - Root common.Hash `json:"stateRoot"` - Uncles uncleStats `json:"uncles"` -} - -// txStats is the information to report about individual transactions. -type txStats struct { - Hash common.Hash `json:"hash"` -} - -// uncleStats is a custom wrapper around an uncle array to force serializing -// empty arrays instead of returning null for them. -type uncleStats []*types.Header - -func (s uncleStats) MarshalJSON() ([]byte, error) { - if uncles := ([]*types.Header)(s); len(uncles) > 0 { - return json.Marshal(uncles) - } - return []byte("[]"), nil -} - -// reportBlock retrieves the current chain head and reports it to the stats server. -func (s *Service) reportBlock(conn *connWrapper, block *types.Block) error { - // Gather the block details from the header or block chain - details := s.assembleBlockStats(block) - - // Assemble the block report and send it to the server - log.Trace("Sending new block to ethstats", "number", details.Number, "hash", details.Hash) - - stats := map[string]interface{}{ - "id": s.node, - "block": details, - } - report := map[string][]interface{}{ - "emit": {"block", stats}, - } - return conn.WriteJSON(report) -} - -// assembleBlockStats retrieves any required metadata to report a single block -// and assembles the block stats. If block is nil, the current head is processed. -func (s *Service) assembleBlockStats(block *types.Block) *blockStats { - // Gather the block infos from the local blockchain - var ( - header *types.Header - td *big.Int - txs []txStats - uncles []*types.Header - ) - - // check if backend is a full node - fullBackend, ok := s.backend.(fullNodeBackend) - if ok { - if block == nil { - block = fullBackend.CurrentBlock() - } - header = block.Header() - td = fullBackend.GetTd(context.Background(), header.Hash()) - - txs = make([]txStats, len(block.Transactions())) - for i, tx := range block.Transactions() { - txs[i].Hash = tx.Hash() - } - uncles = block.Uncles() - } else { - // Light nodes would need on-demand lookups for transactions/uncles, skip - if block != nil { - header = block.Header() - } else { - header = s.backend.CurrentHeader() - } - td = s.backend.GetTd(context.Background(), header.Hash()) - txs = []txStats{} - } - - // Assemble and return the block stats - author, _ := s.engine.Author(header) - - return &blockStats{ - Number: header.Number, - Hash: header.Hash(), - ParentHash: header.ParentHash, - Timestamp: new(big.Int).SetUint64(header.Time), - Miner: author, - GasUsed: header.GasUsed, - GasLimit: header.GasLimit, - Diff: header.Difficulty.String(), - TotalDiff: td.String(), - Txs: txs, - TxHash: header.TxHash, - Root: header.Root, - Uncles: uncles, - } -} - -// reportHistory retrieves the most recent batch of blocks and reports it to the -// stats server. -func (s *Service) reportHistory(conn *connWrapper, list []uint64) error { - // Figure out the indexes that need reporting - indexes := make([]uint64, 0, historyUpdateRange) - if len(list) > 0 { - // Specific indexes requested, send them back in particular - indexes = append(indexes, list...) - } else { - // No indexes requested, send back the top ones - head := s.backend.CurrentHeader().Number.Int64() - start := head - historyUpdateRange + 1 - if start < 0 { - start = 0 - } - for i := uint64(start); i <= uint64(head); i++ { - indexes = append(indexes, i) - } - } - // Gather the batch of blocks to report - history := make([]*blockStats, len(indexes)) - for i, number := range indexes { - fullBackend, ok := s.backend.(fullNodeBackend) - // Retrieve the next block if it's known to us - var block *types.Block - if ok { - block, _ = fullBackend.BlockByNumber(context.Background(), rpc.BlockNumber(number)) // TODO ignore error here ? - } else { - if header, _ := s.backend.HeaderByNumber(context.Background(), rpc.BlockNumber(number)); header != nil { - block = types.NewBlockWithHeader(header) - } - } - // If we do have the block, add to the history and continue - if block != nil { - history[len(history)-1-i] = s.assembleBlockStats(block) - continue - } - // Ran out of blocks, cut the report short and send - history = history[len(history)-i:] - break - } - // Assemble the history report and send it to the server - if len(history) > 0 { - log.Trace("Sending historical blocks to ethstats", "first", history[0].Number, "last", history[len(history)-1].Number) - } else { - log.Trace("No history to send to stats server") - } - stats := map[string]interface{}{ - "id": s.node, - "history": history, - } - report := map[string][]interface{}{ - "emit": {"history", stats}, - } - return conn.WriteJSON(report) -} - -// pendStats is the information to report about pending transactions. -type pendStats struct { - Pending int `json:"pending"` -} - -// reportPending retrieves the current number of pending transactions and reports -// it to the stats server. -func (s *Service) reportPending(conn *connWrapper) error { - // Retrieve the pending count from the local blockchain - pending, _ := s.backend.Stats() - // Assemble the transaction stats and send it to the server - log.Trace("Sending pending transactions to ethstats", "count", pending) - - stats := map[string]interface{}{ - "id": s.node, - "stats": &pendStats{ - Pending: pending, - }, - } - report := map[string][]interface{}{ - "emit": {"pending", stats}, - } - return conn.WriteJSON(report) -} - -// nodeStats is the information to report about the local node. -type nodeStats struct { - Active bool `json:"active"` - Syncing bool `json:"syncing"` - Mining bool `json:"mining"` - Hashrate int `json:"hashrate"` - Peers int `json:"peers"` - GasPrice int `json:"gasPrice"` - Uptime int `json:"uptime"` -} - -// reportStats retrieves various stats about the node at the networking and -// mining layer and reports it to the stats server. -func (s *Service) reportStats(conn *connWrapper) error { - // Gather the syncing and mining infos from the local miner instance - var ( - mining bool - hashrate int - syncing bool - gasprice int - ) - // check if backend is a full node - fullBackend, ok := s.backend.(fullNodeBackend) - if ok { - mining = fullBackend.Miner().Mining() - hashrate = int(fullBackend.Miner().HashRate()) - - sync := fullBackend.Downloader().Progress() - syncing = fullBackend.CurrentHeader().Number.Uint64() >= sync.HighestBlock - - price, _ := fullBackend.SuggestPrice(context.Background()) - gasprice = int(price.Uint64()) - } else { - sync := s.backend.Downloader().Progress() - syncing = s.backend.CurrentHeader().Number.Uint64() >= sync.HighestBlock - } - // Assemble the node stats and send it to the server - log.Trace("Sending node details to ethstats") - - stats := map[string]interface{}{ - "id": s.node, - "stats": &nodeStats{ - Active: true, - Mining: mining, - Hashrate: hashrate, - Peers: s.server.PeerCount(), - GasPrice: gasprice, - Syncing: syncing, - Uptime: 100, - }, - } - report := map[string][]interface{}{ - "emit": {"stats", stats}, - } - return conn.WriteJSON(report) -} @@ -4,7 +4,7 @@ go 1.14 require ( github.com/VictoriaMetrics/fastcache v1.5.7 - github.com/ava-labs/avalanchego v1.0.6-rc.2 + github.com/ava-labs/avalanchego v1.1.0 github.com/davecgh/go-spew v1.1.1 github.com/deckarep/golang-set v1.7.1 github.com/edsrzf/mmap-go v1.0.0 @@ -46,8 +46,8 @@ github.com/aristanetworks/goarista v0.0.0-20170210015632-ea17b1a17847/go.mod h1: github.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e/go.mod h1:3U/XgcO3hCbHZ8TKRvWD2dDTCfh9M9ya+I9JpbB7O8o= github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da/go.mod h1:Q73ZrmVTwzkszR9V5SSuryQ31EELlFMUz1kKyl939pY= github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8= -github.com/ava-labs/avalanchego v1.0.6-rc.2 h1:71wo8rMgZ/eC49G4bVk36JWeZpjAhdLUdbQf9/gxCYg= -github.com/ava-labs/avalanchego v1.0.6-rc.2/go.mod h1:Q/I7LaMv2EYL8plNVRbcpBJsDk2py2XISfov0KK1MgU= +github.com/ava-labs/avalanchego v1.1.0 h1:kJuOXGI+kt/jXi1TVJ0F4z1CP68YoDfYUHb4HHpjGHU= +github.com/ava-labs/avalanchego v1.1.0/go.mod h1:Q/I7LaMv2EYL8plNVRbcpBJsDk2py2XISfov0KK1MgU= github.com/aws/aws-sdk-go v1.25.48/go.mod h1:KmX6BPdI08NWTb3/sm4ZGu5ShLoqVDhKgpiN924inxo= 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= diff --git a/miner/worker.go b/miner/worker.go index 72597b0..2b44691 100644 --- a/miner/worker.go +++ b/miner/worker.go @@ -913,12 +913,15 @@ func (w *worker) commitNewWork(interrupt *int32, noempty bool, timestamp int64) //timestamp = int64(parent.Time() + 1) timestamp = int64(parent.Time()) } - // this will ensure we're not going off too far in the future - if now := time.Now().Unix(); timestamp > now+1 { - wait := time.Duration(timestamp-now) * time.Second - log.Info("Mining too far in the future", "wait", common.PrettyDuration(wait)) - time.Sleep(wait) - } + // This block has been removed to prevent BuildBlock + // from blocking until this chain advances to timestamp + // This prevents a block issued up to 30s in the future + // from causing an unnecessary sleep + // if now := time.Now().Unix(); timestamp > now+1 { + // wait := time.Duration(timestamp-now) * time.Second + // log.Info("Mining too far in the future", "wait", common.PrettyDuration(wait)) + // time.Sleep(wait) + // } num := parent.Number() header := &types.Header{ diff --git a/node/api.go b/node/api.go index 4589d25..af6ae69 100644 --- a/node/api.go +++ b/node/api.go @@ -19,7 +19,6 @@ package node import ( "context" "fmt" - //"strings" "github.com/ava-labs/coreth/internal/debug" "github.com/ava-labs/coreth/rpc" diff --git a/node/config.go b/node/config.go index 57bb7a1..73b59e7 100644 --- a/node/config.go +++ b/node/config.go @@ -30,7 +30,7 @@ import ( "github.com/ava-labs/coreth/accounts/external" "github.com/ava-labs/coreth/accounts/keystore" "github.com/ava-labs/coreth/accounts/scwallet" - //"github.com/ava-labs/coreth/accounts/usbwallet" + "github.com/ava-labs/coreth/rpc" "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/crypto" diff --git a/node/node.go b/node/node.go index e0a6424..341f213 100644 --- a/node/node.go +++ b/node/node.go @@ -55,7 +55,6 @@ type Node struct { const ( initializingState = iota - runningState closedState ) diff --git a/notes/copied-list.txt b/notes/copied-list.txt index 94353ff..e098f57 100644 --- a/notes/copied-list.txt +++ b/notes/copied-list.txt @@ -70,10 +70,7 @@ ./eth/filters/api.go ./eth/filters/filter.go ./eth/filters/filter_system.go -./eth/gasprice/gasprice.go ./eth/metrics.go -./eth/protocol.go -./ethstats/ethstats.go ./eth/tracers/internal/tracers/tracers.go ./eth/tracers/tracer.go ./eth/tracers/tracers.go diff --git a/notes/hacked-list.txt b/notes/hacked-list.txt index f4545ba..f951ffc 100644 --- a/notes/hacked-list.txt +++ b/notes/hacked-list.txt @@ -28,6 +28,7 @@ ./eth/api_tracer.go ./eth/backend.go ./eth/config.go +./eth/gasprice/gasprice.go ./eth/gen_config.go ./eth/tracers/internal/tracers/assets.go ./ethclient/ethclient.go @@ -44,3 +45,4 @@ ./rpc/client.go ./rpc/types.go ./interfaces.go +./accounts/keystore/account_cache.go (minor formatting change to pass linting) diff --git a/plugin/evm/block.go b/plugin/evm/block.go index 7c23c17..5a0d377 100644 --- a/plugin/evm/block.go +++ b/plugin/evm/block.go @@ -75,6 +75,11 @@ func (b *Block) Parent() snowman.Block { return &missing.Block{BlkID: parentID} } +// Height implements the snowman.Block interface +func (b *Block) Height() uint64 { + return b.ethBlock.Number().Uint64() +} + // Verify implements the snowman.Block interface func (b *Block) Verify() error { // Only enforce a minimum fee when bootstrapping has finished diff --git a/plugin/evm/error.go b/plugin/evm/error.go index 0554349..d65d28b 100644 --- a/plugin/evm/error.go +++ b/plugin/evm/error.go @@ -3,7 +3,7 @@ package evm -// TxError provides the ability for errors to be distinguished as permenant or +// TxError provides the ability for errors to be distinguished as permanent or // temporary type TxError interface { error diff --git a/plugin/evm/export_tx_test.go b/plugin/evm/export_tx_test.go index 6fdf3a2..c1ddcda 100644 --- a/plugin/evm/export_tx_test.go +++ b/plugin/evm/export_tx_test.go @@ -126,7 +126,3 @@ func TestExportTxVerify(t *testing.T) { t.Fatal("ExportTx should have failed verification due to invalid output") } } - -func TestExportTxSemanticVerify(t *testing.T) { - -} diff --git a/plugin/evm/import_tx_test.go b/plugin/evm/import_tx_test.go index 973802a..139aa4e 100644 --- a/plugin/evm/import_tx_test.go +++ b/plugin/evm/import_tx_test.go @@ -178,7 +178,7 @@ func TestImportTxSemanticVerify(t *testing.T) { Outs: []EVMOutput{evmOutput}, } - state, err := vm.chain.BlockState(vm.lastAccepted.ethBlock) + state, err := vm.chain.CurrentState() if err != nil { t.Fatalf("Failed to get last accepted stateDB due to: %s", err) } @@ -291,6 +291,12 @@ func TestImportTxSemanticVerify(t *testing.T) { func TestNewImportTx(t *testing.T) { _, vm, _, sharedMemory := GenesisVM(t, true) + defer func() { + if err := vm.Shutdown(); err != nil { + t.Fatal(err) + } + }() + importAmount := uint64(1000000) utxoID := avax.UTXOID{ TxID: ids.ID{ diff --git a/plugin/evm/service.go b/plugin/evm/service.go index 2bb06df..8c9e16c 100644 --- a/plugin/evm/service.go +++ b/plugin/evm/service.go @@ -24,7 +24,7 @@ import ( ) const ( - version = "coreth-v0.3.15" + version = "coreth-v0.3.17" ) // test constants @@ -330,7 +330,6 @@ func (service *AvaxAPI) GetUTXOs(r *http.Request, args *api.GetUTXOsArgs, reply return fmt.Errorf("number of addresses given, %d, exceeds maximum, %d", len(args.Addresses), maxGetUTXOsAddrs) } - sourceChain := ids.ID{} if args.SourceChain == "" { return errNoSourceChain } @@ -339,7 +338,7 @@ func (service *AvaxAPI) GetUTXOs(r *http.Request, args *api.GetUTXOsArgs, reply if err != nil { return fmt.Errorf("problem parsing source chainID %q: %w", args.SourceChain, err) } - sourceChain = chainID + sourceChain := chainID addrSet := ids.ShortSet{} for _, addrStr := range args.Addresses { diff --git a/plugin/evm/vm.go b/plugin/evm/vm.go index 58ab600..9335b51 100644 --- a/plugin/evm/vm.go +++ b/plugin/evm/vm.go @@ -69,19 +69,15 @@ const ( maxBlockTime = 1000 * time.Millisecond batchSize = 250 maxUTXOsToFetch = 1024 - blockCacheSize = 1 << 10 // 1024 + blockCacheSize = 1024 codecVersion = uint16(0) ) const ( - bdTimerStateMin = iota - bdTimerStateMax - bdTimerStateLong + txFee = units.MilliAvax ) var ( - txFee = units.MilliAvax - errEmptyBlock = errors.New("empty block") errCreateBlock = errors.New("couldn't create block") errUnknownBlock = errors.New("unknown block") @@ -103,6 +99,17 @@ var ( errInvalidNonce = errors.New("invalid nonce") ) +// mayBuildBlockStatus denotes whether the engine should be notified +// that a block should be built, or whether more time has to pass +// before doing so. See VM's [mayBuildBlock]. +type mayBuildBlockStatus uint8 + +const ( + waitToBuild mayBuildBlockStatus = iota + conditionalWaitToBuild + mayBuild +) + func maxDuration(x, y time.Duration) time.Duration { if x > y { return x @@ -145,37 +152,53 @@ type VM struct { CLIConfig CommandLineConfig - chainID *big.Int - networkID uint64 - genesisHash common.Hash - chain *coreth.ETHChain - chaindb Database - newBlockChan chan *Block - networkChan chan<- commonEng.Message - newMinedBlockSub *event.TypeMuxSubscription + chainID *big.Int + networkID uint64 + genesisHash common.Hash + chain *coreth.ETHChain + chaindb Database + newBlockChan chan *Block + // A message is sent on this channel when a new block + // is ready to be build. This notifies the consensus engine. + notifyBuildBlockChan chan<- commonEng.Message + newMinedBlockSub *event.TypeMuxSubscription acceptedDB database.Database - txPoolStabilizedHead common.Hash - txPoolStabilizedOk chan struct{} - txPoolStabilizedLock sync.Mutex - txPoolStabilizedShutdownChan chan struct{} + txPoolStabilizedLock sync.Mutex + txPoolStabilizedHead common.Hash + txPoolStabilizedOk chan struct{} metalock sync.Mutex blockCache, blockStatusCache cache.LRU lastAccepted *Block writingMetadata uint32 - bdlock sync.Mutex - blockDelayTimer *timer.Timer - bdTimerState int8 - bdGenWaitFlag bool - bdGenFlag bool + // [buildBlockLock] must be held when accessing [mayBuildBlock], + // [tryToBuildBlock] or [awaitingBuildBlock]. + buildBlockLock sync.Mutex + // [buildBlockTimer] periodically fires in order to update [mayBuildBlock] + // and to try to build a block, if applicable. + buildBlockTimer *timer.Timer + // [mayBuildBlock] == [wait] means that the next block may be built + // only after more time has elapsed. + // [mayBuildBlock] == [conditionalWait] means that the next block may be built + // only if it has more than [batchSize] txs in it. Otherwise, wait until more + // time has elapsed. + // [mayBuildBlock] == [build] means that the next block may be built + // at any time. + mayBuildBlock mayBuildBlockStatus + // If true, try to notify the engine that a block should be built. + // Engine may not be notified because [mayBuildBlock] says to wait. + tryToBuildBlock bool + // If true, the engine has been notified that it should build a block + // but has not done so yet. If this is the case, wait until it has + // built a block before notifying it again. + awaitingBuildBlock bool genlock sync.Mutex txSubmitChan <-chan struct{} atomicTxSubmitChan chan struct{} - shutdownSubmitChan chan struct{} baseCodec codec.Codec codec codec.Manager clock timer.Clock @@ -183,7 +206,8 @@ type VM struct { pendingAtomicTxs chan *Tx blockAtomicInputCache cache.LRU - shutdownWg sync.WaitGroup + shutdownChan chan struct{} + shutdownWg sync.WaitGroup fx secp256k1fx.Fx } @@ -232,6 +256,7 @@ func (vm *VM) Initialize( return errUnsupportedFXs } + vm.shutdownChan = make(chan struct{}, 1) vm.ctx = ctx vm.chaindb = Database{db} g := new(core.Genesis) @@ -329,32 +354,35 @@ func (vm *VM) Initialize( vm.blockStatusCache = cache.LRU{Size: blockCacheSize} vm.blockAtomicInputCache = cache.LRU{Size: blockCacheSize} vm.newBlockChan = make(chan *Block) - vm.networkChan = toEngine - vm.blockDelayTimer = timer.NewTimer(func() { - vm.bdlock.Lock() - switch vm.bdTimerState { - case bdTimerStateMin: - vm.bdTimerState = bdTimerStateMax - vm.blockDelayTimer.SetTimeoutIn(maxDuration(maxBlockTime-minBlockTime, 0)) - case bdTimerStateMax: - vm.bdTimerState = bdTimerStateLong + vm.notifyBuildBlockChan = toEngine + + // Periodically updates [vm.mayBuildBlock] and tries to notify the engine to build + // a new block, if applicable. + vm.buildBlockTimer = timer.NewTimer(func() { + vm.buildBlockLock.Lock() + switch vm.mayBuildBlock { + case waitToBuild: + // Some time has passed. Allow block to be built if it has enough txs in it. + vm.mayBuildBlock = conditionalWaitToBuild + vm.buildBlockTimer.SetTimeoutIn(maxDuration(maxBlockTime-minBlockTime, 0)) + case conditionalWaitToBuild: + // More time has passed. Allow block to be built regardless of tx count. + vm.mayBuildBlock = mayBuild } - tryAgain := vm.bdGenWaitFlag - vm.bdlock.Unlock() - if tryAgain { + tryBuildBlock := vm.tryToBuildBlock + vm.buildBlockLock.Unlock() + if tryBuildBlock { vm.tryBlockGen() } }) - go ctx.Log.RecoverAndPanic(vm.blockDelayTimer.Dispatch) + go ctx.Log.RecoverAndPanic(vm.buildBlockTimer.Dispatch) - vm.bdTimerState = bdTimerStateLong - vm.bdGenWaitFlag = true + vm.mayBuildBlock = mayBuild + vm.tryToBuildBlock = true vm.txPoolStabilizedOk = make(chan struct{}, 1) - vm.txPoolStabilizedShutdownChan = make(chan struct{}, 1) // Signal goroutine to shutdown // TODO: read size from options vm.pendingAtomicTxs = make(chan *Tx, 1024) vm.atomicTxSubmitChan = make(chan struct{}, 1) - vm.shutdownSubmitChan = make(chan struct{}, 1) vm.newMinedBlockSub = vm.chain.SubscribeNewMinedBlockEvent() vm.shutdownWg.Add(1) go ctx.Log.RecoverAndPanic(vm.awaitTxPoolStabilized) @@ -401,7 +429,10 @@ func (vm *VM) Bootstrapping() error { return vm.fx.Bootstrapping() } // Bootstrapped notifies this VM that the consensus engine has finished // bootstrapping -func (vm *VM) Bootstrapped() error { return vm.fx.Bootstrapped() } +func (vm *VM) Bootstrapped() error { + vm.ctx.Bootstrapped() + return vm.fx.Bootstrapped() +} // Shutdown implements the snowman.ChainVM interface func (vm *VM) Shutdown() error { @@ -410,8 +441,8 @@ func (vm *VM) Shutdown() error { } vm.writeBackMetadata() - close(vm.txPoolStabilizedShutdownChan) - close(vm.shutdownSubmitChan) + vm.buildBlockTimer.Stop() + close(vm.shutdownChan) vm.chain.Stop() vm.shutdownWg.Wait() return nil @@ -421,18 +452,20 @@ func (vm *VM) Shutdown() error { func (vm *VM) BuildBlock() (snowman.Block, error) { vm.chain.GenBlock() block := <-vm.newBlockChan + + vm.buildBlockLock.Lock() + // Specify that we should wait before trying to build another block. + vm.mayBuildBlock = waitToBuild + vm.tryToBuildBlock = false + vm.awaitingBuildBlock = false + vm.buildBlockTimer.SetTimeoutIn(minBlockTime) + vm.buildBlockLock.Unlock() + if block == nil { return nil, errCreateBlock } - // reset the min block time timer - vm.bdlock.Lock() - vm.bdTimerState = bdTimerStateMin - vm.bdGenWaitFlag = false - vm.bdGenFlag = false - vm.blockDelayTimer.SetTimeoutIn(minBlockTime) - vm.bdlock.Unlock() - log.Debug(fmt.Sprintf("built block %s", block.ID())) + log.Debug(fmt.Sprintf("Built block %s", block.ID())) // make sure Tx Pool is updated <-vm.txPoolStabilizedOk return block, nil @@ -573,13 +606,14 @@ func (vm *VM) updateStatus(blockID ids.ID, status choices.Status) { } func (vm *VM) tryBlockGen() error { - vm.bdlock.Lock() - defer vm.bdlock.Unlock() - if vm.bdGenFlag { - // skip if one call already generates a block in this round + vm.buildBlockLock.Lock() + defer vm.buildBlockLock.Unlock() + if vm.awaitingBuildBlock { + // We notified the engine that a block should be built but it hasn't + // done so yet. Wait until it has done so before notifying again. return nil } - vm.bdGenWaitFlag = true + vm.tryToBuildBlock = true vm.genlock.Lock() defer vm.genlock.Unlock() @@ -592,20 +626,21 @@ func (vm *VM) tryBlockGen() error { return nil } - switch vm.bdTimerState { - case bdTimerStateMin: + switch vm.mayBuildBlock { + case waitToBuild: // Wait more time before notifying engine to building a block return nil - case bdTimerStateMax: + case conditionalWaitToBuild: // Notify engine only if there are enough pending txs if size < batchSize { return nil } - case bdTimerStateLong: - // timeout; go ahead and generate a new block anyway + case mayBuild: // Notify engine + default: + panic(fmt.Sprintf("mayBuildBlock has unexpected value %d", vm.mayBuildBlock)) } select { - case vm.networkChan <- commonEng.PendingTxs: - // successfully push out the notification; this round ends - vm.bdGenFlag = true + case vm.notifyBuildBlockChan <- commonEng.PendingTxs: + // Notify engine to build a block + vm.awaitingBuildBlock = true default: return errBlockFrequency } @@ -734,12 +769,18 @@ func (vm *VM) writeBackMetadata() { // awaitTxPoolStabilized waits for a txPoolHead channel event // and notifies the VM when the tx pool has stabilized to the // expected block hash -// Waits for signal to shutdown from txPoolStabilizedShutdownChan chan +// Waits for signal to shutdown from [vm.shutdownChan] func (vm *VM) awaitTxPoolStabilized() { defer vm.shutdownWg.Done() for { select { - case e := <-vm.newMinedBlockSub.Chan(): + case e, ok := <-vm.newMinedBlockSub.Chan(): + if !ok { + return + } + if e == nil { + continue + } switch h := e.Data.(type) { case core.NewMinedBlockEvent: vm.txPoolStabilizedLock.Lock() @@ -750,7 +791,7 @@ func (vm *VM) awaitTxPoolStabilized() { vm.txPoolStabilizedLock.Unlock() default: } - case <-vm.txPoolStabilizedShutdownChan: + case <-vm.shutdownChan: return } } @@ -769,7 +810,7 @@ func (vm *VM) awaitSubmittedTxs() { vm.tryBlockGen() case <-time.After(5 * time.Second): vm.tryBlockGen() - case <-vm.shutdownSubmitChan: + case <-vm.shutdownChan: return } } diff --git a/plugin/evm/vm_test.go b/plugin/evm/vm_test.go index 0e9c102..dbceb1b 100644 --- a/plugin/evm/vm_test.go +++ b/plugin/evm/vm_test.go @@ -4,20 +4,30 @@ package evm import ( + "crypto/rand" "encoding/json" + "math/big" "testing" + "time" "github.com/ava-labs/avalanchego/api/keystore" + "github.com/ava-labs/avalanchego/cache" "github.com/ava-labs/avalanchego/chains/atomic" "github.com/ava-labs/avalanchego/database/memdb" "github.com/ava-labs/avalanchego/database/prefixdb" "github.com/ava-labs/avalanchego/ids" "github.com/ava-labs/avalanchego/snow" + "github.com/ava-labs/avalanchego/snow/choices" engCommon "github.com/ava-labs/avalanchego/snow/engine/common" "github.com/ava-labs/avalanchego/utils/crypto" "github.com/ava-labs/avalanchego/utils/formatting" "github.com/ava-labs/avalanchego/utils/logging" + "github.com/ava-labs/avalanchego/vms/components/avax" + "github.com/ava-labs/avalanchego/vms/secp256k1fx" + "github.com/ava-labs/coreth" "github.com/ava-labs/coreth/core" + "github.com/ava-labs/coreth/core/types" + "github.com/ava-labs/coreth/params" "github.com/ethereum/go-ethereum/common" ) @@ -130,9 +140,397 @@ func GenesisVM(t *testing.T, finishBootstrapping bool) (chan engCommon.Message, t.Fatal(err) } + if finishBootstrapping { + if err := vm.Bootstrapping(); err != nil { + t.Fatal(err) + } + + if err := vm.Bootstrapped(); err != nil { + t.Fatal(err) + } + } + return issuer, vm, genesisBytes, m } func TestVMGenesis(t *testing.T) { - _, _, _, _ = GenesisVM(t, true) + _, vm, _, _ := GenesisVM(t, true) + + shutdownChan := make(chan error, 1) + shutdownFunc := func() { + err := vm.Shutdown() + shutdownChan <- err + } + + go shutdownFunc() + shutdownTimeout := 10 * time.Millisecond + ticker := time.NewTicker(shutdownTimeout) + select { + case <-ticker.C: + t.Fatalf("VM shutdown took longer than timeout: %v", shutdownTimeout) + case err := <-shutdownChan: + if err != nil { + t.Fatalf("Shutdown errored: %s", err) + } + } +} + +func TestIssueAtomicTxs(t *testing.T) { + issuer, vm, _, sharedMemory := GenesisVM(t, true) + + defer func() { + if err := vm.Shutdown(); err != nil { + t.Fatal(err) + } + }() + + importAmount := uint64(10000000) + utxoID := avax.UTXOID{ + TxID: ids.ID{ + 0x0f, 0x2f, 0x4f, 0x6f, 0x8e, 0xae, 0xce, 0xee, + 0x0d, 0x2d, 0x4d, 0x6d, 0x8c, 0xac, 0xcc, 0xec, + 0x0b, 0x2b, 0x4b, 0x6b, 0x8a, 0xaa, 0xca, 0xea, + 0x09, 0x29, 0x49, 0x69, 0x88, 0xa8, 0xc8, 0xe8, + }, + } + + utxo := &avax.UTXO{ + UTXOID: utxoID, + Asset: avax.Asset{ID: vm.ctx.AVAXAssetID}, + Out: &secp256k1fx.TransferOutput{ + Amt: importAmount, + OutputOwners: secp256k1fx.OutputOwners{ + Threshold: 1, + Addrs: []ids.ShortID{testKeys[0].PublicKey().Address()}, + }, + }, + } + utxoBytes, err := vm.codec.Marshal(codecVersion, utxo) + if err != nil { + t.Fatal(err) + } + + xChainSharedMemory := sharedMemory.NewSharedMemory(vm.ctx.XChainID) + inputID := utxo.InputID() + if err := xChainSharedMemory.Put(vm.ctx.ChainID, []*atomic.Element{{ + Key: inputID[:], + Value: utxoBytes, + Traits: [][]byte{ + testKeys[0].PublicKey().Address().Bytes(), + }, + }}); err != nil { + t.Fatal(err) + } + + importTx, err := vm.newImportTx(vm.ctx.XChainID, testEthAddrs[0], []*crypto.PrivateKeySECP256K1R{testKeys[0]}) + if err != nil { + t.Fatal(err) + } + + if err := vm.issueTx(importTx); err != nil { + t.Fatal(err) + } + + <-issuer + + blk, err := vm.BuildBlock() + if err != nil { + t.Fatal(err) + } + + if err := blk.Verify(); err != nil { + t.Fatal(err) + } + + if status := blk.Status(); status != choices.Processing { + t.Fatalf("Expected status of built block to be %s, but found %s", choices.Processing, status) + } + + if err := blk.Accept(); err != nil { + t.Fatal(err) + } + + if status := blk.Status(); status != choices.Accepted { + t.Fatalf("Expected status of accepted block to be %s, but found %s", choices.Accepted, status) + } + + lastAcceptedID := vm.LastAccepted() + if lastAcceptedID != blk.ID() { + t.Fatalf("Expected last accepted blockID to be the accepted block: %s, but found %s", blk.ID(), lastAcceptedID) + } + + exportTx, err := vm.newExportTx(vm.ctx.AVAXAssetID, importAmount-vm.txFee-1, vm.ctx.XChainID, testShortIDAddrs[0], []*crypto.PrivateKeySECP256K1R{testKeys[0]}) + if err != nil { + t.Fatal(err) + } + + if err := vm.issueTx(exportTx); err != nil { + t.Fatal(err) + } + + <-issuer + + blk2, err := vm.BuildBlock() + if err != nil { + t.Fatal(err) + } + + if err := blk2.Verify(); err != nil { + t.Fatal(err) + } + + if status := blk2.Status(); status != choices.Processing { + t.Fatalf("Expected status of built block to be %s, but found %s", choices.Processing, status) + } + + if err := blk2.Accept(); err != nil { + t.Fatal(err) + } + + if status := blk2.Status(); status != choices.Accepted { + t.Fatalf("Expected status of accepted block to be %s, but found %s", choices.Accepted, status) + } + + lastAcceptedID = vm.LastAccepted() + if lastAcceptedID != blk2.ID() { + t.Fatalf("Expected last accepted blockID to be the accepted block: %s, but found %s", blk2.ID(), lastAcceptedID) + } +} + +func TestBuildEthTxBlock(t *testing.T) { + issuer, vm, _, sharedMemory := GenesisVM(t, true) + + defer func() { + if err := vm.Shutdown(); err != nil { + t.Fatal(err) + } + }() + + key, err := coreth.NewKey(rand.Reader) + if err != nil { + t.Fatal(err) + } + + importAmount := uint64(10000000) + utxoID := avax.UTXOID{ + TxID: ids.ID{ + 0x0f, 0x2f, 0x4f, 0x6f, 0x8e, 0xae, 0xce, 0xee, + 0x0d, 0x2d, 0x4d, 0x6d, 0x8c, 0xac, 0xcc, 0xec, + 0x0b, 0x2b, 0x4b, 0x6b, 0x8a, 0xaa, 0xca, 0xea, + 0x09, 0x29, 0x49, 0x69, 0x88, 0xa8, 0xc8, 0xe8, + }, + } + + utxo := &avax.UTXO{ + UTXOID: utxoID, + Asset: avax.Asset{ID: vm.ctx.AVAXAssetID}, + Out: &secp256k1fx.TransferOutput{ + Amt: importAmount, + OutputOwners: secp256k1fx.OutputOwners{ + Threshold: 1, + Addrs: []ids.ShortID{testKeys[0].PublicKey().Address()}, + }, + }, + } + utxoBytes, err := vm.codec.Marshal(codecVersion, utxo) + if err != nil { + t.Fatal(err) + } + + xChainSharedMemory := sharedMemory.NewSharedMemory(vm.ctx.XChainID) + inputID := utxo.InputID() + if err := xChainSharedMemory.Put(vm.ctx.ChainID, []*atomic.Element{{ + Key: inputID[:], + Value: utxoBytes, + Traits: [][]byte{ + testKeys[0].PublicKey().Address().Bytes(), + }, + }}); err != nil { + t.Fatal(err) + } + + importTx, err := vm.newImportTx(vm.ctx.XChainID, key.Address, []*crypto.PrivateKeySECP256K1R{testKeys[0]}) + if err != nil { + t.Fatal(err) + } + + if err := vm.issueTx(importTx); err != nil { + t.Fatal(err) + } + + <-issuer + + blk, err := vm.BuildBlock() + if err != nil { + t.Fatal(err) + } + + if err := blk.Verify(); err != nil { + t.Fatal(err) + } + + if status := blk.Status(); status != choices.Processing { + t.Fatalf("Expected status of built block to be %s, but found %s", choices.Processing, status) + } + + if err := blk.Accept(); err != nil { + t.Fatal(err) + } + + txs := make([]*types.Transaction, 10) + for i := 0; i < 10; i++ { + tx := types.NewTransaction(uint64(i), key.Address, big.NewInt(10), 21000, params.MinGasPrice, nil) + signedTx, err := types.SignTx(tx, types.NewEIP155Signer(vm.chainID), key.PrivateKey) + if err != nil { + t.Fatal(err) + } + txs[i] = signedTx + } + errs := vm.chain.AddRemoteTxs(txs) + for i, err := range errs { + if err != nil { + t.Fatalf("Failed to add tx at index %d: %s", i, err) + } + } + + <-issuer + + blk, err = vm.BuildBlock() + if err != nil { + t.Fatal(err) + } + + if err := blk.Verify(); err != nil { + t.Fatal(err) + } + + if status := blk.Status(); status != choices.Processing { + t.Fatalf("Expected status of built block to be %s, but found %s", choices.Processing, status) + } + + if err := blk.Accept(); err != nil { + t.Fatal(err) + } + + if status := blk.Status(); status != choices.Accepted { + t.Fatalf("Expected status of accepted block to be %s, but found %s", choices.Accepted, status) + } + + lastAcceptedID := vm.LastAccepted() + if lastAcceptedID != blk.ID() { + t.Fatalf("Expected last accepted blockID to be the accepted block: %s, but found %s", blk.ID(), lastAcceptedID) + } +} + +func TestConflictingImportTxs(t *testing.T) { + issuer, vm, _, sharedMemory := GenesisVM(t, true) + + defer func() { + if err := vm.Shutdown(); err != nil { + t.Fatal(err) + } + }() + + conflictKey, err := coreth.NewKey(rand.Reader) + if err != nil { + t.Fatal(err) + } + + xChainSharedMemory := sharedMemory.NewSharedMemory(vm.ctx.XChainID) + importTxs := make([]*Tx, 0, 3) + conflictTxs := make([]*Tx, 0, 3) + for i, key := range testKeys { + importAmount := uint64(10000000) + utxoID := avax.UTXOID{ + TxID: ids.ID{byte(i)}, + } + + utxo := &avax.UTXO{ + UTXOID: utxoID, + Asset: avax.Asset{ID: vm.ctx.AVAXAssetID}, + Out: &secp256k1fx.TransferOutput{ + Amt: importAmount, + OutputOwners: secp256k1fx.OutputOwners{ + Threshold: 1, + Addrs: []ids.ShortID{key.PublicKey().Address()}, + }, + }, + } + utxoBytes, err := vm.codec.Marshal(codecVersion, utxo) + if err != nil { + t.Fatal(err) + } + + inputID := utxo.InputID() + if err := xChainSharedMemory.Put(vm.ctx.ChainID, []*atomic.Element{{ + Key: inputID[:], + Value: utxoBytes, + Traits: [][]byte{ + key.PublicKey().Address().Bytes(), + }, + }}); err != nil { + t.Fatal(err) + } + + importTx, err := vm.newImportTx(vm.ctx.XChainID, testEthAddrs[i], []*crypto.PrivateKeySECP256K1R{key}) + if err != nil { + t.Fatal(err) + } + importTxs = append(importTxs, importTx) + + conflictTx, err := vm.newImportTx(vm.ctx.XChainID, conflictKey.Address, []*crypto.PrivateKeySECP256K1R{key}) + if err != nil { + t.Fatal(err) + } + conflictTxs = append(conflictTxs, conflictTx) + } + + expectedParentBlkID := vm.LastAccepted() + for i, tx := range importTxs { + if err := vm.issueTx(tx); err != nil { + t.Fatal(err) + } + + <-issuer + + blk, err := vm.BuildBlock() + if err != nil { + t.Fatal(err) + } + + if err := blk.Verify(); err != nil { + t.Fatal(err) + } + + if status := blk.Status(); status != choices.Processing { + t.Fatalf("Expected status of built block %d to be %s, but found %s", i, choices.Processing, status) + } + + if parentID := blk.Parent().ID(); parentID != expectedParentBlkID { + t.Fatalf("Expected parent to have blockID %s, but found %s", expectedParentBlkID, parentID) + } + + expectedParentBlkID = blk.ID() + vm.SetPreference(blk.ID()) + } + + // Shrink the atomic input cache to ensure that + // verification handles cache misses correctly. + vm.blockAtomicInputCache = cache.LRU{Size: 1} + + for i, tx := range conflictTxs { + if err := vm.issueTx(tx); err != nil { + t.Fatal(err) + } + + <-issuer + + _, err := vm.BuildBlock() + // The new block is verified in BuildBlock, so + // BuildBlock should fail due to an attempt to + // double spend an atomic UTXO. + if err == nil { + t.Fatalf("Block verification should have failed in BuildBlock %d due to double spending atomic UTXO", i) + } + } } |