package mov import ( "math" "os" "testing" "github.com/vapor/application/mov/common" "github.com/vapor/application/mov/database" "github.com/vapor/application/mov/mock" "github.com/vapor/consensus" dbm "github.com/vapor/database/leveldb" "github.com/vapor/protocol/bc" "github.com/vapor/protocol/bc/types" "github.com/vapor/protocol/vm" "github.com/vapor/testutil" ) /* @addTest:BeforeProposalBlock: will gas affect generate tx? will be packed tx affect generate tx? @addTest:TestApplyBlock: one block has two different trade pairs & different trade pair won't affect each order(attach & detach) @addTest:TestApplyBlock: node packed maker tx and match transaction in random order(attach & detach) @addTest:TestValidateBlock: one tx has trade input and cancel input mixed @addTest:TestValidateBlock: regular match transaction's seller program is also a P2WMCProgram */ func TestApplyBlock(t *testing.T) { initBlockHeader := &types.BlockHeader{Height: 1, PreviousBlockHash: bc.Hash{}} cases := []struct { desc string block *types.Block blockFunc testFun initOrders []*common.Order wantOrders []*common.Order wantDBState *common.MovDatabaseState wantError error }{ { desc: "apply block has pending order transaction", block: &types.Block{ BlockHeader: types.BlockHeader{Height: 2, PreviousBlockHash: initBlockHeader.Hash()}, Transactions: []*types.Tx{ mock.Btc2EthMakerTxs[0], mock.Eth2BtcMakerTxs[0], }, }, blockFunc: applyBlock, wantOrders: []*common.Order{mock.MustNewOrderFromOutput(mock.Btc2EthMakerTxs[0], 0), mock.MustNewOrderFromOutput(mock.Eth2BtcMakerTxs[0], 0)}, wantDBState: &common.MovDatabaseState{Height: 2, Hash: hashPtr(testutil.MustDecodeHash("88dbcde57bb2b53b107d7494f20f1f1a892307a019705980c3510890449c0020"))}, }, { desc: "apply block has full matched transaction", block: &types.Block{ BlockHeader: types.BlockHeader{Height: 2, PreviousBlockHash: initBlockHeader.Hash()}, Transactions: []*types.Tx{ mock.MatchedTxs[1], }, }, blockFunc: applyBlock, initOrders: []*common.Order{mock.Btc2EthOrders[0], mock.Btc2EthOrders[1], mock.Eth2BtcOrders[0]}, wantOrders: []*common.Order{mock.Btc2EthOrders[1]}, wantDBState: &common.MovDatabaseState{Height: 2, Hash: hashPtr(testutil.MustDecodeHash("88dbcde57bb2b53b107d7494f20f1f1a892307a019705980c3510890449c0020"))}, }, { desc: "apply block has partial matched transaction", block: &types.Block{ BlockHeader: types.BlockHeader{Height: 2, PreviousBlockHash: initBlockHeader.Hash()}, Transactions: []*types.Tx{ mock.MatchedTxs[0], }, }, blockFunc: applyBlock, initOrders: []*common.Order{mock.Btc2EthOrders[0], mock.Eth2BtcOrders[1]}, wantOrders: []*common.Order{mock.MustNewOrderFromOutput(mock.MatchedTxs[0], 1)}, wantDBState: &common.MovDatabaseState{Height: 2, Hash: hashPtr(testutil.MustDecodeHash("88dbcde57bb2b53b107d7494f20f1f1a892307a019705980c3510890449c0020"))}, }, { desc: "apply block has two partial matched transaction", block: &types.Block{ BlockHeader: types.BlockHeader{Height: 2, PreviousBlockHash: initBlockHeader.Hash()}, Transactions: []*types.Tx{ mock.MatchedTxs[2], mock.MatchedTxs[3], }, }, blockFunc: applyBlock, initOrders: []*common.Order{mock.Btc2EthOrders[0], mock.Btc2EthOrders[1], mock.Eth2BtcOrders[2]}, wantOrders: []*common.Order{mock.MustNewOrderFromOutput(mock.MatchedTxs[3], 1)}, wantDBState: &common.MovDatabaseState{Height: 2, Hash: hashPtr(testutil.MustDecodeHash("88dbcde57bb2b53b107d7494f20f1f1a892307a019705980c3510890449c0020"))}, }, { desc: "apply block has partial matched transaction by pending orders from tx pool", block: &types.Block{ BlockHeader: types.BlockHeader{Height: 2, PreviousBlockHash: initBlockHeader.Hash()}, Transactions: []*types.Tx{ mock.Btc2EthMakerTxs[0], mock.Eth2BtcMakerTxs[1], mock.MatchedTxs[4], }, }, blockFunc: applyBlock, initOrders: []*common.Order{}, wantOrders: []*common.Order{mock.MustNewOrderFromOutput(mock.MatchedTxs[4], 1)}, wantDBState: &common.MovDatabaseState{Height: 2, Hash: hashPtr(testutil.MustDecodeHash("88dbcde57bb2b53b107d7494f20f1f1a892307a019705980c3510890449c0020"))}, }, { desc: "detach block has pending order transaction", block: &types.Block{ BlockHeader: *initBlockHeader, Transactions: []*types.Tx{ mock.Btc2EthMakerTxs[0], mock.Eth2BtcMakerTxs[1], }, }, blockFunc: detachBlock, initOrders: []*common.Order{mock.MustNewOrderFromOutput(mock.Btc2EthMakerTxs[0], 0), mock.MustNewOrderFromOutput(mock.Eth2BtcMakerTxs[1], 0)}, wantOrders: []*common.Order{}, wantDBState: &common.MovDatabaseState{Height: 0, Hash: &bc.Hash{}}, }, { desc: "detach block has full matched transaction", block: &types.Block{ BlockHeader: *initBlockHeader, Transactions: []*types.Tx{ mock.MatchedTxs[1], }, }, blockFunc: detachBlock, initOrders: []*common.Order{mock.Btc2EthOrders[1]}, wantOrders: []*common.Order{mock.Btc2EthOrders[0], mock.Btc2EthOrders[1], mock.Eth2BtcOrders[0]}, wantDBState: &common.MovDatabaseState{Height: 0, Hash: &bc.Hash{}}, }, { desc: "detach block has partial matched transaction", block: &types.Block{ BlockHeader: *initBlockHeader, Transactions: []*types.Tx{ mock.MatchedTxs[0], }, }, blockFunc: detachBlock, initOrders: []*common.Order{mock.MustNewOrderFromOutput(mock.MatchedTxs[0], 1)}, wantOrders: []*common.Order{mock.Btc2EthOrders[0], mock.Eth2BtcOrders[1]}, wantDBState: &common.MovDatabaseState{Height: 0, Hash: &bc.Hash{}}, }, { desc: "detach block has two partial matched transaction", block: &types.Block{ BlockHeader: *initBlockHeader, Transactions: []*types.Tx{ mock.MatchedTxs[2], mock.MatchedTxs[3], }, }, blockFunc: detachBlock, initOrders: []*common.Order{mock.MustNewOrderFromOutput(mock.MatchedTxs[3], 1)}, wantOrders: []*common.Order{mock.Btc2EthOrders[0], mock.Btc2EthOrders[1], mock.Eth2BtcOrders[2]}, wantDBState: &common.MovDatabaseState{Height: 0, Hash: &bc.Hash{}}, }, } defer os.RemoveAll("temp") for i, c := range cases { testDB := dbm.NewDB("testdb", "leveldb", "temp") store := database.NewLevelDBMovStore(testDB) if err := store.InitDBState(0, &bc.Hash{}); err != nil { t.Fatal(err) } if err := store.ProcessOrders(c.initOrders, nil, initBlockHeader); err != nil { t.Fatal(err) } movCore := &MovCore{movStore: store} if err := c.blockFunc(movCore, c.block); err != c.wantError { t.Errorf("#%d(%s):apply block want error(%v), got error(%v)", i, c.desc, c.wantError, err) } gotOrders := queryAllOrders(store) if !ordersEquals(c.wantOrders, gotOrders) { t.Errorf("#%d(%s):apply block want orders(%v), got orders(%v)", i, c.desc, c.wantOrders, gotOrders) } dbState, err := store.GetMovDatabaseState() if err != nil { t.Fatal(err) } if !testutil.DeepEqual(c.wantDBState, dbState) { t.Errorf("#%d(%s):apply block want db state(%v), got db state(%v)", i, c.desc, c.wantDBState, dbState) } testDB.Close() os.RemoveAll("temp") } } func TestValidateBlock(t *testing.T) { cases := []struct { desc string block *types.Block verifyResults []*bc.TxVerifyResult wantError error }{ { desc: "block only has maker tx", block: &types.Block{ Transactions: []*types.Tx{ mock.Eth2BtcMakerTxs[0], mock.Btc2EthMakerTxs[0], }, }, verifyResults: []*bc.TxVerifyResult{{StatusFail: false}, {StatusFail: false}}, wantError: nil, }, { desc: "block only has matched tx", block: &types.Block{ Transactions: []*types.Tx{ mock.MatchedTxs[0], mock.MatchedTxs[1], mock.MatchedTxs[2], }, }, verifyResults: []*bc.TxVerifyResult{{StatusFail: false}, {StatusFail: false}, {StatusFail: false}}, wantError: nil, }, { desc: "block has maker tx and matched tx", block: &types.Block{ Transactions: []*types.Tx{ mock.Eth2BtcMakerTxs[0], mock.Btc2EthMakerTxs[0], mock.MatchedTxs[0], mock.MatchedTxs[1], mock.MatchedTxs[2], }, }, verifyResults: []*bc.TxVerifyResult{{StatusFail: false}, {StatusFail: false}, {StatusFail: false}, {StatusFail: false}, {StatusFail: false}}, wantError: nil, }, { desc: "status fail of maker tx is true", block: &types.Block{ Transactions: []*types.Tx{ mock.Eth2BtcMakerTxs[0], mock.Btc2EthMakerTxs[0], }, }, verifyResults: []*bc.TxVerifyResult{{StatusFail: false}, {StatusFail: true}}, wantError: errStatusFailMustFalse, }, { desc: "status fail of matched tx is true", block: &types.Block{ Transactions: []*types.Tx{ mock.MatchedTxs[1], mock.MatchedTxs[2], }, }, verifyResults: []*bc.TxVerifyResult{{StatusFail: false}, {StatusFail: true}}, wantError: errStatusFailMustFalse, }, { desc: "asset id in matched tx is not unique", block: &types.Block{ Transactions: []*types.Tx{ types.NewTx(types.TxData{ Inputs: []*types.TxInput{ types.NewSpendInput([][]byte{vm.Int64Bytes(0), vm.Int64Bytes(1)}, *mock.Btc2EthOrders[0].Utxo.SourceID, *mock.Btc2EthOrders[0].FromAssetID, mock.Btc2EthOrders[0].Utxo.Amount, mock.Btc2EthOrders[0].Utxo.SourcePos, mock.Btc2EthOrders[0].Utxo.ControlProgram), types.NewSpendInput([][]byte{vm.Int64Bytes(1), vm.Int64Bytes(1)}, *mock.Eth2BtcOrders[0].Utxo.SourceID, *mock.Btc2EthOrders[0].FromAssetID, mock.Eth2BtcOrders[0].Utxo.Amount, mock.Eth2BtcOrders[0].Utxo.SourcePos, mock.Eth2BtcOrders[0].Utxo.ControlProgram), }, Outputs: []*types.TxOutput{ types.NewIntraChainOutput(*mock.Btc2EthOrders[0].ToAssetID, 500, testutil.MustDecodeHexString("51")), types.NewIntraChainOutput(*mock.Eth2BtcOrders[0].ToAssetID, 10, testutil.MustDecodeHexString("53")), types.NewIntraChainOutput(*mock.Btc2EthOrders[0].ToAssetID, 10, []byte{0x51}), }, }), }, }, verifyResults: []*bc.TxVerifyResult{{StatusFail: false}, {StatusFail: true}}, wantError: errAssetIDMustUniqueInMatchedTx, }, { desc: "common input in the matched tx", block: &types.Block{ Transactions: []*types.Tx{ types.NewTx(types.TxData{ Inputs: []*types.TxInput{ types.NewSpendInput([][]byte{vm.Int64Bytes(0), vm.Int64Bytes(1)}, *mock.Btc2EthOrders[0].Utxo.SourceID, *mock.Btc2EthOrders[0].FromAssetID, mock.Btc2EthOrders[0].Utxo.Amount, mock.Btc2EthOrders[0].Utxo.SourcePos, mock.Btc2EthOrders[0].Utxo.ControlProgram), types.NewSpendInput([][]byte{vm.Int64Bytes(1), vm.Int64Bytes(1)}, *mock.Eth2BtcOrders[0].Utxo.SourceID, *mock.Eth2BtcOrders[0].FromAssetID, mock.Eth2BtcOrders[0].Utxo.Amount, mock.Eth2BtcOrders[0].Utxo.SourcePos, mock.Eth2BtcOrders[0].Utxo.ControlProgram), types.NewSpendInput(nil, testutil.MustDecodeHash("28b7b53d8dc90006bf97e0a4eaae2a72ec3d869873188698b694beaf20789f21"), *consensus.BTMAssetID, 100, 0, []byte{0x51}), }, Outputs: []*types.TxOutput{ types.NewIntraChainOutput(*mock.Btc2EthOrders[0].ToAssetID, 500, testutil.MustDecodeHexString("51")), types.NewIntraChainOutput(*mock.Eth2BtcOrders[0].ToAssetID, 10, testutil.MustDecodeHexString("53")), types.NewIntraChainOutput(*mock.Btc2EthOrders[0].ToAssetID, 10, []byte{0x51}), types.NewIntraChainOutput(*consensus.BTMAssetID, 100, []byte{0x51}), }, }), }, }, verifyResults: []*bc.TxVerifyResult{{StatusFail: false}}, wantError: errInputProgramMustP2WMCScript, }, { desc: "cancel order in the matched tx", block: &types.Block{ Transactions: []*types.Tx{ types.NewTx(types.TxData{ Inputs: []*types.TxInput{ types.NewSpendInput([][]byte{vm.Int64Bytes(0), vm.Int64Bytes(1)}, *mock.Btc2EthOrders[0].Utxo.SourceID, *mock.Btc2EthOrders[0].FromAssetID, mock.Btc2EthOrders[0].Utxo.Amount, mock.Btc2EthOrders[0].Utxo.SourcePos, mock.Btc2EthOrders[0].Utxo.ControlProgram), types.NewSpendInput([][]byte{vm.Int64Bytes(1), vm.Int64Bytes(1)}, *mock.Eth2BtcOrders[0].Utxo.SourceID, *mock.Eth2BtcOrders[0].FromAssetID, mock.Eth2BtcOrders[0].Utxo.Amount, mock.Eth2BtcOrders[0].Utxo.SourcePos, mock.Eth2BtcOrders[0].Utxo.ControlProgram), types.NewSpendInput([][]byte{{}, {}, vm.Int64Bytes(2)}, *mock.Btc2EthOrders[0].Utxo.SourceID, *mock.Btc2EthOrders[0].FromAssetID, mock.Btc2EthOrders[0].Utxo.Amount, mock.Btc2EthOrders[0].Utxo.SourcePos, mock.Btc2EthOrders[0].Utxo.ControlProgram), }, Outputs: []*types.TxOutput{ types.NewIntraChainOutput(*mock.Btc2EthOrders[0].ToAssetID, 500, testutil.MustDecodeHexString("51")), types.NewIntraChainOutput(*mock.Eth2BtcOrders[0].ToAssetID, 10, testutil.MustDecodeHexString("53")), types.NewIntraChainOutput(*mock.Btc2EthOrders[0].ToAssetID, 10, []byte{0x51}), types.NewIntraChainOutput(*consensus.BTMAssetID, 100, []byte{0x51}), }, }), }, }, verifyResults: []*bc.TxVerifyResult{{StatusFail: false}}, wantError: errExistCancelOrderInMatchedTx, }, { desc: "common input in the cancel order tx", block: &types.Block{ Transactions: []*types.Tx{ types.NewTx(types.TxData{ Inputs: []*types.TxInput{ types.NewSpendInput([][]byte{{}, {}, vm.Int64Bytes(2)}, *mock.Btc2EthOrders[0].Utxo.SourceID, *mock.Btc2EthOrders[0].FromAssetID, mock.Btc2EthOrders[0].Utxo.Amount, mock.Btc2EthOrders[0].Utxo.SourcePos, mock.Btc2EthOrders[0].Utxo.ControlProgram), types.NewSpendInput(nil, testutil.MustDecodeHash("28b7b53d8dc90006bf97e0a4eaae2a72ec3d869873188698b694beaf20789f21"), *consensus.BTMAssetID, 100, 0, []byte{0x51}), }, Outputs: []*types.TxOutput{ types.NewIntraChainOutput(*mock.Btc2EthOrders[0].FromAssetID, 10, testutil.MustDecodeHexString("51")), types.NewIntraChainOutput(*consensus.BTMAssetID, 100, []byte{0x51}), }, }), }, }, verifyResults: []*bc.TxVerifyResult{{StatusFail: false}}, wantError: errInputProgramMustP2WMCScript, }, { desc: "amount of fee greater than max fee amount", block: &types.Block{ Transactions: []*types.Tx{ types.NewTx(types.TxData{ Inputs: []*types.TxInput{ types.NewSpendInput([][]byte{vm.Int64Bytes(0), vm.Int64Bytes(1)}, *mock.Btc2EthOrders[0].Utxo.SourceID, *mock.Btc2EthOrders[0].FromAssetID, mock.Btc2EthOrders[0].Utxo.Amount, mock.Btc2EthOrders[0].Utxo.SourcePos, mock.Btc2EthOrders[0].Utxo.ControlProgram), types.NewSpendInput([][]byte{vm.Int64Bytes(10), vm.Int64Bytes(1), vm.Int64Bytes(0)}, *mock.Eth2BtcOrders[2].Utxo.SourceID, *mock.Eth2BtcOrders[2].FromAssetID, mock.Eth2BtcOrders[2].Utxo.Amount, mock.Eth2BtcOrders[2].Utxo.SourcePos, mock.Eth2BtcOrders[2].Utxo.ControlProgram), }, Outputs: []*types.TxOutput{ types.NewIntraChainOutput(*mock.Btc2EthOrders[0].ToAssetID, 500, testutil.MustDecodeHexString("51")), types.NewIntraChainOutput(*mock.Eth2BtcOrders[2].ToAssetID, 10, testutil.MustDecodeHexString("55")), // re-order types.NewIntraChainOutput(*mock.Eth2BtcOrders[2].FromAssetID, 270, mock.Eth2BtcOrders[2].Utxo.ControlProgram), // fee types.NewIntraChainOutput(*mock.Eth2BtcOrders[2].FromAssetID, 40, []byte{0x59}), }, }), }, }, verifyResults: []*bc.TxVerifyResult{{StatusFail: false}}, wantError: errAmountOfFeeGreaterThanMaximum, }, { desc: "ratio numerator is zero", block: &types.Block{ Transactions: []*types.Tx{ types.NewTx(types.TxData{ Inputs: []*types.TxInput{types.NewSpendInput(nil, *mock.Btc2EthOrders[0].Utxo.SourceID, *mock.Btc2EthOrders[0].FromAssetID, mock.Btc2EthOrders[0].Utxo.Amount, mock.Btc2EthOrders[0].Utxo.SourcePos, []byte{0x51})}, Outputs: []*types.TxOutput{types.NewIntraChainOutput(*mock.Btc2EthOrders[0].FromAssetID, mock.Btc2EthOrders[0].Utxo.Amount, mock.MustCreateP2WMCProgram(mock.ETH, testutil.MustDecodeHexString("51"), 0, 1))}, }), }, }, verifyResults: []*bc.TxVerifyResult{{StatusFail: false}}, wantError: errRatioOfTradeLessThanZero, }, { desc: "ratio denominator is zero", block: &types.Block{ Transactions: []*types.Tx{ types.NewTx(types.TxData{ Inputs: []*types.TxInput{types.NewSpendInput(nil, *mock.Btc2EthOrders[0].Utxo.SourceID, *mock.Btc2EthOrders[0].FromAssetID, mock.Btc2EthOrders[0].Utxo.Amount, mock.Btc2EthOrders[0].Utxo.SourcePos, []byte{0x51})}, Outputs: []*types.TxOutput{types.NewIntraChainOutput(*mock.Btc2EthOrders[0].FromAssetID, mock.Btc2EthOrders[0].Utxo.Amount, mock.MustCreateP2WMCProgram(mock.ETH, testutil.MustDecodeHexString("51"), 1, 0))}, }), }, }, verifyResults: []*bc.TxVerifyResult{{StatusFail: false}}, wantError: errRatioOfTradeLessThanZero, }, { desc: "ratio numerator product input amount is overflow", block: &types.Block{ Transactions: []*types.Tx{ types.NewTx(types.TxData{ Inputs: []*types.TxInput{types.NewSpendInput(nil, *mock.Btc2EthOrders[0].Utxo.SourceID, *mock.Btc2EthOrders[0].FromAssetID, mock.Btc2EthOrders[0].Utxo.Amount, mock.Btc2EthOrders[0].Utxo.SourcePos, []byte{0x51})}, Outputs: []*types.TxOutput{types.NewIntraChainOutput(*mock.Btc2EthOrders[0].FromAssetID, mock.Btc2EthOrders[0].Utxo.Amount, mock.MustCreateP2WMCProgram(mock.ETH, testutil.MustDecodeHexString("51"), math.MaxInt64, 10))}, }), }, }, verifyResults: []*bc.TxVerifyResult{{StatusFail: false}}, wantError: errNumeratorOfRatioIsOverflow, }, } for i, c := range cases { movCore := &MovCore{} if err := movCore.ValidateBlock(c.block, c.verifyResults); err != c.wantError { t.Errorf("#%d(%s):validate block want error(%v), got error(%v)", i, c.desc, c.wantError, err) } } } type testFun func(movCore *MovCore, block *types.Block) error func applyBlock(movCore *MovCore, block *types.Block) error { return movCore.ApplyBlock(block) } func detachBlock(movCore *MovCore, block *types.Block) error { return movCore.DetachBlock(block) } func queryAllOrders(store *database.LevelDBMovStore) []*common.Order { var orders []*common.Order tradePairIterator := database.NewTradePairIterator(store) for tradePairIterator.HasNext() { orderIterator := database.NewOrderIterator(store, tradePairIterator.Next()) for orderIterator.HasNext() { orders = append(orders, orderIterator.NextBatch()...) } } return orders } func ordersEquals(orders1 []*common.Order, orders2 []*common.Order) bool { orderMap1 := make(map[string]*common.Order) for _, order := range orders1 { orderMap1[order.Key()] = order } orderMap2 := make(map[string]*common.Order) for _, order := range orders2 { orderMap2[order.Key()] = order } return testutil.DeepEqual(orderMap1, orderMap2) } func hashPtr(hash bc.Hash) *bc.Hash { return &hash }