mirror of
https://github.com/luxfi/platformvm.git
synced 2026-10-30 15:22:12 +00:00
- Drop atomic_block.go + legacy *_block_test.go (consolidated) - Add block/v0/ subdir + lift_v0.go for old version handling - Copyright header: 'Lux Industries, Inc.' → 'Lux Industries Inc' (no comma) - block_adapter.go: new adapter layer
275 lines
7.0 KiB
Go
275 lines
7.0 KiB
Go
// Copyright (C) 2019-2025, Lux Industries Inc. All rights reserved.
|
|
// See the file LICENSE for licensing terms.
|
|
|
|
package utxo
|
|
|
|
import (
|
|
"fmt"
|
|
|
|
"github.com/luxfi/ids"
|
|
lux "github.com/luxfi/utxo"
|
|
"github.com/luxfi/node/vms/components/verify"
|
|
"github.com/luxfi/node/vms/platformvm/stakeable"
|
|
"github.com/luxfi/node/vms/platformvm/txs"
|
|
"github.com/luxfi/timer/mockable"
|
|
hash "github.com/luxfi/crypto/hash"
|
|
"github.com/luxfi/math"
|
|
"github.com/luxfi/node/vms/platformvm/fx"
|
|
)
|
|
|
|
var _ Verifier = (*verifier)(nil)
|
|
|
|
// NewVerifier creates a new UTXO verifier
|
|
func NewVerifier(clk *mockable.Clock, fx fx.Fx) *verifier {
|
|
return &verifier{
|
|
clk: clk,
|
|
fx: fx,
|
|
}
|
|
}
|
|
|
|
type verifier struct {
|
|
clk *mockable.Clock
|
|
fx fx.Fx
|
|
}
|
|
|
|
func (h *verifier) VerifySpend(
|
|
tx txs.UnsignedTx,
|
|
utxoDB lux.UTXOGetter,
|
|
ins []*lux.TransferableInput,
|
|
outs []*lux.TransferableOutput,
|
|
creds []verify.Verifiable,
|
|
unlockedProduced map[ids.ID]uint64,
|
|
) error {
|
|
utxos := make([]*lux.UTXO, len(ins))
|
|
for index, input := range ins {
|
|
utxo, err := utxoDB.GetUTXO(input.InputID())
|
|
if err != nil {
|
|
return fmt.Errorf(
|
|
"failed to read consumed UTXO %s due to: %w",
|
|
&input.UTXOID,
|
|
err,
|
|
)
|
|
}
|
|
utxos[index] = utxo
|
|
}
|
|
|
|
return h.VerifySpendUTXOs(tx, utxos, ins, outs, creds, unlockedProduced)
|
|
}
|
|
|
|
func (h *verifier) VerifySpendUTXOs(
|
|
tx txs.UnsignedTx,
|
|
utxos []*lux.UTXO,
|
|
ins []*lux.TransferableInput,
|
|
outs []*lux.TransferableOutput,
|
|
creds []verify.Verifiable,
|
|
unlockedProduced map[ids.ID]uint64,
|
|
) error {
|
|
if len(ins) != len(creds) {
|
|
return fmt.Errorf(
|
|
"%w: %d inputs != %d credentials",
|
|
errWrongNumberCredentials,
|
|
len(ins),
|
|
len(creds),
|
|
)
|
|
}
|
|
if len(ins) != len(utxos) {
|
|
return fmt.Errorf(
|
|
"%w: %d inputs != %d utxos",
|
|
errWrongNumberUTXOs,
|
|
len(ins),
|
|
len(utxos),
|
|
)
|
|
}
|
|
for _, cred := range creds { // Verify credentials are well-formed.
|
|
if err := cred.Verify(); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
// Time this transaction is being verified
|
|
now := uint64(h.clk.Time().Unix())
|
|
|
|
// Track the amount of unlocked transfers
|
|
// assetID -> amount
|
|
unlockedConsumed := make(map[ids.ID]uint64)
|
|
|
|
// Track the amount of locked transfers and their owners
|
|
// assetID -> locktime -> ownerID -> amount
|
|
lockedProduced := make(map[ids.ID]map[uint64]map[ids.ID]uint64)
|
|
lockedConsumed := make(map[ids.ID]map[uint64]map[ids.ID]uint64)
|
|
|
|
for index, input := range ins {
|
|
utxo := utxos[index] // The UTXO consumed by [input]
|
|
|
|
realAssetID := utxo.AssetID()
|
|
claimedAssetID := input.AssetID()
|
|
if realAssetID != claimedAssetID {
|
|
return fmt.Errorf(
|
|
"%w: %s != %s",
|
|
errAssetIDMismatch,
|
|
claimedAssetID,
|
|
realAssetID,
|
|
)
|
|
}
|
|
|
|
out := utxo.Out
|
|
locktime := uint64(0)
|
|
// Set [locktime] to this UTXO's locktime, if applicable
|
|
if inner, ok := out.(*stakeable.LockOut); ok {
|
|
out = inner.TransferableOut
|
|
locktime = inner.Locktime
|
|
}
|
|
|
|
in := input.In
|
|
// The UTXO says it's locked until [locktime], but this input, which
|
|
// consumes it, is not locked even though [locktime] hasn't passed. This
|
|
// is invalid.
|
|
if inner, ok := in.(*stakeable.LockIn); now < locktime && !ok {
|
|
return errLockedFundsNotMarkedAsLocked
|
|
} else if ok {
|
|
if inner.Locktime != locktime {
|
|
// This input is locked, but its locktime is wrong
|
|
return fmt.Errorf(
|
|
"%w: %d != %d",
|
|
errLocktimeMismatch,
|
|
inner.Locktime,
|
|
locktime,
|
|
)
|
|
}
|
|
in = inner.TransferableIn
|
|
}
|
|
|
|
// Verify that this tx's credentials allow [in] to be spent
|
|
if err := h.fx.VerifyTransfer(tx, in, creds[index], out); err != nil {
|
|
return fmt.Errorf("failed to verify transfer: %w", err)
|
|
}
|
|
|
|
amount := in.Amount()
|
|
|
|
if now >= locktime {
|
|
newUnlockedConsumed, err := math.Add(unlockedConsumed[realAssetID], amount)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
unlockedConsumed[realAssetID] = newUnlockedConsumed
|
|
continue
|
|
}
|
|
|
|
owned, ok := out.(fx.Owned)
|
|
if !ok {
|
|
return fmt.Errorf("expected fx.Owned but got %T", out)
|
|
}
|
|
owner := owned.Owners()
|
|
ownerBytes, err := txs.Codec.Marshal(txs.CodecVersion, owner)
|
|
if err != nil {
|
|
return fmt.Errorf("couldn't marshal owner: %w", err)
|
|
}
|
|
lockedConsumedAsset, ok := lockedConsumed[realAssetID]
|
|
if !ok {
|
|
lockedConsumedAsset = make(map[uint64]map[ids.ID]uint64)
|
|
lockedConsumed[realAssetID] = lockedConsumedAsset
|
|
}
|
|
ownerID := hash.ComputeHash256Array(ownerBytes)
|
|
owners, ok := lockedConsumedAsset[locktime]
|
|
if !ok {
|
|
owners = make(map[ids.ID]uint64)
|
|
lockedConsumedAsset[locktime] = owners
|
|
}
|
|
newAmount, err := math.Add(owners[ownerID], amount)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
owners[ownerID] = newAmount
|
|
}
|
|
|
|
for _, out := range outs {
|
|
assetID := out.AssetID()
|
|
|
|
output := out.Output()
|
|
locktime := uint64(0)
|
|
// Set [locktime] to this output's locktime, if applicable
|
|
if inner, ok := output.(*stakeable.LockOut); ok {
|
|
output = inner.TransferableOut
|
|
locktime = inner.Locktime
|
|
}
|
|
|
|
amount := output.Amount()
|
|
|
|
if locktime == 0 {
|
|
newUnlockedProduced, err := math.Add(unlockedProduced[assetID], amount)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
unlockedProduced[assetID] = newUnlockedProduced
|
|
continue
|
|
}
|
|
|
|
owned, ok := output.(fx.Owned)
|
|
if !ok {
|
|
return fmt.Errorf("expected fx.Owned but got %T", out)
|
|
}
|
|
owner := owned.Owners()
|
|
ownerBytes, err := txs.Codec.Marshal(txs.CodecVersion, owner)
|
|
if err != nil {
|
|
return fmt.Errorf("couldn't marshal owner: %w", err)
|
|
}
|
|
lockedProducedAsset, ok := lockedProduced[assetID]
|
|
if !ok {
|
|
lockedProducedAsset = make(map[uint64]map[ids.ID]uint64)
|
|
lockedProduced[assetID] = lockedProducedAsset
|
|
}
|
|
ownerID := hash.ComputeHash256Array(ownerBytes)
|
|
owners, ok := lockedProducedAsset[locktime]
|
|
if !ok {
|
|
owners = make(map[ids.ID]uint64)
|
|
lockedProducedAsset[locktime] = owners
|
|
}
|
|
newAmount, err := math.Add(owners[ownerID], amount)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
owners[ownerID] = newAmount
|
|
}
|
|
|
|
// Make sure that for each assetID and locktime, tokens produced <= tokens consumed
|
|
for assetID, producedAssetAmounts := range lockedProduced {
|
|
lockedConsumedAsset := lockedConsumed[assetID]
|
|
for locktime, producedAmounts := range producedAssetAmounts {
|
|
consumedAmounts := lockedConsumedAsset[locktime]
|
|
for ownerID, producedAmount := range producedAmounts {
|
|
consumedAmount := consumedAmounts[ownerID]
|
|
|
|
if producedAmount > consumedAmount {
|
|
increase := producedAmount - consumedAmount
|
|
unlockedConsumedAsset := unlockedConsumed[assetID]
|
|
if increase > unlockedConsumedAsset {
|
|
return fmt.Errorf(
|
|
"%w: %s needs %d more %s for locktime %d",
|
|
ErrInsufficientLockedFunds,
|
|
ownerID,
|
|
increase-unlockedConsumedAsset,
|
|
assetID,
|
|
locktime,
|
|
)
|
|
}
|
|
unlockedConsumed[assetID] = unlockedConsumedAsset - increase
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
for assetID, unlockedProducedAsset := range unlockedProduced {
|
|
unlockedConsumedAsset := unlockedConsumed[assetID]
|
|
// More unlocked tokens produced than consumed. Invalid.
|
|
if unlockedProducedAsset > unlockedConsumedAsset {
|
|
return fmt.Errorf(
|
|
"%w: needs %d more %s",
|
|
ErrInsufficientUnlockedFunds,
|
|
unlockedProducedAsset-unlockedConsumedAsset,
|
|
assetID,
|
|
)
|
|
}
|
|
}
|
|
return nil
|
|
}
|