From 14fe03cbfecf508848bcd1172ea8d2ee3e41fda4 Mon Sep 17 00:00:00 2001 From: Tyler Smith Date: Thu, 30 Jul 2020 22:00:38 -0700 Subject: TWEAK: Set minimum gas price. --- plugin/evm/block.go | 9 +++++++++ plugin/evm/vm.go | 10 ++++++++++ 2 files changed, 19 insertions(+) (limited to 'plugin') diff --git a/plugin/evm/block.go b/plugin/evm/block.go index 449e261..1a2bf9e 100644 --- a/plugin/evm/block.go +++ b/plugin/evm/block.go @@ -61,6 +61,15 @@ func (b *Block) Parent() snowman.Block { // Verify implements the snowman.Block interface func (b *Block) Verify() error { + // Ensure the minimum gas price is paid for every transaction + for _, tx := range b.ethBlock.Transactions() { + if tx.GasPrice().Cmp(minGasPrice) < 0 { + return errInvalidBlock + } + } + + // Attempt to add the block to the chain and consider the block valid iff it's + // accepted _, err := b.vm.chain.InsertChain([]*types.Block{b.ethBlock}) return err } diff --git a/plugin/evm/vm.go b/plugin/evm/vm.go index cf5ef8a..7b4d064 100644 --- a/plugin/evm/vm.go +++ b/plugin/evm/vm.go @@ -60,6 +60,10 @@ const ( ) var ( + // minGasPrice is the number of nAVAX required per gas unit for a transaction + // to be valid + minGasPrice = big.NewInt(85) + errEmptyBlock = errors.New("empty block") errCreateBlock = errors.New("couldn't create block") errUnknownBlock = errors.New("unknown block") @@ -140,6 +144,12 @@ func (vm *VM) Initialize( 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 + if err := config.SetGCMode("archive"); err != nil { panic(err) } -- cgit v1.2.3-70-g09d2 From 89df16782ed7a6b45600f3685371b8574b12f1ff Mon Sep 17 00:00:00 2001 From: StephenButtolph Date: Wed, 12 Aug 2020 21:49:21 -0400 Subject: increased cache sizes --- plugin/evm/vm.go | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) (limited to 'plugin') diff --git a/plugin/evm/vm.go b/plugin/evm/vm.go index c710b9d..c22d9d1 100644 --- a/plugin/evm/vm.go +++ b/plugin/evm/vm.go @@ -53,6 +53,7 @@ const ( minBlockTime = 250 * time.Millisecond maxBlockTime = 1000 * time.Millisecond batchSize = 250 + cacheSize = 1 << 15 // 32768 ) const ( @@ -183,8 +184,8 @@ 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} + vm.blockCache = cache.LRU{Size: cacheSize} + vm.blockStatusCache = cache.LRU{Size: cacheSize} vm.newBlockChan = make(chan *Block) vm.networkChan = toEngine vm.blockDelayTimer = timer.NewTimer(func() { -- cgit v1.2.3-70-g09d2 From 4945ae1427399d9a0f6b3561306f50f360011613 Mon Sep 17 00:00:00 2001 From: StephenButtolph Date: Wed, 12 Aug 2020 22:50:17 -0400 Subject: read blocks with caching --- plugin/evm/block.go | 6 +----- plugin/evm/vm.go | 13 +++++-------- 2 files changed, 6 insertions(+), 13 deletions(-) (limited to 'plugin') diff --git a/plugin/evm/block.go b/plugin/evm/block.go index eaa78a5..2912d2c 100644 --- a/plugin/evm/block.go +++ b/plugin/evm/block.go @@ -50,11 +50,7 @@ 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, - } + block := b.vm.getBlock(parentID) b.vm.ctx.Log.Verbo("Parent(%s) has status: %s", block.ID(), block.Status()) return block } diff --git a/plugin/evm/vm.go b/plugin/evm/vm.go index c22d9d1..6c96870 100644 --- a/plugin/evm/vm.go +++ b/plugin/evm/vm.go @@ -422,10 +422,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() @@ -474,10 +470,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. @@ -488,7 +485,7 @@ 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()) + highBlock = vm.getBlock(ids.NewID(highBlock.ParentHash())).ethBlock } if highBlock.Hash() == lowBlock.Hash() { // on the same branch @@ -508,7 +505,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 } -- cgit v1.2.3-70-g09d2 From 4676ebb4aca5464e9ecba47f9db7e63593e92a0e Mon Sep 17 00:00:00 2001 From: StephenButtolph Date: Thu, 13 Aug 2020 01:00:42 -0400 Subject: fixed nil pointer error in getCachedStatus + removed ip filter --- plugin/evm/vm.go | 34 ++++++++++------------------------ 1 file changed, 10 insertions(+), 24 deletions(-) (limited to 'plugin') diff --git a/plugin/evm/vm.go b/plugin/evm/vm.go index 6c96870..7e9e205 100644 --- a/plugin/evm/vm.go +++ b/plugin/evm/vm.go @@ -10,8 +10,6 @@ import ( "errors" "fmt" "math/big" - "net" - "net/http" "sync" "sync/atomic" "time" @@ -359,23 +357,6 @@ func (vm *VM) LastAccepted() ids.ID { return vm.lastAccepted.ID() } -type ipFilter struct { - handler http.Handler -} - -func (ipf ipFilter) ServeHTTP(writer http.ResponseWriter, request *http.Request) { - if ips, _, err := net.SplitHostPort(request.RemoteAddr); err == nil && ips == "127.0.0.1" { - ipf.handler.ServeHTTP(writer, request) - return - } - writer.WriteHeader(404) - writer.Write([]byte("404 page not found\r\n")) -} - -func newIPFilter(handler http.Handler) http.Handler { - return ipFilter{handler} -} - // CreateHandlers makes new http handlers that can handle API calls func (vm *VM) CreateHandlers() map[string]*commonEng.HTTPHandler { handler := vm.chain.NewRPCHandler() @@ -387,8 +368,8 @@ func (vm *VM) CreateHandlers() map[string]*commonEng.HTTPHandler { handler.RegisterName("admin", &admin.Performance{}) return map[string]*commonEng.HTTPHandler{ - "/rpc": &commonEng.HTTPHandler{LockOptions: commonEng.NoLock, Handler: newIPFilter(handler)}, - "/ws": &commonEng.HTTPHandler{LockOptions: commonEng.NoLock, Handler: newIPFilter(handler.WebsocketHandler([]string{"*"}))}, + "/rpc": &commonEng.HTTPHandler{LockOptions: commonEng.NoLock, Handler: handler}, + "/ws": &commonEng.HTTPHandler{LockOptions: commonEng.NoLock, Handler: handler.WebsocketHandler([]string{"*"})}, } } @@ -397,8 +378,8 @@ func (vm *VM) CreateStaticHandlers() map[string]*commonEng.HTTPHandler { handler := rpc.NewServer() handler.RegisterName("static", &StaticService{}) return map[string]*commonEng.HTTPHandler{ - "/rpc": &commonEng.HTTPHandler{LockOptions: commonEng.NoLock, Handler: newIPFilter(handler)}, - "/ws": &commonEng.HTTPHandler{LockOptions: commonEng.NoLock, Handler: newIPFilter(handler.WebsocketHandler([]string{"*"}))}, + "/rpc": &commonEng.HTTPHandler{LockOptions: commonEng.NoLock, Handler: handler}, + "/ws": &commonEng.HTTPHandler{LockOptions: commonEng.NoLock, Handler: handler.WebsocketHandler([]string{"*"})}, } } @@ -485,7 +466,12 @@ func (vm *VM) getCachedStatus(blockID ids.ID) choices.Status { highBlock, lowBlock = lowBlock, highBlock } for highBlock.Number().Cmp(lowBlock.Number()) > 0 { - highBlock = vm.getBlock(ids.NewID(highBlock.ParentHash())).ethBlock + 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 -- cgit v1.2.3-70-g09d2 From 9616c7265c06952530674fdbc193c824e099c2c4 Mon Sep 17 00:00:00 2001 From: StephenButtolph Date: Thu, 13 Aug 2020 01:22:15 -0400 Subject: fixed parent block nil pointer --- plugin/evm/block.go | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) (limited to 'plugin') diff --git a/plugin/evm/block.go b/plugin/evm/block.go index 2912d2c..80c03a2 100644 --- a/plugin/evm/block.go +++ b/plugin/evm/block.go @@ -12,6 +12,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 @@ -50,9 +51,12 @@ 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 := b.vm.getBlock(parentID) - b.vm.ctx.Log.Verbo("Parent(%s) has status: %s", block.ID(), block.Status()) - return block + 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", parentID, choices.Unknown) + return &missing.Block{BlkID: parentID} } // Verify implements the snowman.Block interface -- cgit v1.2.3-70-g09d2 From 0d2a35293b7aa49d143ce215591104aba1c89388 Mon Sep 17 00:00:00 2001 From: StephenButtolph Date: Thu, 13 Aug 2020 02:34:53 -0400 Subject: yuge cache, all the cache --- plugin/evm/vm.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'plugin') diff --git a/plugin/evm/vm.go b/plugin/evm/vm.go index 7e9e205..c55bfe6 100644 --- a/plugin/evm/vm.go +++ b/plugin/evm/vm.go @@ -51,7 +51,7 @@ const ( minBlockTime = 250 * time.Millisecond maxBlockTime = 1000 * time.Millisecond batchSize = 250 - cacheSize = 1 << 15 // 32768 + cacheSize = 1 << 17 // 131072 ) const ( -- cgit v1.2.3-70-g09d2 From 88cc3698b3663972cd9b60faf5c14a7e1bbee965 Mon Sep 17 00:00:00 2001 From: Determinant Date: Thu, 13 Aug 2020 21:11:56 -0400 Subject: WIP: X-to-C transfer --- go.mod | 1 + go.sum | 3 + plugin/evm/atomic_tx.go | 64 ++++++++++ plugin/evm/base_tx.go | 105 ++++++++++++++++ plugin/evm/error.go | 19 +++ plugin/evm/factory.go | 12 +- plugin/evm/import_tx.go | 320 ++++++++++++++++++++++++++++++++++++++++++++++++ plugin/evm/service.go | 109 +++++++++++++++++ plugin/evm/user.go | 142 +++++++++++++++++++++ plugin/evm/vm.go | 76 +++++++++++- 10 files changed, 848 insertions(+), 3 deletions(-) create mode 100644 plugin/evm/atomic_tx.go create mode 100644 plugin/evm/base_tx.go create mode 100644 plugin/evm/error.go create mode 100644 plugin/evm/import_tx.go create mode 100644 plugin/evm/user.go (limited to 'plugin') diff --git a/go.mod b/go.mod index ff6b943..752be2f 100644 --- a/go.mod +++ b/go.mod @@ -12,6 +12,7 @@ require ( github.com/fjl/memsize v0.0.0-20190710130421-bcb5799ab5e5 github.com/gballet/go-libpcsclite v0.0.0-20191108122812-4678299bea08 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 diff --git a/go.sum b/go.sum index f1916c2..832a8bf 100644 --- a/go.sum +++ b/go.sum @@ -181,6 +181,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 +223,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= @@ -349,6 +351,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/plugin/evm/atomic_tx.go b/plugin/evm/atomic_tx.go new file mode 100644 index 0000000..e8e48f7 --- /dev/null +++ b/plugin/evm/atomic_tx.go @@ -0,0 +1,64 @@ +// (c) 2019-2020, Ava Labs, Inc. All rights reserved. +// See the file LICENSE for licensing terms. + +package evm + +import ( + "fmt" + + "github.com/ava-labs/gecko/database" + "github.com/ava-labs/gecko/ids" + "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" +) + +// UnsignedAtomicTx ... +type UnsignedAtomicTx interface { + initialize(vm *VM, bytes []byte) error + ID() ids.ID + // UTXOs this tx consumes + InputUTXOs() ids.Set + // Attempts to verify this transaction with the provided state. + SemanticVerify(db database.Database, creds []verify.Verifiable) TxError + Accept(database.Batch) error +} + +// AtomicTx is an operation that can be decided without being proposed, but must +// have special control over database commitment +type AtomicTx struct { + UnsignedAtomicTx `serialize:"true"` + // Credentials that authorize the inputs to be spent + Credentials []verify.Verifiable `serialize:"true" json:"credentials"` +} + +func (vm *VM) signAtomicTx(tx *AtomicTx, signers [][]*crypto.PrivateKeySECP256K1R) error { + unsignedBytes, err := vm.codec.Marshal(tx.UnsignedAtomicTx) + if err != nil { + return fmt.Errorf("couldn't marshal UnsignedAtomicTx: %w", err) + } + + // Attach credentials + hash := hashing.ComputeHash256(unsignedBytes) + tx.Credentials = make([]verify.Verifiable, len(signers)) + for i, credKeys := range signers { + cred := &secp256k1fx.Credential{ + Sigs: make([][crypto.SECP256K1RSigLen]byte, len(credKeys)), + } + for j, key := range credKeys { + sig, err := key.SignHash(hash) // Sign hash + if err != nil { + return fmt.Errorf("problem generating credential: %w", err) + } + copy(cred.Sigs[j][:], sig) + } + tx.Credentials[i] = cred // Attach credential + } + + txBytes, err := vm.codec.Marshal(tx) + if err != nil { + return fmt.Errorf("couldn't marshal AtomicTx: %w", err) + } + return tx.initialize(vm, txBytes) +} diff --git a/plugin/evm/base_tx.go b/plugin/evm/base_tx.go new file mode 100644 index 0000000..5ffc58e --- /dev/null +++ b/plugin/evm/base_tx.go @@ -0,0 +1,105 @@ +package evm + +import ( + "errors" + "fmt" + + "github.com/ava-labs/gecko/ids" + "github.com/ava-labs/gecko/vms/components/ava" + "github.com/ava-labs/go-ethereum/common" +) + +// Max size of memo field +// Don't change without also changing avm.maxMemoSize +const maxMemoSize = 256 + +var ( + errVMNil = errors.New("tx.vm is nil") + errWrongBlockchainID = errors.New("wrong blockchain ID provided") + errWrongNetworkID = errors.New("tx was issued with a different network ID") + errNilTx = errors.New("tx is nil") + errInvalidID = errors.New("invalid ID") + errOutputsNotSorted = errors.New("outputs not sorted") +) + +type EVMOutput struct { + Address common.Address `serialize:"true" json:"address"` + Amount uint64 `serialize:"true" json:"amount"` +} + +// BaseTx contains fields common to many transaction types. It should be +// embedded in transaction implementations. The serialized fields of this struct +// should be exactly the same as those of avm.BaseTx. Do not change this +// struct's serialized fields without doing the same on avm.BaseTx. +// TODO: Factor out this and avm.BaseTX +type BaseTx struct { + vm *VM + // true iff this transaction has already passed syntactic verification + syntacticallyVerified bool + // ID of this tx + id ids.ID + // Byte representation of this unsigned tx + unsignedBytes []byte + // Byte representation of the signed transaction (ie with credentials) + bytes []byte + + // ID of the network on which this tx was issued + NetworkID uint32 `serialize:"true" json:"networkID"` + // ID of this blockchain. In practice is always the empty ID. + // This is only here to match avm.BaseTx's format + BlockchainID ids.ID `serialize:"true" json:"blockchainID"` + // Outputs + Outs []EVMOutput `serialize:"true" json:"outputs"` + // Inputs consumed by this tx + Ins []*ava.TransferableInput `serialize:"true" json:"inputs"` + // Memo field contains arbitrary bytes, up to maxMemoSize + Memo []byte `serialize:"true" json:"memo"` +} + +// UnsignedBytes returns the byte representation of this unsigned tx +func (tx *BaseTx) UnsignedBytes() []byte { return tx.unsignedBytes } + +// Bytes returns the byte representation of this tx +func (tx *BaseTx) Bytes() []byte { return tx.bytes } + +// ID returns this transaction's ID +func (tx *BaseTx) ID() ids.ID { return tx.id } + +// Verify returns nil iff this tx is well formed +func (tx *BaseTx) Verify() error { + switch { + case tx == nil: + return errNilTx + case tx.syntacticallyVerified: // already passed syntactic verification + return nil + case tx.id.IsZero(): + return errInvalidID + case tx.vm == nil: + return errVMNil + case tx.NetworkID != tx.vm.ctx.NetworkID: + return errWrongNetworkID + case !tx.vm.ctx.ChainID.Equals(tx.BlockchainID): + return errWrongBlockchainID + case len(tx.Memo) > maxMemoSize: + return fmt.Errorf("memo length, %d, exceeds maximum memo length, %d", + len(tx.Memo), maxMemoSize) + } + //for _, out := range tx.Outs { + // if err := out.Verify(); err != nil { + // return err + // } + //} + for _, in := range tx.Ins { + if err := in.Verify(); err != nil { + return err + } + } + switch { + //case !ava.IsSortedTransferableOutputs(tx.Outs, Codec): + // return errOutputsNotSorted + case !ava.IsSortedAndUniqueTransferableInputs(tx.Ins): + return errInputsNotSortedUnique + default: + return nil + } +} 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/factory.go b/plugin/evm/factory.go index a4c0eca..31a617a 100644 --- a/plugin/evm/factory.go +++ b/plugin/evm/factory.go @@ -13,7 +13,15 @@ var ( ) // Factory ... -type Factory struct{} +type Factory struct { + AVA ids.ID + Fee uint64 +} // New ... -func (f *Factory) New() interface{} { return &VM{} } +func (f *Factory) New() interface{} { + return &VM{ + avaxAssetID: f.AVA, + txFee: f.Fee, + } +} diff --git a/plugin/evm/import_tx.go b/plugin/evm/import_tx.go new file mode 100644 index 0000000..36750a6 --- /dev/null +++ b/plugin/evm/import_tx.go @@ -0,0 +1,320 @@ +// (c) 2019-2020, Ava Labs, Inc. All rights reserved. +// See the file LICENSE for licensing terms. + +package evm + +import ( + "crypto/ecdsa" + "errors" + "fmt" + + //"github.com/ava-labs/gecko/chains/atomic" + "github.com/ava-labs/gecko/database" + //"github.com/ava-labs/gecko/database/versiondb" + "github.com/ava-labs/gecko/ids" + avacrypto "github.com/ava-labs/gecko/utils/crypto" + "github.com/ava-labs/gecko/utils/hashing" + "github.com/ava-labs/gecko/utils/math" + "github.com/ava-labs/gecko/vms/components/ava" + "github.com/ava-labs/gecko/vms/components/verify" + "github.com/ava-labs/gecko/vms/secp256k1fx" + "github.com/ava-labs/go-ethereum/common" + "github.com/ava-labs/go-ethereum/crypto" +) + +var ( + 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") +) + +// UnsignedImportTx is an unsigned ImportTx +type UnsignedImportTx struct { + BaseTx `serialize:"true"` + // Inputs that consume UTXOs produced on the X-Chain + ImportedInputs []*ava.TransferableInput `serialize:"true" json:"importedInputs"` +} + +// initialize [tx]. Sets [tx.vm], [tx.unsignedBytes], [tx.bytes], [tx.id] +func (tx *UnsignedImportTx) initialize(vm *VM, bytes []byte) error { + if tx.vm != nil { // already been initialized + return nil + } + tx.vm = vm + tx.bytes = bytes + tx.id = ids.NewID(hashing.ComputeHash256Array(bytes)) + var err error + tx.unsignedBytes, err = Codec.Marshal(interface{}(tx)) + if err != nil { + return fmt.Errorf("couldn't marshal UnsignedImportTx: %w", err) + } + return nil +} + +// 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 +} + +var ( + errInputOverflow = errors.New("inputs overflowed uint64") + errOutputOverflow = errors.New("outputs overflowed uint64") +) + +// Verify that: +// * inputs are all AVAX +// * sum(inputs{unlocked}) >= sum(outputs{unlocked}) + burnAmount{unlocked} +func syntacticVerifySpend( + ins []*ava.TransferableInput, + unlockedOuts []EVMOutput, + burnedUnlocked uint64, + avaxAssetID ids.ID, +) error { + // AVAX consumed in this tx + consumedUnlocked := uint64(0) + for _, in := range ins { + if assetID := in.AssetID(); !assetID.Equals(avaxAssetID) { // all inputs must be AVAX + return fmt.Errorf("input has unexpected asset ID %s expected %s", assetID, avaxAssetID) + } + + in := in.Input() + consumed := in.Amount() + newConsumed, err := math.Add64(consumedUnlocked, consumed) + if err != nil { + return errInputOverflow + } + consumedUnlocked = newConsumed + } + + // AVAX produced in this tx + producedUnlocked := burnedUnlocked + for _, out := range unlockedOuts { + produced := out.Amount + newProduced, err := math.Add64(producedUnlocked, produced) + if err != nil { + return errOutputOverflow + } + producedUnlocked = newProduced + } + + if producedUnlocked > consumedUnlocked { + return fmt.Errorf("tx unlocked outputs (%d) + burn amount (%d) > inputs (%d)", + producedUnlocked-burnedUnlocked, burnedUnlocked, consumedUnlocked) + } + return nil +} + +// Verify this transaction is well-formed +func (tx *UnsignedImportTx) Verify() error { + switch { + case tx == nil: + return errNilTx + case tx.syntacticallyVerified: // already passed syntactic verification + return nil + case len(tx.ImportedInputs) == 0: + return errNoImportInputs + } + + if err := tx.BaseTx.Verify(); err != nil { + return err + } + + for _, in := range tx.ImportedInputs { + if err := in.Verify(); err != nil { + return err + } + } + if !ava.IsSortedAndUniqueTransferableInputs(tx.ImportedInputs) { + return errInputsNotSortedUnique + } + + allIns := make([]*ava.TransferableInput, len(tx.Ins)+len(tx.ImportedInputs)) + copy(allIns, tx.Ins) + copy(allIns[len(tx.Ins):], tx.ImportedInputs) + if err := syntacticVerifySpend(allIns, tx.Outs, tx.vm.txFee, tx.vm.avaxAssetID); err != nil { + return err + } + + tx.syntacticallyVerified = true + return nil +} + +// SemanticVerify this transaction is valid. +func (tx *UnsignedImportTx) SemanticVerify(db database.Database, creds []verify.Verifiable) TxError { + if err := tx.Verify(); err != nil { + return permError{err} + } + + //// Verify (but don't spend) imported inputs + //smDB := tx.vm.ctx.SharedMemory.GetDatabase(tx.vm.avm) + //defer tx.vm.ctx.SharedMemory.ReleaseDatabase(tx.vm.avm) + + //utxos := make([]*ava.UTXO, len(tx.Ins)+len(tx.ImportedInputs)) + //for index, input := range tx.Ins { + // utxoID := input.UTXOID.InputID() + // utxo, err := tx.vm.getUTXO(db, utxoID) + // if err != nil { + // return tempError{err} + // } + // utxos[index] = utxo + //} + + //state := ava.NewPrefixedState(smDB, Codec) + //for index, input := range tx.ImportedInputs { + // utxoID := input.UTXOID.InputID() + // utxo, err := state.AVMUTXO(utxoID) + // if err != nil { // Get the UTXO + // return tempError{err} + // } + // utxos[index+len(tx.Ins)] = utxo + //} + + //ins := make([]*ava.TransferableInput, len(tx.Ins)+len(tx.ImportedInputs)) + //copy(ins, tx.Ins) + //copy(ins[len(tx.Ins):], tx.ImportedInputs) + + //// Verify the flowcheck + //if err := tx.vm.semanticVerifySpendUTXOs(tx, utxos, ins, tx.Outs, creds); err != nil { + // return err + //} + + //txID := tx.ID() + + //// Consume the UTXOS + //if err := tx.vm.consumeInputs(db, tx.Ins); err != nil { + // return tempError{err} + //} + //// Produce the UTXOS + //if err := tx.vm.produceOutputs(db, txID, tx.Outs); 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(batch database.Batch) error { + //smDB := tx.vm.ctx.SharedMemory.GetDatabase(tx.vm.avm) + //defer tx.vm.ctx.SharedMemory.ReleaseDatabase(tx.vm.avm) + + //vsmDB := versiondb.New(smDB) + //state := ava.NewPrefixedState(vsmDB, Codec) + + //// Spend imported UTXOs + //for _, in := range tx.ImportedInputs { + // utxoID := in.InputID() + // if err := state.SpendAVMUTXO(utxoID); err != nil { + // return err + // } + //} + + //sharedBatch, err := vsmDB.CommitBatch() + //if err != nil { + // return err + //} + //return atomic.WriteAll(batch, sharedBatch) + return nil +} + +// Create a new transaction +func (vm *VM) newImportTx( + to common.Address, // Address of recipient + keys []*ecdsa.PrivateKey, // Keys to import the funds +) (*AtomicTx, error) { + kc := secp256k1fx.NewKeychain() + factory := &avacrypto.FactorySECP256K1R{} + for _, key := range keys { + sk, err := factory.ToPrivateKey(crypto.FromECDSA(key)) + if err != nil { + panic(err) + } + kc.Add(sk.(*avacrypto.PrivateKeySECP256K1R)) + } + + addrSet := ids.Set{} + for _, addr := range kc.Addresses().List() { + addrSet.Add(ids.NewID(hashing.ComputeHash256Array(addr.Bytes()))) + } + atomicUTXOs, err := vm.GetAtomicUTXOs(addrSet) + if err != nil { + return nil, fmt.Errorf("problem retrieving atomic UTXOs: %w", err) + } + + importedInputs := []*ava.TransferableInput{} + signers := [][]*avacrypto.PrivateKeySECP256K1R{} + + importedAmount := uint64(0) + now := vm.clock.Unix() + for _, utxo := range atomicUTXOs { + if !utxo.AssetID().Equals(vm.avaxAssetID) { + continue + } + inputIntf, utxoSigners, err := kc.Spend(utxo.Out, now) + if err != nil { + continue + } + input, ok := inputIntf.(ava.TransferableIn) + if !ok { + continue + } + importedAmount, err = math.Add64(importedAmount, input.Amount()) + if err != nil { + return nil, err + } + importedInputs = append(importedInputs, &ava.TransferableInput{ + UTXOID: utxo.UTXOID, + Asset: utxo.Asset, + In: input, + }) + signers = append(signers, utxoSigners) + } + ava.SortTransferableInputsWithSigners(importedInputs, signers) + + if importedAmount == 0 { + return nil, errNoFunds // No imported UTXOs were spendable + } + + ins := []*ava.TransferableInput{} + outs := []EVMOutput{} + if importedAmount < vm.txFee { // imported amount goes toward paying tx fee + //var baseSigners [][]*avacrypto.PrivateKeySECP256K1R + //ins, outs, _, baseSigners, err = vm.spend(vm.DB, keys, 0, vm.txFee-importedAmount) + //if err != nil { + // return nil, fmt.Errorf("couldn't generate tx inputs/outputs: %w", err) + //} + //signers = append(baseSigners, signers...) + } else if importedAmount > vm.txFee { + outs = append(outs, EVMOutput{ + Address: to, + Amount: importedAmount - vm.txFee}) + } + + // Create the transaction + utx := &UnsignedImportTx{ + BaseTx: BaseTx{ + NetworkID: vm.ctx.NetworkID, + BlockchainID: vm.ctx.ChainID, + Outs: outs, + Ins: ins, + }, + ImportedInputs: importedInputs, + } + tx := &AtomicTx{UnsignedAtomicTx: utx} + if err := vm.signAtomicTx(tx, signers); err != nil { + return nil, err + } + return tx, utx.Verify() +} diff --git a/plugin/evm/service.go b/plugin/evm/service.go index 62b124f..75e7c31 100644 --- a/plugin/evm/service.go +++ b/plugin/evm/service.go @@ -8,10 +8,14 @@ import ( "crypto/rand" "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/go-ethereum/common" "github.com/ava-labs/go-ethereum/common/hexutil" "github.com/ava-labs/go-ethereum/crypto" @@ -36,6 +40,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} } @@ -120,3 +126,106 @@ 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.ParseAddress(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 = common.ToHex(crypto.FromECDSA(sk)) + //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) + 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} + + if !strings.HasPrefix(args.PrivateKey, constants.SecretKeyPrefix) { + return fmt.Errorf("private key missing %s prefix", constants.SecretKeyPrefix) + } + sk, err := crypto.ToECDSA(common.FromHex(args.PrivateKey)) + if err != nil { + return fmt.Errorf("invalid private key") + } + if err = user.putAddress(sk); err != nil { + return fmt.Errorf("problem saving key %w", err) + } + + reply.Address, err = service.vm.FormatAddress(crypto.PubkeyToAddress(sk.PublicKey)) + if err != nil { + return fmt.Errorf("problem formatting address: %w", err) + } + return nil +} + +// ImportAVAArgs are the arguments to ImportAVA +type ImportAVAArgs struct { + api.UserPass + // The address that will receive the imported funds + To string `json:"to"` +} + +// ImportAVA returns an unsigned transaction to import AVA from the X-Chain. +// The AVA must have already been exported from the X-Chain. +func (service *AvaAPI) ImportAVA(_ *http.Request, args *ImportAVAArgs, response *api.JsonTxID) error { + service.vm.ctx.Log.Info("Platform: ImportAVA called") + + // 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.ParseAddress(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(to, privKeys) + if err != nil { + return err + } + + response.TxID = tx.ID() + return service.vm.issueTx(tx) +} diff --git a/plugin/evm/user.go b/plugin/evm/user.go new file mode 100644 index 0000000..651f202 --- /dev/null +++ b/plugin/evm/user.go @@ -0,0 +1,142 @@ +// (c) 2019-2020, Ava Labs, Inc. All rights reserved. +// See the file LICENSE for licensing terms. + +package evm + +import ( + "errors" + + "crypto/ecdsa" + "github.com/ava-labs/gecko/database" + "github.com/ava-labs/gecko/ids" + "github.com/ava-labs/go-ethereum/common" + "github.com/ava-labs/go-ethereum/crypto" +) + +// 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 *ecdsa.PrivateKey) error { + if privKey == nil { + return errKeyNil + } + + address := crypto.PubkeyToAddress(privKey.PublicKey) // 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(), crypto.FromECDSA(privKey)); 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) (*ecdsa.PrivateKey, error) { + if u.db == nil { + return nil, errDBNil + //} else if address.IsZero() { + // return nil, errEmptyAddress + } + + bytes, err := u.db.Get(address.Bytes()) + if err != nil { + return nil, err + } + sk, err := crypto.ToECDSA(bytes) + if err != nil { + return nil, err + } + return sk, nil +} + +// Return all private keys controlled by this user +func (u *user) getKeys() ([]*ecdsa.PrivateKey, error) { + addrs, err := u.getAddresses() + if err != nil { + return nil, err + } + keys := make([]*ecdsa.PrivateKey, 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..18eca47 100644 --- a/plugin/evm/vm.go +++ b/plugin/evm/vm.go @@ -20,9 +20,10 @@ import ( "github.com/ava-labs/coreth/eth" "github.com/ava-labs/coreth/node" - "github.com/ava-labs/coreth/rpc" "github.com/ava-labs/go-ethereum/common" "github.com/ava-labs/go-ethereum/rlp" + "github.com/ava-labs/go-ethereum/rpc" + avarpc "github.com/gorilla/rpc/v2" "github.com/ava-labs/gecko/api/admin" "github.com/ava-labs/gecko/cache" @@ -31,7 +32,11 @@ 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" + avajson "github.com/ava-labs/gecko/utils/json" "github.com/ava-labs/gecko/utils/timer" + "github.com/ava-labs/gecko/utils/wrappers" + "github.com/ava-labs/gecko/vms/components/ava" commonEng "github.com/ava-labs/gecko/snow/engine/common" ) @@ -59,6 +64,10 @@ const ( bdTimerStateLong ) +const ( + addressSep = "-" +) + var ( errEmptyBlock = errors.New("empty block") errCreateBlock = errors.New("couldn't create block") @@ -66,6 +75,7 @@ var ( errBlockFrequency = errors.New("too frequent block issuance") errUnsupportedFXs = errors.New("unsupported feature extensions") errInvalidBlock = errors.New("invalid block") + errInvalidAddr = errors.New("invalid hex address") ) func maxDuration(x, y time.Duration) time.Duration { @@ -75,6 +85,21 @@ 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{}), + ) + if errs.Errored() { + panic(errs.Err) + } +} + // VM implements the snowman.ChainVM interface type VM struct { ctx *snow.Context @@ -105,6 +130,11 @@ type VM struct { genlock sync.Mutex txSubmitChan <-chan struct{} + codec codec.Codec + clock timer.Clock + avaxAssetID ids.ID + txFee uint64 + //atomicTxPool [] } /* @@ -259,6 +289,7 @@ func (vm *VM) Initialize( } } }) + vm.codec = Codec return nil } @@ -356,6 +387,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 := avarpc.NewServer() + server.RegisterCodec(avajson.NewCodec(), "application/json") + server.RegisterCodec(avajson.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 +419,7 @@ func (vm *VM) CreateHandlers() map[string]*commonEng.HTTPHandler { return map[string]*commonEng.HTTPHandler{ "/rpc": &commonEng.HTTPHandler{LockOptions: commonEng.NoLock, Handler: handler}, + "/ava": newHandler("", &AvaAPI{vm}), "/ws": &commonEng.HTTPHandler{LockOptions: commonEng.NoLock, Handler: handler.WebsocketHandler([]string{"*"})}, } } @@ -531,3 +583,25 @@ func (vm *VM) getLastAccepted() *Block { return vm.lastAccepted } + +func (vm *VM) ParseAddress(addrStr string) (common.Address, error) { + if !common.IsHexAddress(addrStr) { + return common.Address{}, errInvalidAddr + } + return common.HexToAddress(addrStr), nil +} + +func (vm *VM) FormatAddress(addr common.Address) (string, error) { + return addr.Hex(), nil +} + +func (vm *VM) issueTx(tx *AtomicTx) error { + return nil +} + +// GetAtomicUTXOs returns the utxos that at least one of the provided addresses is +// referenced in. +func (vm *VM) GetAtomicUTXOs(addrs ids.Set) ([]*ava.UTXO, error) { + utxos := []*ava.UTXO{} + return utxos, nil +} -- cgit v1.2.3-70-g09d2 From b989b5f949424f72b125cbec460824b94b7c55ab Mon Sep 17 00:00:00 2001 From: Determinant Date: Thu, 13 Aug 2020 21:30:56 -0400 Subject: ... --- plugin/evm/base_tx.go | 14 +++++---- plugin/evm/import_tx.go | 75 ++----------------------------------------------- plugin/evm/vm.go | 1 + 3 files changed, 13 insertions(+), 77 deletions(-) (limited to 'plugin') diff --git a/plugin/evm/base_tx.go b/plugin/evm/base_tx.go index 5ffc58e..7fd5a31 100644 --- a/plugin/evm/base_tx.go +++ b/plugin/evm/base_tx.go @@ -27,6 +27,10 @@ type EVMOutput struct { Amount uint64 `serialize:"true" json:"amount"` } +func (out *EVMOutput) Verify() error { + return nil +} + // BaseTx contains fields common to many transaction types. It should be // embedded in transaction implementations. The serialized fields of this struct // should be exactly the same as those of avm.BaseTx. Do not change this @@ -84,11 +88,11 @@ func (tx *BaseTx) Verify() error { return fmt.Errorf("memo length, %d, exceeds maximum memo length, %d", len(tx.Memo), maxMemoSize) } - //for _, out := range tx.Outs { - // if err := out.Verify(); err != nil { - // return err - // } - //} + for _, out := range tx.Outs { + if err := out.Verify(); err != nil { + return err + } + } for _, in := range tx.Ins { if err := in.Verify(); err != nil { return err diff --git a/plugin/evm/import_tx.go b/plugin/evm/import_tx.go index 36750a6..bbbdb95 100644 --- a/plugin/evm/import_tx.go +++ b/plugin/evm/import_tx.go @@ -8,9 +8,7 @@ import ( "errors" "fmt" - //"github.com/ava-labs/gecko/chains/atomic" "github.com/ava-labs/gecko/database" - //"github.com/ava-labs/gecko/database/versiondb" "github.com/ava-labs/gecko/ids" avacrypto "github.com/ava-labs/gecko/utils/crypto" "github.com/ava-labs/gecko/utils/hashing" @@ -153,51 +151,7 @@ func (tx *UnsignedImportTx) SemanticVerify(db database.Database, creds []verify. if err := tx.Verify(); err != nil { return permError{err} } - - //// Verify (but don't spend) imported inputs - //smDB := tx.vm.ctx.SharedMemory.GetDatabase(tx.vm.avm) - //defer tx.vm.ctx.SharedMemory.ReleaseDatabase(tx.vm.avm) - - //utxos := make([]*ava.UTXO, len(tx.Ins)+len(tx.ImportedInputs)) - //for index, input := range tx.Ins { - // utxoID := input.UTXOID.InputID() - // utxo, err := tx.vm.getUTXO(db, utxoID) - // if err != nil { - // return tempError{err} - // } - // utxos[index] = utxo - //} - - //state := ava.NewPrefixedState(smDB, Codec) - //for index, input := range tx.ImportedInputs { - // utxoID := input.UTXOID.InputID() - // utxo, err := state.AVMUTXO(utxoID) - // if err != nil { // Get the UTXO - // return tempError{err} - // } - // utxos[index+len(tx.Ins)] = utxo - //} - - //ins := make([]*ava.TransferableInput, len(tx.Ins)+len(tx.ImportedInputs)) - //copy(ins, tx.Ins) - //copy(ins[len(tx.Ins):], tx.ImportedInputs) - - //// Verify the flowcheck - //if err := tx.vm.semanticVerifySpendUTXOs(tx, utxos, ins, tx.Outs, creds); err != nil { - // return err - //} - - //txID := tx.ID() - - //// Consume the UTXOS - //if err := tx.vm.consumeInputs(db, tx.Ins); err != nil { - // return tempError{err} - //} - //// Produce the UTXOS - //if err := tx.vm.produceOutputs(db, txID, tx.Outs); err != nil { - // return tempError{err} - //} - + // TODO: verify UTXO inputs via gRPC return nil } @@ -207,25 +161,7 @@ func (tx *UnsignedImportTx) SemanticVerify(db database.Database, creds []verify. // 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(batch database.Batch) error { - //smDB := tx.vm.ctx.SharedMemory.GetDatabase(tx.vm.avm) - //defer tx.vm.ctx.SharedMemory.ReleaseDatabase(tx.vm.avm) - - //vsmDB := versiondb.New(smDB) - //state := ava.NewPrefixedState(vsmDB, Codec) - - //// Spend imported UTXOs - //for _, in := range tx.ImportedInputs { - // utxoID := in.InputID() - // if err := state.SpendAVMUTXO(utxoID); err != nil { - // return err - // } - //} - - //sharedBatch, err := vsmDB.CommitBatch() - //if err != nil { - // return err - //} - //return atomic.WriteAll(batch, sharedBatch) + // TODO: finish this function via gRPC return nil } @@ -290,12 +226,7 @@ func (vm *VM) newImportTx( ins := []*ava.TransferableInput{} outs := []EVMOutput{} if importedAmount < vm.txFee { // imported amount goes toward paying tx fee - //var baseSigners [][]*avacrypto.PrivateKeySECP256K1R - //ins, outs, _, baseSigners, err = vm.spend(vm.DB, keys, 0, vm.txFee-importedAmount) - //if err != nil { - // return nil, fmt.Errorf("couldn't generate tx inputs/outputs: %w", err) - //} - //signers = append(baseSigners, signers...) + // TODO: spend EVM balance to compensate vm.txFee-importedAmount } else if importedAmount > vm.txFee { outs = append(outs, EVMOutput{ Address: to, diff --git a/plugin/evm/vm.go b/plugin/evm/vm.go index 18eca47..1010d60 100644 --- a/plugin/evm/vm.go +++ b/plugin/evm/vm.go @@ -602,6 +602,7 @@ func (vm *VM) issueTx(tx *AtomicTx) error { // GetAtomicUTXOs returns the utxos that at least one of the provided addresses is // referenced in. func (vm *VM) GetAtomicUTXOs(addrs ids.Set) ([]*ava.UTXO, error) { + // TODO: finish this function via gRPC utxos := []*ava.UTXO{} return utxos, nil } -- cgit v1.2.3-70-g09d2 From 0844c8c6919f6d98ebe8a9501360ef5bc362dff3 Mon Sep 17 00:00:00 2001 From: Determinant Date: Wed, 19 Aug 2020 01:25:20 -0400 Subject: catch up with the new P-Chain cross-chain impl --- go.mod | 1 + go.sum | 10 ++++ plugin/evm/atomic_tx.go | 64 -------------------- plugin/evm/base_tx.go | 42 +++---------- plugin/evm/block.go | 21 +++++++ plugin/evm/factory.go | 8 ++- plugin/evm/import_tx.go | 156 ++++++++++++++++-------------------------------- plugin/evm/service.go | 56 ++++++++++++----- plugin/evm/tx.go | 105 ++++++++++++++++++++++++++++++++ plugin/evm/user.go | 26 ++++---- plugin/evm/vm.go | 83 +++++++++++++++++++------- 11 files changed, 321 insertions(+), 251 deletions(-) delete mode 100644 plugin/evm/atomic_tx.go create mode 100644 plugin/evm/tx.go (limited to 'plugin') diff --git a/go.mod b/go.mod index 752be2f..5ef12a7 100644 --- a/go.mod +++ b/go.mod @@ -11,6 +11,7 @@ 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 diff --git a/go.sum b/go.sum index 832a8bf..d4c1885 100644 --- a/go.sum +++ b/go.sum @@ -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= @@ -313,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= @@ -321,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= diff --git a/plugin/evm/atomic_tx.go b/plugin/evm/atomic_tx.go deleted file mode 100644 index e8e48f7..0000000 --- a/plugin/evm/atomic_tx.go +++ /dev/null @@ -1,64 +0,0 @@ -// (c) 2019-2020, Ava Labs, Inc. All rights reserved. -// See the file LICENSE for licensing terms. - -package evm - -import ( - "fmt" - - "github.com/ava-labs/gecko/database" - "github.com/ava-labs/gecko/ids" - "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" -) - -// UnsignedAtomicTx ... -type UnsignedAtomicTx interface { - initialize(vm *VM, bytes []byte) error - ID() ids.ID - // UTXOs this tx consumes - InputUTXOs() ids.Set - // Attempts to verify this transaction with the provided state. - SemanticVerify(db database.Database, creds []verify.Verifiable) TxError - Accept(database.Batch) error -} - -// AtomicTx is an operation that can be decided without being proposed, but must -// have special control over database commitment -type AtomicTx struct { - UnsignedAtomicTx `serialize:"true"` - // Credentials that authorize the inputs to be spent - Credentials []verify.Verifiable `serialize:"true" json:"credentials"` -} - -func (vm *VM) signAtomicTx(tx *AtomicTx, signers [][]*crypto.PrivateKeySECP256K1R) error { - unsignedBytes, err := vm.codec.Marshal(tx.UnsignedAtomicTx) - if err != nil { - return fmt.Errorf("couldn't marshal UnsignedAtomicTx: %w", err) - } - - // Attach credentials - hash := hashing.ComputeHash256(unsignedBytes) - tx.Credentials = make([]verify.Verifiable, len(signers)) - for i, credKeys := range signers { - cred := &secp256k1fx.Credential{ - Sigs: make([][crypto.SECP256K1RSigLen]byte, len(credKeys)), - } - for j, key := range credKeys { - sig, err := key.SignHash(hash) // Sign hash - if err != nil { - return fmt.Errorf("problem generating credential: %w", err) - } - copy(cred.Sigs[j][:], sig) - } - tx.Credentials[i] = cred // Attach credential - } - - txBytes, err := vm.codec.Marshal(tx) - if err != nil { - return fmt.Errorf("couldn't marshal AtomicTx: %w", err) - } - return tx.initialize(vm, txBytes) -} diff --git a/plugin/evm/base_tx.go b/plugin/evm/base_tx.go index 7fd5a31..b3e85cd 100644 --- a/plugin/evm/base_tx.go +++ b/plugin/evm/base_tx.go @@ -2,10 +2,10 @@ package evm import ( "errors" - "fmt" "github.com/ava-labs/gecko/ids" - "github.com/ava-labs/gecko/vms/components/ava" + "github.com/ava-labs/gecko/snow" + "github.com/ava-labs/gecko/vms/components/avax" "github.com/ava-labs/go-ethereum/common" ) @@ -37,16 +37,9 @@ func (out *EVMOutput) Verify() error { // struct's serialized fields without doing the same on avm.BaseTx. // TODO: Factor out this and avm.BaseTX type BaseTx struct { - vm *VM + avax.Metadata // true iff this transaction has already passed syntactic verification syntacticallyVerified bool - // ID of this tx - id ids.ID - // Byte representation of this unsigned tx - unsignedBytes []byte - // Byte representation of the signed transaction (ie with credentials) - bytes []byte - // ID of the network on which this tx was issued NetworkID uint32 `serialize:"true" json:"networkID"` // ID of this blockchain. In practice is always the empty ID. @@ -55,38 +48,22 @@ type BaseTx struct { // Outputs Outs []EVMOutput `serialize:"true" json:"outputs"` // Inputs consumed by this tx - Ins []*ava.TransferableInput `serialize:"true" json:"inputs"` + Ins []*avax.TransferableInput `serialize:"true" json:"inputs"` // Memo field contains arbitrary bytes, up to maxMemoSize Memo []byte `serialize:"true" json:"memo"` } -// UnsignedBytes returns the byte representation of this unsigned tx -func (tx *BaseTx) UnsignedBytes() []byte { return tx.unsignedBytes } - -// Bytes returns the byte representation of this tx -func (tx *BaseTx) Bytes() []byte { return tx.bytes } - -// ID returns this transaction's ID -func (tx *BaseTx) ID() ids.ID { return tx.id } - // Verify returns nil iff this tx is well formed -func (tx *BaseTx) Verify() error { +func (tx *BaseTx) Verify(ctx *snow.Context) error { switch { case tx == nil: return errNilTx case tx.syntacticallyVerified: // already passed syntactic verification return nil - case tx.id.IsZero(): - return errInvalidID - case tx.vm == nil: - return errVMNil - case tx.NetworkID != tx.vm.ctx.NetworkID: + case tx.NetworkID != ctx.NetworkID: return errWrongNetworkID - case !tx.vm.ctx.ChainID.Equals(tx.BlockchainID): + case !ctx.ChainID.Equals(tx.BlockchainID): return errWrongBlockchainID - case len(tx.Memo) > maxMemoSize: - return fmt.Errorf("memo length, %d, exceeds maximum memo length, %d", - len(tx.Memo), maxMemoSize) } for _, out := range tx.Outs { if err := out.Verify(); err != nil { @@ -99,9 +76,8 @@ func (tx *BaseTx) Verify() error { } } switch { - //case !ava.IsSortedTransferableOutputs(tx.Outs, Codec): - // return errOutputsNotSorted - case !ava.IsSortedAndUniqueTransferableInputs(tx.Ins): + // TODO: check whether output addreses are sorted? + case !avax.IsSortedAndUniqueTransferableInputs(tx.Ins): return errInputsNotSortedUnique default: return nil diff --git a/plugin/evm/block.go b/plugin/evm/block.go index 449e261..9c15834 100644 --- a/plugin/evm/block.go +++ b/plugin/evm/block.go @@ -61,6 +61,27 @@ func (b *Block) Parent() snowman.Block { // Verify implements the snowman.Block interface func (b *Block) Verify() error { + p := b + path := []*Block{} + for { + if p.Status() == choices.Accepted { + break + } + path = append(path, p) + p = p.Parent().(*Block) + } + inputs := new(ids.Set) + for i := len(path) - 1; i >= 0; i-- { + p := path[i] + atx := p.vm.getAtomicTx(p.ethBlock) + inputs.Union(atx.UnsignedTx.(UnsignedAtomicTx).InputUTXOs()) + } + tx := b.vm.getAtomicTx(b.ethBlock) + atx := tx.UnsignedTx.(*UnsignedImportTx) + if atx.SemanticVerify(b.vm, tx) != nil { + return errInvalidBlock + } + _, err := b.vm.chain.InsertChain([]*types.Block{b.ethBlock}) return err } diff --git a/plugin/evm/factory.go b/plugin/evm/factory.go index 31a617a..ae2ea27 100644 --- a/plugin/evm/factory.go +++ b/plugin/evm/factory.go @@ -14,14 +14,16 @@ var ( // Factory ... type Factory struct { - AVA ids.ID - Fee uint64 + AVAX ids.ID + AVM ids.ID + Fee uint64 } // New ... func (f *Factory) New() interface{} { return &VM{ - avaxAssetID: f.AVA, + avaxAssetID: f.AVAX, + avm: f.AVM, txFee: f.Fee, } } diff --git a/plugin/evm/import_tx.go b/plugin/evm/import_tx.go index bbbdb95..84f28de 100644 --- a/plugin/evm/import_tx.go +++ b/plugin/evm/import_tx.go @@ -4,20 +4,20 @@ package evm import ( - "crypto/ecdsa" + //"crypto/ecdsa" "errors" "fmt" "github.com/ava-labs/gecko/database" "github.com/ava-labs/gecko/ids" + "github.com/ava-labs/gecko/snow" avacrypto "github.com/ava-labs/gecko/utils/crypto" - "github.com/ava-labs/gecko/utils/hashing" "github.com/ava-labs/gecko/utils/math" - "github.com/ava-labs/gecko/vms/components/ava" - "github.com/ava-labs/gecko/vms/components/verify" + "github.com/ava-labs/gecko/vms/components/avax" + //"github.com/ava-labs/gecko/vms/components/verify" "github.com/ava-labs/gecko/vms/secp256k1fx" "github.com/ava-labs/go-ethereum/common" - "github.com/ava-labs/go-ethereum/crypto" + //"github.com/ava-labs/go-ethereum/crypto" ) var ( @@ -29,29 +29,18 @@ var ( 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") ) // UnsignedImportTx is an unsigned ImportTx type UnsignedImportTx struct { BaseTx `serialize:"true"` - // Inputs that consume UTXOs produced on the X-Chain - ImportedInputs []*ava.TransferableInput `serialize:"true" json:"importedInputs"` -} -// initialize [tx]. Sets [tx.vm], [tx.unsignedBytes], [tx.bytes], [tx.id] -func (tx *UnsignedImportTx) initialize(vm *VM, bytes []byte) error { - if tx.vm != nil { // already been initialized - return nil - } - tx.vm = vm - tx.bytes = bytes - tx.id = ids.NewID(hashing.ComputeHash256Array(bytes)) - var err error - tx.unsignedBytes, err = Codec.Marshal(interface{}(tx)) - if err != nil { - return fmt.Errorf("couldn't marshal UnsignedImportTx: %w", err) - } - return nil + // 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"` } // InputUTXOs returns the UTXOIDs of the imported funds @@ -63,66 +52,28 @@ func (tx *UnsignedImportTx) InputUTXOs() ids.Set { return set } -var ( - errInputOverflow = errors.New("inputs overflowed uint64") - errOutputOverflow = errors.New("outputs overflowed uint64") -) - -// Verify that: -// * inputs are all AVAX -// * sum(inputs{unlocked}) >= sum(outputs{unlocked}) + burnAmount{unlocked} -func syntacticVerifySpend( - ins []*ava.TransferableInput, - unlockedOuts []EVMOutput, - burnedUnlocked uint64, - avaxAssetID ids.ID, -) error { - // AVAX consumed in this tx - consumedUnlocked := uint64(0) - for _, in := range ins { - if assetID := in.AssetID(); !assetID.Equals(avaxAssetID) { // all inputs must be AVAX - return fmt.Errorf("input has unexpected asset ID %s expected %s", assetID, avaxAssetID) - } - - in := in.Input() - consumed := in.Amount() - newConsumed, err := math.Add64(consumedUnlocked, consumed) - if err != nil { - return errInputOverflow - } - consumedUnlocked = newConsumed - } - - // AVAX produced in this tx - producedUnlocked := burnedUnlocked - for _, out := range unlockedOuts { - produced := out.Amount - newProduced, err := math.Add64(producedUnlocked, produced) - if err != nil { - return errOutputOverflow - } - producedUnlocked = newProduced - } - - if producedUnlocked > consumedUnlocked { - return fmt.Errorf("tx unlocked outputs (%d) + burn amount (%d) > inputs (%d)", - producedUnlocked-burnedUnlocked, burnedUnlocked, consumedUnlocked) - } - return nil -} - // Verify this transaction is well-formed -func (tx *UnsignedImportTx) Verify() error { +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): + // TODO: remove this check if we allow for P->C swaps + return errWrongChainID case len(tx.ImportedInputs) == 0: return errNoImportInputs } - if err := tx.BaseTx.Verify(); err != nil { + if err := tx.BaseTx.Verify(ctx); err != nil { return err } @@ -131,27 +82,24 @@ func (tx *UnsignedImportTx) Verify() error { return err } } - if !ava.IsSortedAndUniqueTransferableInputs(tx.ImportedInputs) { + if !avax.IsSortedAndUniqueTransferableInputs(tx.ImportedInputs) { return errInputsNotSortedUnique } - allIns := make([]*ava.TransferableInput, len(tx.Ins)+len(tx.ImportedInputs)) - copy(allIns, tx.Ins) - copy(allIns[len(tx.Ins):], tx.ImportedInputs) - if err := syntacticVerifySpend(allIns, tx.Outs, tx.vm.txFee, tx.vm.avaxAssetID); err != nil { - return err - } - tx.syntacticallyVerified = true return nil } // SemanticVerify this transaction is valid. -func (tx *UnsignedImportTx) SemanticVerify(db database.Database, creds []verify.Verifiable) TxError { - if err := tx.Verify(); err != nil { +func (tx *UnsignedImportTx) SemanticVerify( + vm *VM, + stx *Tx, +) TxError { + if err := tx.Verify(vm.avm, vm.ctx, vm.txFee, vm.avaxAssetID); err != nil { return permError{err} } - // TODO: verify UTXO inputs via gRPC + // TODO: verify using avax.VerifyTx(vm.txFee, vm.avaxAssetID, tx.Ins, outs) + // TODO: verify UTXO inputs via gRPC (with creds) return nil } @@ -167,29 +115,25 @@ func (tx *UnsignedImportTx) Accept(batch database.Batch) error { // Create a new transaction func (vm *VM) newImportTx( + chainID ids.ID, // chain to import from to common.Address, // Address of recipient - keys []*ecdsa.PrivateKey, // Keys to import the funds -) (*AtomicTx, error) { + keys []*avacrypto.PrivateKeySECP256K1R, // Keys to import the funds +) (*Tx, error) { + if !vm.avm.Equals(chainID) { + return nil, errWrongChainID + } + kc := secp256k1fx.NewKeychain() - factory := &avacrypto.FactorySECP256K1R{} for _, key := range keys { - sk, err := factory.ToPrivateKey(crypto.FromECDSA(key)) - if err != nil { - panic(err) - } - kc.Add(sk.(*avacrypto.PrivateKeySECP256K1R)) + kc.Add(key) } - addrSet := ids.Set{} - for _, addr := range kc.Addresses().List() { - addrSet.Add(ids.NewID(hashing.ComputeHash256Array(addr.Bytes()))) - } - atomicUTXOs, err := vm.GetAtomicUTXOs(addrSet) + 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 := []*ava.TransferableInput{} + importedInputs := []*avax.TransferableInput{} signers := [][]*avacrypto.PrivateKeySECP256K1R{} importedAmount := uint64(0) @@ -202,7 +146,7 @@ func (vm *VM) newImportTx( if err != nil { continue } - input, ok := inputIntf.(ava.TransferableIn) + input, ok := inputIntf.(avax.TransferableIn) if !ok { continue } @@ -210,27 +154,28 @@ func (vm *VM) newImportTx( if err != nil { return nil, err } - importedInputs = append(importedInputs, &ava.TransferableInput{ + importedInputs = append(importedInputs, &avax.TransferableInput{ UTXOID: utxo.UTXOID, Asset: utxo.Asset, In: input, }) signers = append(signers, utxoSigners) } - ava.SortTransferableInputsWithSigners(importedInputs, signers) + avax.SortTransferableInputsWithSigners(importedInputs, signers) if importedAmount == 0 { return nil, errNoFunds // No imported UTXOs were spendable } - ins := []*ava.TransferableInput{} + ins := []*avax.TransferableInput{} outs := []EVMOutput{} if importedAmount < vm.txFee { // imported amount goes toward paying tx fee // TODO: spend EVM balance to compensate vm.txFee-importedAmount } else if importedAmount > vm.txFee { outs = append(outs, EVMOutput{ Address: to, - Amount: importedAmount - vm.txFee}) + Amount: importedAmount - vm.txFee, + }) } // Create the transaction @@ -241,11 +186,12 @@ func (vm *VM) newImportTx( Outs: outs, Ins: ins, }, + SourceChain: chainID, ImportedInputs: importedInputs, } - tx := &AtomicTx{UnsignedAtomicTx: utx} - if err := vm.signAtomicTx(tx, signers); err != nil { + tx := &Tx{UnsignedTx: utx} + if err := tx.Sign(vm.codec, signers); err != nil { return nil, err } - return tx, utx.Verify() + return tx, utx.Verify(vm.avm, vm.ctx, vm.txFee, vm.avaxAssetID) } diff --git a/plugin/evm/service.go b/plugin/evm/service.go index 75e7c31..c05b0a4 100644 --- a/plugin/evm/service.go +++ b/plugin/evm/service.go @@ -16,6 +16,8 @@ import ( "github.com/ava-labs/coreth/core/types" "github.com/ava-labs/gecko/api" "github.com/ava-labs/gecko/utils/constants" + avacrypto "github.com/ava-labs/gecko/utils/crypto" + "github.com/ava-labs/gecko/utils/formatting" "github.com/ava-labs/go-ethereum/common" "github.com/ava-labs/go-ethereum/common/hexutil" "github.com/ava-labs/go-ethereum/crypto" @@ -147,13 +149,12 @@ func (service *AvaAPI) ExportKey(r *http.Request, args *ExportKeyArgs, reply *Ex return fmt.Errorf("problem retrieving user '%s': %w", args.Username, err) } user := user{db: db} - if address, err := service.vm.ParseAddress(args.Address); err != nil { + if address, err := service.vm.ParseLocalAddress(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 = common.ToHex(crypto.FromECDSA(sk)) - //constants.SecretKeyPrefix + formatting.CB58{Bytes: sk.Bytes()}.String() + reply.PrivateKey = constants.SecretKeyPrefix + formatting.CB58{Bytes: sk.Bytes()}.String() return nil } } @@ -167,6 +168,10 @@ type ImportKeyArgs struct { // 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) @@ -174,35 +179,56 @@ func (service *AvaAPI) ImportKey(r *http.Request, args *ImportKeyArgs, reply *ap user := user{db: db} + factory := avacrypto.FactorySECP256K1R{} + if !strings.HasPrefix(args.PrivateKey, constants.SecretKeyPrefix) { return fmt.Errorf("private key missing %s prefix", constants.SecretKeyPrefix) } - sk, err := crypto.ToECDSA(common.FromHex(args.PrivateKey)) + 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("invalid private key") + return fmt.Errorf("problem parsing private key: %w", err) } - if err = user.putAddress(sk); err != nil { + sk := skIntf.(*avacrypto.PrivateKeySECP256K1R) + + if err := user.putAddress(sk); err != nil { return fmt.Errorf("problem saving key %w", err) } - reply.Address, err = service.vm.FormatAddress(crypto.PubkeyToAddress(sk.PublicKey)) + // TODO: return eth address here + reply.Address, err = service.vm.FormatAddress( + crypto.PubkeyToAddress(*(sk.PublicKey().(*avacrypto.PublicKeySECP256K1R).ToECDSA()))) if err != nil { return fmt.Errorf("problem formatting address: %w", err) } return nil } -// ImportAVAArgs are the arguments to ImportAVA -type ImportAVAArgs struct { +// 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"` } -// ImportAVA returns an unsigned transaction to import AVA from the X-Chain. -// The AVA must have already been exported from the X-Chain. -func (service *AvaAPI) ImportAVA(_ *http.Request, args *ImportAVAArgs, response *api.JsonTxID) error { - service.vm.ctx.Log.Info("Platform: ImportAVA called") +// 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) @@ -211,7 +237,7 @@ func (service *AvaAPI) ImportAVA(_ *http.Request, args *ImportAVAArgs, response } user := user{db: db} - to, err := service.vm.ParseAddress(args.To) + to, err := service.vm.ParseLocalAddress(args.To) if err != nil { // Parse address return fmt.Errorf("couldn't parse argument 'to' to an address: %w", err) } @@ -221,7 +247,7 @@ func (service *AvaAPI) ImportAVA(_ *http.Request, args *ImportAVAArgs, response return fmt.Errorf("couldn't get keys controlled by the user: %w", err) } - tx, err := service.vm.newImportTx(to, privKeys) + tx, err := service.vm.newImportTx(chainID, to, privKeys) if err != nil { return err } diff --git a/plugin/evm/tx.go b/plugin/evm/tx.go new file mode 100644 index 0000000..024e54d --- /dev/null +++ b/plugin/evm/tx.go @@ -0,0 +1,105 @@ +// (c) 2019-2020, Ava Labs, Inc. All rights reserved. +// See the file LICENSE for licensing terms. + +package evm + +import ( + "fmt" + + "github.com/ava-labs/gecko/database" + "github.com/ava-labs/gecko/database/versiondb" + "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" +) + +// UnsignedTx is an unsigned transaction +type UnsignedTx interface { + Initialize(unsignedBytes, signedBytes []byte) + ID() ids.ID + UnsignedBytes() []byte + Bytes() []byte +} + +// UnsignedDecisionTx is an unsigned operation that can be immediately decided +type UnsignedDecisionTx interface { + UnsignedTx + + // Attempts to verify this transaction with the provided state. + SemanticVerify(vm *VM, db database.Database, stx *Tx) ( + onAcceptFunc func() error, + err TxError, + ) +} + +// UnsignedProposalTx is an unsigned operation that can be proposed +type UnsignedProposalTx interface { + UnsignedTx + + // Attempts to verify this transaction with the provided state. + SemanticVerify(vm *VM, db database.Database, stx *Tx) ( + onCommitDB *versiondb.Database, + onAbortDB *versiondb.Database, + onCommitFunc func() error, + onAbortFunc func() error, + err TxError, + ) + InitiallyPrefersCommit(vm *VM) bool +} + +// 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, db database.Database, stx *Tx) TxError + + // Accept this transaction with the additionally provided state transitions. + Accept(ctx *snow.Context, batch database.Batch) 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"` +} + +// 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 index 651f202..5d7a037 100644 --- a/plugin/evm/user.go +++ b/plugin/evm/user.go @@ -5,12 +5,13 @@ package evm import ( "errors" + "fmt" - "crypto/ecdsa" "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" - "github.com/ava-labs/go-ethereum/crypto" + ethcrypto "github.com/ava-labs/go-ethereum/crypto" ) // Key in the database whose corresponding value is the list of @@ -66,12 +67,13 @@ func (u *user) controlsAddress(address common.Address) (bool, error) { } // putAddress persists that this user controls address controlled by [privKey] -func (u *user) putAddress(privKey *ecdsa.PrivateKey) error { +func (u *user) putAddress(privKey *crypto.PrivateKeySECP256K1R) error { if privKey == nil { return errKeyNil } - address := crypto.PubkeyToAddress(privKey.PublicKey) // address the privKey controls + address := ethcrypto.PubkeyToAddress( + (*privKey.PublicKey().(*crypto.PublicKeySECP256K1R).ToECDSA())) // address the privKey controls controlsAddress, err := u.controlsAddress(address) if err != nil { return err @@ -80,7 +82,7 @@ func (u *user) putAddress(privKey *ecdsa.PrivateKey) error { return nil } - if err := u.db.Put(address.Bytes(), crypto.FromECDSA(privKey)); err != nil { // Address --> private key + if err := u.db.Put(address.Bytes(), privKey.Bytes()); err != nil { // Address --> private key return err } @@ -106,31 +108,35 @@ func (u *user) putAddress(privKey *ecdsa.PrivateKey) error { } // Key returns the private key that controls the given address -func (u *user) getKey(address common.Address) (*ecdsa.PrivateKey, error) { +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 := crypto.ToECDSA(bytes) + sk, err := factory.ToPrivateKey(bytes) if err != nil { return nil, err } - return sk, nil + 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() ([]*ecdsa.PrivateKey, error) { +func (u *user) getKeys() ([]*crypto.PrivateKeySECP256K1R, error) { addrs, err := u.getAddresses() if err != nil { return nil, err } - keys := make([]*ecdsa.PrivateKey, len(addrs)) + keys := make([]*crypto.PrivateKeySECP256K1R, len(addrs)) for i, addr := range addrs { key, err := u.getKey(addr) if err != nil { diff --git a/plugin/evm/vm.go b/plugin/evm/vm.go index 1010d60..4fe4cc0 100644 --- a/plugin/evm/vm.go +++ b/plugin/evm/vm.go @@ -16,6 +16,7 @@ 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" @@ -33,10 +34,12 @@ import ( "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/formatting" avajson "github.com/ava-labs/gecko/utils/json" "github.com/ava-labs/gecko/utils/timer" "github.com/ava-labs/gecko/utils/wrappers" - "github.com/ava-labs/gecko/vms/components/ava" + "github.com/ava-labs/gecko/vms/components/avax" commonEng "github.com/ava-labs/gecko/snow/engine/common" ) @@ -69,13 +72,14 @@ const ( ) 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") - errInvalidAddr = errors.New("invalid hex address") + 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 atomix txs") ) func maxDuration(x, y time.Duration) time.Duration { @@ -128,13 +132,25 @@ type VM struct { bdGenWaitFlag bool bdGenFlag bool - genlock sync.Mutex - txSubmitChan <-chan struct{} - codec codec.Codec - clock timer.Clock - avaxAssetID ids.ID - txFee uint64 - //atomicTxPool [] + genlock sync.Mutex + txSubmitChan <-chan struct{} + codec codec.Codec + clock timer.Clock + avaxAssetID ids.ID + avm ids.ID + txFee uint64 + pendingAtomicTxs chan *Tx + blockAtomicInputCache cache.LRU +} + +func (vm *VM) getAtomicTx(block *types.Block) *Tx { + var atx *Tx + if extdata := block.ExtraData(); extdata != nil { + if err := vm.codec.Unmarshal(block.ExtraData(), atx); err != nil { + panic(err) + } + } + return atx } /* @@ -191,6 +207,11 @@ func (vm *VM) Initialize( vm.newBlockChan <- nil return errEmptyBlock } + select { + case atx := <-vm.pendingAtomicTxs: + raw, _ := vm.codec.Marshal(atx) + block.SetExtraData(raw) + } return nil }) chain.SetOnSealFinish(func(block *types.Block) error { @@ -211,8 +232,14 @@ func (vm *VM) Initialize( chain.SetOnQueryAcceptedBlock(func() *types.Block { return vm.getLastAccepted().ethBlock }) + chain.SetOnExtraStateChange(func(block *types.Block, statedb *state.StateDB) error { + atx := vm.getAtomicTx(block).UnsignedTx.(*UnsignedImportTx) + vm.ctx.Log.Info(atx.ID().String()) + return nil + }) vm.blockCache = cache.LRU{Size: 2048} vm.blockStatusCache = cache.LRU{Size: 1024} + vm.blockAtomicInputCache = cache.LRU{Size: 4096} vm.newBlockChan = make(chan *Block) vm.networkChan = toEngine vm.blockDelayTimer = timer.NewTimer(func() { @@ -236,6 +263,8 @@ 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) chain.GetTxPool().SubscribeNewHeadEvent(vm.newTxPoolHeadChan) // TODO: shutdown this go routine go ctx.Log.RecoverAndPanic(func() { @@ -419,7 +448,7 @@ func (vm *VM) CreateHandlers() map[string]*commonEng.HTTPHandler { return map[string]*commonEng.HTTPHandler{ "/rpc": &commonEng.HTTPHandler{LockOptions: commonEng.NoLock, Handler: handler}, - "/ava": newHandler("", &AvaAPI{vm}), + "/ava": newHandler("ava", &AvaAPI{vm}), "/ws": &commonEng.HTTPHandler{LockOptions: commonEng.NoLock, Handler: handler.WebsocketHandler([]string{"*"})}, } } @@ -584,7 +613,8 @@ func (vm *VM) getLastAccepted() *Block { return vm.lastAccepted } -func (vm *VM) ParseAddress(addrStr string) (common.Address, error) { +// ParseLocalAddress takes in an address for this chain and produces the ID +func (vm *VM) ParseLocalAddress(addrStr string) (common.Address, error) { if !common.IsHexAddress(addrStr) { return common.Address{}, errInvalidAddr } @@ -595,14 +625,25 @@ func (vm *VM) FormatAddress(addr common.Address) (string, error) { return addr.Hex(), nil } -func (vm *VM) issueTx(tx *AtomicTx) error { +func (vm *VM) issueTx(tx *Tx) error { + select { + case vm.pendingAtomicTxs <- tx: + default: + return errTooManyAtomicTx + } return nil } // GetAtomicUTXOs returns the utxos that at least one of the provided addresses is // referenced in. -func (vm *VM) GetAtomicUTXOs(addrs ids.Set) ([]*ava.UTXO, error) { +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) { // TODO: finish this function via gRPC - utxos := []*ava.UTXO{} - return utxos, nil + utxos := []*avax.UTXO{} + return utxos, ids.ShortEmpty, ids.Empty, nil } -- cgit v1.2.3-70-g09d2 From 315ad2c63fa4879e5a91c3aa013c93f8f4969362 Mon Sep 17 00:00:00 2001 From: Determinant Date: Wed, 19 Aug 2020 11:16:35 -0400 Subject: finish the block verification impl --- plugin/evm/block.go | 24 ++++++++++++++++++++---- plugin/evm/vm.go | 6 ++++++ 2 files changed, 26 insertions(+), 4 deletions(-) (limited to 'plugin') diff --git a/plugin/evm/block.go b/plugin/evm/block.go index 9c15834..45a888f 100644 --- a/plugin/evm/block.go +++ b/plugin/evm/block.go @@ -61,23 +61,39 @@ func (b *Block) Parent() snowman.Block { // Verify implements the snowman.Block interface func (b *Block) Verify() error { - p := b + vm := b.vm + if b.ethBlock.Hash() == vm.genesisHash { + return nil + } + p := b.Parent() path := []*Block{} + inputs := new(ids.Set) for { if p.Status() == choices.Accepted { break } - path = append(path, p) + if ret, hit := vm.blockAtomicInputCache.Get(p.ID()); hit { + inputs = ret.(*ids.Set) + break + } + path = append(path, p.(*Block)) p = p.Parent().(*Block) } - inputs := new(ids.Set) for i := len(path) - 1; i >= 0; i-- { + inputs_copy := new(ids.Set) p := path[i] - atx := p.vm.getAtomicTx(p.ethBlock) + atx := vm.getAtomicTx(p.ethBlock) inputs.Union(atx.UnsignedTx.(UnsignedAtomicTx).InputUTXOs()) + inputs_copy.Union(*inputs) + vm.blockAtomicInputCache.Put(p.ID(), inputs_copy) } tx := b.vm.getAtomicTx(b.ethBlock) atx := tx.UnsignedTx.(*UnsignedImportTx) + for _, in := range atx.InputUTXOs().List() { + if inputs.Contains(in) { + return errInvalidBlock + } + } if atx.SemanticVerify(b.vm, tx) != nil { return errInvalidBlock } diff --git a/plugin/evm/vm.go b/plugin/evm/vm.go index 4fe4cc0..7a310bf 100644 --- a/plugin/evm/vm.go +++ b/plugin/evm/vm.go @@ -134,6 +134,7 @@ type VM struct { genlock sync.Mutex txSubmitChan <-chan struct{} + atomicTxSubmitChan chan struct{} codec codec.Codec clock timer.Clock avaxAssetID ids.ID @@ -211,6 +212,7 @@ func (vm *VM) Initialize( case atx := <-vm.pendingAtomicTxs: raw, _ := vm.codec.Marshal(atx) block.SetExtraData(raw) + // TODO: make sure the atomic Tx is valid } return nil }) @@ -313,6 +315,9 @@ 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() } @@ -628,6 +633,7 @@ func (vm *VM) FormatAddress(addr common.Address) (string, error) { func (vm *VM) issueTx(tx *Tx) error { select { case vm.pendingAtomicTxs <- tx: + vm.atomicTxSubmitChan <- struct{}{} default: return errTooManyAtomicTx } -- cgit v1.2.3-70-g09d2 From fbcfc73c7a92d0a2615ef90c4dffa4746161b9c7 Mon Sep 17 00:00:00 2001 From: Determinant Date: Wed, 19 Aug 2020 11:45:32 -0400 Subject: ... --- plugin/evm/block.go | 6 +++--- plugin/evm/vm.go | 7 +++++++ 2 files changed, 10 insertions(+), 3 deletions(-) (limited to 'plugin') diff --git a/plugin/evm/block.go b/plugin/evm/block.go index 45a888f..a2ffb69 100644 --- a/plugin/evm/block.go +++ b/plugin/evm/block.go @@ -80,12 +80,12 @@ func (b *Block) Verify() error { p = p.Parent().(*Block) } for i := len(path) - 1; i >= 0; i-- { - inputs_copy := new(ids.Set) + inputsCopy := new(ids.Set) p := path[i] atx := vm.getAtomicTx(p.ethBlock) inputs.Union(atx.UnsignedTx.(UnsignedAtomicTx).InputUTXOs()) - inputs_copy.Union(*inputs) - vm.blockAtomicInputCache.Put(p.ID(), inputs_copy) + inputsCopy.Union(*inputs) + vm.blockAtomicInputCache.Put(p.ID(), inputsCopy) } tx := b.vm.getAtomicTx(b.ethBlock) atx := tx.UnsignedTx.(*UnsignedImportTx) diff --git a/plugin/evm/vm.go b/plugin/evm/vm.go index 7a310bf..b72ac49 100644 --- a/plugin/evm/vm.go +++ b/plugin/evm/vm.go @@ -173,6 +173,8 @@ func (vm *VM) Initialize( } vm.ctx = ctx + vm.avaxAssetID = ctx.AVAXAssetID + vm.avm = ctx.XChainID vm.chaindb = Database{db} g := new(core.Genesis) err := json.Unmarshal(b, g) @@ -237,6 +239,11 @@ func (vm *VM) Initialize( chain.SetOnExtraStateChange(func(block *types.Block, statedb *state.StateDB) error { atx := vm.getAtomicTx(block).UnsignedTx.(*UnsignedImportTx) vm.ctx.Log.Info(atx.ID().String()) + for _, to := range atx.Outs { + amount := new(big.Int) + amount.SetUint64(to.Amount) + statedb.AddBalance(to.Address, amount) + } return nil }) vm.blockCache = cache.LRU{Size: 2048} -- cgit v1.2.3-70-g09d2 From ec103c07738b3db70501dee7a7da113edbb68875 Mon Sep 17 00:00:00 2001 From: Determinant Date: Wed, 19 Aug 2020 16:13:57 -0400 Subject: ... --- plugin/evm/import_tx.go | 2 ++ plugin/evm/service.go | 2 ++ plugin/evm/vm.go | 15 ++++++++++++++- 3 files changed, 18 insertions(+), 1 deletion(-) (limited to 'plugin') diff --git a/plugin/evm/import_tx.go b/plugin/evm/import_tx.go index 84f28de..ec2ffa4 100644 --- a/plugin/evm/import_tx.go +++ b/plugin/evm/import_tx.go @@ -17,6 +17,7 @@ import ( //"github.com/ava-labs/gecko/vms/components/verify" "github.com/ava-labs/gecko/vms/secp256k1fx" "github.com/ava-labs/go-ethereum/common" + "github.com/ava-labs/go-ethereum/log" //"github.com/ava-labs/go-ethereum/crypto" ) @@ -191,6 +192,7 @@ func (vm *VM) newImportTx( } tx := &Tx{UnsignedTx: utx} if err := tx.Sign(vm.codec, signers); err != nil { + log.Info("hey here1", "err", err, "utx", utx) return nil, err } return tx, utx.Verify(vm.avm, vm.ctx, vm.txFee, vm.avaxAssetID) diff --git a/plugin/evm/service.go b/plugin/evm/service.go index c05b0a4..41cce50 100644 --- a/plugin/evm/service.go +++ b/plugin/evm/service.go @@ -21,6 +21,7 @@ import ( "github.com/ava-labs/go-ethereum/common" "github.com/ava-labs/go-ethereum/common/hexutil" "github.com/ava-labs/go-ethereum/crypto" + "github.com/ava-labs/go-ethereum/log" ) const ( @@ -253,5 +254,6 @@ func (service *AvaAPI) ImportAVAX(_ *http.Request, args *ImportAVAXArgs, respons } response.TxID = tx.ID() + log.Info("hey here2") return service.vm.issueTx(tx) } diff --git a/plugin/evm/vm.go b/plugin/evm/vm.go index b72ac49..7cbfabd 100644 --- a/plugin/evm/vm.go +++ b/plugin/evm/vm.go @@ -34,6 +34,7 @@ import ( "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/vms/secp256k1fx" //"github.com/ava-labs/gecko/utils/constants" //"github.com/ava-labs/gecko/utils/formatting" avajson "github.com/ava-labs/gecko/utils/json" @@ -98,6 +99,13 @@ func init() { errs := wrappers.Errs{} errs.Add( Codec.RegisterType(&UnsignedImportTx{}), + Codec.RegisterType(&secp256k1fx.TransferInput{}), + Codec.RegisterType(&secp256k1fx.Input{}), + Codec.RegisterType(&secp256k1fx.Credential{}), + Codec.RegisterType(&secp256k1fx.TransferOutput{}), + Codec.RegisterType(&secp256k1fx.OutputOwners{}), + Codec.RegisterType(&secp256k1fx.MintOperation{}), + Codec.RegisterType(&secp256k1fx.MintOutput{}), ) if errs.Errored() { panic(errs.Err) @@ -657,6 +665,11 @@ func (vm *VM) GetAtomicUTXOs( limit int, ) ([]*avax.UTXO, ids.ShortID, ids.ID, error) { // TODO: finish this function via gRPC - utxos := []*avax.UTXO{} + utxos := []*avax.UTXO{{ + Asset: avax.Asset{ID: vm.ctx.AVAXAssetID}, + Out: &secp256k1fx.TransferOutput{ + Amt: 100, + }, + }} return utxos, ids.ShortEmpty, ids.Empty, nil } -- cgit v1.2.3-70-g09d2 From 1e9599e88a5d88e0090b0ebddfae756e343e605a Mon Sep 17 00:00:00 2001 From: Determinant Date: Wed, 19 Aug 2020 20:32:18 -0400 Subject: make the basic X-to-C logic work --- consensus/clique/clique.go | 2 +- consensus/dummy/consensus.go | 11 ++++++++--- consensus/ethash/consensus.go | 2 +- core/genesis.go | 2 +- core/rawdb/accessors_chain.go | 2 +- core/types/block.go | 14 ++++++++++--- miner/worker.go | 1 + plugin/evm/block.go | 2 +- plugin/evm/import_tx.go | 13 ++++-------- plugin/evm/service.go | 16 +++++++-------- plugin/evm/vm.go | 46 ++++++++++++++++++++++++++----------------- 11 files changed, 64 insertions(+), 47 deletions(-) (limited to 'plugin') 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/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]) } 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 a2ffb69..1cbf0b7 100644 --- a/plugin/evm/block.go +++ b/plugin/evm/block.go @@ -69,7 +69,7 @@ func (b *Block) Verify() error { path := []*Block{} inputs := new(ids.Set) for { - if p.Status() == choices.Accepted { + if p.Status() == choices.Accepted || p.(*Block).ethBlock.Hash() == vm.genesisHash { break } if ret, hit := vm.blockAtomicInputCache.Get(p.ID()); hit { diff --git a/plugin/evm/import_tx.go b/plugin/evm/import_tx.go index ec2ffa4..a61adaa 100644 --- a/plugin/evm/import_tx.go +++ b/plugin/evm/import_tx.go @@ -4,21 +4,17 @@ package evm import ( - //"crypto/ecdsa" "errors" "fmt" "github.com/ava-labs/gecko/database" "github.com/ava-labs/gecko/ids" "github.com/ava-labs/gecko/snow" - avacrypto "github.com/ava-labs/gecko/utils/crypto" + crypto "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/components/verify" "github.com/ava-labs/gecko/vms/secp256k1fx" "github.com/ava-labs/go-ethereum/common" - "github.com/ava-labs/go-ethereum/log" - //"github.com/ava-labs/go-ethereum/crypto" ) var ( @@ -68,7 +64,6 @@ func (tx *UnsignedImportTx) Verify( case tx.SourceChain.IsZero(): return errWrongChainID case !tx.SourceChain.Equals(avmID): - // TODO: remove this check if we allow for P->C swaps return errWrongChainID case len(tx.ImportedInputs) == 0: return errNoImportInputs @@ -118,7 +113,7 @@ func (tx *UnsignedImportTx) Accept(batch database.Batch) error { func (vm *VM) newImportTx( chainID ids.ID, // chain to import from to common.Address, // Address of recipient - keys []*avacrypto.PrivateKeySECP256K1R, // Keys to import the funds + keys []*crypto.PrivateKeySECP256K1R, // Keys to import the funds ) (*Tx, error) { if !vm.avm.Equals(chainID) { return nil, errWrongChainID @@ -135,7 +130,7 @@ func (vm *VM) newImportTx( } importedInputs := []*avax.TransferableInput{} - signers := [][]*avacrypto.PrivateKeySECP256K1R{} + signers := [][]*crypto.PrivateKeySECP256K1R{} importedAmount := uint64(0) now := vm.clock.Unix() @@ -172,6 +167,7 @@ func (vm *VM) newImportTx( 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, @@ -192,7 +188,6 @@ func (vm *VM) newImportTx( } tx := &Tx{UnsignedTx: utx} if err := tx.Sign(vm.codec, signers); err != nil { - log.Info("hey here1", "err", err, "utx", utx) return nil, err } return tx, utx.Verify(vm.avm, vm.ctx, vm.txFee, vm.avaxAssetID) diff --git a/plugin/evm/service.go b/plugin/evm/service.go index 41cce50..86bc706 100644 --- a/plugin/evm/service.go +++ b/plugin/evm/service.go @@ -16,12 +16,11 @@ import ( "github.com/ava-labs/coreth/core/types" "github.com/ava-labs/gecko/api" "github.com/ava-labs/gecko/utils/constants" - avacrypto "github.com/ava-labs/gecko/utils/crypto" + "github.com/ava-labs/gecko/utils/crypto" "github.com/ava-labs/gecko/utils/formatting" "github.com/ava-labs/go-ethereum/common" "github.com/ava-labs/go-ethereum/common/hexutil" - "github.com/ava-labs/go-ethereum/crypto" - "github.com/ava-labs/go-ethereum/log" + ethcrypto "github.com/ava-labs/go-ethereum/crypto" ) const ( @@ -64,7 +63,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 @@ -101,7 +100,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 } @@ -180,7 +179,7 @@ func (service *AvaAPI) ImportKey(r *http.Request, args *ImportKeyArgs, reply *ap user := user{db: db} - factory := avacrypto.FactorySECP256K1R{} + factory := crypto.FactorySECP256K1R{} if !strings.HasPrefix(args.PrivateKey, constants.SecretKeyPrefix) { return fmt.Errorf("private key missing %s prefix", constants.SecretKeyPrefix) @@ -195,7 +194,7 @@ func (service *AvaAPI) ImportKey(r *http.Request, args *ImportKeyArgs, reply *ap if err != nil { return fmt.Errorf("problem parsing private key: %w", err) } - sk := skIntf.(*avacrypto.PrivateKeySECP256K1R) + sk := skIntf.(*crypto.PrivateKeySECP256K1R) if err := user.putAddress(sk); err != nil { return fmt.Errorf("problem saving key %w", err) @@ -203,7 +202,7 @@ func (service *AvaAPI) ImportKey(r *http.Request, args *ImportKeyArgs, reply *ap // TODO: return eth address here reply.Address, err = service.vm.FormatAddress( - crypto.PubkeyToAddress(*(sk.PublicKey().(*avacrypto.PublicKeySECP256K1R).ToECDSA()))) + ethcrypto.PubkeyToAddress(*(sk.PublicKey().(*crypto.PublicKeySECP256K1R).ToECDSA()))) if err != nil { return fmt.Errorf("problem formatting address: %w", err) } @@ -254,6 +253,5 @@ func (service *AvaAPI) ImportAVAX(_ *http.Request, args *ImportAVAXArgs, respons } response.TxID = tx.ID() - log.Info("hey here2") return service.vm.issueTx(tx) } diff --git a/plugin/evm/vm.go b/plugin/evm/vm.go index 7cbfabd..baf6106 100644 --- a/plugin/evm/vm.go +++ b/plugin/evm/vm.go @@ -34,13 +34,11 @@ import ( "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/vms/secp256k1fx" - //"github.com/ava-labs/gecko/utils/constants" - //"github.com/ava-labs/gecko/utils/formatting" avajson "github.com/ava-labs/gecko/utils/json" "github.com/ava-labs/gecko/utils/timer" "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" ) @@ -153,9 +151,9 @@ type VM struct { } func (vm *VM) getAtomicTx(block *types.Block) *Tx { - var atx *Tx + atx := new(Tx) if extdata := block.ExtraData(); extdata != nil { - if err := vm.codec.Unmarshal(block.ExtraData(), atx); err != nil { + if err := vm.codec.Unmarshal(extdata, atx); err != nil { panic(err) } } @@ -212,19 +210,24 @@ 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: + for _, to := range atx.UnsignedTx.(*UnsignedImportTx).Outs { + amount := new(big.Int) + amount.SetUint64(to.Amount) + state.AddBalance(to.Address, amount) + } raw, _ := vm.codec.Marshal(atx) - block.SetExtraData(raw) - // TODO: make sure the atomic Tx is valid + 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") @@ -234,6 +237,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() @@ -246,7 +252,6 @@ func (vm *VM) Initialize( }) chain.SetOnExtraStateChange(func(block *types.Block, statedb *state.StateDB) error { atx := vm.getAtomicTx(block).UnsignedTx.(*UnsignedImportTx) - vm.ctx.Log.Info(atx.ID().String()) for _, to := range atx.Outs { amount := new(big.Int) amount.SetUint64(to.Amount) @@ -282,6 +287,7 @@ func (vm *VM) Initialize( 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() { @@ -523,7 +529,7 @@ func (vm *VM) tryBlockGen() error { if err != nil { return err } - if size == 0 { + if size == 0 && len(vm.pendingAtomicTxs) == 0 { return nil } @@ -648,7 +654,10 @@ func (vm *VM) FormatAddress(addr common.Address) (string, error) { func (vm *VM) issueTx(tx *Tx) error { select { case vm.pendingAtomicTxs <- tx: - vm.atomicTxSubmitChan <- struct{}{} + select { + case vm.atomicTxSubmitChan <- struct{}{}: + default: + } default: return errTooManyAtomicTx } @@ -666,7 +675,8 @@ func (vm *VM) GetAtomicUTXOs( ) ([]*avax.UTXO, ids.ShortID, ids.ID, error) { // TODO: finish this function via gRPC utxos := []*avax.UTXO{{ - Asset: avax.Asset{ID: vm.ctx.AVAXAssetID}, + UTXOID: avax.UTXOID{TxID: ids.Empty}, + Asset: avax.Asset{ID: vm.ctx.AVAXAssetID}, Out: &secp256k1fx.TransferOutput{ Amt: 100, }, -- cgit v1.2.3-70-g09d2 From b9b9f523b3a745ca0c5809ea2d1c76c80a4134cb Mon Sep 17 00:00:00 2001 From: Determinant Date: Wed, 19 Aug 2020 21:02:34 -0400 Subject: finish flow-checking --- plugin/evm/import_tx.go | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) (limited to 'plugin') diff --git a/plugin/evm/import_tx.go b/plugin/evm/import_tx.go index a61adaa..0e77efc 100644 --- a/plugin/evm/import_tx.go +++ b/plugin/evm/import_tx.go @@ -94,7 +94,22 @@ func (tx *UnsignedImportTx) SemanticVerify( if err := tx.Verify(vm.avm, vm.ctx, vm.txFee, vm.avaxAssetID); err != nil { return permError{err} } - // TODO: verify using avax.VerifyTx(vm.txFee, vm.avaxAssetID, tx.Ins, outs) + + // do flow-checking + fc := avax.NewFlowChecker() + fc.Produce(vm.avaxAssetID, vm.txFee) + + for _, out := range tx.Outs { + fc.Produce(vm.avaxAssetID, out.Amount) + } + + for _, in := range tx.ImportedInputs { + fc.Consume(in.AssetID(), in.Input().Amount()) + } + if err := fc.Verify(); err != nil { + return permError{err} + } + // TODO: verify UTXO inputs via gRPC (with creds) return nil } -- cgit v1.2.3-70-g09d2 From 0b934ef60fd652de038a21e6f25691b11cea7121 Mon Sep 17 00:00:00 2001 From: Determinant Date: Wed, 19 Aug 2020 21:20:58 -0400 Subject: remove base_tx.go --- plugin/evm/base_tx.go | 85 ------------------------------------------------- plugin/evm/import_tx.go | 35 +++++++++++++------- plugin/evm/tx.go | 21 ++++++++++++ 3 files changed, 45 insertions(+), 96 deletions(-) delete mode 100644 plugin/evm/base_tx.go (limited to 'plugin') diff --git a/plugin/evm/base_tx.go b/plugin/evm/base_tx.go deleted file mode 100644 index b3e85cd..0000000 --- a/plugin/evm/base_tx.go +++ /dev/null @@ -1,85 +0,0 @@ -package evm - -import ( - "errors" - - "github.com/ava-labs/gecko/ids" - "github.com/ava-labs/gecko/snow" - "github.com/ava-labs/gecko/vms/components/avax" - "github.com/ava-labs/go-ethereum/common" -) - -// Max size of memo field -// Don't change without also changing avm.maxMemoSize -const maxMemoSize = 256 - -var ( - errVMNil = errors.New("tx.vm is nil") - errWrongBlockchainID = errors.New("wrong blockchain ID provided") - errWrongNetworkID = errors.New("tx was issued with a different network ID") - errNilTx = errors.New("tx is nil") - errInvalidID = errors.New("invalid ID") - errOutputsNotSorted = errors.New("outputs not sorted") -) - -type EVMOutput struct { - Address common.Address `serialize:"true" json:"address"` - Amount uint64 `serialize:"true" json:"amount"` -} - -func (out *EVMOutput) Verify() error { - return nil -} - -// BaseTx contains fields common to many transaction types. It should be -// embedded in transaction implementations. The serialized fields of this struct -// should be exactly the same as those of avm.BaseTx. Do not change this -// struct's serialized fields without doing the same on avm.BaseTx. -// TODO: Factor out this and avm.BaseTX -type BaseTx 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. In practice is always the empty ID. - // This is only here to match avm.BaseTx's format - BlockchainID ids.ID `serialize:"true" json:"blockchainID"` - // Outputs - Outs []EVMOutput `serialize:"true" json:"outputs"` - // Inputs consumed by this tx - Ins []*avax.TransferableInput `serialize:"true" json:"inputs"` - // Memo field contains arbitrary bytes, up to maxMemoSize - Memo []byte `serialize:"true" json:"memo"` -} - -// Verify returns nil iff this tx is well formed -func (tx *BaseTx) Verify(ctx *snow.Context) error { - switch { - case tx == nil: - return errNilTx - case tx.syntacticallyVerified: // already passed syntactic verification - return nil - 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.Ins { - if err := in.Verify(); err != nil { - return err - } - } - switch { - // TODO: check whether output addreses are sorted? - case !avax.IsSortedAndUniqueTransferableInputs(tx.Ins): - return errInputsNotSortedUnique - default: - return nil - } -} diff --git a/plugin/evm/import_tx.go b/plugin/evm/import_tx.go index 0e77efc..2e49493 100644 --- a/plugin/evm/import_tx.go +++ b/plugin/evm/import_tx.go @@ -31,7 +31,18 @@ var ( // UnsignedImportTx is an unsigned ImportTx type UnsignedImportTx struct { - BaseTx `serialize:"true"` + 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. In practice is always the empty ID. + // This is only here to match avm.BaseTx's format + BlockchainID ids.ID `serialize:"true" json:"blockchainID"` + // Outputs + Outs []EVMOutput `serialize:"true" json:"outputs"` + // Memo field contains arbitrary bytes, up to maxMemoSize + Memo []byte `serialize:"true" json:"memo"` // Which chain to consume the funds from SourceChain ids.ID `serialize:"true" json:"sourceChain"` @@ -67,10 +78,16 @@ func (tx *UnsignedImportTx) Verify( return errWrongChainID case len(tx.ImportedInputs) == 0: return errNoImportInputs + case tx.NetworkID != ctx.NetworkID: + return errWrongNetworkID + case !ctx.ChainID.Equals(tx.BlockchainID): + return errWrongBlockchainID } - if err := tx.BaseTx.Verify(ctx); err != nil { - return err + for _, out := range tx.Outs { + if err := out.Verify(); err != nil { + return err + } } for _, in := range tx.ImportedInputs { @@ -178,7 +195,6 @@ func (vm *VM) newImportTx( return nil, errNoFunds // No imported UTXOs were spendable } - ins := []*avax.TransferableInput{} outs := []EVMOutput{} if importedAmount < vm.txFee { // imported amount goes toward paying tx fee // TODO: spend EVM balance to compensate vm.txFee-importedAmount @@ -192,14 +208,11 @@ func (vm *VM) newImportTx( // Create the transaction utx := &UnsignedImportTx{ - BaseTx: BaseTx{ - NetworkID: vm.ctx.NetworkID, - BlockchainID: vm.ctx.ChainID, - Outs: outs, - Ins: ins, - }, - SourceChain: chainID, + 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 { diff --git a/plugin/evm/tx.go b/plugin/evm/tx.go index 024e54d..6196eb8 100644 --- a/plugin/evm/tx.go +++ b/plugin/evm/tx.go @@ -4,6 +4,7 @@ package evm import ( + "errors" "fmt" "github.com/ava-labs/gecko/database" @@ -15,8 +16,28 @@ import ( "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"` +} + +func (out *EVMOutput) Verify() error { + return nil +} + // UnsignedTx is an unsigned transaction type UnsignedTx interface { Initialize(unsignedBytes, signedBytes []byte) -- cgit v1.2.3-70-g09d2 From 40879ea67433b73b464bd8b012a9809fbb646cfd Mon Sep 17 00:00:00 2001 From: Determinant Date: Wed, 19 Aug 2020 23:34:12 -0400 Subject: WIP: C-to-X transfer --- plugin/evm/import_tx.go | 16 ++++++------- plugin/evm/service.go | 3 +-- plugin/evm/tx.go | 37 ++++++++---------------------- plugin/evm/user.go | 4 +--- plugin/evm/vm.go | 61 ++++++++++++++++++++++++++++++++++++++++--------- 5 files changed, 68 insertions(+), 53 deletions(-) (limited to 'plugin') diff --git a/plugin/evm/import_tx.go b/plugin/evm/import_tx.go index 2e49493..68f34b0 100644 --- a/plugin/evm/import_tx.go +++ b/plugin/evm/import_tx.go @@ -27,6 +27,7 @@ var ( 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") ) // UnsignedImportTx is an unsigned ImportTx @@ -36,19 +37,16 @@ type UnsignedImportTx struct { syntacticallyVerified bool // ID of the network on which this tx was issued NetworkID uint32 `serialize:"true" json:"networkID"` - // ID of this blockchain. In practice is always the empty ID. - // This is only here to match avm.BaseTx's format + // ID of this blockchain. BlockchainID ids.ID `serialize:"true" json:"blockchainID"` - // Outputs - Outs []EVMOutput `serialize:"true" json:"outputs"` - // Memo field contains arbitrary bytes, up to maxMemoSize - Memo []byte `serialize:"true" json:"memo"` - // 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"` + // Memo field contains arbitrary bytes, up to maxMemoSize + Memo []byte `serialize:"true" json:"memo"` } // InputUTXOs returns the UTXOIDs of the imported funds @@ -147,7 +145,7 @@ func (vm *VM) newImportTx( to common.Address, // Address of recipient keys []*crypto.PrivateKeySECP256K1R, // Keys to import the funds ) (*Tx, error) { - if !vm.avm.Equals(chainID) { + if !vm.ctx.XChainID.Equals(chainID) { return nil, errWrongChainID } diff --git a/plugin/evm/service.go b/plugin/evm/service.go index 86bc706..3165ca7 100644 --- a/plugin/evm/service.go +++ b/plugin/evm/service.go @@ -201,8 +201,7 @@ func (service *AvaAPI) ImportKey(r *http.Request, args *ImportKeyArgs, reply *ap } // TODO: return eth address here - reply.Address, err = service.vm.FormatAddress( - ethcrypto.PubkeyToAddress(*(sk.PublicKey().(*crypto.PublicKeySECP256K1R).ToECDSA()))) + reply.Address, err = service.vm.FormatAddress(GetEthAddress(sk)) if err != nil { return fmt.Errorf("problem formatting address: %w", err) } diff --git a/plugin/evm/tx.go b/plugin/evm/tx.go index 6196eb8..90cd232 100644 --- a/plugin/evm/tx.go +++ b/plugin/evm/tx.go @@ -8,7 +8,6 @@ import ( "fmt" "github.com/ava-labs/gecko/database" - "github.com/ava-labs/gecko/database/versiondb" "github.com/ava-labs/gecko/ids" "github.com/ava-labs/gecko/snow" "github.com/ava-labs/gecko/utils/codec" @@ -38,6 +37,12 @@ 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) @@ -46,32 +51,6 @@ type UnsignedTx interface { Bytes() []byte } -// UnsignedDecisionTx is an unsigned operation that can be immediately decided -type UnsignedDecisionTx interface { - UnsignedTx - - // Attempts to verify this transaction with the provided state. - SemanticVerify(vm *VM, db database.Database, stx *Tx) ( - onAcceptFunc func() error, - err TxError, - ) -} - -// UnsignedProposalTx is an unsigned operation that can be proposed -type UnsignedProposalTx interface { - UnsignedTx - - // Attempts to verify this transaction with the provided state. - SemanticVerify(vm *VM, db database.Database, stx *Tx) ( - onCommitDB *versiondb.Database, - onAbortDB *versiondb.Database, - onCommitFunc func() error, - onAbortFunc func() error, - err TxError, - ) - InitiallyPrefersCommit(vm *VM) bool -} - // UnsignedAtomicTx is an unsigned operation that can be atomically accepted type UnsignedAtomicTx interface { UnsignedTx @@ -79,7 +58,7 @@ type UnsignedAtomicTx interface { // UTXOs this tx consumes InputUTXOs() ids.Set // Attempts to verify this transaction with the provided state. - SemanticVerify(vm *VM, db database.Database, stx *Tx) TxError + SemanticVerify(vm *VM, stx *Tx) TxError // Accept this transaction with the additionally provided state transitions. Accept(ctx *snow.Context, batch database.Batch) error @@ -94,6 +73,8 @@ type Tx struct { 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) diff --git a/plugin/evm/user.go b/plugin/evm/user.go index 5d7a037..fbf2981 100644 --- a/plugin/evm/user.go +++ b/plugin/evm/user.go @@ -11,7 +11,6 @@ import ( "github.com/ava-labs/gecko/ids" "github.com/ava-labs/gecko/utils/crypto" "github.com/ava-labs/go-ethereum/common" - ethcrypto "github.com/ava-labs/go-ethereum/crypto" ) // Key in the database whose corresponding value is the list of @@ -72,8 +71,7 @@ func (u *user) putAddress(privKey *crypto.PrivateKeySECP256K1R) error { return errKeyNil } - address := ethcrypto.PubkeyToAddress( - (*privKey.PublicKey().(*crypto.PublicKeySECP256K1R).ToECDSA())) // address the privKey controls + address := GetEthAddress(privKey) // address the privKey controls controlsAddress, err := u.controlsAddress(address) if err != nil { return err diff --git a/plugin/evm/vm.go b/plugin/evm/vm.go index baf6106..5bb8ac0 100644 --- a/plugin/evm/vm.go +++ b/plugin/evm/vm.go @@ -22,6 +22,7 @@ import ( "github.com/ava-labs/coreth/node" "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" avarpc "github.com/gorilla/rpc/v2" @@ -34,6 +35,7 @@ import ( "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/crypto" avajson "github.com/ava-labs/gecko/utils/json" "github.com/ava-labs/gecko/utils/timer" "github.com/ava-labs/gecko/utils/wrappers" @@ -48,6 +50,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 ( @@ -160,6 +163,15 @@ func (vm *VM) getAtomicTx(block *types.Block) *Tx { return atx } +func importTxStateTransfer(tx *UnsignedImportTx, state *state.StateDB) { + for _, to := range tx.Outs { + amount := new(big.Int).SetUint64(to.Amount) + state.AddBalance(to.Address, new(big.Int).Mul(amount, x2cRate)) + nonce := state.GetNonce(to.Address) + state.SetNonce(to.Address, nonce+1) + } +} + /* ****************************************************************************** ********************************* Snowman API ******************************** @@ -213,11 +225,7 @@ func (vm *VM) Initialize( chain.SetOnFinalizeAndAssemble(func(state *state.StateDB, txs []*types.Transaction) ([]byte, error) { select { case atx := <-vm.pendingAtomicTxs: - for _, to := range atx.UnsignedTx.(*UnsignedImportTx).Outs { - amount := new(big.Int) - amount.SetUint64(to.Amount) - state.AddBalance(to.Address, amount) - } + importTxStateTransfer(atx.UnsignedTx.(*UnsignedImportTx), state) raw, _ := vm.codec.Marshal(atx) return raw, nil default: @@ -250,13 +258,9 @@ func (vm *VM) Initialize( chain.SetOnQueryAcceptedBlock(func() *types.Block { return vm.getLastAccepted().ethBlock }) - chain.SetOnExtraStateChange(func(block *types.Block, statedb *state.StateDB) error { + chain.SetOnExtraStateChange(func(block *types.Block, state *state.StateDB) error { atx := vm.getAtomicTx(block).UnsignedTx.(*UnsignedImportTx) - for _, to := range atx.Outs { - amount := new(big.Int) - amount.SetUint64(to.Amount) - statedb.AddBalance(to.Address, amount) - } + importTxStateTransfer(atx, state) return nil }) vm.blockCache = cache.LRU{Size: 2048} @@ -683,3 +687,38 @@ func (vm *VM) GetAtomicUTXOs( }} return utxos, ids.ShortEmpty, ids.Empty, nil } + +func GetEthAddress(privKey *crypto.PrivateKeySECP256K1R) common.Address { + return ethcrypto.PubkeyToAddress( + (*privKey.PublicKey().(*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 amount < balance { + balance = amount + } + inputs = append(inputs, EVMInput{ + Address: addr, + Amount: balance, + }) + signers = append(signers, []*crypto.PrivateKeySECP256K1R{key}) + amount -= balance + } + if amount > 0 { + return nil, nil, errInsufficientFunds + } + return inputs, signers, nil +} -- cgit v1.2.3-70-g09d2 From f3eb89cbe4611c9372ece05eaf5cb7a609954b8e Mon Sep 17 00:00:00 2001 From: Determinant Date: Wed, 19 Aug 2020 23:34:33 -0400 Subject: ... --- plugin/evm/export_tx.go | 155 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 155 insertions(+) create mode 100644 plugin/evm/export_tx.go (limited to 'plugin') diff --git a/plugin/evm/export_tx.go b/plugin/evm/export_tx.go new file mode 100644 index 0000000..3a544a6 --- /dev/null +++ b/plugin/evm/export_tx.go @@ -0,0 +1,155 @@ +// (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/snow" + "github.com/ava-labs/gecko/utils/crypto" + "github.com/ava-labs/gecko/vms/components/avax" + "github.com/ava-labs/gecko/vms/secp256k1fx" + + safemath "github.com/ava-labs/gecko/utils/math" +) + +var ( + errNoExportOutputs = errors.New("no export outputs") + errOutputsNotSorted = errors.New("outputs not sorted") + errOverflowExport = errors.New("overflow when computing export amount + txFee") + + _ UnsignedAtomicTx = &UnsignedExportTx{} +) + +// 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} + } + + // TODO: credential check + // TODO: flow check + // TODO: verify UTXO outputs via gRPC + return nil +} + +// Accept this transaction. +func (tx *UnsignedExportTx) Accept(ctx *snow.Context, batch database.Batch) error { + // TODO: finish this function via gRPC + return nil +} + +// 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) +} -- cgit v1.2.3-70-g09d2 From c6fbdb0dc9453bf4dbf43490d7d83b7a4de2f182 Mon Sep 17 00:00:00 2001 From: Determinant Date: Wed, 19 Aug 2020 23:42:43 -0400 Subject: clean up code --- plugin/evm/export_tx.go | 2 -- plugin/evm/import_tx.go | 14 +++++++++++++- plugin/evm/tx.go | 4 ++++ plugin/evm/vm.go | 14 ++------------ 4 files changed, 19 insertions(+), 15 deletions(-) (limited to 'plugin') diff --git a/plugin/evm/export_tx.go b/plugin/evm/export_tx.go index 3a544a6..0314f0c 100644 --- a/plugin/evm/export_tx.go +++ b/plugin/evm/export_tx.go @@ -21,8 +21,6 @@ var ( errNoExportOutputs = errors.New("no export outputs") errOutputsNotSorted = errors.New("outputs not sorted") errOverflowExport = errors.New("overflow when computing export amount + txFee") - - _ UnsignedAtomicTx = &UnsignedExportTx{} ) // UnsignedExportTx is an unsigned ExportTx diff --git a/plugin/evm/import_tx.go b/plugin/evm/import_tx.go index 68f34b0..d6e03dc 100644 --- a/plugin/evm/import_tx.go +++ b/plugin/evm/import_tx.go @@ -6,6 +6,9 @@ package evm import ( "errors" "fmt" + "math/big" + + "github.com/ava-labs/coreth/core/state" "github.com/ava-labs/gecko/database" "github.com/ava-labs/gecko/ids" @@ -134,7 +137,7 @@ func (tx *UnsignedImportTx) SemanticVerify( // 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(batch database.Batch) error { +func (tx *UnsignedImportTx) Accept(ctx *snow.Context, batch database.Batch) error { // TODO: finish this function via gRPC return nil } @@ -218,3 +221,12 @@ func (vm *VM) newImportTx( } return tx, utx.Verify(vm.avm, vm.ctx, vm.txFee, vm.avaxAssetID) } + +func (tx *UnsignedImportTx) EVMStateTransfer(state *state.StateDB) { + for _, to := range tx.Outs { + amount := new(big.Int).SetUint64(to.Amount) + state.AddBalance(to.Address, new(big.Int).Mul(amount, x2cRate)) + nonce := state.GetNonce(to.Address) + state.SetNonce(to.Address, nonce+1) + } +} diff --git a/plugin/evm/tx.go b/plugin/evm/tx.go index 90cd232..dfd49f0 100644 --- a/plugin/evm/tx.go +++ b/plugin/evm/tx.go @@ -7,6 +7,8 @@ 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" @@ -62,6 +64,8 @@ type UnsignedAtomicTx interface { // Accept this transaction with the additionally provided state transitions. Accept(ctx *snow.Context, batch database.Batch) error + + EVMStateTransfer(state *state.StateDB) } // Tx is a signed transaction diff --git a/plugin/evm/vm.go b/plugin/evm/vm.go index 5bb8ac0..292e383 100644 --- a/plugin/evm/vm.go +++ b/plugin/evm/vm.go @@ -163,15 +163,6 @@ func (vm *VM) getAtomicTx(block *types.Block) *Tx { return atx } -func importTxStateTransfer(tx *UnsignedImportTx, state *state.StateDB) { - for _, to := range tx.Outs { - amount := new(big.Int).SetUint64(to.Amount) - state.AddBalance(to.Address, new(big.Int).Mul(amount, x2cRate)) - nonce := state.GetNonce(to.Address) - state.SetNonce(to.Address, nonce+1) - } -} - /* ****************************************************************************** ********************************* Snowman API ******************************** @@ -225,7 +216,7 @@ func (vm *VM) Initialize( chain.SetOnFinalizeAndAssemble(func(state *state.StateDB, txs []*types.Transaction) ([]byte, error) { select { case atx := <-vm.pendingAtomicTxs: - importTxStateTransfer(atx.UnsignedTx.(*UnsignedImportTx), state) + atx.UnsignedTx.(UnsignedAtomicTx).EVMStateTransfer(state) raw, _ := vm.codec.Marshal(atx) return raw, nil default: @@ -259,8 +250,7 @@ func (vm *VM) Initialize( return vm.getLastAccepted().ethBlock }) chain.SetOnExtraStateChange(func(block *types.Block, state *state.StateDB) error { - atx := vm.getAtomicTx(block).UnsignedTx.(*UnsignedImportTx) - importTxStateTransfer(atx, state) + vm.getAtomicTx(block).UnsignedTx.(UnsignedAtomicTx).EVMStateTransfer(state) return nil }) vm.blockCache = cache.LRU{Size: 2048} -- cgit v1.2.3-70-g09d2 From 2d409cb9790e55fd014546222f448786bbefa46b Mon Sep 17 00:00:00 2001 From: Determinant Date: Wed, 19 Aug 2020 23:48:47 -0400 Subject: ... --- plugin/evm/export_tx.go | 7 ------- plugin/evm/factory.go | 8 ++------ plugin/evm/import_tx.go | 26 ++++++-------------------- plugin/evm/vm.go | 33 +++++++++++++++++++++------------ 4 files changed, 29 insertions(+), 45 deletions(-) (limited to 'plugin') diff --git a/plugin/evm/export_tx.go b/plugin/evm/export_tx.go index 0314f0c..249b7cf 100644 --- a/plugin/evm/export_tx.go +++ b/plugin/evm/export_tx.go @@ -4,7 +4,6 @@ package evm import ( - "errors" "fmt" "github.com/ava-labs/gecko/database" @@ -17,12 +16,6 @@ import ( safemath "github.com/ava-labs/gecko/utils/math" ) -var ( - errNoExportOutputs = errors.New("no export outputs") - errOutputsNotSorted = errors.New("outputs not sorted") - errOverflowExport = errors.New("overflow when computing export amount + txFee") -) - // UnsignedExportTx is an unsigned ExportTx type UnsignedExportTx struct { avax.Metadata diff --git a/plugin/evm/factory.go b/plugin/evm/factory.go index ae2ea27..6ae62ae 100644 --- a/plugin/evm/factory.go +++ b/plugin/evm/factory.go @@ -14,16 +14,12 @@ var ( // Factory ... type Factory struct { - AVAX ids.ID - AVM ids.ID - Fee uint64 + Fee uint64 } // New ... func (f *Factory) New() interface{} { return &VM{ - avaxAssetID: f.AVAX, - avm: f.AVM, - txFee: f.Fee, + txFee: f.Fee, } } diff --git a/plugin/evm/import_tx.go b/plugin/evm/import_tx.go index d6e03dc..995e488 100644 --- a/plugin/evm/import_tx.go +++ b/plugin/evm/import_tx.go @@ -4,7 +4,6 @@ package evm import ( - "errors" "fmt" "math/big" @@ -13,26 +12,13 @@ import ( "github.com/ava-labs/gecko/database" "github.com/ava-labs/gecko/ids" "github.com/ava-labs/gecko/snow" - crypto "github.com/ava-labs/gecko/utils/crypto" + "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" ) -var ( - 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") -) - // UnsignedImportTx is an unsigned ImportTx type UnsignedImportTx struct { avax.Metadata @@ -109,16 +95,16 @@ func (tx *UnsignedImportTx) SemanticVerify( vm *VM, stx *Tx, ) TxError { - if err := tx.Verify(vm.avm, vm.ctx, vm.txFee, vm.avaxAssetID); err != nil { + 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.avaxAssetID, vm.txFee) + fc.Produce(vm.ctx.AVAXAssetID, vm.txFee) for _, out := range tx.Outs { - fc.Produce(vm.avaxAssetID, out.Amount) + fc.Produce(vm.ctx.AVAXAssetID, out.Amount) } for _, in := range tx.ImportedInputs { @@ -168,7 +154,7 @@ func (vm *VM) newImportTx( importedAmount := uint64(0) now := vm.clock.Unix() for _, utxo := range atomicUTXOs { - if !utxo.AssetID().Equals(vm.avaxAssetID) { + if !utxo.AssetID().Equals(vm.ctx.AVAXAssetID) { continue } inputIntf, utxoSigners, err := kc.Spend(utxo.Out, now) @@ -219,7 +205,7 @@ func (vm *VM) newImportTx( if err := tx.Sign(vm.codec, signers); err != nil { return nil, err } - return tx, utx.Verify(vm.avm, vm.ctx, vm.txFee, vm.avaxAssetID) + return tx, utx.Verify(vm.ctx.XChainID, vm.ctx, vm.txFee, vm.ctx.AVAXAssetID) } func (tx *UnsignedImportTx) EVMStateTransfer(state *state.StateDB) { diff --git a/plugin/evm/vm.go b/plugin/evm/vm.go index 292e383..53810ee 100644 --- a/plugin/evm/vm.go +++ b/plugin/evm/vm.go @@ -74,14 +74,27 @@ const ( ) 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") - errInvalidAddr = errors.New("invalid hex address") - errTooManyAtomicTx = errors.New("too many pending atomix txs") + 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 atomix 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") ) func maxDuration(x, y time.Duration) time.Duration { @@ -146,8 +159,6 @@ type VM struct { atomicTxSubmitChan chan struct{} codec codec.Codec clock timer.Clock - avaxAssetID ids.ID - avm ids.ID txFee uint64 pendingAtomicTxs chan *Tx blockAtomicInputCache cache.LRU @@ -182,8 +193,6 @@ func (vm *VM) Initialize( } vm.ctx = ctx - vm.avaxAssetID = ctx.AVAXAssetID - vm.avm = ctx.XChainID vm.chaindb = Database{db} g := new(core.Genesis) err := json.Unmarshal(b, g) -- cgit v1.2.3-70-g09d2 From 0c417aaa02c5cc8bb6a9629ac2502e0c4d200071 Mon Sep 17 00:00:00 2001 From: Determinant Date: Thu, 20 Aug 2020 00:03:12 -0400 Subject: impl state transfer for C-to-X Tx --- plugin/evm/export_tx.go | 24 ++++++++++++++++++++++-- plugin/evm/import_tx.go | 23 ++++++++++++++++------- plugin/evm/tx.go | 2 +- plugin/evm/vm.go | 9 +++++++++ 4 files changed, 48 insertions(+), 10 deletions(-) (limited to 'plugin') diff --git a/plugin/evm/export_tx.go b/plugin/evm/export_tx.go index 249b7cf..9af11e0 100644 --- a/plugin/evm/export_tx.go +++ b/plugin/evm/export_tx.go @@ -5,15 +5,17 @@ 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" + safemath "github.com/ava-labs/gecko/utils/math" "github.com/ava-labs/gecko/vms/components/avax" "github.com/ava-labs/gecko/vms/secp256k1fx" - - safemath "github.com/ava-labs/gecko/utils/math" ) // UnsignedExportTx is an unsigned ExportTx @@ -31,6 +33,8 @@ type UnsignedExportTx struct { Ins []EVMInput `serialize:"true" json:"inputs"` // Outputs that are exported to the chain ExportedOutputs []*avax.TransferableOutput `serialize:"true" json:"exportedOutputs"` + // EVM nonce + nonce uint64 } // InputUTXOs returns an empty set @@ -144,3 +148,19 @@ func (vm *VM) newExportTx( } 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 { + 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) != tx.nonce { + return errInvalidNonce + } + state.SetNonce(from.Address, tx.nonce+1) + } + return nil +} diff --git a/plugin/evm/import_tx.go b/plugin/evm/import_tx.go index 995e488..68e5fed 100644 --- a/plugin/evm/import_tx.go +++ b/plugin/evm/import_tx.go @@ -34,8 +34,8 @@ type UnsignedImportTx struct { ImportedInputs []*avax.TransferableInput `serialize:"true" json:"importedInputs"` // Outputs Outs []EVMOutput `serialize:"true" json:"outputs"` - // Memo field contains arbitrary bytes, up to maxMemoSize - Memo []byte `serialize:"true" json:"memo"` + // EVM nonce + nonce uint64 } // InputUTXOs returns the UTXOIDs of the imported funds @@ -193,6 +193,10 @@ func (vm *VM) newImportTx( }) } + nonce, err := vm.GetAcceptedNonce(to) + if err != nil { + return nil, err + } // Create the transaction utx := &UnsignedImportTx{ NetworkID: vm.ctx.NetworkID, @@ -200,6 +204,7 @@ func (vm *VM) newImportTx( Outs: outs, ImportedInputs: importedInputs, SourceChain: chainID, + nonce: nonce, } tx := &Tx{UnsignedTx: utx} if err := tx.Sign(vm.codec, signers); err != nil { @@ -208,11 +213,15 @@ func (vm *VM) newImportTx( return tx, utx.Verify(vm.ctx.XChainID, vm.ctx, vm.txFee, vm.ctx.AVAXAssetID) } -func (tx *UnsignedImportTx) EVMStateTransfer(state *state.StateDB) { +func (tx *UnsignedImportTx) EVMStateTransfer(state *state.StateDB) error { for _, to := range tx.Outs { - amount := new(big.Int).SetUint64(to.Amount) - state.AddBalance(to.Address, new(big.Int).Mul(amount, x2cRate)) - nonce := state.GetNonce(to.Address) - state.SetNonce(to.Address, nonce+1) + state.AddBalance(to.Address, + new(big.Int).Mul( + new(big.Int).SetUint64(to.Amount), x2cRate)) + if state.GetNonce(to.Address) != tx.nonce { + return errInvalidNonce + } + state.SetNonce(to.Address, tx.nonce+1) } + return nil } diff --git a/plugin/evm/tx.go b/plugin/evm/tx.go index dfd49f0..7e60c79 100644 --- a/plugin/evm/tx.go +++ b/plugin/evm/tx.go @@ -65,7 +65,7 @@ type UnsignedAtomicTx interface { // Accept this transaction with the additionally provided state transitions. Accept(ctx *snow.Context, batch database.Batch) error - EVMStateTransfer(state *state.StateDB) + EVMStateTransfer(state *state.StateDB) error } // Tx is a signed transaction diff --git a/plugin/evm/vm.go b/plugin/evm/vm.go index 53810ee..6155728 100644 --- a/plugin/evm/vm.go +++ b/plugin/evm/vm.go @@ -95,6 +95,7 @@ var ( 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 { @@ -721,3 +722,11 @@ func (vm *VM) GetSpendableCanonical(keys []*crypto.PrivateKeySECP256K1R, amount } 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 +} -- cgit v1.2.3-70-g09d2 From 9f503c997bdb67a40ac2817c6cf0eb780a86f3c1 Mon Sep 17 00:00:00 2001 From: Determinant Date: Thu, 20 Aug 2020 00:14:34 -0400 Subject: add C-to-X RPC --- plugin/evm/service.go | 60 ++++++++++++++++++++++++++++++++++++++++++++++++--- plugin/evm/vm.go | 43 +++++++++++++++++++++++++++++------- 2 files changed, 92 insertions(+), 11 deletions(-) (limited to 'plugin') diff --git a/plugin/evm/service.go b/plugin/evm/service.go index 3165ca7..29ef35d 100644 --- a/plugin/evm/service.go +++ b/plugin/evm/service.go @@ -6,6 +6,7 @@ package evm import ( "context" "crypto/rand" + "errors" "fmt" "math/big" "net/http" @@ -18,6 +19,7 @@ import ( "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" ethcrypto "github.com/ava-labs/go-ethereum/crypto" @@ -149,7 +151,7 @@ func (service *AvaAPI) ExportKey(r *http.Request, args *ExportKeyArgs, reply *Ex return fmt.Errorf("problem retrieving user '%s': %w", args.Username, err) } user := user{db: db} - if address, err := service.vm.ParseLocalAddress(args.Address); err != nil { + 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) @@ -201,7 +203,7 @@ func (service *AvaAPI) ImportKey(r *http.Request, args *ImportKeyArgs, reply *ap } // TODO: return eth address here - reply.Address, err = service.vm.FormatAddress(GetEthAddress(sk)) + reply.Address, err = service.vm.FormatEthAddress(GetEthAddress(sk)) if err != nil { return fmt.Errorf("problem formatting address: %w", err) } @@ -236,7 +238,7 @@ func (service *AvaAPI) ImportAVAX(_ *http.Request, args *ImportAVAXArgs, respons } user := user{db: db} - to, err := service.vm.ParseLocalAddress(args.To) + 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) } @@ -254,3 +256,55 @@ func (service *AvaAPI) ImportAVAX(_ *http.Request, args *ImportAVAXArgs, respons 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/vm.go b/plugin/evm/vm.go index 6155728..f62bc7b 100644 --- a/plugin/evm/vm.go +++ b/plugin/evm/vm.go @@ -25,7 +25,7 @@ import ( ethcrypto "github.com/ava-labs/go-ethereum/crypto" "github.com/ava-labs/go-ethereum/rlp" "github.com/ava-labs/go-ethereum/rpc" - avarpc "github.com/gorilla/rpc/v2" + geckorpc "github.com/gorilla/rpc/v2" "github.com/ava-labs/gecko/api/admin" "github.com/ava-labs/gecko/cache" @@ -35,8 +35,10 @@ import ( "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" - avajson "github.com/ava-labs/gecko/utils/json" + "github.com/ava-labs/gecko/utils/formatting" + geckojson "github.com/ava-labs/gecko/utils/json" "github.com/ava-labs/gecko/utils/timer" "github.com/ava-labs/gecko/utils/wrappers" "github.com/ava-labs/gecko/vms/components/avax" @@ -454,9 +456,9 @@ func (vm *VM) LastAccepted() ids.ID { // 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 := avarpc.NewServer() - server.RegisterCodec(avajson.NewCodec(), "application/json") - server.RegisterCodec(avajson.NewCodec(), "application/json;charset=UTF-8") + 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 @@ -643,18 +645,43 @@ func (vm *VM) getLastAccepted() *Block { return vm.lastAccepted } -// ParseLocalAddress takes in an address for this chain and produces the ID -func (vm *VM) ParseLocalAddress(addrStr string) (common.Address, error) { +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) FormatAddress(addr common.Address) (string, error) { +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: -- cgit v1.2.3-70-g09d2 From eeb62be039927d461bcd5bebc456e3ab1a31307c Mon Sep 17 00:00:00 2001 From: Determinant Date: Thu, 20 Aug 2020 00:27:32 -0400 Subject: move nonce to EVMOutput/Input --- plugin/evm/export_tx.go | 6 ++---- plugin/evm/import_tx.go | 17 ++++++++--------- plugin/evm/tx.go | 1 + plugin/evm/vm.go | 5 +++++ 4 files changed, 16 insertions(+), 13 deletions(-) (limited to 'plugin') diff --git a/plugin/evm/export_tx.go b/plugin/evm/export_tx.go index 9af11e0..423c754 100644 --- a/plugin/evm/export_tx.go +++ b/plugin/evm/export_tx.go @@ -33,8 +33,6 @@ type UnsignedExportTx struct { Ins []EVMInput `serialize:"true" json:"inputs"` // Outputs that are exported to the chain ExportedOutputs []*avax.TransferableOutput `serialize:"true" json:"exportedOutputs"` - // EVM nonce - nonce uint64 } // InputUTXOs returns an empty set @@ -157,10 +155,10 @@ func (tx *UnsignedExportTx) EVMStateTransfer(state *state.StateDB) error { return errInsufficientFunds } state.SubBalance(from.Address, amount) - if state.GetNonce(from.Address) != tx.nonce { + if state.GetNonce(from.Address) != from.Nonce { return errInvalidNonce } - state.SetNonce(from.Address, tx.nonce+1) + state.SetNonce(from.Address, from.Nonce+1) } return nil } diff --git a/plugin/evm/import_tx.go b/plugin/evm/import_tx.go index 68e5fed..2b4f995 100644 --- a/plugin/evm/import_tx.go +++ b/plugin/evm/import_tx.go @@ -34,8 +34,6 @@ type UnsignedImportTx struct { ImportedInputs []*avax.TransferableInput `serialize:"true" json:"importedInputs"` // Outputs Outs []EVMOutput `serialize:"true" json:"outputs"` - // EVM nonce - nonce uint64 } // InputUTXOs returns the UTXOIDs of the imported funds @@ -182,6 +180,11 @@ func (vm *VM) newImportTx( 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 @@ -190,13 +193,10 @@ func (vm *VM) newImportTx( outs = append(outs, EVMOutput{ Address: to, Amount: importedAmount - vm.txFee, + Nonce: nonce, }) } - nonce, err := vm.GetAcceptedNonce(to) - if err != nil { - return nil, err - } // Create the transaction utx := &UnsignedImportTx{ NetworkID: vm.ctx.NetworkID, @@ -204,7 +204,6 @@ func (vm *VM) newImportTx( Outs: outs, ImportedInputs: importedInputs, SourceChain: chainID, - nonce: nonce, } tx := &Tx{UnsignedTx: utx} if err := tx.Sign(vm.codec, signers); err != nil { @@ -218,10 +217,10 @@ func (tx *UnsignedImportTx) EVMStateTransfer(state *state.StateDB) error { state.AddBalance(to.Address, new(big.Int).Mul( new(big.Int).SetUint64(to.Amount), x2cRate)) - if state.GetNonce(to.Address) != tx.nonce { + if state.GetNonce(to.Address) != to.Nonce { return errInvalidNonce } - state.SetNonce(to.Address, tx.nonce+1) + state.SetNonce(to.Address, to.Nonce+1) } return nil } diff --git a/plugin/evm/tx.go b/plugin/evm/tx.go index 7e60c79..789ce56 100644 --- a/plugin/evm/tx.go +++ b/plugin/evm/tx.go @@ -33,6 +33,7 @@ var ( 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 { diff --git a/plugin/evm/vm.go b/plugin/evm/vm.go index f62bc7b..38646c9 100644 --- a/plugin/evm/vm.go +++ b/plugin/evm/vm.go @@ -737,9 +737,14 @@ func (vm *VM) GetSpendableCanonical(keys []*crypto.PrivateKeySECP256K1R, amount 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 -- cgit v1.2.3-70-g09d2 From 5fc26bb832c715cfd2628585d41bb82209ecdfe4 Mon Sep 17 00:00:00 2001 From: Determinant Date: Thu, 20 Aug 2020 00:44:56 -0400 Subject: ... --- plugin/evm/block.go | 65 +++++++++++++++++++++++++++++------------------------ plugin/evm/vm.go | 1 + 2 files changed, 37 insertions(+), 29 deletions(-) (limited to 'plugin') diff --git a/plugin/evm/block.go b/plugin/evm/block.go index 1cbf0b7..d75ea1d 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" @@ -62,39 +63,45 @@ func (b *Block) Parent() snowman.Block { // Verify implements the snowman.Block interface func (b *Block) Verify() error { vm := b.vm - 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 + tx := vm.getAtomicTx(b.ethBlock) + switch atx := tx.UnsignedTx.(type) { + case *UnsignedImportTx: + if b.ethBlock.Hash() == vm.genesisHash { + return nil } - if ret, hit := vm.blockAtomicInputCache.Get(p.ID()); hit { - inputs = ret.(*ids.Set) - break + 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) } - 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) - inputs.Union(atx.UnsignedTx.(UnsignedAtomicTx).InputUTXOs()) - inputsCopy.Union(*inputs) - vm.blockAtomicInputCache.Put(p.ID(), inputsCopy) - } - tx := b.vm.getAtomicTx(b.ethBlock) - atx := tx.UnsignedTx.(*UnsignedImportTx) - for _, in := range atx.InputUTXOs().List() { - if inputs.Contains(in) { - return errInvalidBlock + for i := len(path) - 1; i >= 0; i-- { + inputsCopy := new(ids.Set) + p := path[i] + atx := vm.getAtomicTx(p.ethBlock) + 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 atx.SemanticVerify(b.vm, tx) != nil { + + if tx.UnsignedTx.(UnsignedAtomicTx).SemanticVerify(vm, tx) != nil { return errInvalidBlock } diff --git a/plugin/evm/vm.go b/plugin/evm/vm.go index 38646c9..fffc0d5 100644 --- a/plugin/evm/vm.go +++ b/plugin/evm/vm.go @@ -116,6 +116,7 @@ func init() { errs := wrappers.Errs{} errs.Add( Codec.RegisterType(&UnsignedImportTx{}), + Codec.RegisterType(&UnsignedExportTx{}), Codec.RegisterType(&secp256k1fx.TransferInput{}), Codec.RegisterType(&secp256k1fx.Input{}), Codec.RegisterType(&secp256k1fx.Credential{}), -- cgit v1.2.3-70-g09d2 From a054d19f05a31a0421d6fe1bc534da46921481d5 Mon Sep 17 00:00:00 2001 From: Determinant Date: Thu, 20 Aug 2020 01:03:31 -0400 Subject: fix minor bugs --- plugin/evm/export_tx.go | 2 ++ plugin/evm/vm.go | 11 ++++++++--- 2 files changed, 10 insertions(+), 3 deletions(-) (limited to 'plugin') diff --git a/plugin/evm/export_tx.go b/plugin/evm/export_tx.go index 423c754..4f47a16 100644 --- a/plugin/evm/export_tx.go +++ b/plugin/evm/export_tx.go @@ -8,6 +8,7 @@ import ( "math/big" "github.com/ava-labs/coreth/core/state" + "github.com/ava-labs/go-ethereum/log" "github.com/ava-labs/gecko/database" "github.com/ava-labs/gecko/ids" @@ -149,6 +150,7 @@ func (vm *VM) newExportTx( 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 { diff --git a/plugin/evm/vm.go b/plugin/evm/vm.go index fffc0d5..8823380 100644 --- a/plugin/evm/vm.go +++ b/plugin/evm/vm.go @@ -229,7 +229,10 @@ func (vm *VM) Initialize( chain.SetOnFinalizeAndAssemble(func(state *state.StateDB, txs []*types.Transaction) ([]byte, error) { select { case atx := <-vm.pendingAtomicTxs: - atx.UnsignedTx.(UnsignedAtomicTx).EVMStateTransfer(state) + if err := atx.UnsignedTx.(UnsignedAtomicTx).EVMStateTransfer(state); err != nil { + vm.newBlockChan <- nil + return nil, err + } raw, _ := vm.codec.Marshal(atx) return raw, nil default: @@ -263,8 +266,7 @@ func (vm *VM) Initialize( return vm.getLastAccepted().ethBlock }) chain.SetOnExtraStateChange(func(block *types.Block, state *state.StateDB) error { - vm.getAtomicTx(block).UnsignedTx.(UnsignedAtomicTx).EVMStateTransfer(state) - return nil + return vm.getAtomicTx(block).UnsignedTx.(UnsignedAtomicTx).EVMStateTransfer(state) }) vm.blockCache = cache.LRU{Size: 2048} vm.blockStatusCache = cache.LRU{Size: 1024} @@ -735,6 +737,9 @@ func (vm *VM) GetSpendableCanonical(keys []*crypto.PrivateKeySECP256K1R, amount } addr := GetEthAddress(key) balance := new(big.Int).Div(state.GetBalance(addr), x2cRate).Uint64() + if balance == 0 { + continue + } if amount < balance { balance = amount } -- cgit v1.2.3-70-g09d2 From 1c078567367ef45884da079cecc09a71d1761b01 Mon Sep 17 00:00:00 2001 From: StephenButtolph Date: Thu, 20 Aug 2020 01:09:43 -0400 Subject: Added first pass of importTx verification --- plugin/evm/block.go | 15 ++++++++-- plugin/evm/import_tx.go | 54 +++++++++++++++++++++++++++++++--- plugin/evm/vm.go | 78 +++++++++++++++++++++++++++++++++++++++---------- 3 files changed, 124 insertions(+), 23 deletions(-) (limited to 'plugin') diff --git a/plugin/evm/block.go b/plugin/evm/block.go index d75ea1d..b3412a0 100644 --- a/plugin/evm/block.go +++ b/plugin/evm/block.go @@ -27,9 +27,18 @@ 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) + utx, ok := tx.UnsignedTx.(UnsignedAtomicTx) + if !ok { + return errors.New("unknown atomic tx type") + } + + return utx.Accept(vm.ctx, nil) } // Reject implements the snowman.Block interface diff --git a/plugin/evm/import_tx.go b/plugin/evm/import_tx.go index 2b4f995..35ba8cc 100644 --- a/plugin/evm/import_tx.go +++ b/plugin/evm/import_tx.go @@ -112,7 +112,49 @@ func (tx *UnsignedImportTx) SemanticVerify( return permError{err} } - // TODO: verify UTXO inputs via gRPC (with creds) + 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 } @@ -121,9 +163,13 @@ func (tx *UnsignedImportTx) SemanticVerify( // 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, batch database.Batch) error { - // TODO: finish this function via gRPC - return nil +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 diff --git a/plugin/evm/vm.go b/plugin/evm/vm.go index fffc0d5..f439f59 100644 --- a/plugin/evm/vm.go +++ b/plugin/evm/vm.go @@ -39,6 +39,7 @@ import ( "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/wrappers" "github.com/ava-labs/gecko/vms/components/avax" @@ -63,6 +64,8 @@ const ( minBlockTime = 250 * time.Millisecond maxBlockTime = 1000 * time.Millisecond batchSize = 250 + + maxUTXOsToFetch = 1024 ) const ( @@ -117,13 +120,16 @@ func init() { errs.Add( Codec.RegisterType(&UnsignedImportTx{}), Codec.RegisterType(&UnsignedExportTx{}), + ) + Codec.Skip(3) + errs.Add( Codec.RegisterType(&secp256k1fx.TransferInput{}), - Codec.RegisterType(&secp256k1fx.Input{}), - Codec.RegisterType(&secp256k1fx.Credential{}), + Codec.RegisterType(&secp256k1fx.MintOutput{}), Codec.RegisterType(&secp256k1fx.TransferOutput{}), - Codec.RegisterType(&secp256k1fx.OutputOwners{}), Codec.RegisterType(&secp256k1fx.MintOperation{}), - Codec.RegisterType(&secp256k1fx.MintOutput{}), + Codec.RegisterType(&secp256k1fx.Credential{}), + Codec.RegisterType(&secp256k1fx.Input{}), + Codec.RegisterType(&secp256k1fx.OutputOwners{}), ) if errs.Errored() { panic(errs.Err) @@ -166,6 +172,8 @@ type VM struct { txFee uint64 pendingAtomicTxs chan *Tx blockAtomicInputCache cache.LRU + + fx secp256k1fx.Fx } func (vm *VM) getAtomicTx(block *types.Block) *Tx { @@ -178,6 +186,15 @@ func (vm *VM) getAtomicTx(block *types.Block) *Tx { 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 ******************************** @@ -353,16 +370,16 @@ func (vm *VM) Initialize( }) 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 { @@ -705,15 +722,44 @@ func (vm *VM) GetAtomicUTXOs( startUTXOID ids.ID, limit int, ) ([]*avax.UTXO, ids.ShortID, ids.ID, error) { - // TODO: finish this function via gRPC - utxos := []*avax.UTXO{{ - UTXOID: avax.UTXOID{TxID: ids.Empty}, - Asset: avax.Asset{ID: vm.ctx.AVAXAssetID}, - Out: &secp256k1fx.TransferOutput{ - Amt: 100, - }, - }} - return utxos, ids.ShortEmpty, ids.Empty, nil + 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 { + lastAddrID = ids.ShortEmpty + } + + 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 { -- cgit v1.2.3-70-g09d2 From b3fb1dc6ed3c5610c03abdd6f2ff7bf164fb83ca Mon Sep 17 00:00:00 2001 From: Determinant Date: Thu, 20 Aug 2020 02:21:57 -0400 Subject: add flow check for ExportTx --- plugin/evm/export_tx.go | 32 ++++++++++++++++++++++++++++++-- plugin/evm/vm.go | 7 ++++++- 2 files changed, 36 insertions(+), 3 deletions(-) (limited to 'plugin') diff --git a/plugin/evm/export_tx.go b/plugin/evm/export_tx.go index 4f47a16..8b3c4f8 100644 --- a/plugin/evm/export_tx.go +++ b/plugin/evm/export_tx.go @@ -91,8 +91,36 @@ func (tx *UnsignedExportTx) SemanticVerify( return permError{err} } - // TODO: credential check - // TODO: flow check + 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 } diff --git a/plugin/evm/vm.go b/plugin/evm/vm.go index 8823380..98ecb6f 100644 --- a/plugin/evm/vm.go +++ b/plugin/evm/vm.go @@ -175,6 +175,7 @@ func (vm *VM) getAtomicTx(block *types.Block) *Tx { panic(err) } } + atx.Sign(vm.codec, nil) return atx } @@ -719,8 +720,12 @@ func (vm *VM) GetAtomicUTXOs( } func GetEthAddress(privKey *crypto.PrivateKeySECP256K1R) common.Address { + return PublicKeyToEthAddress(privKey.PublicKey()) +} + +func PublicKeyToEthAddress(pubKey crypto.PublicKey) common.Address { return ethcrypto.PubkeyToAddress( - (*privKey.PublicKey().(*crypto.PublicKeySECP256K1R).ToECDSA())) + (*pubKey.(*crypto.PublicKeySECP256K1R).ToECDSA())) } func (vm *VM) GetSpendableCanonical(keys []*crypto.PrivateKeySECP256K1R, amount uint64) ([]EVMInput, [][]*crypto.PrivateKeySECP256K1R, error) { -- cgit v1.2.3-70-g09d2 From 52c6ccd2f063a6cccc35f5b4380b22fb9dd0f102 Mon Sep 17 00:00:00 2001 From: StephenButtolph Date: Thu, 20 Aug 2020 09:42:06 -0400 Subject: implemented exportTx accept --- plugin/evm/block.go | 78 ++++++++++++++++++++++++++----------------------- plugin/evm/export_tx.go | 35 ++++++++++++++++++++-- plugin/evm/vm.go | 13 +++++---- 3 files changed, 82 insertions(+), 44 deletions(-) (limited to 'plugin') diff --git a/plugin/evm/block.go b/plugin/evm/block.go index b3412a0..8e13c67 100644 --- a/plugin/evm/block.go +++ b/plugin/evm/block.go @@ -33,9 +33,12 @@ func (b *Block) Accept() error { 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 atomic tx type") + return errors.New("unknown tx type") } return utx.Accept(vm.ctx, nil) @@ -73,47 +76,50 @@ func (b *Block) Parent() snowman.Block { func (b *Block) Verify() error { vm := b.vm tx := vm.getAtomicTx(b.ethBlock) - 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 tx != nil { + switch atx := tx.UnsignedTx.(type) { + case *UnsignedImportTx: + if b.ethBlock.Hash() == vm.genesisHash { + return nil } - if ret, hit := vm.blockAtomicInputCache.Get(p.ID()); hit { - inputs = ret.(*ids.Set) - break + 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) } - 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) - 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 + 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") } - case *UnsignedExportTx: - default: - return errors.New("unknown atomic tx type") - } - if tx.UnsignedTx.(UnsignedAtomicTx).SemanticVerify(vm, tx) != nil { - return errInvalidBlock + 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/export_tx.go b/plugin/evm/export_tx.go index 8b3c4f8..31b4681 100644 --- a/plugin/evm/export_tx.go +++ b/plugin/evm/export_tx.go @@ -10,6 +10,7 @@ import ( "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" @@ -126,9 +127,37 @@ func (tx *UnsignedExportTx) SemanticVerify( } // Accept this transaction. -func (tx *UnsignedExportTx) Accept(ctx *snow.Context, batch database.Batch) error { - // TODO: finish this function via gRPC - return nil +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 diff --git a/plugin/evm/vm.go b/plugin/evm/vm.go index 0910f6d..4ea3793 100644 --- a/plugin/evm/vm.go +++ b/plugin/evm/vm.go @@ -177,11 +177,10 @@ type VM struct { } func (vm *VM) getAtomicTx(block *types.Block) *Tx { + extdata := block.ExtraData() atx := new(Tx) - if extdata := block.ExtraData(); extdata != nil { - if err := vm.codec.Unmarshal(extdata, atx); err != nil { - panic(err) - } + if err := vm.codec.Unmarshal(extdata, atx); err != nil { + return nil } atx.Sign(vm.codec, nil) return atx @@ -284,7 +283,11 @@ func (vm *VM) Initialize( return vm.getLastAccepted().ethBlock }) chain.SetOnExtraStateChange(func(block *types.Block, state *state.StateDB) error { - return vm.getAtomicTx(block).UnsignedTx.(UnsignedAtomicTx).EVMStateTransfer(state) + tx := vm.getAtomicTx(block) + if tx == nil { + return nil + } + return tx.UnsignedTx.(UnsignedAtomicTx).EVMStateTransfer(state) }) vm.blockCache = cache.LRU{Size: 2048} vm.blockStatusCache = cache.LRU{Size: 1024} -- cgit v1.2.3-70-g09d2 From 6aa9e31ed3082df34373679f98ad2b004bef2ffa Mon Sep 17 00:00:00 2001 From: StephenButtolph Date: Thu, 20 Aug 2020 11:31:24 -0400 Subject: fixed copy paste bug --- plugin/evm/vm.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'plugin') diff --git a/plugin/evm/vm.go b/plugin/evm/vm.go index 4ea3793..cc58e8b 100644 --- a/plugin/evm/vm.go +++ b/plugin/evm/vm.go @@ -754,7 +754,7 @@ func (vm *VM) GetAtomicUTXOs( } lastUTXOID, err := ids.ToID(lastUTXO) if err != nil { - lastAddrID = ids.ShortEmpty + lastUTXOID = ids.Empty } utxos := make([]*avax.UTXO, len(allUTXOBytes)) -- cgit v1.2.3-70-g09d2 From 9f9c3ac8b840e99db942c01505be987d8b2663d4 Mon Sep 17 00:00:00 2001 From: Tyler Smith Date: Mon, 24 Aug 2020 19:27:32 -0700 Subject: TWEAK: Set TxPool PriceLimit to minGas. --- plugin/evm/vm.go | 1 + 1 file changed, 1 insertion(+) (limited to 'plugin') diff --git a/plugin/evm/vm.go b/plugin/evm/vm.go index 08d7bcb..f656d02 100644 --- a/plugin/evm/vm.go +++ b/plugin/evm/vm.go @@ -241,6 +241,7 @@ func (vm *VM) Initialize( // 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) -- cgit v1.2.3-70-g09d2 From b53cf297e5cccd577af725bacdb7f3ccb61336bd Mon Sep 17 00:00:00 2001 From: Tyler Smith Date: Mon, 24 Aug 2020 22:09:52 -0700 Subject: BUGFIX: Fix minGasPrice. --- plugin/evm/vm.go | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) (limited to 'plugin') diff --git a/plugin/evm/vm.go b/plugin/evm/vm.go index f656d02..b4026a8 100644 --- a/plugin/evm/vm.go +++ b/plugin/evm/vm.go @@ -20,6 +20,7 @@ import ( "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/go-ethereum/common" ethcrypto "github.com/ava-labs/go-ethereum/crypto" @@ -81,8 +82,8 @@ const ( var ( // minGasPrice is the number of nAVAX required per gas unit for a transaction - // to be valid - minGasPrice = big.NewInt(47) + // to be valid, measured in wei + minGasPrice = big.NewInt(47*params.GWei) txFee = units.MilliAvax -- cgit v1.2.3-70-g09d2 From b05d11d451fb73843abcf6bd5fc8664b069433a4 Mon Sep 17 00:00:00 2001 From: StephenButtolph Date: Tue, 25 Aug 2020 13:20:28 -0400 Subject: fixed potential vulnerability with bootstrapping blocks --- plugin/evm/block.go | 11 +++++++---- plugin/evm/vm.go | 4 ++-- 2 files changed, 9 insertions(+), 6 deletions(-) (limited to 'plugin') diff --git a/plugin/evm/block.go b/plugin/evm/block.go index 375dc6d..5c30fe1 100644 --- a/plugin/evm/block.go +++ b/plugin/evm/block.go @@ -74,10 +74,13 @@ func (b *Block) Parent() snowman.Block { // Verify implements the snowman.Block interface func (b *Block) Verify() error { - // Ensure the minimum gas price is paid for every transaction - for _, tx := range b.ethBlock.Transactions() { - if tx.GasPrice().Cmp(minGasPrice) < 0 { - return errInvalidBlock + // 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 + } } } diff --git a/plugin/evm/vm.go b/plugin/evm/vm.go index b4026a8..e1fdc33 100644 --- a/plugin/evm/vm.go +++ b/plugin/evm/vm.go @@ -83,7 +83,7 @@ const ( var ( // 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) + minGasPrice = big.NewInt(47 * params.GWei) txFee = units.MilliAvax @@ -94,7 +94,7 @@ var ( errUnsupportedFXs = errors.New("unsupported feature extensions") errInvalidBlock = errors.New("invalid block") errInvalidAddr = errors.New("invalid hex address") - errTooManyAtomicTx = errors.New("too many pending atomix txs") + 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") -- cgit v1.2.3-70-g09d2