// (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/avalanchego/database"
"github.com/ava-labs/avalanchego/ids"
"github.com/ava-labs/avalanchego/snow"
"github.com/ava-labs/avalanchego/utils/crypto"
"github.com/ava-labs/avalanchego/utils/math"
"github.com/ava-labs/avalanchego/vms/components/avax"
"github.com/ava-labs/avalanchego/vms/secp256k1fx"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/log"
)
// 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
}
}
if !IsSortedAndUniqueEVMOutputs(tx.Outs) {
return errExportOutputsNotSortedAndUnique
}
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(out.AssetID, 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}
}
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)
}
// newImportTx returns a new ImportTx
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 := make(map[[32]byte]uint64)
now := vm.clock.Unix()
for _, utxo := range atomicUTXOs {
inputIntf, utxoSigners, err := kc.Spend(utxo.Out, now)
if err != nil {
continue
}
input, ok := inputIntf.(avax.TransferableIn)
if !ok {
continue
}
aid := utxo.AssetID()
aidKey := aid.Key()
importedAmount[aidKey], err = math.Add64(importedAmount[aidKey], 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)
//importedAVAXAmount := importedAmount[vm.ctx.AVAXAssetID.Key()]
outs := []EVMOutput{}
//if importedAVAXAmount == 0 {
// return nil, errNoFunds // No imported UTXOs were spendable
//}
//// AVAX output
//if importedAVAXAmount < vm.txFee { // imported amount goes toward paying tx fee
// // TODO: spend EVM balance to compensate vm.txFee-importedAmount
// return nil, errNoFunds
//} else if importedAVAXAmount > vm.txFee {
// outs = append(outs, EVMOutput{
// Address: to,
// Amount: importedAVAXAmount - vm.txFee,
// AssetID: vm.ctx.AVAXAssetID,
// })
//}
// This will create unique outputs (in the context of sorting)
// since each output will have a unique assetID
for assetKey, amount := range importedAmount {
assetID := ids.NewID(assetKey)
//if assetID.Equals(vm.ctx.AVAXAssetID) || amount == 0 {
if amount == 0 {
continue
}
outs = append(outs, EVMOutput{
Address: to,
Amount: amount,
AssetID: assetID,
})
}
SortEVMOutputs(outs)
// 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)
}
// EVMStateTransfer performs the state transfer to increase the balances of
// accounts accordingly with the imported EVMOutputs
func (tx *UnsignedImportTx) EVMStateTransfer(vm *VM, state *state.StateDB) error {
for _, to := range tx.Outs {
log.Info("crosschain X->C", "addr", to.Address, "amount", to.Amount)
if to.AssetID.Equals(vm.ctx.AVAXAssetID) {
amount := new(big.Int).Mul(
new(big.Int).SetUint64(to.Amount), x2cRate)
state.AddBalance(to.Address, amount)
} else {
amount := new(big.Int).SetUint64(to.Amount)
state.AddBalanceMultiCoin(to.Address, to.AssetID.Key(), amount)
}
}
return nil
}