mirror of
https://github.com/luxfi/node.git
synced 2026-10-30 15:24:05 +00:00
1201 lines
27 KiB
Go
1201 lines
27 KiB
Go
// Copyright (C) 2019-2025, Lux Industries Inc. All rights reserved.
|
|
// See the file LICENSE for licensing terms.
|
|
|
|
package executor
|
|
|
|
import (
|
|
"context"
|
|
"testing"
|
|
|
|
"github.com/luxfi/mock/gomock"
|
|
"github.com/stretchr/testify/require"
|
|
|
|
consensustest "github.com/luxfi/consensus/test/helpers"
|
|
"github.com/luxfi/constants"
|
|
"github.com/luxfi/crypto/secp256k1"
|
|
"github.com/luxfi/database"
|
|
"github.com/luxfi/database/memdb"
|
|
"github.com/luxfi/database/prefixdb"
|
|
"github.com/luxfi/ids"
|
|
"github.com/luxfi/log"
|
|
"github.com/luxfi/node/vms/components/verify"
|
|
"github.com/luxfi/node/vms/xvm/fxs"
|
|
"github.com/luxfi/node/vms/xvm/state"
|
|
"github.com/luxfi/node/vms/xvm/state/statemock"
|
|
"github.com/luxfi/node/vms/xvm/txs"
|
|
"github.com/luxfi/runtime"
|
|
"github.com/luxfi/timer/mockable"
|
|
lux "github.com/luxfi/utxo"
|
|
"github.com/luxfi/utxo/secp256k1fx"
|
|
validators "github.com/luxfi/validators"
|
|
"github.com/luxfi/vm/chains/atomic"
|
|
)
|
|
|
|
// testSharedMemory adapts atomic.SharedMemory to executor.SharedMemory
|
|
type testSharedMemory struct {
|
|
sm atomic.SharedMemory
|
|
}
|
|
|
|
func (t *testSharedMemory) Get(peerChainID ids.ID, keys [][]byte) ([][]byte, error) {
|
|
return t.sm.Get(peerChainID, keys)
|
|
}
|
|
|
|
func (t *testSharedMemory) Apply(requests map[ids.ID]interface{}, batch ...interface{}) error {
|
|
// We don't use Apply in these tests, just Get
|
|
return nil
|
|
}
|
|
|
|
func TestSemanticVerifierBaseTx(t *testing.T) {
|
|
ctx := context.Background()
|
|
cChainID := ids.GenerateTestID()
|
|
|
|
fxIndex := txs.NewFxIndex()
|
|
secpFx := &secp256k1fx.Fx{}
|
|
_, err := txs.NewCustomParser(
|
|
fxIndex,
|
|
new(mockable.Clock),
|
|
log.NoLog{},
|
|
[]fxs.Fx{
|
|
secpFx,
|
|
},
|
|
)
|
|
require.NoError(t, err)
|
|
|
|
txID := ids.GenerateTestID()
|
|
utxoID := lux.UTXOID{
|
|
TxID: txID,
|
|
OutputIndex: 2,
|
|
}
|
|
asset := lux.Asset{
|
|
ID: ids.GenerateTestID(),
|
|
}
|
|
inputSigner := secp256k1fx.Input{
|
|
SigIndices: []uint32{
|
|
0,
|
|
},
|
|
}
|
|
fxInput := secp256k1fx.TransferInput{
|
|
Amt: 12345,
|
|
Input: inputSigner,
|
|
}
|
|
input := lux.TransferableInput{
|
|
UTXOID: utxoID,
|
|
Asset: asset,
|
|
In: &fxInput,
|
|
}
|
|
baseTx := txs.BaseTx{
|
|
BaseTx: lux.BaseTx{
|
|
Ins: []*lux.TransferableInput{
|
|
&input,
|
|
},
|
|
},
|
|
}
|
|
|
|
testChainID := ids.GenerateTestID()
|
|
backendObj := &Backend{
|
|
Ctx: ctx,
|
|
Runtime: &runtime.Runtime{
|
|
ChainID: ids.GenerateTestID(),
|
|
ValidatorState: &testValidatorState{chainID: testChainID},
|
|
},
|
|
CChainID: cChainID,
|
|
Config: &feeConfig,
|
|
Fxs: []*fxs.ParsedFx{
|
|
{
|
|
ID: secp256k1fx.ID,
|
|
Fx: secpFx,
|
|
},
|
|
},
|
|
FxIndex: fxIndex,
|
|
FeeAssetID: ids.GenerateTestID(),
|
|
Bootstrapped: true,
|
|
}
|
|
require.NoError(t, secpFx.Bootstrapped())
|
|
|
|
outputOwners := secp256k1fx.OutputOwners{
|
|
Threshold: 1,
|
|
Addrs: []ids.ShortID{
|
|
keys[0].Address(),
|
|
},
|
|
}
|
|
output := secp256k1fx.TransferOutput{
|
|
Amt: 12345,
|
|
OutputOwners: outputOwners,
|
|
}
|
|
utxo := lux.UTXO{
|
|
UTXOID: utxoID,
|
|
Asset: asset,
|
|
Out: &output,
|
|
}
|
|
unsignedCreateAssetTx := txs.CreateAssetTx{
|
|
States: []*txs.InitialState{{
|
|
FxIndex: 0,
|
|
}},
|
|
}
|
|
createAssetTx := txs.Tx{
|
|
Unsigned: &unsignedCreateAssetTx,
|
|
}
|
|
|
|
tests := []struct {
|
|
name string
|
|
stateFunc func(*gomock.Controller) state.Chain
|
|
txFunc func(*require.Assertions) *txs.Tx
|
|
err error
|
|
}{
|
|
{
|
|
name: "valid",
|
|
stateFunc: func(ctrl *gomock.Controller) state.Chain {
|
|
state := statemock.NewChain(ctrl)
|
|
|
|
state.EXPECT().GetUTXO(utxoID.InputID()).Return(&utxo, nil)
|
|
state.EXPECT().GetTx(asset.ID).Return(&createAssetTx, nil)
|
|
|
|
return state
|
|
},
|
|
txFunc: func(require *require.Assertions) *txs.Tx {
|
|
tx := &txs.Tx{
|
|
Unsigned: &baseTx,
|
|
}
|
|
require.NoError(tx.SignSECP256K1Fx(
|
|
[][]*secp256k1.PrivateKey{
|
|
{keys[0]},
|
|
},
|
|
))
|
|
return tx
|
|
},
|
|
err: nil,
|
|
},
|
|
{
|
|
name: "assetID mismatch",
|
|
stateFunc: func(ctrl *gomock.Controller) state.Chain {
|
|
state := statemock.NewChain(ctrl)
|
|
|
|
utxo := utxo
|
|
utxo.Asset.ID = ids.GenerateTestID()
|
|
|
|
state.EXPECT().GetUTXO(utxoID.InputID()).Return(&utxo, nil)
|
|
|
|
return state
|
|
},
|
|
txFunc: func(require *require.Assertions) *txs.Tx {
|
|
tx := &txs.Tx{
|
|
Unsigned: &baseTx,
|
|
}
|
|
require.NoError(tx.SignSECP256K1Fx(
|
|
[][]*secp256k1.PrivateKey{
|
|
{keys[0]},
|
|
},
|
|
))
|
|
return tx
|
|
},
|
|
err: errAssetIDMismatch,
|
|
},
|
|
{
|
|
name: "not allowed input feature extension",
|
|
stateFunc: func(ctrl *gomock.Controller) state.Chain {
|
|
state := statemock.NewChain(ctrl)
|
|
|
|
unsignedCreateAssetTx := unsignedCreateAssetTx
|
|
unsignedCreateAssetTx.States = nil
|
|
|
|
createAssetTx := txs.Tx{
|
|
Unsigned: &unsignedCreateAssetTx,
|
|
}
|
|
|
|
state.EXPECT().GetUTXO(utxoID.InputID()).Return(&utxo, nil)
|
|
state.EXPECT().GetTx(asset.ID).Return(&createAssetTx, nil)
|
|
|
|
return state
|
|
},
|
|
txFunc: func(require *require.Assertions) *txs.Tx {
|
|
tx := &txs.Tx{
|
|
Unsigned: &baseTx,
|
|
}
|
|
require.NoError(tx.SignSECP256K1Fx(
|
|
[][]*secp256k1.PrivateKey{
|
|
{keys[0]},
|
|
},
|
|
))
|
|
return tx
|
|
},
|
|
err: errIncompatibleFx,
|
|
},
|
|
{
|
|
name: "invalid signature",
|
|
stateFunc: func(ctrl *gomock.Controller) state.Chain {
|
|
state := statemock.NewChain(ctrl)
|
|
|
|
state.EXPECT().GetUTXO(utxoID.InputID()).Return(&utxo, nil)
|
|
state.EXPECT().GetTx(asset.ID).Return(&createAssetTx, nil)
|
|
|
|
return state
|
|
},
|
|
txFunc: func(require *require.Assertions) *txs.Tx {
|
|
tx := &txs.Tx{
|
|
Unsigned: &baseTx,
|
|
}
|
|
require.NoError(tx.SignSECP256K1Fx(
|
|
[][]*secp256k1.PrivateKey{
|
|
{keys[1]},
|
|
},
|
|
))
|
|
return tx
|
|
},
|
|
err: secp256k1fx.ErrWrongSig,
|
|
},
|
|
{
|
|
name: "missing UTXO",
|
|
stateFunc: func(ctrl *gomock.Controller) state.Chain {
|
|
state := statemock.NewChain(ctrl)
|
|
|
|
state.EXPECT().GetUTXO(utxoID.InputID()).Return(nil, database.ErrNotFound)
|
|
|
|
return state
|
|
},
|
|
txFunc: func(require *require.Assertions) *txs.Tx {
|
|
tx := &txs.Tx{
|
|
Unsigned: &baseTx,
|
|
}
|
|
require.NoError(tx.SignSECP256K1Fx(
|
|
[][]*secp256k1.PrivateKey{
|
|
{keys[0]},
|
|
},
|
|
))
|
|
return tx
|
|
},
|
|
err: database.ErrNotFound,
|
|
},
|
|
{
|
|
name: "invalid UTXO amount",
|
|
stateFunc: func(ctrl *gomock.Controller) state.Chain {
|
|
state := statemock.NewChain(ctrl)
|
|
|
|
output := output
|
|
output.Amt--
|
|
|
|
utxo := utxo
|
|
utxo.Out = &output
|
|
|
|
state.EXPECT().GetUTXO(utxoID.InputID()).Return(&utxo, nil)
|
|
state.EXPECT().GetTx(asset.ID).Return(&createAssetTx, nil)
|
|
|
|
return state
|
|
},
|
|
txFunc: func(require *require.Assertions) *txs.Tx {
|
|
tx := &txs.Tx{
|
|
Unsigned: &baseTx,
|
|
}
|
|
require.NoError(tx.SignSECP256K1Fx(
|
|
[][]*secp256k1.PrivateKey{
|
|
{keys[0]},
|
|
},
|
|
))
|
|
return tx
|
|
},
|
|
err: secp256k1fx.ErrMismatchedAmounts,
|
|
},
|
|
{
|
|
name: "not allowed output feature extension",
|
|
stateFunc: func(ctrl *gomock.Controller) state.Chain {
|
|
state := statemock.NewChain(ctrl)
|
|
|
|
unsignedCreateAssetTx := unsignedCreateAssetTx
|
|
unsignedCreateAssetTx.States = nil
|
|
|
|
createAssetTx := txs.Tx{
|
|
Unsigned: &unsignedCreateAssetTx,
|
|
}
|
|
|
|
state.EXPECT().GetTx(asset.ID).Return(&createAssetTx, nil)
|
|
|
|
return state
|
|
},
|
|
txFunc: func(require *require.Assertions) *txs.Tx {
|
|
baseTx := baseTx
|
|
baseTx.Ins = nil
|
|
baseTx.Outs = []*lux.TransferableOutput{
|
|
{
|
|
Asset: asset,
|
|
Out: &output,
|
|
},
|
|
}
|
|
tx := &txs.Tx{
|
|
Unsigned: &baseTx,
|
|
}
|
|
require.NoError(tx.SignSECP256K1Fx(
|
|
[][]*secp256k1.PrivateKey{},
|
|
))
|
|
return tx
|
|
},
|
|
err: errIncompatibleFx,
|
|
},
|
|
{
|
|
name: "unknown asset",
|
|
stateFunc: func(ctrl *gomock.Controller) state.Chain {
|
|
state := statemock.NewChain(ctrl)
|
|
|
|
state.EXPECT().GetUTXO(utxoID.InputID()).Return(&utxo, nil)
|
|
state.EXPECT().GetTx(asset.ID).Return(nil, database.ErrNotFound)
|
|
|
|
return state
|
|
},
|
|
txFunc: func(require *require.Assertions) *txs.Tx {
|
|
tx := &txs.Tx{
|
|
Unsigned: &baseTx,
|
|
}
|
|
require.NoError(tx.SignSECP256K1Fx(
|
|
[][]*secp256k1.PrivateKey{
|
|
{keys[0]},
|
|
},
|
|
))
|
|
return tx
|
|
},
|
|
err: database.ErrNotFound,
|
|
},
|
|
{
|
|
name: "not an asset",
|
|
stateFunc: func(ctrl *gomock.Controller) state.Chain {
|
|
state := statemock.NewChain(ctrl)
|
|
|
|
tx := txs.Tx{
|
|
Unsigned: &baseTx,
|
|
}
|
|
|
|
state.EXPECT().GetUTXO(utxoID.InputID()).Return(&utxo, nil)
|
|
state.EXPECT().GetTx(asset.ID).Return(&tx, nil)
|
|
|
|
return state
|
|
},
|
|
txFunc: func(require *require.Assertions) *txs.Tx {
|
|
tx := &txs.Tx{
|
|
Unsigned: &baseTx,
|
|
}
|
|
require.NoError(tx.SignSECP256K1Fx(
|
|
[][]*secp256k1.PrivateKey{
|
|
{keys[0]},
|
|
},
|
|
))
|
|
return tx
|
|
},
|
|
err: errNotAnAsset,
|
|
},
|
|
}
|
|
for _, test := range tests {
|
|
t.Run(test.name, func(t *testing.T) {
|
|
require := require.New(t)
|
|
ctrl := gomock.NewController(t)
|
|
|
|
state := test.stateFunc(ctrl)
|
|
tx := test.txFunc(require)
|
|
|
|
err := tx.Unsigned.Visit(&SemanticVerifier{
|
|
Backend: backendObj,
|
|
State: state,
|
|
Tx: tx,
|
|
})
|
|
require.ErrorIs(err, test.err)
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestSemanticVerifierExportTx(t *testing.T) {
|
|
ctx := context.Background()
|
|
cChainID := ids.GenerateTestID()
|
|
chainID := ids.GenerateTestID()
|
|
|
|
fxIndex := txs.NewFxIndex()
|
|
secpFx := &secp256k1fx.Fx{}
|
|
_, err := txs.NewCustomParser(
|
|
fxIndex,
|
|
new(mockable.Clock),
|
|
log.NoLog{},
|
|
[]fxs.Fx{
|
|
secpFx,
|
|
},
|
|
)
|
|
require.NoError(t, err)
|
|
|
|
txID := ids.GenerateTestID()
|
|
utxoID := lux.UTXOID{
|
|
TxID: txID,
|
|
OutputIndex: 2,
|
|
}
|
|
asset := lux.Asset{
|
|
ID: ids.GenerateTestID(),
|
|
}
|
|
inputSigner := secp256k1fx.Input{
|
|
SigIndices: []uint32{
|
|
0,
|
|
},
|
|
}
|
|
fxInput := secp256k1fx.TransferInput{
|
|
Amt: 12345,
|
|
Input: inputSigner,
|
|
}
|
|
input := lux.TransferableInput{
|
|
UTXOID: utxoID,
|
|
Asset: asset,
|
|
In: &fxInput,
|
|
}
|
|
baseTx := txs.BaseTx{
|
|
BaseTx: lux.BaseTx{
|
|
NetworkID: constants.UnitTestID,
|
|
BlockchainID: chainID,
|
|
Ins: []*lux.TransferableInput{
|
|
&input,
|
|
},
|
|
},
|
|
}
|
|
exportTx := txs.ExportTx{
|
|
BaseTx: baseTx,
|
|
DestinationChain: cChainID,
|
|
}
|
|
|
|
backendObj := &Backend{
|
|
Ctx: ctx,
|
|
Runtime: &runtime.Runtime{
|
|
ChainID: chainID, // Use same chainID as baseTx
|
|
ValidatorState: &testValidatorState{chainID: chainID},
|
|
},
|
|
CChainID: cChainID,
|
|
Config: &feeConfig,
|
|
Fxs: []*fxs.ParsedFx{
|
|
{
|
|
ID: secp256k1fx.ID,
|
|
Fx: secpFx,
|
|
},
|
|
},
|
|
FxIndex: fxIndex,
|
|
FeeAssetID: ids.GenerateTestID(),
|
|
Bootstrapped: true,
|
|
}
|
|
require.NoError(t, secpFx.Bootstrapped())
|
|
|
|
outputOwners := secp256k1fx.OutputOwners{
|
|
Threshold: 1,
|
|
Addrs: []ids.ShortID{
|
|
keys[0].Address(),
|
|
},
|
|
}
|
|
output := secp256k1fx.TransferOutput{
|
|
Amt: 12345,
|
|
OutputOwners: outputOwners,
|
|
}
|
|
utxo := lux.UTXO{
|
|
UTXOID: utxoID,
|
|
Asset: asset,
|
|
Out: &output,
|
|
}
|
|
unsignedCreateAssetTx := txs.CreateAssetTx{
|
|
States: []*txs.InitialState{{
|
|
FxIndex: 0,
|
|
}},
|
|
}
|
|
createAssetTx := txs.Tx{
|
|
Unsigned: &unsignedCreateAssetTx,
|
|
}
|
|
|
|
tests := []struct {
|
|
name string
|
|
stateFunc func(*gomock.Controller) state.Chain
|
|
txFunc func(*require.Assertions) *txs.Tx
|
|
err error
|
|
}{
|
|
{
|
|
name: "valid",
|
|
stateFunc: func(ctrl *gomock.Controller) state.Chain {
|
|
state := statemock.NewChain(ctrl)
|
|
|
|
state.EXPECT().GetUTXO(utxoID.InputID()).Return(&utxo, nil)
|
|
state.EXPECT().GetTx(asset.ID).Return(&createAssetTx, nil)
|
|
|
|
return state
|
|
},
|
|
txFunc: func(require *require.Assertions) *txs.Tx {
|
|
tx := &txs.Tx{
|
|
Unsigned: &exportTx,
|
|
}
|
|
require.NoError(tx.SignSECP256K1Fx(
|
|
[][]*secp256k1.PrivateKey{
|
|
{keys[0]},
|
|
},
|
|
))
|
|
return tx
|
|
},
|
|
err: nil,
|
|
},
|
|
{
|
|
name: "assetID mismatch",
|
|
stateFunc: func(ctrl *gomock.Controller) state.Chain {
|
|
state := statemock.NewChain(ctrl)
|
|
|
|
utxo := utxo
|
|
utxo.Asset.ID = ids.GenerateTestID()
|
|
|
|
state.EXPECT().GetUTXO(utxoID.InputID()).Return(&utxo, nil)
|
|
|
|
return state
|
|
},
|
|
txFunc: func(require *require.Assertions) *txs.Tx {
|
|
tx := &txs.Tx{
|
|
Unsigned: &exportTx,
|
|
}
|
|
require.NoError(tx.SignSECP256K1Fx(
|
|
[][]*secp256k1.PrivateKey{
|
|
{keys[0]},
|
|
},
|
|
))
|
|
return tx
|
|
},
|
|
err: errAssetIDMismatch,
|
|
},
|
|
{
|
|
name: "not allowed input feature extension",
|
|
stateFunc: func(ctrl *gomock.Controller) state.Chain {
|
|
state := statemock.NewChain(ctrl)
|
|
|
|
unsignedCreateAssetTx := unsignedCreateAssetTx
|
|
unsignedCreateAssetTx.States = nil
|
|
|
|
createAssetTx := txs.Tx{
|
|
Unsigned: &unsignedCreateAssetTx,
|
|
}
|
|
|
|
state.EXPECT().GetUTXO(utxoID.InputID()).Return(&utxo, nil)
|
|
state.EXPECT().GetTx(asset.ID).Return(&createAssetTx, nil)
|
|
|
|
return state
|
|
},
|
|
txFunc: func(require *require.Assertions) *txs.Tx {
|
|
tx := &txs.Tx{
|
|
Unsigned: &exportTx,
|
|
}
|
|
require.NoError(tx.SignSECP256K1Fx(
|
|
[][]*secp256k1.PrivateKey{
|
|
{keys[0]},
|
|
},
|
|
))
|
|
return tx
|
|
},
|
|
err: errIncompatibleFx,
|
|
},
|
|
{
|
|
name: "invalid signature",
|
|
stateFunc: func(ctrl *gomock.Controller) state.Chain {
|
|
state := statemock.NewChain(ctrl)
|
|
|
|
state.EXPECT().GetUTXO(utxoID.InputID()).Return(&utxo, nil)
|
|
state.EXPECT().GetTx(asset.ID).Return(&createAssetTx, nil)
|
|
|
|
return state
|
|
},
|
|
txFunc: func(require *require.Assertions) *txs.Tx {
|
|
tx := &txs.Tx{
|
|
Unsigned: &exportTx,
|
|
}
|
|
require.NoError(tx.SignSECP256K1Fx(
|
|
[][]*secp256k1.PrivateKey{
|
|
{keys[1]},
|
|
},
|
|
))
|
|
return tx
|
|
},
|
|
err: secp256k1fx.ErrWrongSig,
|
|
},
|
|
{
|
|
name: "missing UTXO",
|
|
stateFunc: func(ctrl *gomock.Controller) state.Chain {
|
|
state := statemock.NewChain(ctrl)
|
|
|
|
state.EXPECT().GetUTXO(utxoID.InputID()).Return(nil, database.ErrNotFound)
|
|
|
|
return state
|
|
},
|
|
txFunc: func(require *require.Assertions) *txs.Tx {
|
|
tx := &txs.Tx{
|
|
Unsigned: &exportTx,
|
|
}
|
|
require.NoError(tx.SignSECP256K1Fx(
|
|
[][]*secp256k1.PrivateKey{
|
|
{keys[0]},
|
|
},
|
|
))
|
|
return tx
|
|
},
|
|
err: database.ErrNotFound,
|
|
},
|
|
{
|
|
name: "invalid UTXO amount",
|
|
stateFunc: func(ctrl *gomock.Controller) state.Chain {
|
|
state := statemock.NewChain(ctrl)
|
|
|
|
output := output
|
|
output.Amt--
|
|
|
|
utxo := utxo
|
|
utxo.Out = &output
|
|
|
|
state.EXPECT().GetUTXO(utxoID.InputID()).Return(&utxo, nil)
|
|
state.EXPECT().GetTx(asset.ID).Return(&createAssetTx, nil)
|
|
|
|
return state
|
|
},
|
|
txFunc: func(require *require.Assertions) *txs.Tx {
|
|
tx := &txs.Tx{
|
|
Unsigned: &exportTx,
|
|
}
|
|
require.NoError(tx.SignSECP256K1Fx(
|
|
[][]*secp256k1.PrivateKey{
|
|
{keys[0]},
|
|
},
|
|
))
|
|
return tx
|
|
},
|
|
err: secp256k1fx.ErrMismatchedAmounts,
|
|
},
|
|
{
|
|
name: "not allowed output feature extension",
|
|
stateFunc: func(ctrl *gomock.Controller) state.Chain {
|
|
state := statemock.NewChain(ctrl)
|
|
|
|
unsignedCreateAssetTx := unsignedCreateAssetTx
|
|
unsignedCreateAssetTx.States = nil
|
|
|
|
createAssetTx := txs.Tx{
|
|
Unsigned: &unsignedCreateAssetTx,
|
|
}
|
|
|
|
state.EXPECT().GetTx(asset.ID).Return(&createAssetTx, nil)
|
|
|
|
return state
|
|
},
|
|
txFunc: func(require *require.Assertions) *txs.Tx {
|
|
exportTx := exportTx
|
|
exportTx.Ins = nil
|
|
exportTx.ExportedOuts = []*lux.TransferableOutput{
|
|
{
|
|
Asset: asset,
|
|
Out: &output,
|
|
},
|
|
}
|
|
tx := &txs.Tx{
|
|
Unsigned: &exportTx,
|
|
}
|
|
require.NoError(tx.SignSECP256K1Fx(
|
|
[][]*secp256k1.PrivateKey{},
|
|
))
|
|
return tx
|
|
},
|
|
err: errIncompatibleFx,
|
|
},
|
|
{
|
|
name: "unknown asset",
|
|
stateFunc: func(ctrl *gomock.Controller) state.Chain {
|
|
state := statemock.NewChain(ctrl)
|
|
|
|
state.EXPECT().GetUTXO(utxoID.InputID()).Return(&utxo, nil)
|
|
state.EXPECT().GetTx(asset.ID).Return(nil, database.ErrNotFound)
|
|
|
|
return state
|
|
},
|
|
txFunc: func(require *require.Assertions) *txs.Tx {
|
|
tx := &txs.Tx{
|
|
Unsigned: &exportTx,
|
|
}
|
|
require.NoError(tx.SignSECP256K1Fx(
|
|
[][]*secp256k1.PrivateKey{
|
|
{keys[0]},
|
|
},
|
|
))
|
|
return tx
|
|
},
|
|
err: database.ErrNotFound,
|
|
},
|
|
{
|
|
name: "not an asset",
|
|
stateFunc: func(ctrl *gomock.Controller) state.Chain {
|
|
state := statemock.NewChain(ctrl)
|
|
|
|
tx := txs.Tx{
|
|
Unsigned: &baseTx,
|
|
}
|
|
|
|
state.EXPECT().GetUTXO(utxoID.InputID()).Return(&utxo, nil)
|
|
state.EXPECT().GetTx(asset.ID).Return(&tx, nil)
|
|
|
|
return state
|
|
},
|
|
txFunc: func(require *require.Assertions) *txs.Tx {
|
|
tx := &txs.Tx{
|
|
Unsigned: &exportTx,
|
|
}
|
|
require.NoError(tx.SignSECP256K1Fx(
|
|
[][]*secp256k1.PrivateKey{
|
|
{keys[0]},
|
|
},
|
|
))
|
|
return tx
|
|
},
|
|
err: errNotAnAsset,
|
|
},
|
|
}
|
|
for _, test := range tests {
|
|
t.Run(test.name, func(t *testing.T) {
|
|
require := require.New(t)
|
|
ctrl := gomock.NewController(t)
|
|
|
|
state := test.stateFunc(ctrl)
|
|
tx := test.txFunc(require)
|
|
|
|
err := tx.Unsigned.Visit(&SemanticVerifier{
|
|
Backend: backendObj,
|
|
State: state,
|
|
Tx: tx,
|
|
})
|
|
require.ErrorIs(err, test.err)
|
|
})
|
|
}
|
|
}
|
|
|
|
// testValidatorState is a simple stub for validators.State used in tests
|
|
type testValidatorState struct {
|
|
chainID ids.ID // The chain/chain ID this validator state returns
|
|
netIDs map[ids.ID]ids.ID
|
|
}
|
|
|
|
var _ validators.State = (*testValidatorState)(nil)
|
|
|
|
func (t *testValidatorState) GetChainID(_ ids.ID) (ids.ID, error) {
|
|
// Returns the chain/chain ID for the given chain
|
|
return t.chainID, nil
|
|
}
|
|
|
|
func (t *testValidatorState) GetNetworkID(chainID ids.ID) (ids.ID, error) {
|
|
if t.netIDs != nil {
|
|
if netID, ok := t.netIDs[chainID]; ok {
|
|
return netID, nil
|
|
}
|
|
}
|
|
return t.chainID, nil
|
|
}
|
|
|
|
func (t *testValidatorState) GetValidatorSet(ctx context.Context, height uint64, netID ids.ID) (map[ids.NodeID]*validators.GetValidatorOutput, error) {
|
|
return make(map[ids.NodeID]*validators.GetValidatorOutput), nil
|
|
}
|
|
|
|
func (t *testValidatorState) GetCurrentValidators(ctx context.Context, height uint64, netID ids.ID) (map[ids.NodeID]*validators.GetValidatorOutput, error) {
|
|
return t.GetValidatorSet(ctx, height, netID)
|
|
}
|
|
|
|
func (t *testValidatorState) GetCurrentHeight(ctx context.Context) (uint64, error) {
|
|
return 0, nil
|
|
}
|
|
|
|
func (t *testValidatorState) GetMinimumHeight(ctx context.Context) (uint64, error) {
|
|
return 0, nil
|
|
}
|
|
|
|
func (t *testValidatorState) GetWarpValidatorSet(ctx context.Context, height uint64, netID ids.ID) (*validators.WarpSet, error) {
|
|
return &validators.WarpSet{
|
|
Height: height,
|
|
Validators: make(map[ids.NodeID]*validators.WarpValidator),
|
|
}, nil
|
|
}
|
|
|
|
func (t *testValidatorState) GetWarpValidatorSets(ctx context.Context, heights []uint64, netIDs []ids.ID) (map[ids.ID]map[uint64]*validators.WarpSet, error) {
|
|
result := make(map[ids.ID]map[uint64]*validators.WarpSet)
|
|
for _, netID := range netIDs {
|
|
result[netID] = make(map[uint64]*validators.WarpSet)
|
|
for _, height := range heights {
|
|
result[netID][height] = &validators.WarpSet{
|
|
Height: height,
|
|
Validators: make(map[ids.NodeID]*validators.WarpValidator),
|
|
}
|
|
}
|
|
}
|
|
return result, nil
|
|
}
|
|
|
|
func TestSemanticVerifierExportTxDifferentNet(t *testing.T) {
|
|
ctrl := gomock.NewController(t)
|
|
cChainID := ids.GenerateTestID()
|
|
|
|
rt := consensustest.Runtime(t, consensustest.XChainID)
|
|
|
|
// Set up a validator state that returns different network IDs to trigger the error
|
|
localNetID := ids.GenerateTestID()
|
|
peerNetID := ids.GenerateTestID()
|
|
rt.ValidatorState = &testValidatorState{
|
|
chainID: localNetID,
|
|
netIDs: map[ids.ID]ids.ID{
|
|
rt.ChainID: localNetID,
|
|
cChainID: peerNetID,
|
|
},
|
|
}
|
|
|
|
fxIndex := txs.NewFxIndex()
|
|
secpFx := &secp256k1fx.Fx{}
|
|
_, err := txs.NewCustomParser(
|
|
fxIndex,
|
|
new(mockable.Clock),
|
|
log.NoLog{},
|
|
[]fxs.Fx{
|
|
secpFx,
|
|
},
|
|
)
|
|
require.NoError(t, err)
|
|
|
|
txID := ids.GenerateTestID()
|
|
utxoID := lux.UTXOID{
|
|
TxID: txID,
|
|
OutputIndex: 2,
|
|
}
|
|
asset := lux.Asset{
|
|
ID: ids.GenerateTestID(),
|
|
}
|
|
inputSigner := secp256k1fx.Input{
|
|
SigIndices: []uint32{
|
|
0,
|
|
},
|
|
}
|
|
fxInput := secp256k1fx.TransferInput{
|
|
Amt: 12345,
|
|
Input: inputSigner,
|
|
}
|
|
input := lux.TransferableInput{
|
|
UTXOID: utxoID,
|
|
Asset: asset,
|
|
In: &fxInput,
|
|
}
|
|
baseTx := txs.BaseTx{
|
|
BaseTx: lux.BaseTx{
|
|
Ins: []*lux.TransferableInput{
|
|
&input,
|
|
},
|
|
},
|
|
}
|
|
exportTx := txs.ExportTx{
|
|
BaseTx: baseTx,
|
|
DestinationChain: cChainID,
|
|
}
|
|
|
|
backendObj := &Backend{
|
|
Ctx: context.Background(),
|
|
Runtime: rt,
|
|
CChainID: cChainID,
|
|
Config: &feeConfig,
|
|
Fxs: []*fxs.ParsedFx{
|
|
{
|
|
ID: secp256k1fx.ID,
|
|
Fx: secpFx,
|
|
},
|
|
},
|
|
FxIndex: fxIndex,
|
|
FeeAssetID: ids.GenerateTestID(),
|
|
Bootstrapped: true,
|
|
}
|
|
require.NoError(t, secpFx.Bootstrapped())
|
|
|
|
outputOwners := secp256k1fx.OutputOwners{
|
|
Threshold: 1,
|
|
Addrs: []ids.ShortID{
|
|
keys[0].Address(),
|
|
},
|
|
}
|
|
output := secp256k1fx.TransferOutput{
|
|
Amt: 12345,
|
|
OutputOwners: outputOwners,
|
|
}
|
|
utxo := lux.UTXO{
|
|
UTXOID: utxoID,
|
|
Asset: asset,
|
|
Out: &output,
|
|
}
|
|
unsignedCreateAssetTx := txs.CreateAssetTx{
|
|
States: []*txs.InitialState{{
|
|
FxIndex: 0,
|
|
}},
|
|
}
|
|
createAssetTx := txs.Tx{
|
|
Unsigned: &unsignedCreateAssetTx,
|
|
}
|
|
|
|
state := statemock.NewChain(ctrl)
|
|
|
|
state.EXPECT().GetUTXO(utxoID.InputID()).Return(&utxo, nil)
|
|
state.EXPECT().GetTx(asset.ID).Return(&createAssetTx, nil)
|
|
|
|
tx := &txs.Tx{
|
|
Unsigned: &exportTx,
|
|
}
|
|
require.NoError(t, tx.SignSECP256K1Fx(
|
|
[][]*secp256k1.PrivateKey{
|
|
{keys[0]},
|
|
},
|
|
))
|
|
|
|
err = tx.Unsigned.Visit(&SemanticVerifier{
|
|
Backend: backendObj,
|
|
State: state,
|
|
Tx: tx,
|
|
})
|
|
require.ErrorIs(t, err, verify.ErrMismatchedNetIDs)
|
|
}
|
|
|
|
func TestSemanticVerifierImportTx(t *testing.T) {
|
|
// Create Runtime for chain operations
|
|
cChainID := ids.GenerateTestID()
|
|
chainID := ids.GenerateTestID()
|
|
_ = consensustest.Runtime(t, chainID)
|
|
ctx := context.Background() // Use standard context for Backend
|
|
m := atomic.NewMemory(prefixdb.New([]byte{0}, memdb.New()))
|
|
|
|
fxIndex := txs.NewFxIndex()
|
|
fx := &secp256k1fx.Fx{}
|
|
_, err := txs.NewCustomParser(
|
|
fxIndex,
|
|
new(mockable.Clock),
|
|
log.NoLog{},
|
|
[]fxs.Fx{
|
|
fx,
|
|
},
|
|
)
|
|
require.NoError(t, err)
|
|
|
|
utxoID := lux.UTXOID{
|
|
TxID: ids.GenerateTestID(),
|
|
OutputIndex: 2,
|
|
}
|
|
|
|
asset := lux.Asset{
|
|
ID: ids.GenerateTestID(),
|
|
}
|
|
outputOwners := secp256k1fx.OutputOwners{
|
|
Threshold: 1,
|
|
Addrs: []ids.ShortID{
|
|
keys[0].Address(),
|
|
},
|
|
}
|
|
baseTx := txs.BaseTx{
|
|
BaseTx: lux.BaseTx{
|
|
NetworkID: constants.UnitTestID,
|
|
BlockchainID: chainID,
|
|
Outs: []*lux.TransferableOutput{{
|
|
Asset: asset,
|
|
Out: &secp256k1fx.TransferOutput{
|
|
Amt: 1000,
|
|
OutputOwners: outputOwners,
|
|
},
|
|
}},
|
|
},
|
|
}
|
|
input := lux.TransferableInput{
|
|
UTXOID: utxoID,
|
|
Asset: asset,
|
|
In: &secp256k1fx.TransferInput{
|
|
Amt: 12345,
|
|
Input: secp256k1fx.Input{
|
|
SigIndices: []uint32{0},
|
|
},
|
|
},
|
|
}
|
|
unsignedImportTx := txs.ImportTx{
|
|
BaseTx: baseTx,
|
|
SourceChain: cChainID,
|
|
ImportedIns: []*lux.TransferableInput{
|
|
&input,
|
|
},
|
|
}
|
|
importTx := &txs.Tx{
|
|
Unsigned: &unsignedImportTx,
|
|
}
|
|
require.NoError(t, importTx.SignSECP256K1Fx(
|
|
[][]*secp256k1.PrivateKey{
|
|
{keys[0]},
|
|
},
|
|
))
|
|
|
|
backendObj := &Backend{
|
|
Ctx: ctx,
|
|
Runtime: &runtime.Runtime{
|
|
ChainID: chainID, // Use same chainID as baseTx
|
|
ValidatorState: &testValidatorState{chainID: chainID},
|
|
},
|
|
CChainID: cChainID,
|
|
Config: &feeConfig,
|
|
Fxs: []*fxs.ParsedFx{
|
|
{
|
|
ID: secp256k1fx.ID,
|
|
Fx: fx,
|
|
},
|
|
},
|
|
FxIndex: fxIndex,
|
|
FeeAssetID: ids.GenerateTestID(),
|
|
Bootstrapped: true,
|
|
SharedMemory: &testSharedMemory{sm: m.NewSharedMemory(chainID)},
|
|
}
|
|
require.NoError(t, fx.Bootstrapped())
|
|
|
|
output := secp256k1fx.TransferOutput{
|
|
Amt: 12345,
|
|
OutputOwners: outputOwners,
|
|
}
|
|
utxo := lux.UTXO{
|
|
UTXOID: utxoID,
|
|
Asset: asset,
|
|
Out: &output,
|
|
}
|
|
utxoBytes, err := utxo.WireBytes()
|
|
require.NoError(t, err)
|
|
|
|
peerSharedMemory := m.NewSharedMemory(cChainID)
|
|
inputID := utxo.InputID()
|
|
require.NoError(t, peerSharedMemory.Apply(map[ids.ID]*atomic.Requests{chainID: {PutRequests: []*atomic.Element{{
|
|
Key: inputID[:],
|
|
Value: utxoBytes,
|
|
Traits: [][]byte{
|
|
keys[0].PublicKey().Address().Bytes(),
|
|
},
|
|
}}}}))
|
|
|
|
unsignedCreateAssetTx := txs.CreateAssetTx{
|
|
States: []*txs.InitialState{{
|
|
FxIndex: 0,
|
|
}},
|
|
}
|
|
createAssetTx := txs.Tx{
|
|
Unsigned: &unsignedCreateAssetTx,
|
|
}
|
|
tests := []struct {
|
|
name string
|
|
stateFunc func(*gomock.Controller) state.Chain
|
|
txFunc func(*require.Assertions) *txs.Tx
|
|
expectedErr error
|
|
}{
|
|
{
|
|
name: "valid",
|
|
stateFunc: func(ctrl *gomock.Controller) state.Chain {
|
|
state := statemock.NewChain(ctrl)
|
|
state.EXPECT().GetUTXO(utxoID.InputID()).Return(&utxo, nil).AnyTimes()
|
|
state.EXPECT().GetTx(asset.ID).Return(&createAssetTx, nil).AnyTimes()
|
|
return state
|
|
},
|
|
txFunc: func(*require.Assertions) *txs.Tx {
|
|
return importTx
|
|
},
|
|
expectedErr: nil,
|
|
},
|
|
{
|
|
name: "not allowed input feature extension",
|
|
stateFunc: func(ctrl *gomock.Controller) state.Chain {
|
|
state := statemock.NewChain(ctrl)
|
|
unsignedCreateAssetTx := unsignedCreateAssetTx
|
|
unsignedCreateAssetTx.States = nil
|
|
createAssetTx := txs.Tx{
|
|
Unsigned: &unsignedCreateAssetTx,
|
|
}
|
|
state.EXPECT().GetUTXO(utxoID.InputID()).Return(&utxo, nil).AnyTimes()
|
|
state.EXPECT().GetTx(asset.ID).Return(&createAssetTx, nil).AnyTimes()
|
|
return state
|
|
},
|
|
txFunc: func(*require.Assertions) *txs.Tx {
|
|
return importTx
|
|
},
|
|
expectedErr: errIncompatibleFx,
|
|
},
|
|
{
|
|
name: "invalid signature",
|
|
stateFunc: func(ctrl *gomock.Controller) state.Chain {
|
|
state := statemock.NewChain(ctrl)
|
|
state.EXPECT().GetUTXO(utxoID.InputID()).Return(&utxo, nil).AnyTimes()
|
|
state.EXPECT().GetTx(asset.ID).Return(&createAssetTx, nil).AnyTimes()
|
|
return state
|
|
},
|
|
txFunc: func(require *require.Assertions) *txs.Tx {
|
|
tx := &txs.Tx{
|
|
Unsigned: &unsignedImportTx,
|
|
}
|
|
require.NoError(tx.SignSECP256K1Fx(
|
|
[][]*secp256k1.PrivateKey{
|
|
{keys[1]},
|
|
},
|
|
))
|
|
return tx
|
|
},
|
|
expectedErr: secp256k1fx.ErrWrongSig,
|
|
},
|
|
{
|
|
name: "not allowed output feature extension",
|
|
stateFunc: func(ctrl *gomock.Controller) state.Chain {
|
|
state := statemock.NewChain(ctrl)
|
|
unsignedCreateAssetTx := unsignedCreateAssetTx
|
|
unsignedCreateAssetTx.States = nil
|
|
createAssetTx := txs.Tx{
|
|
Unsigned: &unsignedCreateAssetTx,
|
|
}
|
|
state.EXPECT().GetTx(asset.ID).Return(&createAssetTx, nil).AnyTimes()
|
|
return state
|
|
},
|
|
txFunc: func(require *require.Assertions) *txs.Tx {
|
|
importTx := unsignedImportTx
|
|
importTx.Ins = nil
|
|
importTx.ImportedIns = []*lux.TransferableInput{
|
|
&input,
|
|
}
|
|
tx := &txs.Tx{
|
|
Unsigned: &importTx,
|
|
}
|
|
require.NoError(tx.SignSECP256K1Fx(
|
|
nil,
|
|
))
|
|
return tx
|
|
},
|
|
expectedErr: errIncompatibleFx,
|
|
},
|
|
{
|
|
name: "unknown asset",
|
|
stateFunc: func(ctrl *gomock.Controller) state.Chain {
|
|
state := statemock.NewChain(ctrl)
|
|
state.EXPECT().GetUTXO(utxoID.InputID()).Return(&utxo, nil).AnyTimes()
|
|
state.EXPECT().GetTx(asset.ID).Return(nil, database.ErrNotFound)
|
|
return state
|
|
},
|
|
txFunc: func(*require.Assertions) *txs.Tx {
|
|
return importTx
|
|
},
|
|
expectedErr: database.ErrNotFound,
|
|
},
|
|
{
|
|
name: "not an asset",
|
|
stateFunc: func(ctrl *gomock.Controller) state.Chain {
|
|
state := statemock.NewChain(ctrl)
|
|
tx := txs.Tx{
|
|
Unsigned: &baseTx,
|
|
}
|
|
state.EXPECT().GetUTXO(utxoID.InputID()).Return(&utxo, nil).AnyTimes()
|
|
state.EXPECT().GetTx(asset.ID).Return(&tx, nil)
|
|
return state
|
|
},
|
|
txFunc: func(*require.Assertions) *txs.Tx {
|
|
return importTx
|
|
},
|
|
expectedErr: errNotAnAsset,
|
|
},
|
|
}
|
|
for _, test := range tests {
|
|
t.Run(test.name, func(t *testing.T) {
|
|
require := require.New(t)
|
|
ctrl := gomock.NewController(t)
|
|
|
|
state := test.stateFunc(ctrl)
|
|
tx := test.txFunc(require)
|
|
err := tx.Unsigned.Visit(&SemanticVerifier{
|
|
Backend: backendObj,
|
|
State: state,
|
|
Tx: tx,
|
|
})
|
|
require.ErrorIs(err, test.expectedErr)
|
|
})
|
|
}
|
|
}
|