diff options
author | Stephen Buttolph <stephen@avalabs.org> | 2020-08-25 13:49:24 -0400 |
---|---|---|
committer | GitHub <noreply@github.com> | 2020-08-25 13:49:24 -0400 |
commit | 5f9c76b5f4eb5bb08b8d3e3125b223f3737631d3 (patch) | |
tree | 9de0af77831340eb38c28c613849f1e7b65f3a54 /plugin | |
parent | 1bb314f767785fe617c3c5efeca1a64127339506 (diff) | |
parent | b05d11d451fb73843abcf6bd5fc8664b069433a4 (diff) |
Merge pull request #11 from ava-labs/TS_fix_gasv0.2.13
Fix gas handling
Diffstat (limited to 'plugin')
-rw-r--r-- | plugin/evm/block.go | 87 | ||||
-rw-r--r-- | plugin/evm/error.go | 19 | ||||
-rw-r--r-- | plugin/evm/export_tx.go | 223 | ||||
-rw-r--r-- | plugin/evm/factory.go | 10 | ||||
-rw-r--r-- | plugin/evm/import_tx.go | 272 | ||||
-rw-r--r-- | plugin/evm/service.go | 194 | ||||
-rw-r--r-- | plugin/evm/tx.go | 112 | ||||
-rw-r--r-- | plugin/evm/user.go | 146 | ||||
-rw-r--r-- | plugin/evm/vm.go | 373 |
9 files changed, 1390 insertions, 46 deletions
diff --git a/plugin/evm/block.go b/plugin/evm/block.go index 449e261..5c30fe1 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" @@ -12,6 +13,7 @@ import ( "github.com/ava-labs/gecko/ids" "github.com/ava-labs/gecko/snow/choices" "github.com/ava-labs/gecko/snow/consensus/snowman" + "github.com/ava-labs/gecko/vms/components/missing" ) // Block implements the snowman.Block interface @@ -26,9 +28,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 @@ -50,17 +64,72 @@ func (b *Block) Status() choices.Status { // Parent implements the snowman.Block interface func (b *Block) Parent() snowman.Block { parentID := ids.NewID(b.ethBlock.ParentHash()) - block := &Block{ - id: parentID, - ethBlock: b.vm.getCachedBlock(parentID), - vm: b.vm, + if block := b.vm.getBlock(parentID); block != nil { + b.vm.ctx.Log.Verbo("Parent(%s) has status: %s", parentID, block.Status()) + return block } - b.vm.ctx.Log.Verbo("Parent(%s) has status: %s", block.ID(), block.Status()) - return block + b.vm.ctx.Log.Verbo("Parent(%s) has status: %s", parentID, choices.Unknown) + return &missing.Block{BlkID: parentID} } // Verify implements the snowman.Block interface func (b *Block) Verify() error { + // Only enforce a minimum fee when bootstrapping has finished + if b.vm.ctx.IsBootstrapped() { + // Ensure the minimum gas price is paid for every transaction + for _, tx := range b.ethBlock.Transactions() { + if tx.GasPrice().Cmp(minGasPrice) < 0 { + return errInvalidBlock + } + } + } + + 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, |