aboutsummaryrefslogtreecommitdiff
path: root/plugin
diff options
context:
space:
mode:
Diffstat (limited to 'plugin')
-rw-r--r--plugin/evm/block.go67
-rw-r--r--plugin/evm/error.go19
-rw-r--r--plugin/evm/export_tx.go223
-rw-r--r--plugin/evm/factory.go10
-rw-r--r--plugin/evm/import_tx.go272
-rw-r--r--plugin/evm/service.go194
-rw-r--r--plugin/evm/tx.go112
-rw-r--r--plugin/evm/user.go146
-rw-r--r--plugin/evm/vm.go337
9 files changed, 1349 insertions, 31 deletions
diff --git a/plugin/evm/block.go b/plugin/evm/block.go
index 1a2bf9e..0779e17 100644
--- a/plugin/evm/block.go
+++ b/plugin/evm/block.go
@@ -4,6 +4,7 @@
package evm
import (
+ "errors"
"fmt"
"github.com/ava-labs/coreth/core/types"
@@ -26,9 +27,21 @@ func (b *Block) ID() ids.ID { return b.id }
// Accept implements the snowman.Block interface
func (b *Block) Accept() error {
- b.vm.ctx.Log.Verbo("Block %s is accepted", b.ID())
- b.vm.updateStatus(b.ID(), choices.Accepted)
- return nil
+ vm := b.vm
+
+ vm.ctx.Log.Verbo("Block %s is accepted", b.ID())
+ vm.updateStatus(b.ID(), choices.Accepted)
+
+ tx := vm.getAtomicTx(b.ethBlock)
+ if tx == nil {
+ return nil
+ }
+ utx, ok := tx.UnsignedTx.(UnsignedAtomicTx)
+ if !ok {
+ return errors.New("unknown tx type")
+ }
+
+ return utx.Accept(vm.ctx, nil)
}
// Reject implements the snowman.Block interface
@@ -68,8 +81,52 @@ func (b *Block) Verify() error {
}
}
- // Attempt to add the block to the chain and consider the block valid iff it's
- // accepted
+ vm := b.vm
+ tx := vm.getAtomicTx(b.ethBlock)
+ if tx != nil {
+ switch atx := tx.UnsignedTx.(type) {
+ case *UnsignedImportTx:
+ if b.ethBlock.Hash() == vm.genesisHash {
+ return nil
+ }
+ p := b.Parent()
+ path := []*Block{}
+ inputs := new(ids.Set)
+ for {
+ if p.Status() == choices.Accepted || p.(*Block).ethBlock.Hash() == vm.genesisHash {
+ break
+ }
+ if ret, hit := vm.blockAtomicInputCache.Get(p.ID()); hit {
+ inputs = ret.(*ids.Set)
+ break
+ }
+ path = append(path, p.(*Block))
+ p = p.Parent().(*Block)
+ }
+ for i := len(path) - 1; i >= 0; i-- {
+ inputsCopy := new(ids.Set)
+ p := path[i]
+ atx := vm.getAtomicTx(p.ethBlock)
+ if atx != nil {
+ inputs.Union(atx.UnsignedTx.(UnsignedAtomicTx).InputUTXOs())
+ inputsCopy.Union(*inputs)
+ }
+ vm.blockAtomicInputCache.Put(p.ID(), inputsCopy)
+ }
+ for _, in := range atx.InputUTXOs().List() {
+ if inputs.Contains(in) {
+ return errInvalidBlock
+ }
+ }
+ case *UnsignedExportTx:
+ default:
+ return errors.New("unknown atomic tx type")
+ }
+
+ if tx.UnsignedTx.(UnsignedAtomicTx).SemanticVerify(vm, tx) != nil {
+ return errInvalidBlock
+ }
+ }
_, err := b.vm.chain.InsertChain([]*types.Block{b.ethBlock})
return err
}
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/export_tx.go b/plugin/evm/export_tx.go
new file mode 100644
index 0000000..31b4681
--- /dev/null
+++ b/plugin/evm/export_tx.go
@@ -0,0 +1,223 @@
+// (c) 2019-2020, Ava Labs, Inc. All rights reserved.
+// See the file LICENSE for licensing terms.
+
+package evm
+
+import (
+ "fmt"
+ "math/big"
+
+ "github.com/ava-labs/coreth/core/state"
+ "github.com/ava-labs/go-ethereum/log"
+
+ "github.com/ava-labs/gecko/chains/atomic"
+ "github.com/ava-labs/gecko/database"
+ "github.com/ava-labs/gecko/ids"
+ "github.com/ava-labs/gecko/snow"
+ "github.com/ava-labs/gecko/utils/crypto"
+ safemath "github.com/ava-labs/gecko/utils/math"
+ "github.com/ava-labs/gecko/vms/components/avax"
+ "github.com/ava-labs/gecko/vms/secp256k1fx"
+)
+
+// UnsignedExportTx is an unsigned ExportTx
+type UnsignedExportTx struct {
+ avax.Metadata
+ // true iff this transaction has already passed syntactic verification
+ syntacticallyVerified bool
+ // ID of the network on which this tx was issued
+ NetworkID uint32 `serialize:"true" json:"networkID"`
+ // ID of this blockchain.
+ BlockchainID ids.ID `serialize:"true" json:"blockchainID"`
+ // Which chain to send the funds to
+ DestinationChain ids.ID `serialize:"true" json:"destinationChain"`
+ // Inputs
+ Ins []EVMInput `serialize:"true" json:"inputs"`
+ // Outputs that are exported to the chain
+ ExportedOutputs []*avax.TransferableOutput `serialize:"true" json:"exportedOutputs"`
+}
+
+// InputUTXOs returns an empty set
+func (tx *UnsignedExportTx) InputUTXOs() ids.Set { return ids.Set{} }
+
+// Verify this transaction is well-formed
+func (tx *UnsignedExportTx) Verify(
+ avmID ids.ID,
+ ctx *snow.Context,
+ feeAmount uint64,
+ feeAssetID ids.ID,
+) error {
+ switch {
+ case tx == nil:
+ return errNilTx
+ case tx.syntacticallyVerified: // already passed syntactic verification
+ return nil
+ case tx.DestinationChain.IsZero():
+ return errWrongChainID
+ case !tx.DestinationChain.Equals(avmID):
+ return errWrongChainID
+ case len(tx.ExportedOutputs) == 0:
+ return errNoExportOutputs
+ case tx.NetworkID != ctx.NetworkID:
+ return errWrongNetworkID
+ case !ctx.ChainID.Equals(tx.BlockchainID):
+ return errWrongBlockchainID
+ }
+
+ for _, in := range tx.Ins {
+ if err := in.Verify(); err != nil {
+ return err
+ }
+ }
+
+ for _, out := range tx.ExportedOutputs {
+ if err := out.Verify(); err != nil {
+ return err
+ }
+ }
+ if !avax.IsSortedTransferableOutputs(tx.ExportedOutputs, Codec) {
+ return errOutputsNotSorted
+ }
+
+ tx.syntacticallyVerified = true
+ return nil
+}
+
+// SemanticVerify this transaction is valid.
+func (tx *UnsignedExportTx) SemanticVerify(
+ vm *VM,
+ stx *Tx,
+) TxError {
+ if err := tx.Verify(vm.ctx.XChainID, vm.ctx, vm.txFee, vm.ctx.AVAXAssetID); err != nil {
+ return permError{err}
+ }
+
+ f := crypto.FactorySECP256K1R{}
+ for i, cred := range stx.Creds {
+ if err := cred.Verify(); err != nil {
+ return permError{err}
+ }
+ pubKey, err := f.RecoverPublicKey(tx.UnsignedBytes(), cred.(*secp256k1fx.Credential).Sigs[0][:])
+ if err != nil {
+ return permError{err}
+ }
+ if tx.Ins[i].Address != PublicKeyToEthAddress(pubKey) {
+ return permError{errPublicKeySignatureMismatch}
+ }
+ }
+
+ // do flow-checking
+ fc := avax.NewFlowChecker()
+ fc.Produce(vm.ctx.AVAXAssetID, vm.txFee)
+
+ for _, out := range tx.ExportedOutputs {
+ fc.Produce(out.AssetID(), out.Output().Amount())
+ }
+
+ for _, in := range tx.Ins {
+ fc.Consume(vm.ctx.AVAXAssetID, in.Amount)
+ }
+
+ if err := fc.Verify(); err != nil {
+ return permError{err}
+ }
+
+ // TODO: verify UTXO outputs via gRPC
+ return nil
+}
+
+// Accept this transaction.
+func (tx *UnsignedExportTx) Accept(ctx *snow.Context, _ database.Batch) error {
+ txID := tx.ID()
+
+ elems := make([]*atomic.Element, len(tx.ExportedOutputs))
+ for i, out := range tx.ExportedOutputs {
+ utxo := &avax.UTXO{
+ UTXOID: avax.UTXOID{
+ TxID: txID,
+ OutputIndex: uint32(i),
+ },
+ Asset: avax.Asset{ID: out.AssetID()},
+ Out: out.Out,
+ }
+
+ utxoBytes, err := Codec.Marshal(utxo)
+ if err != nil {
+ return err
+ }
+
+ elem := &atomic.Element{
+ Key: utxo.InputID().Bytes(),
+ Value: utxoBytes,
+ }
+ if out, ok := utxo.Out.(avax.Addressable); ok {
+ elem.Traits = out.Addresses()
+ }
+
+ elems[i] = elem
+ }
+
+ return ctx.SharedMemory.Put(tx.DestinationChain, elems)
+}
+
+// Create a new transaction
+func (vm *VM) newExportTx(
+ amount uint64, // Amount of tokens to export
+ chainID ids.ID, // Chain to send the UTXOs to
+ to ids.ShortID, // Address of chain recipient
+ keys []*crypto.PrivateKeySECP256K1R, // Pay the fee and provide the tokens
+) (*Tx, error) {
+ if !vm.ctx.XChainID.Equals(chainID) {
+ return nil, errWrongChainID
+ }
+
+ toBurn, err := safemath.Add64(amount, vm.txFee)
+ if err != nil {
+ return nil, errOverflowExport
+ }
+ ins, signers, err := vm.GetSpendableCanonical(keys, toBurn)
+ if err != nil {
+ return nil, fmt.Errorf("couldn't generate tx inputs/outputs: %w", err)
+ }
+
+ // Create the transaction
+ utx := &UnsignedExportTx{
+ NetworkID: vm.ctx.NetworkID,
+ BlockchainID: vm.ctx.ChainID,
+ DestinationChain: chainID,
+ Ins: ins,
+ ExportedOutputs: []*avax.TransferableOutput{{ // Exported to X-Chain
+ Asset: avax.Asset{ID: vm.ctx.AVAXAssetID},
+ Out: &secp256k1fx.TransferOutput{
+ Amt: amount,
+ OutputOwners: secp256k1fx.OutputOwners{
+ Locktime: 0,
+ Threshold: 1,
+ Addrs: []ids.ShortID{to},
+ },
+ },
+ }},
+ }
+ tx := &Tx{UnsignedTx: utx}
+ if err := tx.Sign(vm.codec, signers); err != nil {
+ return nil, err
+ }
+ return tx, utx.Verify(vm.ctx.XChainID, vm.ctx, vm.txFee, vm.ctx.AVAXAssetID)
+}
+
+func (tx *UnsignedExportTx) EVMStateTransfer(state *state.StateDB) error {
+ for _, from := range tx.Ins {
+ log.Info("consume", "in", from.Address, "amount", from.Amount, "nonce", from.Nonce, "nonce0", state.GetNonce(from.Address))
+ amount := new(big.Int).Mul(
+ new(big.Int).SetUint64(from.Amount), x2cRate)
+ if state.GetBalance(from.Address).Cmp(amount) < 0 {
+ return errInsufficientFunds
+ }
+ state.SubBalance(from.Address, amount)
+ if state.GetNonce(from.Address) != from.Nonce {
+ return errInvalidNonce
+ }
+ state.SetNonce(from.Address, from.Nonce+1)
+ }
+ return nil
+}
diff --git a/plugin/evm/factory.go b/plugin/evm/factory.go
index a4c0eca..6ae62ae 100644
--- a/plugin/evm/factory.go
+++ b/plugin/evm/factory.go
@@ -13,7 +13,13 @@ var (
)
// Factory ...
-type Factory struct{}
+type Factory struct {
+ Fee uint64
+}
// New ...
-func (f *Factory) New() interface{} { return &VM{} }
+func (f *Factory) New() interface{} {
+ return &VM{
+ txFee: f.Fee,
+ }
+}
diff --git a/plugin/evm/import_tx.go b/plugin/evm/import_tx.go
new file mode 100644
index 0000000..35ba8cc
--- /dev/null
+++ b/plugin/evm/import_tx.go
@@ -0,0 +1,272 @@
+// (c) 2019-2020, Ava Labs, Inc. All rights reserved.
+// See the file LICENSE for licensing terms.
+
+package evm
+
+import (
+ "fmt"
+ "math/big"
+
+ "github.com/ava-labs/coreth/core/state"
+
+ "github.com/ava-labs/gecko/database"
+ "github.com/ava-labs/gecko/ids"
+ "github.com/ava-labs/gecko/snow"
+ "github.com/ava-labs/gecko/utils/crypto"
+ "github.com/ava-labs/gecko/utils/math"
+ "github.com/ava-labs/gecko/vms/components/avax"
+ "github.com/ava-labs/gecko/vms/secp256k1fx"
+ "github.com/ava-labs/go-ethereum/common"
+)
+
+// UnsignedImportTx is an unsigned ImportTx
+type UnsignedImportTx struct {
+ avax.Metadata
+ // true iff this transaction has already passed syntactic verification
+ syntacticallyVerified bool
+ // ID of the network on which this tx was issued
+ NetworkID uint32 `serialize:"true" json:"networkID"`
+ // ID of this blockchain.
+ BlockchainID ids.ID `serialize:"true" json:"blockchainID"`
+ // Which chain to consume the funds from
+ SourceChain ids.ID `serialize:"true" json:"sourceChain"`
+ // Inputs that consume UTXOs produced on the chain
+ ImportedInputs []*avax.TransferableInput `serialize:"true" json:"importedInputs"`
+ // Outputs
+ Outs []EVMOutput `serialize:"true" json:"outputs"`
+}
+
+// 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
+}
+
+// Verify this transaction is well-formed
+func (tx *UnsignedImportTx) Verify(
+ avmID ids.ID,
+ ctx *snow.Context,
+ feeAmount uint64,
+ feeAssetID ids.ID,
+) error {
+ switch {
+ case tx == nil:
+ return errNilTx
+ case tx.syntacticallyVerified: // already passed syntactic verification
+ return nil
+ case tx.SourceChain.IsZero():
+ return errWrongChainID
+ case !tx.SourceChain.Equals(avmID):
+ return errWrongChainID
+ case len(tx.ImportedInputs) == 0:
+ return errNoImportInputs
+ case tx.NetworkID != ctx.NetworkID:
+ return errWrongNetworkID
+ case !ctx.ChainID.Equals(tx.BlockchainID):
+ return errWrongBlockchainID
+ }
+
+ for _, out := range tx.Outs {
+ if err := out.Verify(); err != nil {
+ return err
+ }
+ }
+
+ for _, in := range tx.ImportedInputs {
+ if err := in.Verify(); err != nil {
+ return err
+ }
+ }
+ if !avax.IsSortedAndUniqueTransferableInputs(tx.ImportedInputs) {
+ return errInputsNotSortedUnique
+ }
+
+ tx.syntacticallyVerified = true
+ return nil
+}
+
+// SemanticVerify this transaction is valid.
+func (tx *UnsignedImportTx) SemanticVerify(
+ vm *VM,
+ stx *Tx,
+) TxError {
+ if err := tx.Verify(vm.ctx.XChainID, vm.ctx, vm.txFee, vm.ctx.AVAXAssetID); err != nil {
+ return permError{err}
+ }
+
+ // do flow-checking
+ fc := avax.NewFlowChecker()
+ fc.Produce(vm.ctx.AVAXAssetID, vm.txFee)
+
+ for _, out := range tx.Outs {
+ fc.Produce(vm.ctx.AVAXAssetID, out.Amount)
+ }
+
+ for _, in := range tx.ImportedInputs {
+ fc.Consume(in.AssetID(), in.Input().Amount())
+ }
+ if err := fc.Verify(); err != nil {
+ return permError{err}
+ }
+
+ if !vm.ctx.IsBootstrapped() {
+ // Allow for force committing during bootstrapping
+ return nil
+ }
+
+ utxoIDs := make([][]byte, len(tx.ImportedInputs))
+ for i, in := range tx.ImportedInputs {
+ utxoIDs[i] = in.UTXOID.InputID().Bytes()
+ }
+ allUTXOBytes, err := vm.ctx.SharedMemory.Get(tx.SourceChain, utxoIDs)
+ if err != nil {
+ return tempError{err}
+ }
+
+ utxos := make([]*avax.UTXO, len(tx.ImportedInputs))
+ for i, utxoBytes := range allUTXOBytes {
+ utxo := &avax.UTXO{}
+ if err := vm.codec.Unmarshal(utxoBytes, utxo); err != nil {
+ return tempError{err}
+ }
+ utxos[i] = utxo
+ }
+
+ for i, in := range tx.ImportedInputs {
+ utxoBytes := allUTXOBytes[i]
+
+ utxo := &avax.UTXO{}
+ if err := vm.codec.Unmarshal(utxoBytes, utxo); err != nil {
+ return tempError{err}
+ }
+
+ cred := stx.Creds[i]
+
+ utxoAssetID := utxo.AssetID()
+ inAssetID := in.AssetID()
+ if !utxoAssetID.Equals(inAssetID) {
+ return permError{errAssetIDMismatch}
+ }
+
+ if err := vm.fx.VerifyTransfer(tx, in.In, cred, utxo.Out); 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(ctx *snow.Context, _ database.Batch) error {
+ // TODO: Is any batch passed in here?
+ utxoIDs := make([][]byte, len(tx.ImportedInputs))
+ for i, in := range tx.ImportedInputs {
+ utxoIDs[i] = in.InputID().Bytes()
+ }
+ return ctx.SharedMemory.Remove(tx.SourceChain, utxoIDs)
+}
+
+// Create a new transaction
+func (vm *VM) newImportTx(
+ chainID ids.ID, // chain to import from
+ to common.Address, // Address of recipient
+ keys []*crypto.PrivateKeySECP256K1R, // Keys to import the funds
+) (*Tx, error) {
+ if !vm.ctx.XChainID.Equals(chainID) {
+ return nil, errWrongChainID
+ }
+
+ kc := secp256k1fx.NewKeychain()
+ for _, key := range keys {
+ kc.Add(key)
+ }
+
+ atomicUTXOs, _, _, err := vm.GetAtomicUTXOs(chainID, kc.Addresses(), ids.ShortEmpty, ids.Empty, -1)
+ if err != nil {
+ return nil, fmt.Errorf("problem retrieving atomic UTXOs: %w", err)
+ }
+
+ importedInputs := []*avax.TransferableInput{}
+ signers := [][]*crypto.PrivateKeySECP256K1R{}
+
+ importedAmount := uint64(0)
+ now := vm.clock.Unix()
+ for _, utxo := range atomicUTXOs {
+ if !utxo.AssetID().Equals(vm.ctx.AVAXAssetID) {
+ continue
+ }
+ inputIntf, utxoSigners, err := kc.Spend(utxo.Out, now)
+ if err != nil {
+ continue
+ }
+ input, ok := inputIntf.(avax.TransferableIn)
+ if !ok {
+ continue
+ }
+ importedAmount, err = math.Add64(importedAmount, input.Amount())
+ if err != nil {
+ return nil, err
+ }
+ importedInputs = append(importedInputs, &avax.TransferableInput{
+ UTXOID: utxo.UTXOID,
+ Asset: utxo.Asset,
+ In: input,
+ })
+ signers = append(signers, utxoSigners)
+ }
+ avax.SortTransferableInputsWithSigners(importedInputs, signers)
+
+ if importedAmount == 0 {
+ return nil, errNoFunds // No imported UTXOs were spendable
+ }
+
+ nonce, err := vm.GetAcceptedNonce(to)
+ if err != nil {
+ return nil, err
+ }
+
+ outs := []EVMOutput{}
+ if importedAmount < vm.txFee { // imported amount goes toward paying tx fee
+ // TODO: spend EVM balance to compensate vm.txFee-importedAmount
+ return nil, errNoFunds
+ } else if importedAmount > vm.txFee {
+ outs = append(outs, EVMOutput{
+ Address: to,
+ Amount: importedAmount - vm.txFee,
+ Nonce: nonce,
+ })
+ }
+
+ // Create the transaction
+ utx := &UnsignedImportTx{
+ NetworkID: vm.ctx.NetworkID,
+ BlockchainID: vm.ctx.ChainID,
+ Outs: outs,
+ ImportedInputs: importedInputs,
+ SourceChain: chainID,
+ }
+ tx := &Tx{UnsignedTx: utx}
+ if err := tx.Sign(vm.codec, signers); err != nil {
+ return nil, err
+ }
+ return tx, utx.Verify(vm.ctx.XChainID, vm.ctx, vm.txFee, vm.ctx.AVAXAssetID)
+}
+
+func (tx *UnsignedImportTx) EVMStateTransfer(state *state.StateDB) error {
+ for _, to := range tx.Outs {
+ state.AddBalance(to.Address,
+ new(big.Int).Mul(
+ new(big.Int).SetUint64(to.Amount), x2cRate))
+ if state.GetNonce(to.Address) != to.Nonce {
+ return errInvalidNonce
+ }
+ state.SetNonce(to.Address, to.Nonce+1)
+ }
+ return nil
+}
diff --git a/plugin/evm/service.go b/plugin/evm/service.go
index 62b124f..29ef35d 100644
--- a/plugin/evm/service.go
+++ b/plugin/evm/service.go
@@ -6,15 +6,23 @@ package evm
import (
"context"
"crypto/rand"
+ "errors"
"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/gecko/utils/crypto"
+ "github.com/ava-labs/gecko/utils/formatting"
+ "github.com/ava-labs/gecko/utils/json"
"github.com/ava-labs/go-ethereum/common"
"github.com/ava-labs/go-ethereum/common/hexutil"
- "github.com/ava-labs/go-ethereum/crypto"
+ ethcrypto "github.com/ava-labs/go-ethereum/crypto"
)
const (
@@ -36,6 +44,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} }
@@ -55,7 +65,7 @@ type Web3API struct{}
func (s *Web3API) ClientVersion() string { return version }
// Sha3 returns the bytes returned by hashing [input] with Keccak256
-func (s *Web3API) Sha3(input hexutil.Bytes) hexutil.Bytes { return crypto.Keccak256(input) }
+func (s *Web3API) Sha3(input hexutil.Bytes) hexutil.Bytes { return ethcrypto.Keccak256(input) }
// GetAcceptedFrontReply defines the reply that will be sent from the
// GetAcceptedFront API call
@@ -92,7 +102,7 @@ func (api *DebugAPI) SpendGenesis(ctx context.Context, nonce uint64) error {
gasLimit := 21000
gasPrice := big.NewInt(1000000000)
- genPrivateKey, err := crypto.HexToECDSA(GenesisTestKey[2:])
+ genPrivateKey, err := ethcrypto.HexToECDSA(GenesisTestKey[2:])
if err != nil {
return err
}
@@ -120,3 +130,181 @@ 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.ParseEthAddress(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 = 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)
+ if service.vm.ctx.Keystore == nil {
+ return fmt.Errorf("oh no")
+ }
+ fmt.Sprintf("good")
+ db, err := service.vm.ctx.Keystore.GetDatabase(args.Username, args.Password)
+ if err != nil {
+ return fmt.Errorf("problem retrieving data: %w", err)
+ }
+
+ user := user{db: db}
+
+ factory := crypto.FactorySECP256K1R{}
+
+ if !strings.HasPrefix(args.PrivateKey, constants.SecretKeyPrefix) {
+ return fmt.Errorf("private key missing %s prefix", constants.SecretKeyPrefix)
+ }
+ trimmedPrivateKey := strings.TrimPrefix(args.PrivateKey, constants.SecretKeyPrefix)
+ formattedPrivateKey := formatting.CB58{}
+ if err := formattedPrivateKey.FromString(trimmedPrivateKey); err != nil {
+ return fmt.Errorf("problem parsing private key: %w", err)
+ }
+
+ skIntf, err := factory.ToPrivateKey(formattedPrivateKey.Bytes)
+ if err != nil {
+ return fmt.Errorf("problem parsing private key: %w", err)
+ }
+ sk := skIntf.(*crypto.PrivateKeySECP256K1R)
+
+ if err := user.putAddress(sk); err != nil {
+ return fmt.Errorf("problem saving key %w", err)
+ }
+
+ // TODO: return eth address here
+ reply.Address, err = service.vm.FormatEthAddress(GetEthAddress(sk))
+ if err != nil {
+ return fmt.Errorf("problem formatting address: %w", err)
+ }
+ return nil
+}
+
+// ImportAVAXArgs are the arguments to ImportAVAX
+type ImportAVAXArgs struct {
+ api.UserPass
+
+ // Chain the funds are coming from
+ SourceChain string `json:"sourceChain"`
+
+ // The address that will receive the imported funds
+ To string `json:"to"`
+}
+
+// ImportAVAX issues a transaction to import AVAX from the X-chain. The AVAX
+// must have already been exported from the X-Chain.
+func (service *AvaAPI) ImportAVAX(_ *http.Request, args *ImportAVAXArgs, response *api.JsonTxID) error {
+ service.vm.ctx.Log.Info("Platform: ImportAVAX called")
+
+ chainID, err := service.vm.ctx.BCLookup.Lookup(args.SourceChain)
+ if err != nil {
+ return fmt.Errorf("problem parsing chainID %q: %w", args.SourceChain, err)
+ }
+
+ // 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.ParseEthAddress(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(chainID, to, privKeys)
+ if err != nil {
+ return err
+ }
+
+ response.TxID = tx.ID()
+ return service.vm.issueTx(tx)
+}
+
+// ExportAVAXArgs are the arguments to ExportAVAX
+type ExportAVAXArgs struct {
+ api.UserPass
+
+ // Amount of AVAX to send
+ Amount json.Uint64 `json:"amount"`
+
+ // ID of the address that will receive the AVAX. This address includes the
+ // chainID, which is used to determine what the destination chain is.
+ To string `json:"to"`
+}
+
+// ExportAVAX exports AVAX from the P-Chain to the X-Chain
+// It must be imported on the X-Chain to complete the transfer
+func (service *AvaAPI) ExportAVAX(_ *http.Request, args *ExportAVAXArgs, response *api.JsonTxID) error {
+ service.vm.ctx.Log.Info("Platform: ExportAVAX called")
+
+ if args.Amount == 0 {
+ return errors.New("argument 'amount' must be > 0")
+ }
+
+ chainID, to, err := service.vm.ParseAddress(args.To)
+ if err != nil {
+ return err
+ }
+
+ // Get this user's data
+ 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}
+ privKeys, err := user.getKeys()
+ if err != nil {
+ return fmt.Errorf("couldn't get addresses controlled by the user: %w", err)
+ }
+
+ // Create the transaction
+ tx, err := service.vm.newExportTx(
+ uint64(args.Amount), // Amount
+ chainID, // ID of the chain to send the funds to
+ to, // Address
+ privKeys, // Private keys
+ )
+ if err != nil {
+ return fmt.Errorf("couldn't create tx: %w", err)
+ }
+
+ response.TxID = tx.ID()
+ return service.vm.issueTx(tx)
+}
diff --git a/plugin/evm/tx.go b/plugin/evm/tx.go
new file mode 100644
index 0000000..789ce56
--- /dev/null
+++ b/plugin/evm/tx.go
@@ -0,0 +1,112 @@
+// (c) 2019-2020, Ava Labs, Inc. All rights reserved.
+// See the file LICENSE for licensing terms.
+
+package evm
+
+import (
+ "errors"
+ "fmt"
+
+ "github.com/ava-labs/coreth/core/state"
+
+ "github.com/ava-labs/gecko/database"
+ "github.com/ava-labs/gecko/ids"
+ "github.com/ava-labs/gecko/snow"
+ "github.com/ava-labs/gecko/utils/codec"
+ "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"
+ "github.com/ava-labs/go-ethereum/common"
+)
+
+// Max size of memo field
+// Don't change without also changing avm.maxMemoSize
+const maxMemoSize = 256
+
+var (
+ errWrongBlockchainID = errors.New("wrong blockchain ID provided")
+ errWrongNetworkID = errors.New("tx was issued with a different network ID")
+ errNilTx = errors.New("tx is nil")
+)
+
+type EVMOutput struct {
+ Address common.Address `serialize:"true" json:"address"`
+ Amount uint64 `serialize:"true" json:"amount"`
+ Nonce uint64 `serialize:"true" json:"nonce"`
+}
+
+func (out *EVMOutput) Verify() error {
+ return nil
+}
+
+type EVMInput EVMOutput
+
+func (in *EVMInput) Verify() error {
+ return nil
+}
+
+// UnsignedTx is an unsigned transaction
+type UnsignedTx interface {
+ Initialize(unsignedBytes, signedBytes []byte)
+ ID() ids.ID
+ UnsignedBytes() []byte
+ Bytes() []byte
+}
+
+// UnsignedAtomicTx is an unsigned operation that can be atomically accepted
+type UnsignedAtomicTx interface {
+ UnsignedTx
+
+ // UTXOs this tx consumes
+ InputUTXOs() ids.Set
+ // Attempts to verify this transaction with the provided state.
+ SemanticVerify(vm *VM, stx *Tx) TxError
+
+ // Accept this transaction with the additionally provided state transitions.
+ Accept(ctx *snow.Context, batch database.Batch) error
+
+ EVMStateTransfer(state *state.StateDB) error
+}
+
+// Tx is a signed transaction
+type Tx struct {
+ // The body of this transaction
+ UnsignedTx `serialize:"true" json:"unsignedTx"`
+
+ // The credentials of this transaction
+ Creds []verify.Verifiable `serialize:"true" json:"credentials"`
+}
+
+// (*secp256k1fx.Credential)
+
+// Sign this transaction with the provided signers
+func (tx *Tx) Sign(c codec.Codec, signers [][]*crypto.PrivateKeySECP256K1R) error {
+ unsignedBytes, err := c.Marshal(&tx.UnsignedTx)
+ if err != nil {
+ return fmt.Errorf("couldn't marshal UnsignedTx: %w", err)
+ }
+
+ // Attach credentials
+ hash := hashing.ComputeHash256(unsignedBytes)
+ for _, keys := range signers {
+ cred := &secp256k1fx.Credential{
+ Sigs: make([][crypto.SECP256K1RSigLen]byte, len(keys)),
+ }
+ for i, key := range keys {
+ sig, err := key.SignHash(hash) // Sign hash
+ if err != nil {
+ return fmt.Errorf("problem generating credential: %w", err)
+ }
+ copy(cred.Sigs[i][:], sig)
+ }
+ tx.Creds = append(tx.Creds, cred) // Attach credential
+ }
+
+ signedBytes, err := c.Marshal(tx)
+ if err != nil {
+ return fmt.Errorf("couldn't marshal ProposalTx: %w", err)
+ }
+ tx.Initialize(unsignedBytes, signedBytes)
+ return nil
+}
diff --git a/plugin/evm/user.go b/plugin/evm/user.go
new file mode 100644
index 0000000..fbf2981
--- /dev/null
+++ b/plugin/evm/user.go
@@ -0,0 +1,146 @@
+// (c) 2019-2020, Ava Labs, Inc. All rights reserved.
+// See the file LICENSE for licensing terms.
+
+package evm
+
+import (
+ "errors"
+ "fmt"
+
+ "github.com/ava-labs/gecko/database"
+ "github.com/ava-labs/gecko/ids"
+ "github.com/ava-labs/gecko/utils/crypto"
+ "github.com/ava-labs/go-ethereum/common"
+)
+
+// 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 *crypto.PrivateKeySECP256K1R) error {
+ if privKey == nil {
+ return errKeyNil
+ }
+
+ address := GetEthAddress(privKey) // 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(), privKey.Bytes()); 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) (*crypto.PrivateKeySECP256K1R, error) {
+ if u.db == nil {
+ return nil, errDBNil
+ //} else if address.IsZero() {
+ // return nil, errEmptyAddress
+ }
+
+ factory := crypto.FactorySECP256K1R{}
+ bytes, err := u.db.Get(address.Bytes())
+ if err != nil {
+ return nil, err
+ }
+ sk, err := factory.ToPrivateKey(bytes)
+ if err != nil {
+ return nil, err
+ }
+ if sk, ok := sk.(*crypto.PrivateKeySECP256K1R); ok {
+ return sk, nil
+ }
+ return nil, fmt.Errorf("expected private key to be type *crypto.PrivateKeySECP256K1R but is type %T", sk)
+}
+
+// Return all private keys controlled by this user
+func (u *user) getKeys() ([]*crypto.PrivateKeySECP256K1R, error) {
+ addrs, err := u.getAddresses()
+ if err != nil {
+ return nil, err
+ }
+ keys := make([]*crypto.PrivateKeySECP256K1R, 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 7b4d064..f97de09 100644
--- a/plugin/evm/vm.go
+++ b/plugin/evm/vm.go
@@ -16,13 +16,16 @@ import (
"github.com/ava-labs/coreth"
"github.com/ava-labs/coreth/core"
+ "github.com/ava-labs/coreth/core/state"
"github.com/ava-labs/coreth/core/types"
"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"
+ ethcrypto "github.com/ava-labs/go-ethereum/crypto"
"github.com/ava-labs/go-ethereum/rlp"
+ "github.com/ava-labs/go-ethereum/rpc"
+ geckorpc "github.com/gorilla/rpc/v2"
"github.com/ava-labs/gecko/api/admin"
"github.com/ava-labs/gecko/cache"
@@ -31,7 +34,17 @@ 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"
+ "github.com/ava-labs/gecko/utils/constants"
+ "github.com/ava-labs/gecko/utils/crypto"
+ "github.com/ava-labs/gecko/utils/formatting"
+ geckojson "github.com/ava-labs/gecko/utils/json"
+ "github.com/ava-labs/gecko/utils/logging"
"github.com/ava-labs/gecko/utils/timer"
+ "github.com/ava-labs/gecko/utils/units"
+ "github.com/ava-labs/gecko/utils/wrappers"
+ "github.com/ava-labs/gecko/vms/components/avax"
+ "github.com/ava-labs/gecko/vms/secp256k1fx"
commonEng "github.com/ava-labs/gecko/snow/engine/common"
)
@@ -41,6 +54,7 @@ var (
0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
}
+ x2cRate = big.NewInt(1000000000)
)
const (
@@ -51,6 +65,8 @@ const (
minBlockTime = 250 * time.Millisecond
maxBlockTime = 1000 * time.Millisecond
batchSize = 250
+
+ maxUTXOsToFetch = 1024
)
const (
@@ -59,17 +75,39 @@ const (
bdTimerStateLong
)
+const (
+ addressSep = "-"
+)
+
var (
// minGasPrice is the number of nAVAX required per gas unit for a transaction
// to be valid
- minGasPrice = big.NewInt(85)
-
- errEmptyBlock = errors.New("empty block")
- errCreateBlock = errors.New("couldn't create block")
- errUnknownBlock = errors.New("unknown block")
- errBlockFrequency = errors.New("too frequent block issuance")
- errUnsupportedFXs = errors.New("unsupported feature extensions")
- errInvalidBlock = errors.New("invalid block")
+ minGasPrice = big.NewInt(47)
+
+ txFee = units.MilliAvax
+
+ errEmptyBlock = errors.New("empty block")
+ errCreateBlock = errors.New("couldn't create block")
+ errUnknownBlock = errors.New("unknown block")
+ errBlockFrequency = errors.New("too frequent block issuance")
+ errUnsupportedFXs = errors.New("unsupported feature extensions")
+ errInvalidBlock = errors.New("invalid block")
+ errInvalidAddr = errors.New("invalid hex address")
+ errTooManyAtomicTx = errors.New("too many pending atomix txs")
+ 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")
+ errWrongChainID = errors.New("tx has wrong chain ID")
+ errInsufficientFunds = errors.New("insufficient funds")
+ errNoExportOutputs = errors.New("no export outputs")
+ errOutputsNotSorted = errors.New("outputs not sorted")
+ errOverflowExport = errors.New("overflow when computing export amount + txFee")
+ errInvalidNonce = errors.New("invalid nonce")
)
func maxDuration(x, y time.Duration) time.Duration {
@@ -79,6 +117,32 @@ 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{}),
+ Codec.RegisterType(&UnsignedExportTx{}),
+ )
+ Codec.Skip(3)
+ errs.Add(
+ Codec.RegisterType(&secp256k1fx.TransferInput{}),
+ Codec.RegisterType(&secp256k1fx.MintOutput{}),
+ Codec.RegisterType(&secp256k1fx.TransferOutput{}),
+ Codec.RegisterType(&secp256k1fx.MintOperation{}),
+ Codec.RegisterType(&secp256k1fx.Credential{}),
+ Codec.RegisterType(&secp256k1fx.Input{}),
+ Codec.RegisterType(&secp256k1fx.OutputOwners{}),
+ )
+ if errs.Errored() {
+ panic(errs.Err)
+ }
+}
+
// VM implements the snowman.ChainVM interface
type VM struct {
ctx *snow.Context
@@ -107,10 +171,37 @@ type VM struct {
bdGenWaitFlag bool
bdGenFlag bool
- genlock sync.Mutex
- txSubmitChan <-chan struct{}
+ genlock sync.Mutex
+ txSubmitChan <-chan struct{}
+ atomicTxSubmitChan chan struct{}
+ codec codec.Codec
+ clock timer.Clock
+ txFee uint64
+ pendingAtomicTxs chan *Tx
+ blockAtomicInputCache cache.LRU
+
+ fx secp256k1fx.Fx
+}
+
+func (vm *VM) getAtomicTx(block *types.Block) *Tx {
+ extdata := block.ExtraData()
+ atx := new(Tx)
+ if err := vm.codec.Unmarshal(extdata, atx); err != nil {
+ return nil
+ }
+ atx.Sign(vm.codec, nil)
+ return atx
}
+// Codec implements the secp256k1fx interface
+func (vm *VM) Codec() codec.Codec { return codec.NewDefault() }
+
+// Clock implements the secp256k1fx interface
+func (vm *VM) Clock() *timer.Clock { return &vm.clock }
+
+// Logger implements the secp256k1fx interface
+func (vm *VM) Logger() logging.Logger { return vm.ctx.Log }
+
/*
******************************************************************************
********************************* Snowman API ********************************
@@ -138,6 +229,7 @@ func (vm *VM) Initialize(
}
vm.chainID = g.Config.ChainID
+ vm.txFee = txFee
config := eth.DefaultConfig
config.ManualCanonical = true
@@ -165,13 +257,23 @@ func (vm *VM) Initialize(
}
header.Extra = append(header.Extra, hid...)
})
- chain.SetOnSeal(func(block *types.Block) error {
- if len(block.Transactions()) == 0 {
- // this could happen due to the async logic of geth tx pool
- vm.newBlockChan <- nil
- return errEmptyBlock
+ chain.SetOnFinalizeAndAssemble(func(state *state.StateDB, txs []*types.Transaction) ([]byte, error) {
+ select {
+ case atx := <-vm.pendingAtomicTxs:
+ if err := atx.UnsignedTx.(UnsignedAtomicTx).EVMStateTransfer(state); err != nil {
+ vm.newBlockChan <- nil
+ return nil, err
+ }
+ raw, _ := vm.codec.Marshal(atx)
+ return raw, nil
+ default:
+ if len(txs) == 0 {
+ // this could happen due to the async logic of geth tx pool
+ vm.newBlockChan <- nil
+ return nil, errEmptyBlock
+ }
}
- return nil
+ return nil, nil
})
chain.SetOnSealFinish(func(block *types.Block) error {
vm.ctx.Log.Verbo("EVM sealed a block")
@@ -181,6 +283,9 @@ func (vm *VM) Initialize(
ethBlock: block,
vm: vm,
}
+ if blk.Verify() != nil {
+ return errInvalidBlock
+ }
vm.newBlockChan <- blk
vm.updateStatus(ids.NewID(block.Hash()), choices.Processing)
vm.txPoolStabilizedLock.Lock()
@@ -191,8 +296,16 @@ func (vm *VM) Initialize(
chain.SetOnQueryAcceptedBlock(func() *types.Block {
return vm.getLastAccepted().ethBlock
})
+ chain.SetOnExtraStateChange(func(block *types.Block, state *state.StateDB) error {
+ tx := vm.getAtomicTx(block)
+ if tx == nil {
+ return nil
+ }
+ return tx.UnsignedTx.(UnsignedAtomicTx).EVMStateTransfer(state)
+ })
vm.blockCache = cache.LRU{Size: 2048}
vm.blockStatusCache = cache.LRU{Size: 1024}
+ vm.blockAtomicInputCache = cache.LRU{Size: 4096}
vm.newBlockChan = make(chan *Block)
vm.networkChan = toEngine
vm.blockDelayTimer = timer.NewTimer(func() {
@@ -216,6 +329,9 @@ func (vm *VM) Initialize(
vm.bdGenWaitFlag = true
vm.newTxPoolHeadChan = make(chan core.NewTxPoolHeadEvent, 1)
vm.txPoolStabilizedOk = make(chan struct{}, 1)
+ // TODO: read size from options
+ vm.pendingAtomicTxs = make(chan *Tx, 1024)
+ vm.atomicTxSubmitChan = make(chan struct{}, 1)
chain.GetTxPool().SubscribeNewHeadEvent(vm.newTxPoolHeadChan)
// TODO: shutdown this go routine
go ctx.Log.RecoverAndPanic(func() {
@@ -264,22 +380,26 @@ func (vm *VM) Initialize(
case <-vm.txSubmitChan:
vm.ctx.Log.Verbo("New tx detected, trying to generate a block")
vm.tryBlockGen()
+ case <-vm.atomicTxSubmitChan:
+ vm.ctx.Log.Verbo("New atomic Tx detected, trying to generate a block")
+ vm.tryBlockGen()
case <-time.After(5 * time.Second):
vm.tryBlockGen()
}
}
})
+ vm.codec = Codec
- return nil
+ return vm.fx.Initialize(vm)
}
// Bootstrapping notifies this VM that the consensus engine is performing
// bootstrapping
-func (vm *VM) Bootstrapping() error { return nil }
+func (vm *VM) Bootstrapping() error { return vm.fx.Bootstrapping() }
// Bootstrapped notifies this VM that the consensus engine has finished
// bootstrapping
-func (vm *VM) Bootstrapped() error { return nil }
+func (vm *VM) Bootstrapped() error { return vm.fx.Bootstrapped() }
// Shutdown implements the snowman.ChainVM interface
func (vm *VM) Shutdown() error {
@@ -366,6 +486,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 := geckorpc.NewServer()
+ server.RegisterCodec(geckojson.NewCodec(), "application/json")
+ server.RegisterCodec(geckojson.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()
@@ -378,6 +518,7 @@ func (vm *VM) CreateHandlers() map[string]*commonEng.HTTPHandler {
return map[string]*commonEng.HTTPHandler{
"/rpc": &commonEng.HTTPHandler{LockOptions: commonEng.NoLock, Handler: handler},
+ "/ava": newHandler("ava", &AvaAPI{vm}),
"/ws": &commonEng.HTTPHandler{LockOptions: commonEng.NoLock, Handler: handler.WebsocketHandler([]string{"*"})},
}
}
@@ -432,7 +573,7 @@ func (vm *VM) tryBlockGen() error {
if err != nil {
return err
}
- if size == 0 {
+ if size == 0 && len(vm.pendingAtomicTxs) == 0 {
return nil
}
@@ -541,3 +682,157 @@ func (vm *VM) getLastAccepted() *Block {
return vm.lastAccepted
}
+
+func (vm *VM) ParseEthAddress(addrStr string) (common.Address, error) {
+ if !common.IsHexAddress(addrStr) {
+ return common.Address{}, errInvalidAddr
+ }
+ return common.HexToAddress(addrStr), nil
+}
+
+func (vm *VM) FormatEthAddress(addr common.Address) (string, error) {
+ return addr.Hex(), nil
+}
+
+// ParseAddress takes in an address and produces the ID of the chain it's for
+// the ID of the address
+func (vm *VM) ParseAddress(addrStr string) (ids.ID, ids.ShortID, error) {
+ chainIDAlias, hrp, addrBytes, err := formatting.ParseAddress(addrStr)
+ if err != nil {
+ return ids.ID{}, ids.ShortID{}, err
+ }
+
+ chainID, err := vm.ctx.BCLookup.Lookup(chainIDAlias)
+ if err != nil {
+ return ids.ID{}, ids.ShortID{}, err
+ }
+
+ expectedHRP := constants.GetHRP(vm.ctx.NetworkID)
+ if hrp != expectedHRP {
+ return ids.ID{}, ids.ShortID{}, fmt.Errorf("expected hrp %q but got %q",
+ expectedHRP, hrp)
+ }
+
+ addr, err := ids.ToShortID(addrBytes)
+ if err != nil {
+ return ids.ID{}, ids.ShortID{}, err
+ }
+ return chainID, addr, nil
+}
+
+func (vm *VM) issueTx(tx *Tx) error {
+ select {
+ case vm.pendingAtomicTxs <- tx:
+ select {
+ case vm.atomicTxSubmitChan <- struct{}{}:
+ default:
+ }
+ default:
+ return errTooManyAtomicTx
+ }
+ return nil
+}
+
+// GetAtomicUTXOs returns the utxos that at least one of the provided addresses is
+// referenced in.
+func (vm *VM) GetAtomicUTXOs(
+ chainID ids.ID,
+ addrs ids.ShortSet,
+ startAddr ids.ShortID,
+ startUTXOID ids.ID,
+ limit int,
+) ([]*avax.UTXO, ids.ShortID, ids.ID, error) {
+ if limit <= 0 || limit > maxUTXOsToFetch {
+ limit = maxUTXOsToFetch
+ }
+
+ addrsList := make([][]byte, addrs.Len())
+ for i, addr := range addrs.List() {
+ addrsList[i] = addr.Bytes()
+ }
+
+ allUTXOBytes, lastAddr, lastUTXO, err := vm.ctx.SharedMemory.Indexed(
+ chainID,
+ addrsList,
+ startAddr.Bytes(),
+ startUTXOID.Bytes(),
+ limit,
+ )
+ if err != nil {
+ return nil, ids.ShortID{}, ids.ID{}, fmt.Errorf("error fetching atomic UTXOs: %w", err)
+ }
+
+ lastAddrID, err := ids.ToShortID(lastAddr)
+ if err != nil {
+ lastAddrID = ids.ShortEmpty
+ }
+ lastUTXOID, err := ids.ToID(lastUTXO)
+ if err != nil {
+ lastUTXOID = ids.Empty
+ }
+
+ utxos := make([]*avax.UTXO, len(allUTXOBytes))
+ for i, utxoBytes := range allUTXOBytes {
+ utxo := &avax.UTXO{}
+ if err := vm.codec.Unmarshal(utxoBytes, utxo); err != nil {
+ return nil, ids.ShortID{}, ids.ID{}, fmt.Errorf("error parsing UTXO: %w", err)
+ }
+ utxos[i] = utxo
+ }
+ return utxos, lastAddrID, lastUTXOID, nil
+}
+
+func GetEthAddress(privKey *crypto.PrivateKeySECP256K1R) common.Address {
+ return PublicKeyToEthAddress(privKey.PublicKey())
+}
+
+func PublicKeyToEthAddress(pubKey crypto.PublicKey) common.Address {
+ return ethcrypto.PubkeyToAddress(
+ (*pubKey.(*crypto.PublicKeySECP256K1R).ToECDSA()))
+}
+
+func (vm *VM) GetSpendableCanonical(keys []*crypto.PrivateKeySECP256K1R, amount uint64) ([]EVMInput, [][]*crypto.PrivateKeySECP256K1R, error) {
+ // NOTE: should we use HEAD block or lastAccepted?
+ state, err := vm.chain.BlockState(vm.lastAccepted.ethBlock)
+ if err != nil {
+ return nil, nil, err
+ }
+ inputs := []EVMInput{}
+ signers := [][]*crypto.PrivateKeySECP256K1R{}
+ for _, key := range keys {
+ if amount == 0 {
+ break
+ }
+ addr := GetEthAddress(key)
+ balance := new(big.Int).Div(state.GetBalance(addr), x2cRate).Uint64()
+ if balance == 0 {
+ continue
+ }
+ if amount < balance {
+ balance = amount
+ }
+ nonce, err := vm.GetAcceptedNonce(addr)
+ if err != nil {
+ return nil, nil, err
+ }
+ inputs = append(inputs, EVMInput{
+ Address: addr,
+ Amount: balance,
+ Nonce: nonce,
+ })
+ signers = append(signers, []*crypto.PrivateKeySECP256K1R{key})
+ amount -= balance
+ }
+ if amount > 0 {
+ return nil, nil, errInsufficientFunds
+ }
+ return inputs, signers, nil
+}
+
+func (vm *VM) GetAcceptedNonce(address common.Address) (uint64, error) {
+ state, err := vm.chain.BlockState(vm.lastAccepted.ethBlock)
+ if err != nil {
+ return 0, err
+ }
+ return state.GetNonce(address), nil
+}