aboutsummaryrefslogtreecommitdiff
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
parent1bb314f767785fe617c3c5efeca1a64127339506 (diff)
WIP: X-to-C transfer
-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