aboutsummaryrefslogtreecommitdiff
path: root/plugin/evm
diff options
context:
space:
mode:
authorDeterminant <tederminant@gmail.com>2020-08-13 21:11:56 -0400
committerDeterminant <tederminant@gmail.com>2020-08-13 21:11:56 -0400
commit88cc3698b3663972cd9b60faf5c14a7e1bbee965 (patch)
treeaafa80495750987f144607bd4b71154a365e7a3b /plugin/evm
parent1bb314f767785fe617c3c5efeca1a64127339506 (diff)
WIP: X-to-C transfer
Diffstat (limited to 'plugin/evm')
-rw-r--r--plugin/evm/atomic_tx.go64
-rw-r--r--plugin/evm/base_tx.go105
-rw-r--r--plugin/evm/error.go19
-rw-r--r--plugin/evm/factory.go12
-rw-r--r--plugin/evm/import_tx.go320
-rw-r--r--plugin/evm/service.go109
-rw-r--r--plugin/evm/user.go142
-rw-r--r--plugin/evm/vm.go76
8 files changed, 844 insertions, 3 deletions
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
+}
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794