github.com/MetalBlockchain/metalgo@v1.11.9/vms/platformvm/utxo/verifier.go (about)

     1  // Copyright (C) 2019-2024, Ava Labs, Inc. All rights reserved.
     2  // See the file LICENSE for licensing terms.
     3  
     4  package utxo
     5  
     6  import (
     7  	"errors"
     8  	"fmt"
     9  
    10  	"github.com/MetalBlockchain/metalgo/ids"
    11  	"github.com/MetalBlockchain/metalgo/snow"
    12  	"github.com/MetalBlockchain/metalgo/utils/hashing"
    13  	"github.com/MetalBlockchain/metalgo/utils/math"
    14  	"github.com/MetalBlockchain/metalgo/utils/timer/mockable"
    15  	"github.com/MetalBlockchain/metalgo/vms/components/avax"
    16  	"github.com/MetalBlockchain/metalgo/vms/components/verify"
    17  	"github.com/MetalBlockchain/metalgo/vms/platformvm/fx"
    18  	"github.com/MetalBlockchain/metalgo/vms/platformvm/stakeable"
    19  	"github.com/MetalBlockchain/metalgo/vms/platformvm/txs"
    20  )
    21  
    22  var (
    23  	_ Verifier = (*verifier)(nil)
    24  
    25  	ErrInsufficientFunds            = errors.New("insufficient funds")
    26  	ErrInsufficientUnlockedFunds    = errors.New("insufficient unlocked funds")
    27  	ErrInsufficientLockedFunds      = errors.New("insufficient locked funds")
    28  	errWrongNumberCredentials       = errors.New("wrong number of credentials")
    29  	errWrongNumberUTXOs             = errors.New("wrong number of UTXOs")
    30  	errAssetIDMismatch              = errors.New("input asset ID does not match UTXO asset ID")
    31  	errLocktimeMismatch             = errors.New("input locktime does not match UTXO locktime")
    32  	errLockedFundsNotMarkedAsLocked = errors.New("locked funds not marked as locked")
    33  )
    34  
    35  type Verifier interface {
    36  	// Verify that [tx] is semantically valid.
    37  	// [ins] and [outs] are the inputs and outputs of [tx].
    38  	// [creds] are the credentials of [tx], which allow [ins] to be spent.
    39  	// [unlockedProduced] is the map of assets that were produced and their
    40  	// amounts.
    41  	// The [ins] must have at least [unlockedProduced] than the [outs].
    42  	//
    43  	// Precondition: [tx] has already been syntactically verified.
    44  	//
    45  	// Note: [unlockedProduced] is modified by this method.
    46  	VerifySpend(
    47  		tx txs.UnsignedTx,
    48  		utxoDB avax.UTXOGetter,
    49  		ins []*avax.TransferableInput,
    50  		outs []*avax.TransferableOutput,
    51  		creds []verify.Verifiable,
    52  		unlockedProduced map[ids.ID]uint64,
    53  	) error
    54  
    55  	// Verify that [tx] is semantically valid.
    56  	// [utxos[i]] is the UTXO being consumed by [ins[i]].
    57  	// [ins] and [outs] are the inputs and outputs of [tx].
    58  	// [creds] are the credentials of [tx], which allow [ins] to be spent.
    59  	// [unlockedProduced] is the map of assets that were produced and their
    60  	// amounts.
    61  	// The [ins] must have at least [unlockedProduced] more than the [outs].
    62  	//
    63  	// Precondition: [tx] has already been syntactically verified.
    64  	//
    65  	// Note: [unlockedProduced] is modified by this method.
    66  	VerifySpendUTXOs(
    67  		tx txs.UnsignedTx,
    68  		utxos []*avax.UTXO,
    69  		ins []*avax.TransferableInput,
    70  		outs []*avax.TransferableOutput,
    71  		creds []verify.Verifiable,
    72  		unlockedProduced map[ids.ID]uint64,
    73  	) error
    74  }
    75  
    76  func NewVerifier(
    77  	ctx *snow.Context,
    78  	clk *mockable.Clock,
    79  	fx fx.Fx,
    80  ) Verifier {
    81  	return &verifier{
    82  		ctx: ctx,
    83  		clk: clk,
    84  		fx:  fx,
    85  	}
    86  }
    87  
    88  type verifier struct {
    89  	ctx *snow.Context
    90  	clk *mockable.Clock
    91  	fx  fx.Fx
    92  }
    93  
    94  func (h *verifier) VerifySpend(
    95  	tx txs.UnsignedTx,
    96  	utxoDB avax.UTXOGetter,
    97  	ins []*avax.TransferableInput,
    98  	outs []*avax.TransferableOutput,
    99  	creds []verify.Verifiable,
   100  	unlockedProduced map[ids.ID]uint64,
   101  ) error {
   102  	utxos := make([]*avax.UTXO, len(ins))
   103  	for index, input := range ins {
   104  		utxo, err := utxoDB.GetUTXO(input.InputID())
   105  		if err != nil {
   106  			return fmt.Errorf(
   107  				"failed to read consumed UTXO %s due to: %w",
   108  				&input.UTXOID,
   109  				err,
   110  			)
   111  		}
   112  		utxos[index] = utxo
   113  	}
   114  
   115  	return h.VerifySpendUTXOs(tx, utxos, ins, outs, creds, unlockedProduced)
   116  }
   117  
   118  func (h *verifier) VerifySpendUTXOs(
   119  	tx txs.UnsignedTx,
   120  	utxos []*avax.UTXO,
   121  	ins []*avax.TransferableInput,
   122  	outs []*avax.TransferableOutput,
   123  	creds []verify.Verifiable,
   124  	unlockedProduced map[ids.ID]uint64,
   125  ) error {
   126  	if len(ins) != len(creds) {
   127  		return fmt.Errorf(
   128  			"%w: %d inputs != %d credentials",
   129  			errWrongNumberCredentials,
   130  			len(ins),
   131  			len(creds),
   132  		)
   133  	}
   134  	if len(ins) != len(utxos) {
   135  		return fmt.Errorf(
   136  			"%w: %d inputs != %d utxos",
   137  			errWrongNumberUTXOs,
   138  			len(ins),
   139  			len(utxos),
   140  		)
   141  	}
   142  	for _, cred := range creds { // Verify credentials are well-formed.
   143  		if err := cred.Verify(); err != nil {
   144  			return err
   145  		}
   146  	}
   147  
   148  	// Time this transaction is being verified
   149  	now := uint64(h.clk.Time().Unix())
   150  
   151  	// Track the amount of unlocked transfers
   152  	// assetID -> amount
   153  	unlockedConsumed := make(map[ids.ID]uint64)
   154  
   155  	// Track the amount of locked transfers and their owners
   156  	// assetID -> locktime -> ownerID -> amount
   157  	lockedProduced := make(map[ids.ID]map[uint64]map[ids.ID]uint64)
   158  	lockedConsumed := make(map[ids.ID]map[uint64]map[ids.ID]uint64)
   159  
   160  	for index, input := range ins {
   161  		utxo := utxos[index] // The UTXO consumed by [input]
   162  
   163  		realAssetID := utxo.AssetID()
   164  		claimedAssetID := input.AssetID()
   165  		if realAssetID != claimedAssetID {
   166  			return fmt.Errorf(
   167  				"%w: %s != %s",
   168  				errAssetIDMismatch,
   169  				claimedAssetID,
   170  				realAssetID,
   171  			)
   172  		}
   173  
   174  		out := utxo.Out
   175  		locktime := uint64(0)
   176  		// Set [locktime] to this UTXO's locktime, if applicable
   177  		if inner, ok := out.(*stakeable.LockOut); ok {
   178  			out = inner.TransferableOut
   179  			locktime = inner.Locktime
   180  		}
   181  
   182  		in := input.In
   183  		// The UTXO says it's locked until [locktime], but this input, which
   184  		// consumes it, is not locked even though [locktime] hasn't passed. This
   185  		// is invalid.
   186  		if inner, ok := in.(*stakeable.LockIn); now < locktime && !ok {
   187  			return errLockedFundsNotMarkedAsLocked
   188  		} else if ok {
   189  			if inner.Locktime != locktime {
   190  				// This input is locked, but its locktime is wrong
   191  				return fmt.Errorf(
   192  					"%w: %d != %d",
   193  					errLocktimeMismatch,
   194  					inner.Locktime,
   195  					locktime,
   196  				)
   197  			}
   198  			in = inner.TransferableIn
   199  		}
   200  
   201  		// Verify that this tx's credentials allow [in] to be spent
   202  		if err := h.fx.VerifyTransfer(tx, in, creds[index], out); err != nil {
   203  			return fmt.Errorf("failed to verify transfer: %w", err)
   204  		}
   205  
   206  		amount := in.Amount()
   207  
   208  		if now >= locktime {
   209  			newUnlockedConsumed, err := math.Add64(unlockedConsumed[realAssetID], amount)
   210  			if err != nil {
   211  				return err
   212  			}
   213  			unlockedConsumed[realAssetID] = newUnlockedConsumed
   214  			continue
   215  		}
   216  
   217  		owned, ok := out.(fx.Owned)
   218  		if !ok {
   219  			return fmt.Errorf("expected fx.Owned but got %T", out)
   220  		}
   221  		owner := owned.Owners()
   222  		ownerBytes, err := txs.Codec.Marshal(txs.CodecVersion, owner)
   223  		if err != nil {
   224  			return fmt.Errorf("couldn't marshal owner: %w", err)
   225  		}
   226  		lockedConsumedAsset, ok := lockedConsumed[realAssetID]
   227  		if !ok {
   228  			lockedConsumedAsset = make(map[uint64]map[ids.ID]uint64)
   229  			lockedConsumed[realAssetID] = lockedConsumedAsset
   230  		}
   231  		ownerID := hashing.ComputeHash256Array(ownerBytes)
   232  		owners, ok := lockedConsumedAsset[locktime]
   233  		if !ok {
   234  			owners = make(map[ids.ID]uint64)
   235  			lockedConsumedAsset[locktime] = owners
   236  		}
   237  		newAmount, err := math.Add64(owners[ownerID], amount)
   238  		if err != nil {
   239  			return err
   240  		}
   241  		owners[ownerID] = newAmount
   242  	}
   243  
   244  	for _, out := range outs {
   245  		assetID := out.AssetID()
   246  
   247  		output := out.Output()
   248  		locktime := uint64(0)
   249  		// Set [locktime] to this output's locktime, if applicable
   250  		if inner, ok := output.(*stakeable.LockOut); ok {
   251  			output = inner.TransferableOut
   252  			locktime = inner.Locktime
   253  		}
   254  
   255  		amount := output.Amount()
   256  
   257  		if locktime == 0 {
   258  			newUnlockedProduced, err := math.Add64(unlockedProduced[assetID], amount)
   259  			if err != nil {
   260  				return err
   261  			}
   262  			unlockedProduced[assetID] = newUnlockedProduced
   263  			continue
   264  		}
   265  
   266  		owned, ok := output.(fx.Owned)
   267  		if !ok {
   268  			return fmt.Errorf("expected fx.Owned but got %T", out)
   269  		}
   270  		owner := owned.Owners()
   271  		ownerBytes, err := txs.Codec.Marshal(txs.CodecVersion, owner)
   272  		if err != nil {
   273  			return fmt.Errorf("couldn't marshal owner: %w", err)
   274  		}
   275  		lockedProducedAsset, ok := lockedProduced[assetID]
   276  		if !ok {
   277  			lockedProducedAsset = make(map[uint64]map[ids.ID]uint64)
   278  			lockedProduced[assetID] = lockedProducedAsset
   279  		}
   280  		ownerID := hashing.ComputeHash256Array(ownerBytes)
   281  		owners, ok := lockedProducedAsset[locktime]
   282  		if !ok {
   283  			owners = make(map[ids.ID]uint64)
   284  			lockedProducedAsset[locktime] = owners
   285  		}
   286  		newAmount, err := math.Add64(owners[ownerID], amount)
   287  		if err != nil {
   288  			return err
   289  		}
   290  		owners[ownerID] = newAmount
   291  	}
   292  
   293  	// Make sure that for each assetID and locktime, tokens produced <= tokens consumed
   294  	for assetID, producedAssetAmounts := range lockedProduced {
   295  		lockedConsumedAsset := lockedConsumed[assetID]
   296  		for locktime, producedAmounts := range producedAssetAmounts {
   297  			consumedAmounts := lockedConsumedAsset[locktime]
   298  			for ownerID, producedAmount := range producedAmounts {
   299  				consumedAmount := consumedAmounts[ownerID]
   300  
   301  				if producedAmount > consumedAmount {
   302  					increase := producedAmount - consumedAmount
   303  					unlockedConsumedAsset := unlockedConsumed[assetID]
   304  					if increase > unlockedConsumedAsset {
   305  						return fmt.Errorf(
   306  							"%w: %s needs %d more %s for locktime %d",
   307  							ErrInsufficientLockedFunds,
   308  							ownerID,
   309  							increase-unlockedConsumedAsset,
   310  							assetID,
   311  							locktime,
   312  						)
   313  					}
   314  					unlockedConsumed[assetID] = unlockedConsumedAsset - increase
   315  				}
   316  			}
   317  		}
   318  	}
   319  
   320  	for assetID, unlockedProducedAsset := range unlockedProduced {
   321  		unlockedConsumedAsset := unlockedConsumed[assetID]
   322  		// More unlocked tokens produced than consumed. Invalid.
   323  		if unlockedProducedAsset > unlockedConsumedAsset {
   324  			return fmt.Errorf(
   325  				"%w: needs %d more %s",
   326  				ErrInsufficientUnlockedFunds,
   327  				unlockedProducedAsset-unlockedConsumedAsset,
   328  				assetID,
   329  			)
   330  		}
   331  	}
   332  	return nil
   333  }