aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--go.mod1
-rw-r--r--go.sum3
-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
10 files changed, 848 insertions, 3 deletions
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
+}
href='#n1667'>1667 1668 1669 1670 1671 1672 1673 1674 1675 1676 1677 1678 1679 1680 1681 1682 1683 1684 1685 1686 1687 1688 1689 1690 1691 1692 1693 1694 1695 1696 1697 1698 1699 1700 1701 1702 1703 1704 1705 1706 1707 1708 1709 1710 1711 1712 1713 1714 1715 1716 1717 1718 1719 1720 1721 1722 1723 1724 1725 1726 1727 1728 1729 1730 1731 1732 1733 1734 1735 1736 1737 1738 1739 1740 1741 1742 1743 1744 1745 1746 1747 1748 1749 1750 1751 1752 1753 1754 1755 1756 1757 1758 1759 1760 1761 1762 1763 1764 1765 1766 1767 1768 1769 1770 1771 1772 1773 1774 1775 1776 1777 1778 1779 1780 1781 1782 1783 1784 1785 1786 1787 1788 1789 1790 1791 1792 1793 1794 1795 1796 1797 1798 1799 1800 1801 1802 1803 1804 1805 1806 1807 1808 1809 1810 1811 1812 1813 1814 1815 1816 1817 1818 1819 1820 1821 1822 1823 1824 1825 1826 1827 1828 1829 1830 1831 1832 1833 1834 1835 1836 1837 1838 1839 1840 1841 1842 1843 1844 1845 1846 1847 1848 1849 1850 1851 1852 1853 1854 1855 1856 1857 1858 1859 1860 1861 1862 1863 1864 1865 1866 1867 1868 1869 1870 1871 1872 1873 1874 1875 1876 1877 1878 1879 1880 1881 1882 1883 1884 1885 1886 1887 1888 1889 1890 1891 1892 1893 1894 1895 1896 1897 1898 1899 1900 1901 1902 1903 1904 1905 1906 1907 1908 1909 1910 1911 1912 1913 1914 1915 1916 1917 1918 1919 1920 1921 1922 1923 1924 1925 1926 1927 1928 1929 1930 1931 1932 1933 1934 1935 1936 1937 1938 1939 1940 1941 1942 1943 1944 1945 1946 1947 1948 1949 1950 1951 1952