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 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