diff options
author | Stephen Buttolph <[email protected]> | 2020-08-25 13:49:24 -0400 |
---|---|---|
committer | GitHub <[email protected]> | 2020-08-25 13:49:24 -0400 |
commit | 5f9c76b5f4eb5bb08b8d3e3125b223f3737631d3 (patch) | |
tree | 9de0af77831340eb38c28c613849f1e7b65f3a54 | |
parent | 1bb314f767785fe617c3c5efeca1a64127339506 (diff) | |
parent | b05d11d451fb73843abcf6bd5fc8664b069433a4 (diff) |
Merge pull request #11 from ava-labs/TS_fix_gasv0.2.13
Fix gas handling
-rw-r--r-- | consensus/clique/clique.go | 2 | ||||
-rw-r--r-- | consensus/dummy/consensus.go | 11 | ||||
-rw-r--r-- | consensus/ethash/consensus.go | 2 | ||||
-rw-r--r-- | core/blockchain.go | 4 | ||||
-rw-r--r-- | core/genesis.go | 2 | ||||
-rw-r--r-- | core/rawdb/accessors_chain.go | 2 | ||||
-rw-r--r-- | core/types/block.go | 14 | ||||
-rw-r--r-- | go.mod | 4 | ||||
-rw-r--r-- | go.sum | 13 | ||||
-rw-r--r-- | miner/worker.go | 1 | ||||
-rw-r--r-- | plugin/evm/block.go | 87 | ||||
-rw-r--r-- | plugin/evm/error.go | 19 | ||||
-rw-r--r-- | plugin/evm/export_tx.go | 223 | ||||
-rw-r--r-- | plugin/evm/factory.go | 10 | ||||
-rw-r--r-- | plugin/evm/import_tx.go | 272 | ||||
-rw-r--r-- | plugin/evm/service.go | 194 | ||||
-rw-r--r-- | plugin/evm/tx.go | 112 | ||||
-rw-r--r-- | plugin/evm/user.go | 146 | ||||
-rw-r--r-- | plugin/evm/vm.go | 373 |
19 files changed, 1434 insertions, 57 deletions
diff --git a/consensus/clique/clique.go b/consensus/clique/clique.go index d3ad1d9..3714733 100644 --- a/consensus/clique/clique.go +++ b/consensus/clique/clique.go @@ -566,7 +566,7 @@ func (c *Clique) FinalizeAndAssemble(chain consensus.ChainReader, header *types. header.UncleHash = types.CalcUncleHash(nil) // Assemble and return the final block for sealing - return types.NewBlock(header, txs, nil, receipts), nil + return types.NewBlock(header, txs, nil, receipts, nil), nil } // Authorize injects a private key into the consensus engine to mint new blocks diff --git a/consensus/dummy/consensus.go b/consensus/dummy/consensus.go index 9d8db1e..494e4be 100644 --- a/consensus/dummy/consensus.go +++ b/consensus/dummy/consensus.go @@ -19,7 +19,7 @@ import ( ) type OnFinalizeCallbackType = func(chain consensus.ChainReader, header *types.Header, state *state.StateDB, txs []*types.Transaction, uncles []*types.Header) -type OnFinalizeAndAssembleCallbackType = func(chain consensus.ChainReader, header *types.Header, state *state.StateDB, txs []*types.Transaction, uncles []*types.Header, receipts []*types.Receipt) +type OnFinalizeAndAssembleCallbackType = func(state *state.StateDB, txs []*types.Transaction) ([]byte, error) type OnAPIsCallbackType = func(consensus.ChainReader) []rpc.API type OnExtraStateChangeType = func(block *types.Block, statedb *state.StateDB) error @@ -261,14 +261,19 @@ func (self *DummyEngine) Finalize( func (self *DummyEngine) FinalizeAndAssemble(chain consensus.ChainReader, header *types.Header, state *state.StateDB, txs []*types.Transaction, uncles []*types.Header, receipts []*types.Receipt) (*types.Block, error) { + var extdata []byte if self.cb.OnFinalizeAndAssemble != nil { - self.cb.OnFinalizeAndAssemble(chain, header, state, txs, uncles, receipts) + ret, err := self.cb.OnFinalizeAndAssemble(state, txs) + extdata = ret + if err != nil { + return nil, err + } } // commit the final state root header.Root = state.IntermediateRoot(chain.Config().IsEIP158(header.Number)) // Header seems complete, assemble into a block and return - return types.NewBlock(header, txs, uncles, receipts), nil + return types.NewBlock(header, txs, uncles, receipts, extdata), nil } func (self *DummyEngine) Seal(chain consensus.ChainReader, block *types.Block, results chan<- *types.Block, stop <-chan struct{}) (err error) { diff --git a/consensus/ethash/consensus.go b/consensus/ethash/consensus.go index c325eea..dc88a79 100644 --- a/consensus/ethash/consensus.go +++ b/consensus/ethash/consensus.go @@ -576,7 +576,7 @@ func (ethash *Ethash) FinalizeAndAssemble(chain consensus.ChainReader, header *t header.Root = state.IntermediateRoot(chain.Config().IsEIP158(header.Number)) // Header seems complete, assemble into a block and return - return types.NewBlock(header, txs, uncles, receipts), nil + return types.NewBlock(header, txs, uncles, receipts, nil), nil } // SealHash returns the hash of a block prior to it being sealed. diff --git a/core/blockchain.go b/core/blockchain.go index 64c2451..6e316c7 100644 --- a/core/blockchain.go +++ b/core/blockchain.go @@ -732,6 +732,8 @@ func (bc *BlockChain) GetBlock(hash common.Hash, number uint64) *types.Block { // GetBlockByHash retrieves a block from the database by hash, caching it if found. func (bc *BlockChain) GetBlockByHash(hash common.Hash) *types.Block { + bc.chainmu.Lock() + defer bc.chainmu.Unlock() number := bc.hc.GetBlockNumber(hash) if number == nil { return nil @@ -742,6 +744,8 @@ func (bc *BlockChain) GetBlockByHash(hash common.Hash) *types.Block { // GetBlockByNumber retrieves a block from the database by number, caching it // (associated with its hash) if found. func (bc *BlockChain) GetBlockByNumber(number uint64) *types.Block { + bc.chainmu.Lock() + defer bc.chainmu.Unlock() hash := rawdb.ReadCanonicalHash(bc.db, number) if hash == (common.Hash{}) { return nil diff --git a/core/genesis.go b/core/genesis.go index ef490bf..7d21d00 100644 --- a/core/genesis.go +++ b/core/genesis.go @@ -294,7 +294,7 @@ func (g *Genesis) ToBlock(db ethdb.Database) *types.Block { statedb.Commit(false) statedb.Database().TrieDB().Commit(root, true) - return types.NewBlock(head, nil, nil, nil) + return types.NewBlock(head, nil, nil, nil, nil) } // Commit writes the block and state of a genesis specification to the database. diff --git a/core/rawdb/accessors_chain.go b/core/rawdb/accessors_chain.go index 7620eac..fdfd6ec 100644 --- a/core/rawdb/accessors_chain.go +++ b/core/rawdb/accessors_chain.go @@ -475,7 +475,7 @@ func ReadBlock(db ethdb.Reader, hash common.Hash, number uint64) *types.Block { if body == nil { return nil } - return types.NewBlockWithHeader(header).WithBody(body.Transactions, body.Uncles) + return types.NewBlockWithHeader(header).WithBody(body.Transactions, body.Uncles, body.Version, body.ExtData) } // WriteBlock serializes a block into the database, header and body separately. diff --git a/core/types/block.go b/core/types/block.go index b3dbcd3..4096d86 100644 --- a/core/types/block.go +++ b/core/types/block.go @@ -144,6 +144,8 @@ func rlpHash(x interface{}) (h common.Hash) { type Body struct { Transactions []*Transaction Uncles []*Header + Version uint32 + ExtData []byte `rlp:"nil"` } // Block represents an entire block in the Ethereum blockchain. @@ -212,7 +214,7 @@ type storageblock struct { // The values of TxHash, UncleHash, ReceiptHash and Bloom in header // are ignored and set to values derived from the given txs, uncles // and receipts. -func NewBlock(header *Header, txs []*Transaction, uncles []*Header, receipts []*Receipt) *Block { +func NewBlock(header *Header, txs []*Transaction, uncles []*Header, receipts []*Receipt, extdata []byte) *Block { b := &Block{header: CopyHeader(header), td: new(big.Int)} // TODO: panic if len(txs) != len(receipts) @@ -241,6 +243,9 @@ func NewBlock(header *Header, txs []*Transaction, uncles []*Header, receipts []* } } + b.extdata = make([]byte, len(extdata)) + copy(b.extdata, extdata) + return b } @@ -387,7 +392,7 @@ func (b *Block) Extra() []byte { return common.CopyBytes(b.header.Ext func (b *Block) Header() *Header { return CopyHeader(b.header) } // Body returns the non-header content of the block. -func (b *Block) Body() *Body { return &Body{b.transactions, b.uncles} } +func (b *Block) Body() *Body { return &Body{b.transactions, b.uncles, b.version, b.extdata} } // Size returns the true RLP encoded storage size of the block, either by encoding // and returning it, or returning a previsouly cached value. @@ -434,13 +439,16 @@ func (b *Block) WithSeal(header *Header) *Block { } // WithBody returns a new block with the given transaction and uncle contents. -func (b *Block) WithBody(transactions []*Transaction, uncles []*Header) *Block { +func (b *Block) WithBody(transactions []*Transaction, uncles []*Header, version uint32, extdata []byte) *Block { block := &Block{ header: CopyHeader(b.header), transactions: make([]*Transaction, len(transactions)), uncles: make([]*Header, len(uncles)), + extdata: make([]byte, len(extdata)), + version: version, } copy(block.transactions, transactions) + copy(block.extdata, extdata) for i := range uncles { block.uncles[i] = CopyHeader(uncles[i]) } @@ -3,7 +3,7 @@ module github.com/ava-labs/coreth go 1.14 require ( - github.com/ava-labs/gecko v0.5.7 + github.com/ava-labs/gecko v0.6.1-rc.1 github.com/ava-labs/go-ethereum v1.9.3 github.com/cespare/cp v1.1.1 // indirect github.com/davecgh/go-spew v1.1.1 @@ -11,7 +11,9 @@ require ( github.com/edsrzf/mmap-go v1.0.0 github.com/fjl/memsize v0.0.0-20190710130421-bcb5799ab5e5 github.com/gballet/go-libpcsclite v0.0.0-20191108122812-4678299bea08 + github.com/golang-collections/collections v0.0.0-20130729185459-604e922904d3 github.com/golang/snappy v0.0.1 + github.com/gorilla/rpc v1.2.0 github.com/gorilla/websocket v1.4.2 github.com/hashicorp/go-plugin v1.3.0 github.com/hashicorp/golang-lru v0.5.4 @@ -74,6 +74,8 @@ github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V 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/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= +github.com/golang-collections/collections v0.0.0-20130729185459-604e922904d3 h1:zN2lZNZRflqFyxVaTIU61KNKQ9C0055u9CAfpmqUvo4= +github.com/golang-collections/collections v0.0.0-20130729185459-604e922904d3/go.mod h1:nPpo7qLxd6XL3hWJG/O60sR8ZKfMCiIoNap5GvD12KU= 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/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= @@ -108,11 +110,14 @@ github.com/gorilla/rpc v1.2.0/go.mod h1:V4h9r+4sF5HnzqbwIez0fKSpANP0zlYd3qR7p36j 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/hashicorp/go-hclog v0.0.0-20180709165350-ff2cf002a8dd h1:rNuUHR+CvK1IS89MMtcF0EpcVMZtjKfPRp4MEmt/aTs= github.com/hashicorp/go-hclog v0.0.0-20180709165350-ff2cf002a8dd/go.mod h1:9bjs9uLqI8l75knNv3lV1kA55veR+WUPSiKIWcQHudI= +github.com/hashicorp/go-plugin v1.3.0 h1:4d/wJojzvHV1I4i/rrjVaeuyxWrLzDE1mDCyDy8fXS8= github.com/hashicorp/go-plugin v1.3.0/go.mod h1:F9eH4LrE/ZsRdbwhfjs9k9HoDUwAHnYtXdgmf1AVNs0= github.com/hashicorp/go-uuid v1.0.2/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= github.com/hashicorp/golang-lru v0.5.4 h1:YDjusn29QI/Das2iO9M0BHnIbxPeyuCHsjMW+lJfyTc= github.com/hashicorp/golang-lru v0.5.4/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4= +github.com/hashicorp/yamux v0.0.0-20180604194846-3520598351bb h1:b5rjCoWHc7eqmAS4/qyk21ZsHyb6Mxv/jykxvNTkU4M= github.com/hashicorp/yamux v0.0.0-20180604194846-3520598351bb/go.mod h1:+NfK9FKeTrX5uv1uIXGdwYDTeHna2qgaIlx54MXqjAM= github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= github.com/huin/goupnp v1.0.0 h1:wg75sLpL6DZqwHQN6E1Cfk6mtfzS45z8OV+ic+DtHRo= @@ -123,6 +128,7 @@ github.com/jackpal/gateway v1.0.6/go.mod h1:lTpwd4ACLXmpyiCTRtfiNyVnUmqT9RivzCDQ github.com/jackpal/go-nat-pmp v1.0.2 h1:KzKSgb7qkJvOUTqYl9/Hg/me3pWgBmERKrTGD7BdWus= github.com/jackpal/go-nat-pmp v1.0.2/go.mod h1:QPH045xvCAeXUZOxsnwmrtiCoxIr9eob+4orBN1SBKc= github.com/jcmturner/gofork v1.0.0/go.mod h1:MK8+TM0La+2rjBD4jE12Kj1pCCxK7d2LK/UM3ncEo0o= +github.com/jhump/protoreflect v1.6.0 h1:h5jfMVslIg6l29nsMs0D8Wj17RDVdNYti0vDN/PZZoE= github.com/jhump/protoreflect v1.6.0/go.mod h1:eaTn3RZAmMBcV0fifFvlm6VHNz3wSkYyXYWUh7ymB74= github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= @@ -153,6 +159,7 @@ github.com/matttproud/golang_protobuf_extensions v1.0.1 h1:4hp9jkHxhMHkqkrB3Ix0j github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y= github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= +github.com/mitchellh/go-testing-interface v0.0.0-20171004221916-a61a99592b77 h1:7GoSOOW2jpsfkntVKaS2rAr1TJqfcxotyaUcuxoZSzg= github.com/mitchellh/go-testing-interface v0.0.0-20171004221916-a61a99592b77/go.mod h1:kRemZodwjscx+RGhAo8eIhFbs2+BFgRtFPeD/KE+zxI= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= @@ -163,6 +170,7 @@ github.com/mr-tron/base58 v1.2.0/go.mod h1:BinMc/sQntlIE1frQmRFPUoPA1Zkr8VRgBdjW github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= github.com/nbutton23/zxcvbn-go v0.0.0-20180912185939-ae427f1e4c1d h1:AREM5mwr4u1ORQBMvzfzBgpsctsbQikCVpvC+tX285E= github.com/nbutton23/zxcvbn-go v0.0.0-20180912185939-ae427f1e4c1d/go.mod h1:o96djdrsSGy3AWPyBgZMAGfxZNfgntdJG+11KU4QvbU= +github.com/oklog/run v1.0.0 h1:Ru7dDtJNOyC66gQ5dQmaCa0qIsAUFY3sFpK1Xk8igrw= github.com/oklog/run v1.0.0/go.mod h1:dlhp/R75TPv97u0XWUtDeV/lRKWPKSdTuV0TZvrmrQA= github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U= github.com/olekukonko/tablewriter v0.0.4 h1:vHD/YYe1Wolo78koG299f7V/VAS08c6IpCLn+Ejf/w8= @@ -181,6 +189,7 @@ github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINE 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/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= @@ -222,6 +231,7 @@ github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+ github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 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 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= github.com/syndtr/goleveldb v1.0.0 h1:fBdIW9lB4Iz0n9khmH8w27SJ3QEJ7+IgjPEwGSZiFdE= github.com/syndtr/goleveldb v1.0.0/go.mod h1:ZVVdQEZoIme9iO1Ch2Jdy24qqXrMMOU6lpPAyBWyWuQ= @@ -311,6 +321,7 @@ google.golang.org/genproto v0.0.0-20170818010345-ee236bd376b0/go.mod h1:JiN7NxoA google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= +google.golang.org/genproto v0.0.0-20200218151345-dad8c97a84f5 h1:jB9+PJSvu5tBfmJHy/OVapFdjDF3WvpkqRhxqrmzoEU= google.golang.org/genproto v0.0.0-20200218151345-dad8c97a84f5/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= google.golang.org/grpc v1.8.0/go.mod h1:yo6s7OP7yaDglbqo1J04qKzAhqBH6lvTonzMVmEdcZw= google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= @@ -319,6 +330,7 @@ google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyac google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY= google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= google.golang.org/grpc v1.27.1/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= +google.golang.org/grpc v1.29.1 h1:EC2SB8S04d2r73uptxphDSUG+kTKVgjRPF+N3xpxRB4= google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk= 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= @@ -349,6 +361,7 @@ 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.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.5/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.8 h1:obN1ZagJSUGI0Ek/LBmuj4SNLPfIny3KsKFopxRdj10= gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= diff --git a/miner/worker.go b/miner/worker.go index feab0b2..4a6303b 100644 --- a/miner/worker.go +++ b/miner/worker.go @@ -751,6 +751,7 @@ func (w *worker) updateSnapshot() { w.current.txs, uncles, w.current.receipts, + nil, ) w.snapshotState = w.current.state.Copy() diff --git a/plugin/evm/block.go b/plugin/evm/block.go index 449e261..5c30fe1 100644 --- a/plugin/evm/block.go +++ b/plugin/evm/block.go @@ -4,6 +4,7 @@ package evm import ( + "errors" "fmt" "github.com/ava-labs/coreth/core/types" @@ -12,6 +13,7 @@ import ( "github.com/ava-labs/gecko/ids" "github.com/ava-labs/gecko/snow/choices" "github.com/ava-labs/gecko/snow/consensus/snowman" + "github.com/ava-labs/gecko/vms/components/missing" ) // Block implements the snowman.Block interface @@ -26,9 +28,21 @@ func (b *Block) ID() ids.ID { return b.id } // Accept implements the snowman.Block interface func (b *Block) Accept() error { - b.vm.ctx.Log.Verbo("Block %s is accepted", b.ID()) - b.vm.updateStatus(b.ID(), choices.Accepted) - return nil + vm := b.vm + + vm.ctx.Log.Verbo("Block %s is accepted", b.ID()) + vm.updateStatus(b.ID(), choices.Accepted) + + tx := vm.getAtomicTx(b.ethBlock) + if tx == nil { + return nil + } + utx, ok := tx.UnsignedTx.(UnsignedAtomicTx) + if !ok { + return errors.New("unknown tx type") + } + + return utx.Accept(vm.ctx, nil) } // Reject implements the snowman.Block interface @@ -50,17 +64,72 @@ func (b *Block) Status() choices.Status { // Parent implements the snowman.Block interface func (b *Block) Parent() snowman.Block { parentID := ids.NewID(b.ethBlock.ParentHash()) - block := &Block{ - id: parentID, - ethBlock: b.vm.getCachedBlock(parentID), - vm: b.vm, + if block := b.vm.getBlock(parentID); block != nil { + b.vm.ctx.Log.Verbo("Parent(%s) has status: %s", parentID, block.Status()) + return block } - b.vm.ctx.Log.Verbo("Parent(%s) has status: %s", block.ID(), block.Status()) - return block + b.vm.ctx.Log.Verbo("Parent(%s) has status: %s", parentID, choices.Unknown) + return &missing.Block{BlkID: parentID} } // Verify implements the snowman.Block interface func (b *Block) Verify() error { + // Only enforce a minimum fee when bootstrapping has finished + if b.vm.ctx.IsBootstrapped() { + // Ensure the minimum gas price is paid for every transaction + for _, tx := range b.ethBlock.Transactions() { + if tx.GasPrice().Cmp(minGasPrice) < 0 { + return errInvalidBlock + } + } + } + + vm := b.vm + tx := vm.getAtomicTx(b.ethBlock) + if tx != nil { + switch atx := tx.UnsignedTx.(type) { + case *UnsignedImportTx: + if b.ethBlock.Hash() == vm.genesisHash { + return nil + } + p := b.Parent() + path := []*Block{} + inputs := new(ids.Set) + for { + if p.Status() == choices.Accepted || p.(*Block).ethBlock.Hash() == vm.genesisHash { + break + } + if ret, hit := vm.blockAtomicInputCache.Get(p.ID()); hit { + inputs = ret.(*ids.Set) + break + } + path = append(path, p.(*Block)) + p = p.Parent().(*Block) + } + for i := len(path) - 1; i >= 0; i-- { + inputsCopy := new(ids.Set) + p := path[i] + atx := vm.getAtomicTx(p.ethBlock) + if atx != nil { + inputs.Union(atx.UnsignedTx.(UnsignedAtomicTx).InputUTXOs()) + inputsCopy.Union(*inputs) + } + vm.blockAtomicInputCache.Put(p.ID(), inputsCopy) + } + for _, in := range atx.InputUTXOs().List() { + if inputs.Contains(in) { + return errInvalidBlock + } + } + case *UnsignedExportTx: + default: + return errors.New("unknown atomic tx type") + } + + if tx.UnsignedTx.(UnsignedAtomicTx).SemanticVerify(vm, tx) != nil { + return errInvalidBlock + } + } _, err := b.vm.chain.InsertChain([]*types.Block{b.ethBlock}) return err } diff --git a/plugin/evm/error.go b/plugin/evm/error.go new file mode 100644 index 0000000..0554349 --- /dev/null +++ b/plugin/evm/error.go @@ -0,0 +1,19 @@ +// (c) 2019-2020, Ava Labs, Inc. All rights reserved. +// See the file LICENSE for licensing terms. + +package evm + +// TxError provides the ability for errors to be distinguished as permenant or +// temporary +type TxError interface { + error + Temporary() bool +} + +type tempError struct{ error } + +func (tempError) Temporary() bool { return true } + +type permError struct{ error } + +func (permError) Temporary() bool { return false } diff --git a/plugin/evm/export_tx.go b/plugin/evm/export_tx.go new file mode 100644 index 0000000..31b4681 --- /dev/null +++ b/plugin/evm/export_tx.go @@ -0,0 +1,223 @@ +// (c) 2019-2020, Ava Labs, Inc. All rights reserved. +// See the file LICENSE for licensing terms. + +package evm + +import ( + "fmt" + "math/big" + + "github.com/ava-labs/coreth/core/state" + "github.com/ava-labs/go-ethereum/log" + + "github.com/ava-labs/gecko/chains/atomic" + "github.com/ava-labs/gecko/database" + "github.com/ava-labs/gecko/ids" + "github.com/ava-labs/gecko/snow" + "github.com/ava-labs/gecko/utils/crypto" + safemath "github.com/ava-labs/gecko/utils/math" + "github.com/ava-labs/gecko/vms/components/avax" + "github.com/ava-labs/gecko/vms/secp256k1fx" +) + +// UnsignedExportTx is an unsigned ExportTx +type UnsignedExportTx struct { + avax.Metadata + // true iff this transaction has already passed syntactic verification + syntacticallyVerified bool + // ID of the network on which this tx was issued + NetworkID uint32 `serialize:"true" json:"networkID"` + // ID of this blockchain. + BlockchainID ids.ID `serialize:"true" json:"blockchainID"` + // Which chain to send the funds to + DestinationChain ids.ID `serialize:"true" json:"destinationChain"` + // Inputs + Ins []EVMInput `serialize:"true" json:"inputs"` + // Outputs that are exported to the chain + ExportedOutputs []*avax.TransferableOutput `serialize:"true" json:"exportedOutputs"` +} + +// InputUTXOs returns an empty set +func (tx *UnsignedExportTx) InputUTXOs() ids.Set { return ids.Set{} } + +// Verify this transaction is well-formed +func (tx *UnsignedExportTx) Verify( + avmID ids.ID, + ctx *snow.Context, + feeAmount uint64, + feeAssetID ids.ID, +) error { + switch { + case tx == nil: + return errNilTx + case tx.syntacticallyVerified: // already passed syntactic verification + return nil + case tx.DestinationChain.IsZero(): + return errWrongChainID + case !tx.DestinationChain.Equals(avmID): + return errWrongChainID + case len(tx.ExportedOutputs) == 0: + return errNoExportOutputs + case tx.NetworkID != ctx.NetworkID: + return errWrongNetworkID + case !ctx.ChainID.Equals(tx.BlockchainID): + return errWrongBlockchainID + } + + for _, in := range tx.Ins { + if err := in.Verify(); err != nil { + return err + } + } + + for _, out := range tx.ExportedOutputs { + if err := out.Verify(); err != nil { + return err + } + } + if !avax.IsSortedTransferableOutputs(tx.ExportedOutputs, Codec) { + return errOutputsNotSorted + } + + tx.syntacticallyVerified = true + return nil +} + +// SemanticVerify this transaction is valid. +func (tx *UnsignedExportTx) SemanticVerify( + vm *VM, + stx *Tx, +) TxError { + if err := tx.Verify(vm.ctx.XChainID, vm.ctx, vm.txFee, vm.ctx.AVAXAssetID); err != nil { + return permError{err} + } + + f := crypto.FactorySECP256K1R{} + for i, cred := range stx.Creds { + if err := cred.Verify(); err != nil { + return permError{err} + } + pubKey, err := f.RecoverPublicKey(tx.UnsignedBytes(), cred.(*secp256k1fx.Credential).Sigs[0][:]) + if err != nil { + return permError{err} + } + if tx.Ins[i].Address != PublicKeyToEthAddress(pubKey) { + return permError{errPublicKeySignatureMismatch} + } + } + + // do flow-checking + fc := avax.NewFlowChecker() + fc.Produce(vm.ctx.AVAXAssetID, vm.txFee) + + for _, out := range tx.ExportedOutputs { + fc.Produce(out.AssetID(), out.Output().Amount()) + } + + for _, in := range tx.Ins { + fc.Consume(vm.ctx.AVAXAssetID, in.Amount) + } + + if err := fc.Verify(); err != nil { + return permError{err} + } + + // TODO: verify UTXO outputs via gRPC + return nil +} + +// Accept this transaction. +func (tx *UnsignedExportTx) Accept(ctx *snow.Context, _ database.Batch) error { + txID := tx.ID() + + elems := make([]*atomic.Element, len(tx.ExportedOutputs)) + for i, out := range tx.ExportedOutputs { + utxo := &avax.UTXO{ + UTXOID: avax.UTXOID{ + TxID: txID, + OutputIndex: uint32(i), + }, + Asset: avax.Asset{ID: out.AssetID()}, + Out: out.Out, + } + + utxoBytes, err := Codec.Marshal(utxo) + if err != nil { + return err + } + + elem := &atomic.Element{ + Key: utxo.InputID().Bytes(), + Value: utxoBytes, + } + if out, ok := utxo.Out.(avax.Addressable); ok { + elem.Traits = out.Addresses() + } + + elems[i] = elem + } + + return ctx.SharedMemory.Put(tx.DestinationChain, elems) +} + +// Create a new transaction +func (vm *VM) newExportTx( + amount uint64, // Amount of tokens to export + chainID ids.ID, // Chain to send the UTXOs to + to ids.ShortID, // Address of chain recipient + keys []*crypto.PrivateKeySECP256K1R, // Pay the fee and provide the tokens +) (*Tx, error) { + if !vm.ctx.XChainID.Equals(chainID) { + return nil, errWrongChainID + } + + toBurn, err := safemath.Add64(amount, vm.txFee) + if err != nil { + return nil, errOverflowExport + } + ins, signers, err := vm.GetSpendableCanonical(keys, toBurn) + if err != nil { + return nil, fmt.Errorf("couldn't generate tx inputs/outputs: %w", err) + } + + // Create the transaction + utx := &UnsignedExportTx{ + NetworkID: vm.ctx.NetworkID, + BlockchainID: vm.ctx.ChainID, + DestinationChain: chainID, + Ins: ins, + ExportedOutputs: []*avax.TransferableOutput{{ // Exported to X-Chain + Asset: avax.Asset{ID: vm.ctx.AVAXAssetID}, + Out: &secp256k1fx.TransferOutput{ + Amt: amount, + OutputOwners: secp256k1fx.OutputOwners{ + Locktime: 0, + Threshold: 1, + Addrs: []ids.ShortID{to}, + }, + }, + }}, + } + tx := &Tx{UnsignedTx: utx} + if err := tx.Sign(vm.codec, signers); err != nil { + return nil, err + } + return tx, utx.Verify(vm.ctx.XChainID, vm.ctx, vm.txFee, vm.ctx.AVAXAssetID) +} + +func (tx *UnsignedExportTx) EVMStateTransfer(state *state.StateDB) error { + for _, from := range tx.Ins { + log.Info("consume", "in", from.Address, "amount", from.Amount, "nonce", from.Nonce, "nonce0", state.GetNonce(from.Address)) + amount := new(big.Int).Mul( + new(big.Int).SetUint64(from.Amount), x2cRate) + if state.GetBalance(from.Address).Cmp(amount) < 0 { + return errInsufficientFunds + } + state.SubBalance(from.Address, amount) + if state.GetNonce(from.Address) != from.Nonce { + return errInvalidNonce + } + state.SetNonce(from.Address, from.Nonce+1) + } + return nil +} diff --git a/plugin/evm/factory.go b/plugin/evm/factory.go index a4c0eca..6ae62ae 100644 --- a/plugin/evm/factory.go +++ b/plugin/evm/factory.go @@ -13,7 +13,13 @@ var ( ) // Factory ... -type Factory struct{} +type Factory struct { + Fee uint64 +} // New ... -func (f *Factory) New() interface{} { return &VM{} } +func (f *Factory) New() interface{} { + return &VM{ + txFee: f.Fee, + } +} diff --git a/plugin/evm/import_tx.go b/plugin/evm/import_tx.go new file mode 100644 index 0000000..35ba8cc --- /dev/null +++ b/plugin/evm/import_tx.go @@ -0,0 +1,272 @@ +// (c) 2019-2020, Ava Labs, Inc. All rights reserved. +// See the file LICENSE for licensing terms. + +package evm + +import ( + "fmt" + "math/big" + + "github.com/ava-labs/coreth/core/state" + + "github.com/ava-labs/gecko/database" + "github.com/ava-labs/gecko/ids" + "github.com/ava-labs/gecko/snow" + "github.com/ava-labs/gecko/utils/crypto" + "github.com/ava-labs/gecko/utils/math" + "github.com/ava-labs/gecko/vms/components/avax" + "github.com/ava-labs/gecko/vms/secp256k1fx" + "github.com/ava-labs/go-ethereum/common" +) + +// UnsignedImportTx is an unsigned ImportTx +type UnsignedImportTx struct { + avax.Metadata + // true iff this transaction has already passed syntactic verification + syntacticallyVerified bool + // ID of the network on which this tx was issued + NetworkID uint32 `serialize:"true" json:"networkID"` + // ID of this blockchain. + BlockchainID ids.ID `serialize:"true" json:"blockchainID"` + // Which chain to consume the funds from + SourceChain ids.ID `serialize:"true" json:"sourceChain"` + // Inputs that consume UTXOs produced on the chain + ImportedInputs []*avax.TransferableInput `serialize:"true" json:"importedInputs"` + // Outputs + Outs []EVMOutput `serialize:"true" json:"outputs"` +} + +// InputUTXOs returns the UTXOIDs of the imported funds +func (tx *UnsignedImportTx) InputUTXOs() ids.Set { + set := ids.Set{} + for _, in := range tx.ImportedInputs { + set.Add(in.InputID()) + } + return set +} + +// Verify this transaction is well-formed +func (tx *UnsignedImportTx) Verify( + avmID ids.ID, + ctx *snow.Context, + feeAmount uint64, + feeAssetID ids.ID, +) error { + switch { + case tx == nil: + return errNilTx + case tx.syntacticallyVerified: // already passed syntactic verification + return nil + case tx.SourceChain.IsZero(): + return errWrongChainID + case !tx.SourceChain.Equals(avmID): + return errWrongChainID + case len(tx.ImportedInputs) == 0: + return errNoImportInputs + case tx.NetworkID != ctx.NetworkID: + return errWrongNetworkID + case !ctx.ChainID.Equals(tx.BlockchainID): + return errWrongBlockchainID + } + + for _, out := range tx.Outs { + if err := out.Verify(); err != nil { + return err + } + } + + for _, in := range tx.ImportedInputs { + if err := in.Verify(); err != nil { + return err + } + } + if !avax.IsSortedAndUniqueTransferableInputs(tx.ImportedInputs) { + return errInputsNotSortedUnique + } + + tx.syntacticallyVerified = true + return nil +} + +// SemanticVerify this transaction is valid. +func (tx *UnsignedImportTx) SemanticVerify( + vm *VM, + stx *Tx, +) TxError { + if err := tx.Verify(vm.ctx.XChainID, vm.ctx, vm.txFee, vm.ctx.AVAXAssetID); err != nil { + return permError{err} + } + + // do flow-checking + fc := avax.NewFlowChecker() + fc.Produce(vm.ctx.AVAXAssetID, vm.txFee) + + for _, out := range tx.Outs { + fc.Produce(vm.ctx.AVAXAssetID, out.Amount) + } + + for _, in := range tx.ImportedInputs { + fc.Consume(in.AssetID(), in.Input().Amount()) + } + if err := fc.Verify(); err != nil { + return permError{err} + } + + if !vm.ctx.IsBootstrapped() { + // Allow for force committing during bootstrapping + return nil + } + + utxoIDs := make([][]byte, len(tx.ImportedInputs)) + for i, in := range tx.ImportedInputs { + utxoIDs[i] = in.UTXOID.InputID().Bytes() + } + allUTXOBytes, err := vm.ctx.SharedMemory.Get(tx.SourceChain, utxoIDs) + if err != nil { + return tempError{err} + } + + utxos := make([]*avax.UTXO, len(tx.ImportedInputs)) + for i, utxoBytes := range allUTXOBytes { + utxo := &avax.UTXO{} + if err := vm.codec.Unmarshal(utxoBytes, utxo); err != nil { + return tempError{err} + } + utxos[i] = utxo + } + + for i, in := range tx.ImportedInputs { + utxoBytes := allUTXOBytes[i] + + utxo := &avax.UTXO{} + if err := vm.codec.Unmarshal(utxoBytes, utxo); err != nil { + return tempError{err} + } + + cred := stx.Creds[i] + + utxoAssetID := utxo.AssetID() + inAssetID := in.AssetID() + if !utxoAssetID.Equals(inAssetID) { + return permError{errAssetIDMismatch} + } + + if err := vm.fx.VerifyTransfer(tx, in.In, cred, utxo.Out); err != nil { + return tempError{err} + } + } + return nil +} + +// Accept this transaction and spend imported inputs +// We spend imported UTXOs here rather than in semanticVerify because +// we don't want to remove an imported UTXO in semanticVerify +// only to have the transaction not be Accepted. This would be inconsistent. +// Recall that imported UTXOs are not kept in a versionDB. +func (tx *UnsignedImportTx) Accept(ctx *snow.Context, _ database.Batch) error { + // TODO: Is any batch passed in here? + utxoIDs := make([][]byte, len(tx.ImportedInputs)) + for i, in := range tx.ImportedInputs { + utxoIDs[i] = in.InputID().Bytes() + } + return ctx.SharedMemory.Remove(tx.SourceChain, utxoIDs) +} + +// Create a new transaction +func (vm *VM) newImportTx( + chainID ids.ID, // chain to import from + to common.Address, // Address of recipient + keys []*crypto.PrivateKeySECP256K1R, // Keys to import the funds +) (*Tx, error) { + if !vm.ctx.XChainID.Equals(chainID) { + return nil, errWrongChainID + } + + kc := secp256k1fx.NewKeychain() + for _, key := range keys { + kc.Add(key) + } + + atomicUTXOs, _, _, err := vm.GetAtomicUTXOs(chainID, kc.Addresses(), ids.ShortEmpty, ids.Empty, -1) + if err != nil { + return nil, fmt.Errorf("problem retrieving atomic UTXOs: %w", err) + } + + importedInputs := []*avax.TransferableInput{} + signers := [][]*crypto.PrivateKeySECP256K1R{} + + importedAmount := uint64(0) + now := vm.clock.Unix() + for _, utxo := range atomicUTXOs { + if !utxo.AssetID().Equals(vm.ctx.AVAXAssetID) { + continue + } + inputIntf, utxoSigners, err := kc.Spend(utxo.Out, now) + if err != nil { + continue + } + input, ok := inputIntf.(avax.TransferableIn) + if !ok { + continue + } + importedAmount, err = math.Add64(importedAmount, input.Amount()) + if err != nil { + return nil, err + } + importedInputs = append(importedInputs, &avax.TransferableInput{ + UTXOID: utxo.UTXOID, + Asset: utxo.Asset, + In: input, + }) + signers = append(signers, utxoSigners) + } + avax.SortTransferableInputsWithSigners(importedInputs, signers) + + if importedAmount == 0 { + return nil, errNoFunds // No imported UTXOs were spendable + } + + nonce, err := vm.GetAcceptedNonce(to) + if err != nil { + return nil, err + } + + outs := []EVMOutput{} + if importedAmount < vm.txFee { // imported amount goes toward paying tx fee + // TODO: spend EVM balance to compensate vm.txFee-importedAmount + return nil, errNoFunds + } else if importedAmount > vm.txFee { + outs = append(outs, EVMOutput{ + Address: to, + Amount: importedAmount - vm.txFee, + Nonce: nonce, + }) + } + + // Create the transaction + utx := &UnsignedImportTx{ + NetworkID: vm.ctx.NetworkID, + BlockchainID: vm.ctx.ChainID, + Outs: outs, + ImportedInputs: importedInputs, + SourceChain: chainID, + } + tx := &Tx{UnsignedTx: utx} + if err := tx.Sign(vm.codec, signers); err != nil { + return nil, err + } + return tx, utx.Verify(vm.ctx.XChainID, vm.ctx, vm.txFee, vm.ctx.AVAXAssetID) +} + +func (tx *UnsignedImportTx) EVMStateTransfer(state *state.StateDB) error { + for _, to := range tx.Outs { + state.AddBalance(to.Address, + new(big.Int).Mul( + new(big.Int).SetUint64(to.Amount), x2cRate)) + if state.GetNonce(to.Address) != to.Nonce { + return errInvalidNonce + } + state.SetNonce(to.Address, to.Nonce+1) + } + return nil +} diff --git a/plugin/evm/service.go b/plugin/evm/service.go index 62b124f..29ef35d 100644 --- a/plugin/evm/service.go +++ b/plugin/evm/service.go @@ -6,15 +6,23 @@ package evm import ( "context" "crypto/rand" + "errors" "fmt" "math/big" + "net/http" + "strings" "github.com/ava-labs/coreth" "github.com/ava-labs/coreth/core/types" + "github.com/ava-labs/gecko/api" + "github.com/ava-labs/gecko/utils/constants" + "github.com/ava-labs/gecko/utils/crypto" + "github.com/ava-labs/gecko/utils/formatting" + "github.com/ava-labs/gecko/utils/json" "github.com/ava-labs/go-ethereum/common" "github.com/ava-labs/go-ethereum/common/hexutil" - "github.com/ava-labs/go-ethereum/crypto" + ethcrypto "github.com/ava-labs/go-ethereum/crypto" ) const ( @@ -36,6 +44,8 @@ type SnowmanAPI struct{ vm *VM } // NetAPI offers network related API methods type NetAPI struct{ vm *VM } +type AvaAPI struct{ vm *VM } + // NewNetAPI creates a new net API instance. func NewNetAPI(vm *VM) *NetAPI { return &NetAPI{vm} } @@ -55,7 +65,7 @@ type Web3API struct{} func (s *Web3API) ClientVersion() string { return version } // Sha3 returns the bytes returned by hashing [input] with Keccak256 -func (s *Web3API) Sha3(input hexutil.Bytes) hexutil.Bytes { return crypto.Keccak256(input) } +func (s *Web3API) Sha3(input hexutil.Bytes) hexutil.Bytes { return ethcrypto.Keccak256(input) } // GetAcceptedFrontReply defines the reply that will be sent from the // GetAcceptedFront API call @@ -92,7 +102,7 @@ func (api *DebugAPI) SpendGenesis(ctx context.Context, nonce uint64) error { gasLimit := 21000 gasPrice := big.NewInt(1000000000) - genPrivateKey, err := crypto.HexToECDSA(GenesisTestKey[2:]) + genPrivateKey, err := ethcrypto.HexToECDSA(GenesisTestKey[2:]) if err != nil { return err } @@ -120,3 +130,181 @@ func (api *DebugAPI) IssueBlock(ctx context.Context) error { return api.vm.tryBlockGen() } + +// ExportKeyArgs are arguments for ExportKey +type ExportKeyArgs struct { + api.UserPass + Address string `json:"address"` +} + +// ExportKeyReply is the response for ExportKey +type ExportKeyReply struct { + // The decrypted PrivateKey for the Address provided in the arguments + PrivateKey string `json:"privateKey"` +} + +// ExportKey returns a private key from the provided user +func (service *AvaAPI) ExportKey(r *http.Request, args *ExportKeyArgs, reply *ExportKeyReply) error { + service.vm.ctx.Log.Info("Platform: ExportKey called") + db, err := service.vm.ctx.Keystore.GetDatabase(args.Username, args.Password) + if err != nil { + return fmt.Errorf("problem retrieving user '%s': %w", args.Username, err) + } + user := user{db: db} + if address, err := service.vm.ParseEthAddress(args.Address); err != nil { + return fmt.Errorf("couldn't parse %s to address: %s", args.Address, err) + } else if sk, err := user.getKey(address); err != nil { + return fmt.Errorf("problem retrieving private key: %w", err) + } else { + reply.PrivateKey = constants.SecretKeyPrefix + formatting.CB58{Bytes: sk.Bytes()}.String() + return nil + } +} + +// ImportKeyArgs are arguments for ImportKey +type ImportKeyArgs struct { + api.UserPass + PrivateKey string `json:"privateKey"` +} + +// ImportKey adds a private key to the provided user +func (service *AvaAPI) ImportKey(r *http.Request, args *ImportKeyArgs, reply *api.JsonAddress) error { + service.vm.ctx.Log.Info("Platform: ImportKey called for user '%s'", args.Username) + if service.vm.ctx.Keystore == nil { + return fmt.Errorf("oh no") + } + fmt.Sprintf("good") + db, err := service.vm.ctx.Keystore.GetDatabase(args.Username, args.Password) + if err != nil { + return fmt.Errorf("problem retrieving data: %w", err) + } + + user := user{db: db} + + factory := crypto.FactorySECP256K1R{} + + if !strings.HasPrefix(args.PrivateKey, constants.SecretKeyPrefix) { + return fmt.Errorf("private key missing %s prefix", constants.SecretKeyPrefix) + } + trimmedPrivateKey := strings.TrimPrefix(args.PrivateKey, constants.SecretKeyPrefix) + formattedPrivateKey := formatting.CB58{} + if err := formattedPrivateKey.FromString(trimmedPrivateKey); err != nil { + return fmt.Errorf("problem parsing private key: %w", err) + } + + skIntf, err := factory.ToPrivateKey(formattedPrivateKey.Bytes) + if err != nil { + return fmt.Errorf("problem parsing private key: %w", err) + } + sk := skIntf.(*crypto.PrivateKeySECP256K1R) + + if err := user.putAddress(sk); err != nil { + return fmt.Errorf("problem saving key %w", err) + } + + // TODO: return eth address here + reply.Address, err = service.vm.FormatEthAddress(GetEthAddress(sk)) + if err != nil { + return fmt.Errorf("problem formatting address: %w", err) + } + return nil +} + +// ImportAVAXArgs are the arguments to ImportAVAX +type ImportAVAXArgs struct { + api.UserPass + + // Chain the funds are coming from + SourceChain string `json:"sourceChain"` + + // The address that will receive the imported funds + To string `json:"to"` +} + +// ImportAVAX issues a transaction to import AVAX from the X-chain. The AVAX +// must have already been exported from the X-Chain. +func (service *AvaAPI) ImportAVAX(_ *http.Request, args *ImportAVAXArgs, response *api.JsonTxID) error { + service.vm.ctx.Log.Info("Platform: ImportAVAX called") + + chainID, err := service.vm.ctx.BCLookup.Lookup(args.SourceChain) + if err != nil { + return fmt.Errorf("problem parsing chainID %q: %w", args.SourceChain, err) + } + + // Get the user's info + db, err := service.vm.ctx.Keystore.GetDatabase(args.Username, args.Password) + if err != nil { + return fmt.Errorf("couldn't get user '%s': %w", args.Username, err) + } + user := user{db: db} + + to, err := service.vm.ParseEthAddress(args.To) + if err != nil { // Parse address + return fmt.Errorf("couldn't parse argument 'to' to an address: %w", err) + } + + privKeys, err := user.getKeys() + if err != nil { // Get keys + return fmt.Errorf("couldn't get keys controlled by the user: %w", err) + } + + tx, err := service.vm.newImportTx(chainID, to, privKeys) + if err != nil { + return err + } + + response.TxID = tx.ID() + return service.vm.issueTx(tx) +} + +// ExportAVAXArgs are the arguments to ExportAVAX +type ExportAVAXArgs struct { + api.UserPass + + // Amount of AVAX to send + Amount json.Uint64 `json:"amount"` + + // ID of the address that will receive the AVAX. This address includes the + // chainID, which is used to determine what the destination chain is. + To string `json:"to"` +} + +// ExportAVAX exports AVAX from the P-Chain to the X-Chain +// It must be imported on the X-Chain to complete the transfer +func (service *AvaAPI) ExportAVAX(_ *http.Request, args *ExportAVAXArgs, response *api.JsonTxID) error { + service.vm.ctx.Log.Info("Platform: ExportAVAX called") + + if args.Amount == 0 { + return errors.New("argument 'amount' must be > 0") + } + + chainID, to, err := service.vm.ParseAddress(args.To) + if err != nil { + return err + } + + // Get this user's data + db, err := service.vm.ctx.Keystore.GetDatabase(args.Username, args.Password) + if err != nil { + return fmt.Errorf("problem retrieving user '%s': %w", args.Username, err) + } + user := user{db: db} + privKeys, err := user.getKeys() + if err != nil { + return fmt.Errorf("couldn't get addresses controlled by the user: %w", err) + } + + // Create the transaction + tx, err := service.vm.newExportTx( + uint64(args.Amount), // Amount + chainID, // ID of the chain to send the funds to + to, // Address + privKeys, // Private keys + ) + if err != nil { + return fmt.Errorf("couldn't create tx: %w", err) + } + + response.TxID = tx.ID() + return service.vm.issueTx(tx) +} diff --git a/plugin/evm/tx.go b/plugin/evm/tx.go new file mode 100644 index 0000000..789ce56 --- /dev/null +++ b/plugin/evm/tx.go @@ -0,0 +1,112 @@ +// (c) 2019-2020, Ava Labs, Inc. All rights reserved. +// See the file LICENSE for licensing terms. + +package evm + +import ( + "errors" + "fmt" + + "github.com/ava-labs/coreth/core/state" + + "github.com/ava-labs/gecko/database" + "github.com/ava-labs/gecko/ids" + "github.com/ava-labs/gecko/snow" + "github.com/ava-labs/gecko/utils/codec" + "github.com/ava-labs/gecko/utils/crypto" + "github.com/ava-labs/gecko/utils/hashing" + "github.com/ava-labs/gecko/vms/components/verify" + "github.com/ava-labs/gecko/vms/secp256k1fx" + "github.com/ava-labs/go-ethereum/common" +) + +// Max size of memo field +// Don't change without also changing avm.maxMemoSize +const maxMemoSize = 256 + +var ( + errWrongBlockchainID = errors.New("wrong blockchain ID provided") + errWrongNetworkID = errors.New("tx was issued with a different network ID") + errNilTx = errors.New("tx is nil") +) + +type EVMOutput struct { + Address common.Address `serialize:"true" json:"address"` + Amount uint64 `serialize:"true" json:"amount"` + Nonce uint64 `serialize:"true" json:"nonce"` +} + +func (out *EVMOutput) Verify() error { + return nil +} + +type EVMInput EVMOutput + +func (in *EVMInput) Verify() error { + return nil +} + +// UnsignedTx is an unsigned transaction +type UnsignedTx interface { + Initialize(unsignedBytes, signedBytes []byte) + ID() ids.ID + UnsignedBytes() []byte + Bytes() []byte +} + +// UnsignedAtomicTx is an unsigned operation that can be atomically accepted +type UnsignedAtomicTx interface { + UnsignedTx + + // UTXOs this tx consumes + InputUTXOs() ids.Set + // Attempts to verify this transaction with the provided state. + SemanticVerify(vm *VM, stx *Tx) TxError + + // Accept this transaction with the additionally provided state transitions. + Accept(ctx *snow.Context, batch database.Batch) error + + EVMStateTransfer(state *state.StateDB) error +} + +// Tx is a signed transaction +type Tx struct { + // The body of this transaction + UnsignedTx `serialize:"true" json:"unsignedTx"` + + // The credentials of this transaction + Creds []verify.Verifiable `serialize:"true" json:"credentials"` +} + +// (*secp256k1fx.Credential) + +// Sign this transaction with the provided signers +func (tx *Tx) Sign(c codec.Codec, signers [][]*crypto.PrivateKeySECP256K1R) error { + unsignedBytes, err := c.Marshal(&tx.UnsignedTx) + if err != nil { + return fmt.Errorf("couldn't marshal UnsignedTx: %w", err) + } + + // Attach credentials + hash := hashing.ComputeHash256(unsignedBytes) + for _, keys := range signers { + cred := &secp256k1fx.Credential{ + Sigs: make([][crypto.SECP256K1RSigLen]byte, len(keys)), + } + for i, key := range keys { + sig, err := key.SignHash(hash) // Sign hash + if err != nil { + return fmt.Errorf("problem generating credential: %w", err) + } + copy(cred.Sigs[i][:], sig) + } + tx.Creds = append(tx.Creds, cred) // Attach credential + } + + signedBytes, err := c.Marshal(tx) + if err != nil { + return fmt.Errorf("couldn't marshal ProposalTx: %w", err) + } + tx.Initialize(unsignedBytes, signedBytes) + return nil +} diff --git a/plugin/evm/user.go b/plugin/evm/user.go new file mode 100644 index 0000000..fbf2981 --- /dev/null +++ b/plugin/evm/user.go @@ -0,0 +1,146 @@ +// (c) 2019-2020, Ava Labs, Inc. All rights reserved. +// See the file LICENSE for licensing terms. + +package evm + +import ( + "errors" + "fmt" + + "github.com/ava-labs/gecko/database" + "github.com/ava-labs/gecko/ids" + "github.com/ava-labs/gecko/utils/crypto" + "github.com/ava-labs/go-ethereum/common" +) + +// Key in the database whose corresponding value is the list of +// addresses this user controls +var addressesKey = ids.Empty.Bytes() + +var ( + errDBNil = errors.New("db uninitialized") + errKeyNil = errors.New("key uninitialized") + errEmptyAddress = errors.New("address is empty") +) + +type user struct { + // This user's database, acquired from the keystore + db database.Database +} + +// Get the addresses controlled by this user +func (u *user) getAddresses() ([]common.Address, error) { + if u.db == nil { + return nil, errDBNil + } + + // If user has no addresses, return empty list + hasAddresses, err := u.db.Has(addressesKey) + if err != nil { + return nil, err + } + if !hasAddresses { + return nil, nil + } + + // User has addresses. Get them. + bytes, err := u.db.Get(addressesKey) + if err != nil { + return nil, err + } + addresses := []common.Address{} + if err := Codec.Unmarshal(bytes, &addresses); err != nil { + return nil, err + } + return addresses, nil +} + +// controlsAddress returns true iff this user controls the given address +func (u *user) controlsAddress(address common.Address) (bool, error) { + if u.db == nil { + return false, errDBNil + //} else if address.IsZero() { + // return false, errEmptyAddress + } + return u.db.Has(address.Bytes()) +} + +// putAddress persists that this user controls address controlled by [privKey] +func (u *user) putAddress(privKey *crypto.PrivateKeySECP256K1R) error { + if privKey == nil { + return errKeyNil + } + + address := GetEthAddress(privKey) // address the privKey controls + controlsAddress, err := u.controlsAddress(address) + if err != nil { + return err + } + if controlsAddress { // user already controls this address. Do nothing. + return nil + } + + if err := u.db.Put(address.Bytes(), privKey.Bytes()); err != nil { // Address --> private key + return err + } + + addresses := make([]common.Address, 0) // Add address to list of addresses user controls + userHasAddresses, err := u.db.Has(addressesKey) + if err != nil { + return err + } + if userHasAddresses { // Get addresses this user already controls, if they exist + if addresses, err = u.getAddresses(); err != nil { + return err + } + } + addresses = append(addresses, address) + bytes, err := Codec.Marshal(addresses) + if err != nil { + return err + } + if err := u.db.Put(addressesKey, bytes); err != nil { + return err + } + return nil +} + +// Key returns the private key that controls the given address +func (u *user) getKey(address common.Address) (*crypto.PrivateKeySECP256K1R, error) { + if u.db == nil { + return nil, errDBNil + //} else if address.IsZero() { + // return nil, errEmptyAddress + } + + factory := crypto.FactorySECP256K1R{} + bytes, err := u.db.Get(address.Bytes()) + if err != nil { + return nil, err + } + sk, err := factory.ToPrivateKey(bytes) + if err != nil { + return nil, err + } + if sk, ok := sk.(*crypto.PrivateKeySECP256K1R); ok { + return sk, nil + } + return nil, fmt.Errorf("expected private key to be type *crypto.PrivateKeySECP256K1R but is type %T", sk) +} + +// Return all private keys controlled by this user +func (u *user) getKeys() ([]*crypto.PrivateKeySECP256K1R, error) { + addrs, err := u.getAddresses() + if err != nil { + return nil, err + } + keys := make([]*crypto.PrivateKeySECP256K1R, len(addrs)) + for i, addr := range addrs { + key, err := u.getKey(addr) + if err != nil { + return nil, err + } + keys[i] = key + } + return keys, nil +} diff --git a/plugin/evm/vm.go b/plugin/evm/vm.go index cf5ef8a..e1fdc33 100644 --- a/plugin/evm/vm.go +++ b/plugin/evm/vm.go @@ -16,13 +16,17 @@ import ( "github.com/ava-labs/coreth" "github.com/ava-labs/coreth/core" + "github.com/ava-labs/coreth/core/state" "github.com/ava-labs/coreth/core/types" "github.com/ava-labs/coreth/eth" "github.com/ava-labs/coreth/node" + "github.com/ava-labs/coreth/params" - "github.com/ava-labs/coreth/rpc" "github.com/ava-labs/go-ethereum/common" + ethcrypto "github.com/ava-labs/go-ethereum/crypto" "github.com/ava-labs/go-ethereum/rlp" + "github.com/ava-labs/go-ethereum/rpc" + geckorpc "github.com/gorilla/rpc/v2" "github.com/ava-labs/gecko/api/admin" "github.com/ava-labs/gecko/cache" @@ -31,7 +35,17 @@ import ( "github.com/ava-labs/gecko/snow" "github.com/ava-labs/gecko/snow/choices" "github.com/ava-labs/gecko/snow/consensus/snowman" + "github.com/ava-labs/gecko/utils/codec" + "github.com/ava-labs/gecko/utils/constants" + "github.com/ava-labs/gecko/utils/crypto" + "github.com/ava-labs/gecko/utils/formatting" + geckojson "github.com/ava-labs/gecko/utils/json" + "github.com/ava-labs/gecko/utils/logging" "github.com/ava-labs/gecko/utils/timer" + "github.com/ava-labs/gecko/utils/units" + "github.com/ava-labs/gecko/utils/wrappers" + "github.com/ava-labs/gecko/vms/components/avax" + "github.com/ava-labs/gecko/vms/secp256k1fx" commonEng "github.com/ava-labs/gecko/snow/engine/common" ) @@ -41,6 +55,7 @@ var ( 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, } + x2cRate = big.NewInt(1000000000) ) const ( @@ -48,9 +63,11 @@ const ( ) const ( - minBlockTime = 250 * time.Millisecond - maxBlockTime = 1000 * time.Millisecond - batchSize = 250 + minBlockTime = 250 * time.Millisecond + maxBlockTime = 1000 * time.Millisecond + batchSize = 250 + maxUTXOsToFetch = 1024 + blockCacheSize = 1 << 17 // 131072 ) const ( @@ -59,13 +76,39 @@ const ( bdTimerStateLong ) +const ( + addressSep = "-" +) + var ( - errEmptyBlock = errors.New("empty block") - errCreateBlock = errors.New("couldn't create block") - errUnknownBlock = errors.New("unknown block") - errBlockFrequency = errors.New("too frequent block issuance") - errUnsupportedFXs = errors.New("unsupported feature extensions") - errInvalidBlock = errors.New("invalid block") + // minGasPrice is the number of nAVAX required per gas unit for a transaction + // to be valid, measured in wei + minGasPrice = big.NewInt(47 * params.GWei) + + txFee = units.MilliAvax + + errEmptyBlock = errors.New("empty block") + errCreateBlock = errors.New("couldn't create block") + errUnknownBlock = errors.New("unknown block") + errBlockFrequency = errors.New("too frequent block issuance") + errUnsupportedFXs = errors.New("unsupported feature extensions") + errInvalidBlock = errors.New("invalid block") + errInvalidAddr = errors.New("invalid hex address") + errTooManyAtomicTx = errors.New("too many pending atomic txs") + errAssetIDMismatch = errors.New("asset IDs in the input don't match the utxo") + errWrongNumberOfCredentials = errors.New("should have the same number of credentials as inputs") + errNoInputs = errors.New("tx has no inputs") + errNoImportInputs = errors.New("tx has no imported inputs") + errInputsNotSortedUnique = errors.New("inputs not sorted and unique") + errPublicKeySignatureMismatch = errors.New("signature doesn't match public key") + errUnknownAsset = errors.New("unknown asset ID") + errNoFunds = errors.New("no spendable funds were found") + errWrongChainID = errors.New("tx has wrong chain ID") + errInsufficientFunds = errors.New("insufficient funds") + errNoExportOutputs = errors.New("no export outputs") + errOutputsNotSorted = errors.New("outputs not sorted") + errOverflowExport = errors.New("overflow when computing export amount + txFee") + errInvalidNonce = errors.New("invalid nonce") ) func maxDuration(x, y time.Duration) time.Duration { @@ -75,6 +118,32 @@ func maxDuration(x, y time.Duration) time.Duration { return y } +// Codec does serialization and deserialization +var Codec codec.Codec + +func init() { + Codec = codec.NewDefault() + + errs := wrappers.Errs{} + errs.Add( + Codec.RegisterType(&UnsignedImportTx{}), + Codec.RegisterType(&UnsignedExportTx{}), + ) + Codec.Skip(3) + errs.Add( + Codec.RegisterType(&secp256k1fx.TransferInput{}), + Codec.RegisterType(&secp256k1fx.MintOutput{}), + Codec.RegisterType(&secp256k1fx.TransferOutput{}), + Codec.RegisterType(&secp256k1fx.MintOperation{}), + Codec.RegisterType(&secp256k1fx.Credential{}), + Codec.RegisterType(&secp256k1fx.Input{}), + Codec.RegisterType(&secp256k1fx.OutputOwners{}), + ) + if errs.Errored() { + panic(errs.Err) + } +} + // VM implements the snowman.ChainVM interface type VM struct { ctx *snow.Context @@ -103,10 +172,37 @@ type VM struct { bdGenWaitFlag bool bdGenFlag bool - genlock sync.Mutex - txSubmitChan <-chan struct{} + genlock sync.Mutex + txSubmitChan <-chan struct{} + atomicTxSubmitChan chan struct{} + codec codec.Codec + clock timer.Clock + txFee uint64 + pendingAtomicTxs chan *Tx + blockAtomicInputCache cache.LRU + + fx secp256k1fx.Fx +} + +func (vm *VM) getAtomicTx(block *types.Block) *Tx { + extdata := block.ExtraData() + atx := new(Tx) + if err := vm.codec.Unmarshal(extdata, atx); err != nil { + return nil + } + atx.Sign(vm.codec, nil) + return atx } +// Codec implements the secp256k1fx interface +func (vm *VM) Codec() codec.Codec { return codec.NewDefault() } + +// Clock implements the secp256k1fx interface +func (vm *VM) Clock() *timer.Clock { return &vm.clock } + +// Logger implements the secp256k1fx interface +func (vm *VM) Logger() logging.Logger { return vm.ctx.Log } + /* ****************************************************************************** ********************************* Snowman API ******************************** @@ -134,12 +230,20 @@ func (vm *VM) Initialize( } vm.chainID = g.Config.ChainID + vm.txFee = txFee config := eth.DefaultConfig config.ManualCanonical = true config.Genesis = g config.Miner.ManualMining = true config.Miner.DisableUncle = true + + // Set minimum price for mining and default gas price oracle value to the min + // gas price to prevent so transactions and blocks all use the correct fees + config.Miner.GasPrice = minGasPrice + config.GPO.Default = minGasPrice + config.TxPool.PriceLimit = minGasPrice.Uint64() + if err := config.SetGCMode("archive"); err != nil { panic(err) } @@ -155,13 +259,23 @@ func (vm *VM) Initialize( } header.Extra = append(header.Extra, hid...) }) - chain.SetOnSeal(func(block *types.Block) error { - if len(block.Transactions()) == 0 { - // this could happen due to the async logic of geth tx pool - vm.newBlockChan <- nil - return errEmptyBlock + chain.SetOnFinalizeAndAssemble(func(state *state.StateDB, txs []*types.Transaction) ([]byte, error) { + select { + case atx := <-vm.pendingAtomicTxs: + if err := atx.UnsignedTx.(UnsignedAtomicTx).EVMStateTransfer(state); err != nil { + vm.newBlockChan <- nil + return nil, err + } + raw, _ := vm.codec.Marshal(atx) + return raw, nil + default: + if len(txs) == 0 { + // this could happen due to the async logic of geth tx pool + vm.newBlockChan <- nil + return nil, errEmptyBlock + } } - return nil + return nil, nil }) chain.SetOnSealFinish(func(block *types.Block) error { vm.ctx.Log.Verbo("EVM sealed a block") @@ -171,6 +285,9 @@ func (vm *VM) Initialize( ethBlock: block, vm: vm, } + if blk.Verify() != nil { + return errInvalidBlock + } vm.newBlockChan <- blk vm.updateStatus(ids.NewID(block.Hash()), choices.Processing) vm.txPoolStabilizedLock.Lock() @@ -181,8 +298,16 @@ func (vm *VM) Initialize( chain.SetOnQueryAcceptedBlock(func() *types.Block { return vm.getLastAccepted().ethBlock }) - vm.blockCache = cache.LRU{Size: 2048} - vm.blockStatusCache = cache.LRU{Size: 1024} + chain.SetOnExtraStateChange(func(block *types.Block, state *state.StateDB) error { + tx := vm.getAtomicTx(block) + if tx == nil { + return nil + } + return tx.UnsignedTx.(UnsignedAtomicTx).EVMStateTransfer(state) + }) + vm.blockCache = cache.LRU{Size: blockCacheSize} + 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() { @@ -206,6 +331,9 @@ func (vm *VM) Initialize( vm.bdGenWaitFlag = true vm.newTxPoolHeadChan = make(chan core.NewTxPoolHeadEvent, 1) vm.txPoolStabilizedOk = make(chan struct{}, 1) + // TODO: read size from options + vm.pendingAtomicTxs = make(chan *Tx, 1024) + vm.atomicTxSubmitChan = make(chan struct{}, 1) chain.GetTxPool().SubscribeNewHeadEvent(vm.newTxPoolHeadChan) // TODO: shutdown this go routine go ctx.Log.RecoverAndPanic(func() { @@ -254,22 +382,26 @@ func (vm *VM) Initialize( case <-vm.txSubmitChan: vm.ctx.Log.Verbo("New tx detected, trying to generate a block") vm.tryBlockGen() + case <-vm.atomicTxSubmitChan: + vm.ctx.Log.Verbo("New atomic Tx detected, trying to generate a block") + vm.tryBlockGen() case <-time.After(5 * time.Second): vm.tryBlockGen() } } }) + vm.codec = Codec - return nil + return vm.fx.Initialize(vm) } // Bootstrapping notifies this VM that the consensus engine is performing // bootstrapping -func (vm *VM) Bootstrapping() error { return nil } +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 nil } +func (vm *VM) Bootstrapped() error { return vm.fx.Bootstrapped() } // Shutdown implements the snowman.ChainVM interface func (vm *VM) Shutdown() error { @@ -356,6 +488,26 @@ func (vm *VM) LastAccepted() ids.ID { return vm.lastAccepted.ID() } +// NewHandler returns a new Handler for a service where: +// * The handler's functionality is defined by [service] +// [service] should be a gorilla RPC service (see https://www.gorillatoolkit.org/pkg/rpc/v2) +// * The name of the service is [name] +// * The LockOption is the first element of [lockOption] +// By default the LockOption is WriteLock +// [lockOption] should have either 0 or 1 elements. Elements beside the first are ignored. +func newHandler(name string, service interface{}, lockOption ...commonEng.LockOption) *commonEng.HTTPHandler { + server := geckorpc.NewServer() + server.RegisterCodec(geckojson.NewCodec(), "application/json") + server.RegisterCodec(geckojson.NewCodec(), "application/json;charset=UTF-8") + server.RegisterService(service, name) + + var lock commonEng.LockOption = commonEng.WriteLock + if len(lockOption) != 0 { + lock = lockOption[0] + } + return &commonEng.HTTPHandler{LockOptions: lock, Handler: server} +} + // CreateHandlers makes new http handlers that can handle API calls func (vm *VM) CreateHandlers() map[string]*commonEng.HTTPHandler { handler := vm.chain.NewRPCHandler() @@ -368,6 +520,7 @@ func (vm *VM) CreateHandlers() map[string]*commonEng.HTTPHandler { return map[string]*commonEng.HTTPHandler{ "/rpc": &commonEng.HTTPHandler{LockOptions: commonEng.NoLock, Handler: handler}, + "/ava": newHandler("ava", &AvaAPI{vm}), "/ws": &commonEng.HTTPHandler{LockOptions: commonEng.NoLock, Handler: handler.WebsocketHandler([]string{"*"})}, } } @@ -402,10 +555,6 @@ func (vm *VM) updateStatus(blockID ids.ID, status choices.Status) { vm.blockStatusCache.Put(blockID, status) } -func (vm *VM) getCachedBlock(blockID ids.ID) *types.Block { - return vm.chain.GetBlockByHash(blockID.Key()) -} - func (vm *VM) tryBlockGen() error { vm.bdlock.Lock() defer vm.bdlock.Unlock() @@ -422,7 +571,7 @@ func (vm *VM) tryBlockGen() error { if err != nil { return err } - if size == 0 { + if size == 0 && len(vm.pendingAtomicTxs) == 0 { return nil } @@ -454,10 +603,11 @@ func (vm *VM) getCachedStatus(blockID ids.ID) choices.Status { if statusIntf, ok := vm.blockStatusCache.Get(blockID); ok { status = statusIntf.(choices.Status) } else { - blk := vm.chain.GetBlockByHash(blockID.Key()) - if blk == nil { + wrappedBlk := vm.getBlock(blockID) + if wrappedBlk == nil { return choices.Unknown } + blk := wrappedBlk.ethBlock acceptedBlk := vm.lastAccepted.ethBlock // TODO: There must be a better way of doing this. @@ -468,7 +618,12 @@ func (vm *VM) getCachedStatus(blockID ids.ID) choices.Status { highBlock, lowBlock = lowBlock, highBlock } for highBlock.Number().Cmp(lowBlock.Number()) > 0 { - highBlock = vm.chain.GetBlockByHash(highBlock.ParentHash()) + parentBlock := vm.getBlock(ids.NewID(highBlock.ParentHash())) + if parentBlock == nil { + vm.blockStatusCache.Put(blockID, choices.Processing) + return choices.Processing + } + highBlock = parentBlock.ethBlock } if highBlock.Hash() == lowBlock.Hash() { // on the same branch @@ -488,7 +643,7 @@ func (vm *VM) getBlock(id ids.ID) *Block { if blockIntf, ok := vm.blockCache.Get(id); ok { return blockIntf.(*Block) } - ethBlock := vm.getCachedBlock(id) + ethBlock := vm.chain.GetBlockByHash(id.Key()) if ethBlock == nil { return nil } @@ -531,3 +686,157 @@ func (vm *VM) getLastAccepted() *Block { return vm.lastAccepted } + +func (vm *VM) ParseEthAddress(addrStr string) (common.Address, error) { + if !common.IsHexAddress(addrStr) { + return common.Address{}, errInvalidAddr + } + return common.HexToAddress(addrStr), nil +} + +func (vm *VM) FormatEthAddress(addr common.Address) (string, error) { + return addr.Hex(), nil +} + +// ParseAddress takes in an address and produces the ID of the chain it's for +// the ID of the address +func (vm *VM) ParseAddress(addrStr string) (ids.ID, ids.ShortID, error) { + chainIDAlias, hrp, addrBytes, err := formatting.ParseAddress(addrStr) + if err != nil { + return ids.ID{}, ids.ShortID{}, err + } + + chainID, err := vm.ctx.BCLookup.Lookup(chainIDAlias) + if err != nil { + return ids.ID{}, ids.ShortID{}, err + } + + expectedHRP := constants.GetHRP(vm.ctx.NetworkID) + if hrp != expectedHRP { + return ids.ID{}, ids.ShortID{}, fmt.Errorf("expected hrp %q but got %q", + expectedHRP, hrp) + } + + addr, err := ids.ToShortID(addrBytes) + if err != nil { + return ids.ID{}, ids.ShortID{}, err + } + return chainID, addr, nil +} + +func (vm *VM) issueTx(tx *Tx) error { + select { + case vm.pendingAtomicTxs <- tx: + select { + case vm.atomicTxSubmitChan <- struct{}{}: + default: + } + default: + return errTooManyAtomicTx + } + return nil +} + +// GetAtomicUTXOs returns the utxos that at least one of the provided addresses is +// referenced in. +func (vm *VM) GetAtomicUTXOs( + chainID ids.ID, + addrs ids.ShortSet, + startAddr ids.ShortID, + startUTXOID ids.ID, + limit int, +) ([]*avax.UTXO, ids.ShortID, ids.ID, error) { + if limit <= 0 || limit > maxUTXOsToFetch { + limit = maxUTXOsToFetch + } + + addrsList := make([][]byte, addrs.Len()) + for i, addr := range addrs.List() { + addrsList[i] = addr.Bytes() + } + + allUTXOBytes, lastAddr, lastUTXO, err := vm.ctx.SharedMemory.Indexed( + chainID, + addrsList, + startAddr.Bytes(), + startUTXOID.Bytes(), + limit, + ) + if err != nil { + return nil, ids.ShortID{}, ids.ID{}, fmt.Errorf("error fetching atomic UTXOs: %w", err) + } + + lastAddrID, err := ids.ToShortID(lastAddr) + if err != nil { + lastAddrID = ids.ShortEmpty + } + lastUTXOID, err := ids.ToID(lastUTXO) + if err != nil { + lastUTXOID = ids.Empty + } + + utxos := make([]*avax.UTXO, len(allUTXOBytes)) + for i, utxoBytes := range allUTXOBytes { + utxo := &avax.UTXO{} + if err := vm.codec.Unmarshal(utxoBytes, utxo); err != nil { + return nil, ids.ShortID{}, ids.ID{}, fmt.Errorf("error parsing UTXO: %w", err) + } + utxos[i] = utxo + } + return utxos, lastAddrID, lastUTXOID, nil +} + +func GetEthAddress(privKey *crypto.PrivateKeySECP256K1R) common.Address { + return PublicKeyToEthAddress(privKey.PublicKey()) +} + +func PublicKeyToEthAddress(pubKey crypto.PublicKey) common.Address { + return ethcrypto.PubkeyToAddress( + (*pubKey.(*crypto.PublicKeySECP256K1R).ToECDSA())) +} + +func (vm *VM) GetSpendableCanonical(keys []*crypto.PrivateKeySECP256K1R, amount uint64) ([]EVMInput, [][]*crypto.PrivateKeySECP256K1R, error) { + // NOTE: should we use HEAD block or lastAccepted? + state, err := vm.chain.BlockState(vm.lastAccepted.ethBlock) + if err != nil { + return nil, nil, err + } + inputs := []EVMInput{} + signers := [][]*crypto.PrivateKeySECP256K1R{} + for _, key := range keys { + if amount == 0 { + break + } + addr := GetEthAddress(key) + balance := new(big.Int).Div(state.GetBalance(addr), x2cRate).Uint64() + if balance == 0 { + continue + } + if amount < balance { + balance = amount + } + nonce, err := vm.GetAcceptedNonce(addr) + if err != nil { + return nil, nil, err + } + inputs = append(inputs, EVMInput{ + Address: addr, + Amount: balance, + Nonce: nonce, + }) + signers = append(signers, []*crypto.PrivateKeySECP256K1R{key}) + amount -= balance + } + if amount > 0 { + return nil, nil, errInsufficientFunds + } + return inputs, signers, nil +} + +func (vm *VM) GetAcceptedNonce(address common.Address) (uint64, error) { + state, err := vm.chain.BlockState(vm.lastAccepted.ethBlock) + if err != nil { + return 0, err + } + return state.GetNonce(address), nil +} |