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/evm') 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 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/evm') 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 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/evm') 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 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/evm') 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 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/evm') 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 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/evm') 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 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 --- 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 +++++++++++- 8 files changed, 844 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/evm') 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 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/evm') 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 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 --- 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 +++++++++++++++++++------- 9 files changed, 310 insertions(+), 251 deletions(-) delete mode 100644 plugin/evm/atomic_tx.go create mode 100644 plugin/evm/tx.go (limited to 'plugin/evm') 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.P