// (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/database"
"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}
}
// TODO: verify UTXO inputs via gRPC
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 {
// TODO: finish this function via gRPC
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
// TODO: spend EVM balance to compensate vm.txFee-importedAmount
} 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()
}