OSDN Git Service

Functional tests (#543)
authorWangYifu <lbqds@cryptape.com>
Thu, 12 Apr 2018 01:54:39 +0000 (20:54 -0500)
committerPaladz <yzhu101@uottawa.ca>
Thu, 12 Apr 2018 01:54:39 +0000 (09:54 +0800)
* add integration test

* tiny refactor

* add chain tests

* update wallet tests

* add wallet rollback test

* mock utxo, create asset, account

* fix can't find btm asset

* fix verify control program failed

* add tx tests

* tx test, output large than input

* add gas used test

* validate tx when gen block

* add tx version tests

* add gas only tx, spend immature coinbase output tests

* add block header tests

* spent retire, insufficient assets tests

* add coinbase tx test

* golint

* add double spend tests

* tx have no outputs test

* chain fork tests

* GenerateGenesisBlock for tests

* separate unit tests and functional tests

* update status with ProcessBlock

* fix tests

* gofmt

* add tx pool tests

* simplify code

* fix typo

* add max block gas test

* remove useless code

* fix chain tests

* serialize tx manually

* remove mux vm verify

* tmp save

* change the code style

* edit code format

* rename the file

* mv validate block to right folder

* delete unused from validation package

* add unit test for checkBlockTime && checkCoinbaseAmount

* delete unused test_file

* update the blockindex import

* fix tests

* format protocol level

* fix for golint

* edit for fix bug

* fix chain rollback tests

29 files changed:
Makefile
protocol/validation/tx_test.go
test/block_test.go [new file with mode: 0644]
test/block_test_util.go [new file with mode: 0644]
test/chain_test.go [new file with mode: 0644]
test/chain_test_util.go [new file with mode: 0644]
test/init_test.go [new file with mode: 0644]
test/integration/standard_transaction_test.go
test/performance/mining_test.go
test/protocol_test.go [new file with mode: 0644]
test/protocol_test_util.go [new file with mode: 0644]
test/testdata/chain_tests/ct_dependency_tx.json [new file with mode: 0644]
test/testdata/chain_tests/ct_double_spend.json [new file with mode: 0644]
test/testdata/chain_tests/ct_normal.json [new file with mode: 0644]
test/testdata/chain_tests/ct_rollback.json [new file with mode: 0644]
test/testdata/tx_tests/tx_tests.json [new file with mode: 0644]
test/testdata/wallet_tests/wt_asset.json [new file with mode: 0644]
test/testdata/wallet_tests/wt_btm.json [new file with mode: 0644]
test/testdata/wallet_tests/wt_invalid_txs.json [new file with mode: 0644]
test/testdata/wallet_tests/wt_multi_sig_asset.json [new file with mode: 0644]
test/testdata/wallet_tests/wt_retire_asset.json [new file with mode: 0644]
test/testdata/wallet_tests/wt_rollback.json [new file with mode: 0644]
test/tx_test.go [new file with mode: 0644]
test/tx_test_util.go [new file with mode: 0644]
test/util.go
test/wallet_test.go [new file with mode: 0644]
test/wallet_test_util.go [new file with mode: 0644]
wallet/wallet.go
wallet/wallet_test.go

index 7571c95..7e20043 100644 (file)
--- a/Makefile
+++ b/Makefile
@@ -107,4 +107,7 @@ test:
 benchmark:
        go test -bench $(PACKAGES)
 
+functional-tests:
+       @go test -v -timeout=30m -tags=functional ./test
+
 .PHONY: all target release-all clean test benchmark
index dd2efb5..2c780ff 100644 (file)
@@ -375,7 +375,7 @@ func TestTimeRange(t *testing.T) {
        for i, c := range cases {
                tx.TimeRange = c.timeRange
                if _, err := ValidateTx(tx, block); (err != nil) != c.err {
-                       t.Errorf("#%d got error %s, want %s", i, !c.err, c.err)
+                       t.Errorf("#%d got error %t, want %t", i, !c.err, c.err)
                }
        }
 }
diff --git a/test/block_test.go b/test/block_test.go
new file mode 100644 (file)
index 0000000..6d1d31c
--- /dev/null
@@ -0,0 +1,197 @@
+// +build functional
+
+package test
+
+import (
+       "os"
+       "testing"
+       "time"
+
+       dbm "github.com/tendermint/tmlibs/db"
+
+       "github.com/bytom/consensus"
+       "github.com/bytom/protocol/bc"
+       "github.com/bytom/protocol/bc/types"
+       "github.com/bytom/protocol/vm"
+)
+
+func TestBlockHeader(t *testing.T) {
+       db := dbm.NewDB("block_test_db", "leveldb", "block_test_db")
+       defer os.RemoveAll("block_test_db")
+       chain, _, _, _ := MockChain(db)
+       genesisHeader := chain.BestBlockHeader()
+       if err := AppendBlocks(chain, 1); err != nil {
+               t.Fatal(err)
+       }
+
+       cases := []struct {
+               desc       string
+               version    func() uint64
+               prevHeight func() uint64
+               timestamp  func() uint64
+               prevHash   func() *bc.Hash
+               bits       func() uint64
+               solve      bool
+               valid      bool
+       }{
+               {
+                       desc:       "block version is 0",
+                       version:    func() uint64 { return 0 },
+                       prevHeight: chain.BestBlockHeight,
+                       timestamp:  func() uint64 { return uint64(time.Now().Unix()) },
+                       prevHash:   chain.BestBlockHash,
+                       bits:       func() uint64 { return chain.BestBlockHeader().Bits },
+                       solve:      true,
+                       valid:      false,
+               },
+               {
+                       desc:       "block version grater than prevBlock.Version",
+                       version:    func() uint64 { return chain.BestBlockHeader().Version + 10 },
+                       prevHeight: chain.BestBlockHeight,
+                       timestamp:  func() uint64 { return uint64(time.Now().Unix()) },
+                       prevHash:   chain.BestBlockHash,
+                       bits:       func() uint64 { return chain.BestBlockHeader().Bits },
+                       solve:      true,
+                       valid:      true,
+               },
+               {
+                       desc:       "invalid block, misorder block height",
+                       version:    func() uint64 { return chain.BestBlockHeader().Version },
+                       prevHeight: func() uint64 { return chain.BestBlockHeight() + 1 },
+                       timestamp:  func() uint64 { return uint64(time.Now().Unix()) },
+                       prevHash:   chain.BestBlockHash,
+                       bits:       func() uint64 { return chain.BestBlockHeader().Bits },
+                       solve:      true,
+                       valid:      false,
+               },
+               {
+                       desc:       "invalid prev hash, prev hash dismatch",
+                       version:    func() uint64 { return chain.BestBlockHeader().Version },
+                       prevHeight: chain.BestBlockHeight,
+                       timestamp:  func() uint64 { return uint64(time.Now().Unix()) },
+                       prevHash:   func() *bc.Hash { hash := genesisHeader.Hash(); return &hash },
+                       bits:       func() uint64 { return chain.BestBlockHeader().Bits },
+                       solve:      true,
+                       valid:      false,
+               },
+               {
+                       desc:       "invalid bits",
+                       version:    func() uint64 { return chain.BestBlockHeader().Version },
+                       prevHeight: chain.BestBlockHeight,
+                       timestamp:  func() uint64 { return uint64(time.Now().Unix()) },
+                       prevHash:   chain.BestBlockHash,
+                       bits:       func() uint64 { return chain.BestBlockHeader().Bits + 100 },
+                       solve:      true,
+                       valid:      false,
+               },
+               {
+                       desc:       "invalid timestamp, greater than MaxTimeOffsetSeconds from system time",
+                       version:    func() uint64 { return chain.BestBlockHeader().Version },
+                       prevHeight: chain.BestBlockHeight,
+                       timestamp:  func() uint64 { return uint64(time.Now().Unix()) + consensus.MaxTimeOffsetSeconds + 60 },
+                       prevHash:   chain.BestBlockHash,
+                       bits:       func() uint64 { return chain.BestBlockHeader().Bits },
+                       solve:      true,
+                       valid:      false,
+               },
+               {
+                       desc:       "valid timestamp, greater than last block",
+                       version:    func() uint64 { return chain.BestBlockHeader().Version },
+                       prevHeight: chain.BestBlockHeight,
+                       timestamp:  func() uint64 { return chain.BestBlockHeader().Timestamp + 3 },
+                       prevHash:   chain.BestBlockHash,
+                       bits:       func() uint64 { return chain.BestBlockHeader().Bits },
+                       solve:      true,
+                       valid:      true,
+               },
+               {
+                       desc:       "valid timestamp, less than last block, but greater than median",
+                       version:    func() uint64 { return chain.BestBlockHeader().Version },
+                       prevHeight: chain.BestBlockHeight,
+                       timestamp:  func() uint64 { return chain.BestBlockHeader().Timestamp - 1 },
+                       prevHash:   chain.BestBlockHash,
+                       bits:       func() uint64 { return chain.BestBlockHeader().Bits },
+                       solve:      true,
+                       valid:      true,
+               },
+               {
+                       desc:       "invalid timestamp, less than median",
+                       version:    func() uint64 { return chain.BestBlockHeader().Version },
+                       prevHeight: chain.BestBlockHeight,
+                       timestamp:  func() uint64 { return genesisHeader.Timestamp },
+                       prevHash:   chain.BestBlockHash,
+                       bits:       func() uint64 { return chain.BestBlockHeader().Bits },
+                       solve:      true,
+                       valid:      false,
+               },
+       }
+
+       for _, c := range cases {
+               block, err := NewBlock(chain, nil, []byte{byte(vm.OP_TRUE)})
+               if err != nil {
+                       t.Fatal(err)
+               }
+
+               block.Version = c.version()
+               block.Height = c.prevHeight() + 1
+               block.Timestamp = c.timestamp()
+               block.PreviousBlockHash = *c.prevHash()
+               block.Bits = c.bits()
+               seed, err := chain.CalcNextSeed(&block.PreviousBlockHash)
+               if err != nil && c.valid {
+                       t.Fatal(err)
+               }
+
+               if c.solve {
+                       Solve(seed, block)
+               }
+               _, err = chain.ProcessBlock(block)
+               result := err == nil
+               if result != c.valid {
+                       t.Fatalf("%s test failed, expected: %t, have: %t, err: %s", c.desc, c.valid, result, err)
+               }
+       }
+}
+
+func TestMaxBlockGas(t *testing.T) {
+       chainDB := dbm.NewDB("test_block_db", "leveldb", "test_block_db")
+       defer os.RemoveAll("test_block_db")
+       chain, _, _, err := MockChain(chainDB)
+       if err != nil {
+               t.Fatal(err)
+       }
+
+       if err := AppendBlocks(chain, 7); err != nil {
+               t.Fatal(err)
+       }
+
+       block, err := chain.GetBlockByHeight(1)
+       if err != nil {
+               t.Fatal(err)
+       }
+
+       tx, err := CreateTxFromTx(block.Transactions[0], 0, 600000000000, []byte{byte(vm.OP_TRUE)})
+       if err != nil {
+               t.Fatal(err)
+       }
+
+       outputAmount := uint64(600000000000)
+       txs := []*types.Tx{tx}
+       for i := 1; i < 50000; i++ {
+               outputAmount -= 10000000
+               tx, err := CreateTxFromTx(txs[i-1], 0, outputAmount, []byte{byte(vm.OP_TRUE)})
+               if err != nil {
+                       t.Fatal(err)
+               }
+               txs = append(txs, tx)
+       }
+
+       block, err = NewBlock(chain, txs, []byte{byte(vm.OP_TRUE)})
+       if err != nil {
+               t.Fatal(err)
+       }
+
+       if err := SolveAndUpdate(chain, block); err == nil {
+               t.Fatalf("test max block gas failed")
+       }
+}
diff --git a/test/block_test_util.go b/test/block_test_util.go
new file mode 100644 (file)
index 0000000..0c839a8
--- /dev/null
@@ -0,0 +1,123 @@
+package test
+
+import (
+       "time"
+
+       "github.com/bytom/consensus"
+       "github.com/bytom/consensus/difficulty"
+       "github.com/bytom/protocol"
+       "github.com/bytom/protocol/bc"
+       "github.com/bytom/protocol/bc/types"
+       "github.com/bytom/protocol/validation"
+       "github.com/bytom/protocol/vm"
+)
+
+// NewBlock create block according to the current status of chain
+func NewBlock(chain *protocol.Chain, txs []*types.Tx, controlProgram []byte) (*types.Block, error) {
+       gasUsed := uint64(0)
+       txsFee := uint64(0)
+       txEntries := []*bc.Tx{nil}
+       txStatus := bc.NewTransactionStatus()
+       txStatus.SetStatus(0, false)
+
+       preBlockHeader := chain.BestBlockHeader()
+       var compareDiffBH *types.BlockHeader
+       if compareDiffBlock, err := chain.GetBlockByHeight(preBlockHeader.Height - consensus.BlocksPerRetarget); err == nil {
+               compareDiffBH = &compareDiffBlock.BlockHeader
+       }
+
+       b := &types.Block{
+               BlockHeader: types.BlockHeader{
+                       Version:           1,
+                       Height:            preBlockHeader.Height + 1,
+                       PreviousBlockHash: preBlockHeader.Hash(),
+                       Timestamp:         uint64(time.Now().Unix()),
+                       BlockCommitment:   types.BlockCommitment{},
+                       Bits:              difficulty.CalcNextRequiredDifficulty(preBlockHeader, compareDiffBH),
+               },
+               Transactions: []*types.Tx{nil},
+       }
+
+       bcBlock := &bc.Block{BlockHeader: &bc.BlockHeader{Height: preBlockHeader.Height + 1}}
+       for _, tx := range txs {
+               gasOnlyTx := false
+               gasStatus, err := validation.ValidateTx(tx.Tx, bcBlock)
+               if err != nil {
+                       if !gasStatus.GasVaild {
+                               continue
+                       }
+                       gasOnlyTx = true
+               }
+
+               txStatus.SetStatus(len(b.Transactions), gasOnlyTx)
+               b.Transactions = append(b.Transactions, tx)
+               txEntries = append(txEntries, tx.Tx)
+               gasUsed += uint64(gasStatus.GasUsed)
+               txsFee += txFee(tx)
+       }
+
+       coinbaseTx, err := CreateCoinbaseTx(controlProgram, preBlockHeader.Height+1, txsFee)
+       if err != nil {
+               return nil, err
+       }
+
+       b.Transactions[0] = coinbaseTx
+       txEntries[0] = coinbaseTx.Tx
+       b.TransactionsMerkleRoot, err = bc.TxMerkleRoot(txEntries)
+       if err != nil {
+               return nil, err
+       }
+
+       b.TransactionStatusHash, err = bc.TxStatusMerkleRoot(txStatus.VerifyStatus)
+       return b, err
+}
+
+// ReplaceCoinbase replace the coinbase tx of block with coinbaseTx
+func ReplaceCoinbase(block *types.Block, coinbaseTx *types.Tx) (err error) {
+       block.Transactions[0] = coinbaseTx
+       txEntires := []*bc.Tx{coinbaseTx.Tx}
+       for i := 1; i < len(block.Transactions); i++ {
+               txEntires = append(txEntires, block.Transactions[i].Tx)
+       }
+
+       block.TransactionsMerkleRoot, err = bc.TxMerkleRoot(txEntires)
+       return
+}
+
+// AppendBlocks append empty blocks to chain, mainly used to mature the coinbase tx
+func AppendBlocks(chain *protocol.Chain, num uint64) error {
+       for i := uint64(0); i < num; i++ {
+               block, err := NewBlock(chain, nil, []byte{byte(vm.OP_TRUE)})
+               if err != nil {
+                       return err
+               }
+               if err := SolveAndUpdate(chain, block); err != nil {
+                       return err
+               }
+       }
+       return nil
+}
+
+// SolveAndUpdate solve difficulty and update chain status
+func SolveAndUpdate(chain *protocol.Chain, block *types.Block) error {
+       seed, err := chain.CalcNextSeed(&block.PreviousBlockHash)
+       if err != nil {
+               return err
+       }
+       Solve(seed, block)
+       _, err = chain.ProcessBlock(block)
+       return err
+}
+
+// Solve solve difficulty
+func Solve(seed *bc.Hash, block *types.Block) error {
+       header := &block.BlockHeader
+       for i := uint64(0); i < maxNonce; i++ {
+               header.Nonce = i
+               headerHash := header.Hash()
+               if difficulty.CheckProofOfWork(&headerHash, seed, header.Bits) {
+                       return nil
+               }
+       }
+       return nil
+}
diff --git a/test/chain_test.go b/test/chain_test.go
new file mode 100644 (file)
index 0000000..052c357
--- /dev/null
@@ -0,0 +1,15 @@
+// +build functional
+
+package test
+
+import (
+       "testing"
+)
+
+func TestChain(t *testing.T) {
+       walk(t, chainTestDir, func(t *testing.T, name string, test *chainTestConfig) {
+               if err := test.Run(); err != nil {
+                       t.Fatal(err)
+               }
+       })
+}
diff --git a/test/chain_test_util.go b/test/chain_test_util.go
new file mode 100644 (file)
index 0000000..f9eed1f
--- /dev/null
@@ -0,0 +1,294 @@
+package test
+
+import (
+       "fmt"
+       "os"
+       "time"
+
+       dbm "github.com/tendermint/tmlibs/db"
+
+       "github.com/bytom/blockchain/txbuilder"
+       "github.com/bytom/consensus"
+       "github.com/bytom/database/leveldb"
+       "github.com/bytom/database/storage"
+       "github.com/bytom/protocol"
+       "github.com/bytom/protocol/bc"
+       "github.com/bytom/protocol/bc/types"
+       "github.com/bytom/protocol/vm"
+       "github.com/golang/protobuf/proto"
+)
+
+const utxoPrefix = "UT:"
+
+type chainTestContext struct {
+       Chain *protocol.Chain
+       DB    dbm.DB
+       Store *leveldb.Store
+}
+
+func (ctx *chainTestContext) validateStatus(block *types.Block) error {
+       // validate in mainchain
+       if !ctx.Chain.InMainChain(block.Hash()) {
+               return fmt.Errorf("block %d is not in mainchain", block.Height)
+       }
+
+       // validate chain status and saved block
+       bestBlockHeader := ctx.Chain.BestBlockHeader()
+       chainBlock, err := ctx.Chain.GetBlockByHeight(block.Height)
+       if err != nil {
+               return err
+       }
+
+       blockHash := block.Hash()
+       if bestBlockHeader.Hash() != blockHash || chainBlock.Hash() != blockHash {
+               return fmt.Errorf("chain status error")
+       }
+
+       // validate tx status
+       txStatus, err := ctx.Chain.GetTransactionStatus(&blockHash)
+       if err != nil {
+               return err
+       }
+
+       txStatusMerkleRoot, err := bc.TxStatusMerkleRoot(txStatus.VerifyStatus)
+       if err != nil {
+               return err
+       }
+
+       if txStatusMerkleRoot != block.TransactionStatusHash {
+               return fmt.Errorf("tx status error")
+       }
+       return nil
+}
+
+func (ctx *chainTestContext) validateExecution(block *types.Block) error {
+       for _, tx := range block.Transactions {
+               for _, spentOutputID := range tx.SpentOutputIDs {
+                       utxoEntry, _ := ctx.Store.GetUtxo(&spentOutputID)
+                       if utxoEntry == nil {
+                               continue
+                       }
+                       if !utxoEntry.IsCoinBase {
+                               return fmt.Errorf("found non-coinbase spent utxo entry")
+                       }
+                       if !utxoEntry.Spent {
+                               return fmt.Errorf("utxo entry status should be spent")
+                       }
+               }
+
+               for _, outputID := range tx.ResultIds {
+                       utxoEntry, _ := ctx.Store.GetUtxo(outputID)
+                       if utxoEntry == nil && isSpent(outputID, block) {
+                               continue
+                       }
+                       if utxoEntry.BlockHeight != block.Height {
+                               return fmt.Errorf("block height error, expected: %d, have: %d", block.Height, utxoEntry.BlockHeight)
+                       }
+                       if utxoEntry.Spent {
+                               return fmt.Errorf("utxo entry status should not be spent")
+                       }
+               }
+       }
+       return nil
+}
+
+func (ctx *chainTestContext) getUtxoEntries() map[string]*storage.UtxoEntry {
+       utxoEntries := make(map[string]*storage.UtxoEntry)
+       iter := ctx.DB.IteratorPrefix([]byte(utxoPrefix))
+       defer iter.Release()
+
+       for iter.Next() {
+               utxoEntry := storage.UtxoEntry{}
+               if err := proto.Unmarshal(iter.Value(), &utxoEntry); err != nil {
+                       return nil
+               }
+               key := string(iter.Key())
+               utxoEntries[key] = &utxoEntry
+       }
+       return utxoEntries
+}
+
+func (ctx *chainTestContext) validateRollback(utxoEntries map[string]*storage.UtxoEntry) error {
+       newUtxoEntries := ctx.getUtxoEntries()
+       for key := range utxoEntries {
+               entry, ok := newUtxoEntries[key]
+               if !ok {
+                       return fmt.Errorf("can't find utxo after rollback")
+               }
+               if entry.Spent != utxoEntries[key].Spent {
+                       return fmt.Errorf("utxo status dismatch after rollback")
+               }
+       }
+       return nil
+}
+
+type chainTestConfig struct {
+       RollbackTo uint64     `json:"rollback_to"`
+       Blocks     []*ctBlock `json:"blocks"`
+}
+
+type ctBlock struct {
+       Transactions []*ctTransaction `json:"transactions"`
+       Append       uint64           `json:"append"`
+       Invalid      bool             `json:"invalid"`
+}
+
+func (b *ctBlock) createBlock(ctx *chainTestContext) (*types.Block, error) {
+       txs := make([]*types.Tx, 0, len(b.Transactions))
+       for _, t := range b.Transactions {
+               tx, err := t.createTransaction(ctx, txs)
+               if err != nil {
+                       return nil, err
+               }
+               txs = append(txs, tx)
+       }
+       return NewBlock(ctx.Chain, txs, []byte{byte(vm.OP_TRUE)})
+}
+
+type ctTransaction struct {
+       Inputs  []*ctInput `json:"inputs"`
+       Outputs []uint64   `json:"outputs"`
+}
+
+type ctInput struct {
+       Height      uint64 `json:"height"`
+       TxIndex     uint64 `json:"tx_index"`
+       OutputIndex uint64 `json:"output_index"`
+}
+
+func (input *ctInput) createTxInput(ctx *chainTestContext) (*types.TxInput, error) {
+       block, err := ctx.Chain.GetBlockByHeight(input.Height)
+       if err != nil {
+               return nil, err
+       }
+
+       spendInput, err := CreateSpendInput(block.Transactions[input.TxIndex], input.OutputIndex)
+       if err != nil {
+               return nil, err
+       }
+
+       return &types.TxInput{
+               AssetVersion: assetVersion,
+               TypedInput:   spendInput,
+       }, nil
+}
+
+// create tx input spent previous tx output in the same block
+func (input *ctInput) createDependencyTxInput(txs []*types.Tx) (*types.TxInput, error) {
+       // sub 1 because of coinbase tx is not included in txs
+       spendInput, err := CreateSpendInput(txs[input.TxIndex-1], input.OutputIndex)
+       if err != nil {
+               return nil, err
+       }
+
+       return &types.TxInput{
+               AssetVersion: assetVersion,
+               TypedInput:   spendInput,
+       }, nil
+}
+
+func (t *ctTransaction) createTransaction(ctx *chainTestContext, txs []*types.Tx) (*types.Tx, error) {
+       builder := txbuilder.NewBuilder(time.Now())
+       sigInst := &txbuilder.SigningInstruction{}
+       currentHeight := ctx.Chain.BestBlockHeight()
+       for _, input := range t.Inputs {
+               var txInput *types.TxInput
+               var err error
+               if input.Height == currentHeight+1 {
+                       txInput, err = input.createDependencyTxInput(txs)
+               } else {
+                       txInput, err = input.createTxInput(ctx)
+               }
+               if err != nil {
+                       return nil, err
+               }
+               builder.AddInput(txInput, sigInst)
+       }
+
+       for _, amount := range t.Outputs {
+               output := types.NewTxOutput(*consensus.BTMAssetID, amount, []byte{byte(vm.OP_TRUE)})
+               builder.AddOutput(output)
+       }
+
+       tpl, _, err := builder.Build()
+       if err != nil {
+               return nil, err
+       }
+
+       txSerialized, err := tpl.Transaction.MarshalText()
+       if err != nil {
+               return nil, err
+       }
+
+       tpl.Transaction.Tx.SerializedSize = uint64(len(txSerialized))
+       tpl.Transaction.TxData.SerializedSize = uint64(len(txSerialized))
+       return tpl.Transaction, err
+}
+
+func (cfg *chainTestConfig) Run() error {
+       db := dbm.NewDB("chain_test_db", "leveldb", "chain_test_db")
+       defer os.RemoveAll("chain_test_db")
+       chain, store, _, _ := MockChain(db)
+       ctx := &chainTestContext{
+               Chain: chain,
+               DB:    db,
+               Store: store,
+       }
+
+       var utxoEntries map[string]*storage.UtxoEntry
+       var rollbackBlock *types.Block
+       for _, blk := range cfg.Blocks {
+               block, err := blk.createBlock(ctx)
+               if err != nil {
+                       return err
+               }
+               err = SolveAndUpdate(ctx.Chain, block)
+               if err != nil && blk.Invalid {
+                       continue
+               }
+               if err != nil {
+                       return err
+               }
+               if err := ctx.validateStatus(block); err != nil {
+                       return err
+               }
+               if err := ctx.validateExecution(block); err != nil {
+                       return err
+               }
+               if block.Height <= cfg.RollbackTo && cfg.RollbackTo <= block.Height+blk.Append {
+                       utxoEntries = ctx.getUtxoEntries()
+                       rollbackBlock = block
+               }
+               if err := AppendBlocks(ctx.Chain, blk.Append); err != nil {
+                       return err
+               }
+       }
+
+       if rollbackBlock == nil {
+               return nil
+       }
+
+       // rollback and validate
+       forkedChain, err := declChain("forked_chain", ctx.Chain, rollbackBlock.Height, ctx.Chain.BestBlockHeight()+1)
+       defer os.RemoveAll("forked_chain")
+       if err != nil {
+               return err
+       }
+
+       if err := merge(forkedChain, ctx.Chain); err != nil {
+               return err
+       }
+       return ctx.validateRollback(utxoEntries)
+}
+
+// if the output(hash) was spent in block
+func isSpent(hash *bc.Hash, block *types.Block) bool {
+       for _, tx := range block.Transactions {
+               for _, spendOutputID := range tx.SpentOutputIDs {
+                       if spendOutputID == *hash {
+                               return true
+                       }
+               }
+       }
+       return false
+}
diff --git a/test/init_test.go b/test/init_test.go
new file mode 100644 (file)
index 0000000..e6e0c73
--- /dev/null
@@ -0,0 +1,116 @@
+package test
+
+import (
+       "encoding/json"
+       "fmt"
+       "io"
+       "io/ioutil"
+       "os"
+       "path/filepath"
+       "reflect"
+       "strings"
+       "testing"
+)
+
+var (
+       baseDir       = filepath.Join(".", "testdata")
+       walletTestDir = filepath.Join(baseDir, "wallet_tests")
+       chainTestDir  = filepath.Join(baseDir, "chain_tests")
+       txTestDir     = filepath.Join(baseDir, "tx_tests")
+)
+
+func readJSON(reader io.Reader, value interface{}) error {
+       data, err := ioutil.ReadAll(reader)
+       if err != nil {
+               return fmt.Errorf("error reading JSON file: %v", err)
+       }
+       if err = json.Unmarshal(data, &value); err != nil {
+               if syntaxerr, ok := err.(*json.SyntaxError); ok {
+                       line := findLine(data, syntaxerr.Offset)
+                       return fmt.Errorf("JSON syntax error at line %v: %v", line, err)
+               }
+               return err
+       }
+       return nil
+}
+
+func readJSONFile(fn string, value interface{}) error {
+       file, err := os.Open(fn)
+       if err != nil {
+               return err
+       }
+       defer file.Close()
+
+       if err := readJSON(file, value); err != nil {
+               return fmt.Errorf("%s in file %s", err.Error(), fn)
+       }
+       return nil
+}
+
+// findLine returns the line number for the given offset into data.
+func findLine(data []byte, offset int64) (line int) {
+       line = 1
+       for i, r := range string(data) {
+               if int64(i) >= offset {
+                       return
+               }
+               if r == '\n' {
+                       line++
+               }
+       }
+       return
+}
+
+// walk invokes its runTest argument for all subtests in the given directory.
+//
+// runTest should be a function of type func(t *testing.T, name string, x <TestType>),
+// where TestType is the type of the test contained in test files.
+func walk(t *testing.T, dir string, runTest interface{}) {
+       // Walk the directory.
+       dirinfo, err := os.Stat(dir)
+       if os.IsNotExist(err) || !dirinfo.IsDir() {
+               fmt.Fprintf(os.Stderr, "can't find test files in %s\n", dir)
+               t.Skip("missing test files")
+       }
+       err = filepath.Walk(dir, func(path string, info os.FileInfo, err error) error {
+               name := filepath.ToSlash(strings.TrimPrefix(path, dir+string(filepath.Separator)))
+               if info.IsDir() {
+                       return nil
+               }
+               if filepath.Ext(path) == ".json" {
+                       t.Run(name, func(t *testing.T) { runTestFile(t, path, name, runTest) })
+               }
+               return nil
+       })
+       if err != nil {
+               t.Fatal(err)
+       }
+}
+
+func runTestFile(t *testing.T, path, name string, runTest interface{}) {
+       m := makeMapFromTestFunc(runTest)
+       if err := readJSONFile(path, m.Addr().Interface()); err != nil {
+               t.Fatal(err)
+       }
+       runTestFunc(runTest, t, name, m)
+}
+
+func makeMapFromTestFunc(f interface{}) reflect.Value {
+       stringT := reflect.TypeOf("")
+       testingT := reflect.TypeOf((*testing.T)(nil))
+       ftyp := reflect.TypeOf(f)
+       if ftyp.Kind() != reflect.Func || ftyp.NumIn() != 3 || ftyp.NumOut() != 0 || ftyp.In(0) != testingT || ftyp.In(1) != stringT {
+               panic(fmt.Sprintf("bad test function type: want func(*testing.T, string, <TestType>), have %s", ftyp))
+       }
+       testType := ftyp.In(2)
+       mp := reflect.New(testType)
+       return mp.Elem()
+}
+
+func runTestFunc(runTest interface{}, t *testing.T, name string, m reflect.Value) {
+       reflect.ValueOf(runTest).Call([]reflect.Value{
+               reflect.ValueOf(t),
+               reflect.ValueOf(name),
+               m,
+       })
+}
index 707a351..25e195f 100644 (file)
@@ -25,7 +25,7 @@ func TestP2PKH(t *testing.T) {
        testDB := dbm.NewDB("testdb", "leveldb", "temp")
        defer os.RemoveAll("temp")
 
-       chain, err := test.MockChain(testDB)
+       chain, _, _, err := test.MockChain(testDB)
        if err != nil {
                t.Fatal(err)
        }
@@ -77,7 +77,7 @@ func TestP2SH(t *testing.T) {
        testDB := dbm.NewDB("testdb", "leveldb", "temp")
        defer os.RemoveAll("temp")
 
-       chain, err := test.MockChain(testDB)
+       chain, _, _, err := test.MockChain(testDB)
        if err != nil {
                t.Fatal(err)
        }
@@ -134,7 +134,7 @@ func TestMutilNodeSign(t *testing.T) {
        testDB := dbm.NewDB("testdb", "leveldb", "temp")
        defer os.RemoveAll("temp")
 
-       chain, err := test.MockChain(testDB)
+       chain, _, _, err := test.MockChain(testDB)
        if err != nil {
                t.Fatal(err)
        }
index b58335a..5aee53f 100644 (file)
@@ -16,7 +16,7 @@ func BenchmarkNewBlockTpl(b *testing.B) {
        testDB := dbm.NewDB("testdb", "leveldb", "temp")
        defer os.RemoveAll("temp")
 
-       chain, err := test.MockChain(testDB)
+       chain, _, _, err := test.MockChain(testDB)
        if err != nil {
                b.Fatal(err)
        }
diff --git a/test/protocol_test.go b/test/protocol_test.go
new file mode 100644 (file)
index 0000000..de509b8
--- /dev/null
@@ -0,0 +1,249 @@
+// +build functional
+
+package test
+
+import (
+       "os"
+       "testing"
+
+       dbm "github.com/tendermint/tmlibs/db"
+
+       "github.com/bytom/protocol/bc/types"
+       "github.com/bytom/protocol/vm"
+)
+
+// case1:           |------c1(height=7)
+// --------(height=5)
+//                  |------------c2(height=9)
+func TestForkCase1(t *testing.T) {
+       c1, err := declChain("chain1", nil, 0, 7)
+       defer os.RemoveAll("chain1")
+       if err != nil {
+               t.Fatal(err)
+       }
+
+       c2, err := declChain("chain2", c1, 5, 9)
+       defer os.RemoveAll("chain2")
+       if err != nil {
+               t.Fatal(err)
+       }
+
+       bestBlockHash := c2.BestBlockHash()
+       if err := merge(c1, c2); err != nil {
+               t.Fatal(err)
+       }
+
+       if *c1.BestBlockHash() != *bestBlockHash || *c2.BestBlockHash() != *bestBlockHash {
+               t.Fatalf("test fork case1 failed")
+       }
+
+       if !c1.InMainChain(*bestBlockHash) || !c2.InMainChain(*bestBlockHash) {
+               t.Fatalf("best block is not in main chain")
+       }
+}
+
+// case2:            |----c1(height=6)
+// ---------(height 5)
+//                   |----c2(height=6)
+func TestForkCase2(t *testing.T) {
+       c1, err := declChain("chain1", nil, 0, 6)
+       defer os.RemoveAll("chain1")
+       if err != nil {
+               t.Fatal(err)
+       }
+
+       c2, err := declChain("chain2", c1, 5, 6)
+       defer os.RemoveAll("chain2")
+       if err != nil {
+               t.Fatal(err)
+       }
+
+       c1BestBlockHash := c1.BestBlockHash()
+       c2BestBlockHash := c2.BestBlockHash()
+       if err := merge(c1, c2); err != nil {
+               t.Fatal(err)
+       }
+
+       if *c1.BestBlockHash() != *c1BestBlockHash || *c2.BestBlockHash() != *c2BestBlockHash {
+               t.Fatalf("test fork case2 failed")
+       }
+
+       if !c1.InMainChain(*c1BestBlockHash) || !c2.InMainChain(*c2BestBlockHash) {
+               t.Fatalf("best block is not in main chain")
+       }
+}
+
+func TestBlockSync(t *testing.T) {
+       c1, err := declChain("chain1", nil, 0, 5)
+       defer os.RemoveAll("chain1")
+       if err != nil {
+               t.Fatal(err)
+       }
+
+       c2, err := declChain("chain2", c1, 5, 8)
+       defer os.RemoveAll("chain2")
+       if err != nil {
+               t.Fatal(err)
+       }
+
+       bestBlockHash := c2.BestBlockHash()
+       if err := merge(c1, c2); err != nil {
+               t.Fatal(err)
+       }
+
+       if *c1.BestBlockHash() != *bestBlockHash || *c2.BestBlockHash() != *bestBlockHash {
+               t.Fatalf("test block sync failed")
+       }
+
+       if !c1.InMainChain(*bestBlockHash) || !c2.InMainChain(*bestBlockHash) {
+               t.Fatalf("test block sync failed, best block is not in main chain")
+       }
+}
+
+func TestDoubleSpentInDiffBlock(t *testing.T) {
+       chainDB := dbm.NewDB("tx_pool_test", "leveldb", "tx_pool_test")
+       defer os.RemoveAll("tx_pool_test")
+       chain, _, txPool, err := MockChain(chainDB)
+       if err != nil {
+               t.Fatal(err)
+       }
+       if err := AppendBlocks(chain, 6); err != nil {
+               t.Fatal(err)
+       }
+
+       // create tx spend the coinbase output in block 1
+       block, err := chain.GetBlockByHeight(1)
+       if err != nil {
+               t.Fatal(err)
+       }
+       tx, err := CreateTxFromTx(block.Transactions[0], 0, 10000, []byte{byte(vm.OP_TRUE)})
+       if err != nil {
+               t.Fatal(err)
+       }
+
+       newBlock, err := NewBlock(chain, []*types.Tx{tx}, []byte{byte(vm.OP_TRUE)})
+       err = SolveAndUpdate(chain, newBlock)
+       if err != nil {
+               t.Fatal(err)
+       }
+
+       // create a double spent tx in another block
+       tx, err = CreateTxFromTx(block.Transactions[0], 0, 10000, []byte{byte(vm.OP_TRUE)})
+       if err != nil {
+               t.Fatal(err)
+       }
+
+       _, err = chain.ValidateTx(tx)
+       if err == nil {
+               t.Fatal("validate double spent tx success")
+       }
+       if txPool.HaveTransaction(&tx.ID) {
+               t.Fatalf("tx pool have double spent tx")
+       }
+}
+
+func TestDoubleSpentInSameBlock(t *testing.T) {
+       chainDB := dbm.NewDB("tx_pool_test", "leveldb", "tx_pool_test")
+       defer os.RemoveAll("tx_pool_test")
+       chain, _, txPool, err := MockChain(chainDB)
+       if err != nil {
+               t.Fatal(err)
+       }
+       if err := AppendBlocks(chain, 7); err != nil {
+               t.Fatal(err)
+       }
+
+       // create tx spend the coinbase output in block 1
+       block, err := chain.GetBlockByHeight(1)
+       if err != nil {
+               t.Fatal(err)
+       }
+       tx1, err := CreateTxFromTx(block.Transactions[0], 0, 10000, []byte{byte(vm.OP_TRUE)})
+       if err != nil {
+               t.Fatal(err)
+       }
+
+       // create tx spend the coinbase output in block 1
+       tx2, err := CreateTxFromTx(block.Transactions[0], 0, 10000, []byte{byte(vm.OP_TRUE)})
+       if err != nil {
+               t.Fatal(err)
+       }
+
+       _, err = chain.ValidateTx(tx1)
+       if err != nil {
+               t.Fatal(err)
+       }
+       _, err = chain.ValidateTx(tx2)
+       if err != nil {
+               t.Fatal(err)
+       }
+
+       if !txPool.HaveTransaction(&tx1.ID) {
+               t.Fatalf("can't find tx in tx pool")
+       }
+       if !txPool.HaveTransaction(&tx2.ID) {
+               t.Fatalf("can't find tx in tx pool")
+       }
+
+       block, err = NewBlock(chain, []*types.Tx{tx1, tx2}, []byte{byte(vm.OP_TRUE)})
+       if err != nil {
+               t.Fatal(err)
+       }
+
+       if err := SolveAndUpdate(chain, block); err == nil {
+               t.Fatalf("process double spent tx success")
+       }
+}
+
+func TestTxPoolDependencyTx(t *testing.T) {
+       chainDB := dbm.NewDB("tx_pool_test", "leveldb", "tx_pool_test")
+       defer os.RemoveAll("tx_pool_test")
+       chain, _, txPool, err := MockChain(chainDB)
+       if err != nil {
+               t.Fatal(err)
+       }
+       if err := AppendBlocks(chain, 7); err != nil {
+               t.Fatal(err)
+       }
+
+       block, err := chain.GetBlockByHeight(1)
+       if err != nil {
+               t.Fatal(err)
+       }
+
+       tx, err := CreateTxFromTx(block.Transactions[0], 0, 5000000000, []byte{byte(vm.OP_TRUE)})
+       if err != nil {
+               t.Fatal(err)
+       }
+
+       outputAmount := uint64(5000000000)
+       txs := []*types.Tx{nil}
+       txs[0] = tx
+       for i := 1; i < 10; i++ {
+               outputAmount -= 50000000
+               tx, err := CreateTxFromTx(txs[i-1], 0, outputAmount, []byte{byte(vm.OP_TRUE)})
+               if err != nil {
+                       t.Fatal(err)
+               }
+               txs = append(txs, tx)
+       }
+
+       // validate tx and put it into tx pool
+       for _, tx := range txs {
+               if _, err := chain.ValidateTx(tx); err != nil {
+                       t.Fatal(err)
+               }
+               if !txPool.HaveTransaction(&tx.ID) {
+                       t.Fatal("can't find tx in txpool")
+               }
+       }
+
+       block, err = NewBlock(chain, txs, []byte{byte(vm.OP_TRUE)})
+       if err != nil {
+               t.Fatal(err)
+       }
+
+       if err := SolveAndUpdate(chain, block); err != nil {
+               t.Fatal("process dependency tx failed")
+       }
+}
diff --git a/test/protocol_test_util.go b/test/protocol_test_util.go
new file mode 100644 (file)
index 0000000..43c3453
--- /dev/null
@@ -0,0 +1,91 @@
+package test
+
+import (
+       "fmt"
+
+       dbm "github.com/tendermint/tmlibs/db"
+
+       "github.com/bytom/protocol"
+       "github.com/bytom/protocol/bc/types"
+)
+
+func declChain(name string, baseChain *protocol.Chain, baseHeight uint64, height uint64) (*protocol.Chain, error) {
+       chainDB := dbm.NewDB(name, "leveldb", name)
+       chain, _, _, err := MockChain(chainDB)
+       if err != nil {
+               return nil, err
+       }
+
+       if baseChain == nil {
+               if err := AppendBlocks(chain, height); err != nil {
+                       return nil, err
+               }
+               return chain, nil
+       }
+
+       for i := uint64(1); i <= baseHeight; i++ {
+               block, err := baseChain.GetBlockByHeight(i)
+               if err != nil {
+                       return nil, err
+               }
+               if err := SolveAndUpdate(chain, block); err != nil {
+                       return nil, err
+               }
+       }
+
+       err = AppendBlocks(chain, height-baseHeight)
+       return chain, err
+}
+
+func ancestorOf(c1 *protocol.Chain, c2 *protocol.Chain) (*types.Block, error) {
+       start := c1.BestBlockHeight()
+       if c2.BestBlockHeight() < c1.BestBlockHeight() {
+               start = c2.BestBlockHeight()
+       }
+
+       for i := start; i >= 0; i-- {
+               b1, err := c1.GetBlockByHeight(i)
+               if err != nil {
+                       return nil, err
+               }
+               b2, err := c2.GetBlockByHeight(i)
+               if err != nil {
+                       return nil, err
+               }
+               if b1.Hash() == b2.Hash() {
+                       return b1, nil
+               }
+       }
+       return nil, fmt.Errorf("can't find ancestor")
+}
+
+func merge(c1 *protocol.Chain, c2 *protocol.Chain) error {
+       // c1 and c2 are same
+       if c1.BestBlockHeight() == c2.BestBlockHeight() && *c1.BestBlockHash() == *c2.BestBlockHash() {
+               return nil
+       }
+
+       ancestor, err := ancestorOf(c1, c2)
+       if err != nil {
+               return err
+       }
+
+       processBlocks := func(dest *protocol.Chain, src *protocol.Chain, height uint64) error {
+               for h := src.BestBlockHeight(); h > height; h-- {
+                       block, err := src.GetBlockByHeight(h)
+                       if err != nil {
+                               return err
+                       }
+                       _, err = dest.ProcessBlock(block)
+                       if err != nil {
+                               return err
+                       }
+               }
+               return nil
+       }
+
+       if err := processBlocks(c1, c2, ancestor.Height); err != nil {
+               return err
+       }
+       return processBlocks(c2, c1, ancestor.Height)
+}
diff --git a/test/testdata/chain_tests/ct_dependency_tx.json b/test/testdata/chain_tests/ct_dependency_tx.json
new file mode 100644 (file)
index 0000000..d756e85
--- /dev/null
@@ -0,0 +1,74 @@
+{
+  "blocks": [
+    {
+      "append": 6
+    },
+    {
+      "transactions": [
+        {
+          "inputs": [
+            {"height": 1, "tx_index": 0, "output_index": 0}
+          ],
+          "outputs": [3000000000, 10000000, 3000000000]
+        }
+      ],
+      "append": 2
+    },
+    {
+      "transactions": [
+        {
+          "inputs": [
+            {"height": 2, "tx_index": 0, "output_index": 0}
+          ],
+          "outputs": [3000000000, 10000000, 3000000000]
+        }
+      ]
+    },
+    {
+      "transactions": [
+        {
+          "inputs": [
+            {"height": 8, "tx_index": 1, "output_index": 0},
+            {"height": 3, "tx_index": 0, "output_index": 0}
+          ],
+          "outputs": [3000000000, 10000000, 3000000000]
+        },
+        {
+          "inputs": [
+            {"height": 11, "tx_index": 1, "output_index": 0},
+            {"height": 4, "tx_index": 0, "output_index": 0}
+          ],
+          "outputs": [3000000000, 10000000, 3000000000]
+        }
+      ]
+    },
+    {
+      "transactions": [
+        {
+          "inputs": [
+            {"height": 12, "tx_index": 1, "output_index": 0},
+            {"height": 12, "tx_index": 2, "output_index": 0},
+            {"height": 12, "tx_index": 2, "output_index": 2}
+          ],
+          "outputs": [3000000000, 10000000, 3000000000]
+        },
+        {
+          "inputs": [
+            {"height": 13, "tx_index": 1, "output_index": 0},
+            {"height": 13, "tx_index": 1, "output_index": 2},
+            {"height": 5, "tx_index": 0, "output_index": 0}
+          ],
+          "outputs": [3000000000, 10000000, 3000000000]
+        },
+        {
+          "inputs": [
+            {"height": 13, "tx_index": 2, "output_index": 0},
+            {"height": 13, "tx_index": 1, "output_index": 1},
+            {"height": 6, "tx_index": 0, "output_index": 0}
+          ],
+          "outputs": [3000000000, 10000000, 3000000000]
+        }
+      ]
+    }
+  ]
+}
\ No newline at end of file
diff --git a/test/testdata/chain_tests/ct_double_spend.json b/test/testdata/chain_tests/ct_double_spend.json
new file mode 100644 (file)
index 0000000..4bcc29e
--- /dev/null
@@ -0,0 +1,47 @@
+{
+  "blocks": [
+    {
+      "append": 6
+    },
+    {
+      "transactions": [
+        {
+          "inputs": [
+            {"height": 1, "tx_index": 0, "output_index": 0}
+          ],
+          "outputs": [3000000000, 10000000, 3000000000]
+        }
+      ],
+      "append": 2
+    },
+    {
+      "transactions": [
+        {
+          "inputs": [
+            {"height": 2, "tx_index": 0, "output_index": 0}
+          ],
+          "outputs": [3000000000, 10000000, 3000000000]
+        },
+           {
+          "inputs": [
+            {"height": 8, "tx_index": 1, "output_index": 0},
+            {"height": 3, "tx_index": 0, "output_index": 0}
+          ],
+          "outputs": [3000000000, 10000000, 3000000000]
+        }
+      ]
+    },
+    {
+      "transactions": [
+        {
+          "inputs": [
+            {"height": 8, "tx_index": 1, "output_index": 0},
+            {"height": 4, "tx_index": 0, "output_index": 0}
+          ],
+          "outputs": [3000000000, 10000000, 3000000000]
+        }
+      ],
+      "invalid": true
+    }
+  ]
+}
diff --git a/test/testdata/chain_tests/ct_normal.json b/test/testdata/chain_tests/ct_normal.json
new file mode 100644 (file)
index 0000000..282bc25
--- /dev/null
@@ -0,0 +1,46 @@
+{
+  "blocks": [
+    {
+      "append": 6
+    },
+    {
+      "transactions": [
+        {
+          "inputs": [
+            {"height": 1, "tx_index": 0, "output_index": 0}
+          ],
+          "outputs": [3000000000, 10000000, 3000000000]
+        }
+      ],
+      "append": 2
+    },
+    {
+      "transactions": [
+        {
+          "inputs": [
+            {"height": 2, "tx_index": 0, "output_index": 0}
+          ],
+          "outputs": [3000000000, 10000000, 3000000000]
+        }
+      ]
+    },
+    {
+      "transactions": [
+        {
+          "inputs": [
+            {"height": 8, "tx_index": 1, "output_index": 0},
+            {"height": 3, "tx_index": 0, "output_index": 0}
+          ],
+          "outputs": [3000000000, 10000000, 3000000000]
+        },
+        {
+          "inputs": [
+            {"height": 11, "tx_index": 1, "output_index": 0},
+            {"height": 4, "tx_index": 0, "output_index": 0}
+          ],
+          "outputs": [3000000000, 10000000, 3000000000]
+        }
+      ]
+    }
+  ]
+}
\ No newline at end of file
diff --git a/test/testdata/chain_tests/ct_rollback.json b/test/testdata/chain_tests/ct_rollback.json
new file mode 100644 (file)
index 0000000..cb77552
--- /dev/null
@@ -0,0 +1,75 @@
+{
+  "rollback_to": 10,
+  "blocks": [
+    {
+      "append": 6
+    },
+    {
+      "transactions": [
+        {
+          "inputs": [
+            {"height": 1, "tx_index": 0, "output_index": 0}
+          ],
+          "outputs": [3000000000, 10000000, 3000000000]
+        }
+      ],
+      "append": 2
+    },
+    {
+      "transactions": [
+        {
+          "inputs": [
+            {"height": 2, "tx_index": 0, "output_index": 0}
+          ],
+          "outputs": [3000000000, 10000000, 3000000000]
+        }
+      ]
+    },
+    {
+      "transactions": [
+        {
+          "inputs": [
+            {"height": 8, "tx_index": 1, "output_index": 0},
+            {"height": 3, "tx_index": 0, "output_index": 0}
+          ],
+          "outputs": [3000000000, 10000000, 3000000000]
+        },
+        {
+          "inputs": [
+            {"height": 11, "tx_index": 1, "output_index": 0},
+            {"height": 4, "tx_index": 0, "output_index": 0}
+          ],
+          "outputs": [3000000000, 10000000, 3000000000]
+        }
+      ]
+    },
+    {
+      "transactions": [
+        {
+          "inputs": [
+            {"height": 12, "tx_index": 1, "output_index": 0},
+            {"height": 12, "tx_index": 2, "output_index": 0},
+            {"height": 12, "tx_index": 2, "output_index": 2}
+          ],
+          "outputs": [3000000000, 10000000, 3000000000]
+        },
+        {
+          "inputs": [
+            {"height": 13, "tx_index": 1, "output_index": 0},
+            {"height": 13, "tx_index": 1, "output_index": 2},
+            {"height": 5, "tx_index": 0, "output_index": 0}
+          ],
+          "outputs": [3000000000, 10000000, 3000000000]
+        },
+        {
+          "inputs": [
+            {"height": 13, "tx_index": 2, "output_index": 0},
+            {"height": 13, "tx_index": 1, "output_index": 1},
+            {"height": 6, "tx_index": 0, "output_index": 0}
+          ],
+          "outputs": [3000000000, 10000000, 3000000000]
+        }
+      ]
+    }
+  ]
+}
\ No newline at end of file
diff --git a/test/testdata/tx_tests/tx_tests.json b/test/testdata/tx_tests/tx_tests.json
new file mode 100644 (file)
index 0000000..5417ce2
--- /dev/null
@@ -0,0 +1,229 @@
+{
+  "keys": [
+    {"name": "alice", "password": "alice"},
+    {"name": "bob", "password": "bob"}
+  ],
+  "accounts": [
+    {"name": "alice", "keys": ["alice"], "quorum": 1},
+    {"name": "bob", "keys": ["bob"], "quorum": 1},
+    {"name": "multi-sig", "keys": ["alice", "bob"], "quorum": 2}
+  ],
+  "transactions": [
+    {
+      "describe": "normal single sign btm tx",
+      "passwords": ["alice"],
+      "inputs": [
+        {"type": "spend_account", "name": "alice", "amount": 100000000000, "asset": "BTM"}
+      ],
+      "outputs": [
+        {"type": "output", "name": "bob", "amount": 40000000000, "asset": "BTM"}
+      ],
+      "valid": true,
+      "gas_only": false,
+      "tx_fee": 60000000000
+    },
+    {
+      "describe": "single sign btm tx, out of gas",
+      "passwords": ["alice"],
+      "inputs": [
+        {"type": "spend_account", "name": "alice", "amount": 100000000000, "asset": "BTM"}
+      ],
+      "outputs": [
+        {"type": "output", "name": "bob", "amount": 100000000000, "asset": "BTM"}
+      ],
+      "gas_only": false,
+      "valid": false
+    },
+    {
+      "describe": "normal multi utxo btm tx",
+      "passwords": ["alice", "bob"],
+      "inputs": [
+        {"type": "spend_account", "name": "alice", "amount": 100000000000, "asset": "BTM"},
+        {"type": "spend_account", "name": "bob", "amount": 100000000000, "asset": "BTM"}
+      ],
+      "outputs": [
+        {"type": "output", "name": "bob", "amount": 30000000000, "asset": "BTM"},
+        {"type": "output", "name": "alice", "amount": 30000000000, "asset": "BTM"}
+      ],
+      "valid": true,
+      "gas_only": false,
+      "tx_fee": 140000000000
+    },
+    {
+      "describe": "single sign asset tx",
+      "passwords": ["alice"],
+      "inputs": [
+        {"type": "spend_account", "name": "alice", "amount": 100000000000, "asset": "BTM"},
+        {"type": "issue", "name": "alice", "amount": 100, "asset": "GOLD"}
+      ],
+      "outputs": [
+        {"type": "output", "name": "alice", "amount": 30000000000, "asset": "BTM"},
+        {"type": "output", "name": "alice", "amount": 100, "asset": "GOLD"}
+      ],
+      "valid": true,
+      "gas_only": false,
+      "tx_fee": 70000000000
+    },
+    {
+      "describe": "single sign asset, out of gas",
+      "passwords": ["alice"],
+      "inputs": [
+        {"type": "spend_account", "name": "alice", "amount": 100000000000, "asset": "BTM"},
+        {"type": "issue", "name": "alice", "amount": 100, "asset": "RMB"}
+      ],
+      "outputs": [
+        {"type": "output", "name": "alice", "amount": 100000000000, "asset": "BTM"},
+        {"type": "output", "name": "alice", "amount": 100, "asset": "RMB"}
+      ],
+      "valid": false,
+      "gas_only": false
+    },
+    {
+      "describe": "single sign asset, input not equal with output",
+      "passwords": ["alice"],
+      "inputs": [
+        {"type": "spend_account", "name": "alice", "amount": 100000000000, "asset": "BTM"},
+        {"type": "issue", "name": "alice", "amount": 100, "asset": "SILVER"}
+      ],
+      "outputs": [
+        {"type": "output", "name": "alice", "amount": 30000000000, "asset": "BTM"},
+        {"type": "output", "name": "alice", "amount": 50, "asset": "SILVER"}
+      ],
+      "valid": false,
+      "gas_only": false
+    },
+    {
+      "describe": "normal single sign with retire",
+      "passwords": ["alice"],
+      "inputs": [
+        {"type": "spend_account", "name": "alice", "amount": 100000000000, "asset": "BTM"},
+        {"type": "spend_account", "name": "alice", "amount": 100, "asset": "GOLD"}
+      ],
+      "outputs": [
+        {"type": "output", "name": "alice", "amount": 30000000000, "asset": "BTM"},
+        {"type": "retire", "name": "alice", "amount": 100, "asset": "GOLD"}
+      ],
+      "valid": true,
+      "gas_only": false,
+      "tx_fee": 70000000000
+    },
+    {
+      "describe": "gas only tx",
+      "passwords": ["alice"],
+      "inputs": [
+        {"type": "spend_account", "name": "alice", "amount": 100000000000, "asset": "BTM"},
+        {"type": "spend_account", "name": "bob", "amount": 100, "asset": "GOLD"}
+      ],
+      "outputs": [
+        {"type": "output", "name": "alice", "amount": 30000000000, "asset": "BTM"},
+        {"type": "retire", "name": "alice", "amount": 100, "asset": "GOLD"}
+      ],
+      "valid": false,
+      "gas_only": true,
+      "tx_fee": 70000000000
+    },
+    {
+      "describe": "normal multi-sig asset with issue and retire",
+      "passwords": ["alice", "bob"],
+      "inputs": [
+        {"type": "spend_account", "name": "alice", "amount": 100000000000, "asset": "BTM"},
+        {"type": "spend_account", "name": "bob", "amount": 100000000000, "asset": "BTM"},
+        {"type": "issue", "name": "multi-sig", "amount": 100, "asset": "MULTI-SIGN-ASSET"},
+        {"type": "spend_account", "name": "alice", "amount": 10, "asset": "GOLD"}
+      ],
+      "outputs": [
+        {"type": "output", "name": "multi-sig", "amount": 199500000000, "asset": "BTM"},
+        {"type": "output", "name": "multi-sig", "amount": 100, "asset": "MULTI-SIGN-ASSET"},
+        {"type": "retire", "name": "alice", "amount": 10, "asset": "GOLD"}
+      ],
+      "valid": true,
+      "gas_only": false,
+      "tx_fee": 500000000
+    },
+    {
+      "describe": "multi-sig asset with issue and retire, out of gas",
+      "passwords": ["alice", "bob"],
+      "inputs": [
+        {"type": "spend_account", "name": "alice", "amount": 100000000000, "asset": "BTM"},
+        {"type": "spend_account", "name": "bob", "amount": 100000000000, "asset": "BTM"},
+        {"type": "issue", "name": "multi-sig", "amount": 100, "asset": "DOLLAR"},
+        {"type": "spend_account", "name": "alice", "amount": 10, "asset": "GOLD"}
+      ],
+      "outputs": [
+        {"type": "output", "name": "multi-sig", "amount": 200000000000, "asset": "BTM"},
+        {"type": "output", "name": "multi-sig", "amount": 100, "asset": "DOLLAR"},
+        {"type": "retire", "name": "alice", "amount": 10, "asset": "GOLD"}
+      ],
+      "gas_only": false,
+      "valid": false
+    },
+    {
+      "describe": "multi utxo btm tx, wrong sig",
+      "passwords": ["alice"],
+      "inputs": [
+        {"type": "spend_account", "name": "alice", "amount": 100000000000, "asset": "BTM"},
+        {"type": "spend_account", "name": "bob", "amount": 100000000000, "asset": "BTM"}
+      ],
+      "outputs": [
+        {"type": "output", "name": "bob", "amount": 30000000000, "asset": "BTM"},
+        {"type": "output", "name": "alice", "amount": 30000000000, "asset": "BTM"}
+      ],
+      "gas_only": false,
+      "valid": false
+    },
+    {
+      "describe": "multi utxo btm tx, output large than input",
+      "passwords": ["alice", "bob"],
+      "inputs": [
+        {"type": "spend_account", "name": "alice", "amount": 100000000000, "asset": "BTM"},
+        {"type": "spend_account", "name": "bob", "amount": 100000000000, "asset": "BTM"}
+      ],
+      "outputs": [
+        {"type": "output", "name": "bob", "amount": 90000000000, "asset": "BTM"},
+        {"type": "output", "name": "alice", "amount": 200000000000, "asset": "BTM"}
+      ],
+      "gas_only": false,
+      "valid": false
+    },
+    {
+      "describe": "version is 0",
+      "passwords": ["alice"],
+      "version": 0,
+      "inputs": [
+        {"type": "spend_account", "name": "alice", "amount": 100000000000, "asset": "BTM"}
+      ],
+      "outputs": [
+        {"type": "output", "name": "bob", "amount": 90000000000, "asset": "BTM"}
+      ],
+      "gas_only": false,
+      "valid": false
+    },
+    {
+      "describe": "version is 1",
+      "passwords": ["alice"],
+      "version": 1,
+      "inputs": [
+        {"type": "spend_account", "name": "alice", "amount": 100000000000, "asset": "BTM"}
+      ],
+      "outputs": [
+        {"type": "output", "name": "bob", "amount": 90000000000, "asset": "BTM"}
+      ],
+      "tx_fee": 10000000000,
+      "gas_only": false,
+      "valid": true
+    },
+    {
+      "describe": "version greater than block version(1)",
+      "passwords": ["alice"],
+      "version": 12,
+      "inputs": [
+        {"type": "spend_account", "name": "alice", "amount": 100000000000, "asset": "BTM"}
+      ],
+      "outputs": [
+        {"type": "output", "name": "bob", "amount": 90000000000, "asset": "BTM"}
+      ],
+      "gas_only": false,
+      "valid": false
+    }
+  ]
+}
\ No newline at end of file
diff --git a/test/testdata/wallet_tests/wt_asset.json b/test/testdata/wallet_tests/wt_asset.json
new file mode 100644 (file)
index 0000000..5024a8b
--- /dev/null
@@ -0,0 +1,93 @@
+{
+  "keys": [
+    {"name": "alice", "password": "alice"},
+    {"name": "bob", "password": "bob"}
+  ],
+  "accounts": [
+    {"name": "alice", "keys": ["alice"], "quorum": 1},
+    {"name": "bob", "keys": ["bob"], "quorum": 1}
+  ],
+  "blocks": [
+    {
+      "coinbase_account": "alice",
+      "post_states": [
+        {"name": "alice", "asset": "BTM", "amount": 41250000000}
+      ],
+      "append": 6
+    },
+    {
+      "coinbase_account": "bob",
+      "transactions": [
+        {
+          "passwords": ["alice"],
+          "inputs": [
+            {"type": "spend_account", "name": "alice", "amount": 6000000000, "asset": "BTM"}
+          ],
+          "outputs": [
+            {"type": "output", "name": "bob", "amount": 1000000000, "asset": "BTM"},
+            {"type": "output", "name": "alice", "amount": 4000000000, "asset": "BTM"}
+          ]
+        }
+      ],
+      "post_states": [
+        {"name": "alice", "asset": "BTM", "amount": 39250000000},
+        {"name": "bob", "asset": "BTM", "amount": 43250000000}
+      ],
+      "append": 6
+    },
+    {
+      "coinbase_account": "bob",
+      "transactions": [
+        {
+          "passwords": ["alice"],
+          "inputs": [
+            {"type": "spend_account", "name": "alice", "amount": 1000000000, "asset": "BTM"},
+            {"type": "issue", "name": "alice", "amount": 100, "asset": "GOLD"}
+          ],
+          "outputs": [
+            {"type": "output", "name": "alice", "amount": 100, "asset": "GOLD"}
+          ]
+        }
+      ],
+      "post_states": [
+        {"name": "alice", "asset": "BTM", "amount": 38250000000},
+        {"name": "alice", "asset": "GOLD", "amount": 100},
+        {"name": "bob", "asset": "BTM", "amount": 85500000000}
+      ],
+      "append": 6
+    },
+    {
+      "coinbase_account": "bob",
+      "transactions": [
+        {
+          "passwords": ["bob"],
+          "inputs": [
+            {"type": "spend_account", "name": "bob", "amount": 6000000000, "asset": "BTM"}
+          ],
+          "outputs": [
+            {"type": "output", "name": "alice", "amount": 1000000000, "asset": "BTM"},
+            {"type": "output", "name": "bob", "amount": 4000000000, "asset": "BTM"}
+          ]
+        },
+        {
+          "passwords": ["alice", "bob"],
+          "inputs": [
+            {"type": "spend_account", "name": "bob", "amount": 2000000000, "asset": "BTM"},
+            {"type": "spend_account", "name": "alice", "amount": 50, "asset": "GOLD"}
+          ],
+          "outputs": [
+            {"type": "output", "name": "bob", "amount": 50, "asset": "GOLD"},
+            {"type": "output", "name": "alice", "amount": 1000000000, "asset": "BTM"}
+          ]
+        }
+      ],
+      "post_states": [
+        {"name": "alice", "asset": "BTM", "amount": 40250000000},
+        {"name": "alice", "asset": "GOLD", "amount": 50},
+        {"name": "bob", "asset": "BTM", "amount": 124750000000},
+        {"name": "bob", "asset": "GOLD", "amount": 50}
+      ],
+      "append": 0
+    }
+  ]
+}
diff --git a/test/testdata/wallet_tests/wt_btm.json b/test/testdata/wallet_tests/wt_btm.json
new file mode 100644 (file)
index 0000000..1299445
--- /dev/null
@@ -0,0 +1,39 @@
+{
+  "keys": [
+    {"name": "alice", "password": "alice"},
+    {"name": "bob", "password": "bob"}
+  ],
+  "accounts": [
+    {"name": "alice", "keys": ["alice"], "quorum": 1},
+    {"name": "bob", "keys": ["bob"], "quorum": 1}
+  ],
+  "blocks": [
+    {
+      "coinbase_account": "alice",
+      "post_states": [
+        {"name": "alice", "asset": "BTM", "amount": 41250000000}
+      ],
+      "append": 6
+    },
+    {
+      "coinbase_account": "bob",
+      "transactions": [
+        {
+          "passwords": ["alice"],
+          "inputs": [
+            {"type": "spend_account", "name": "alice", "amount": 6000000000, "asset": "BTM"}
+          ],
+          "outputs": [
+            {"type": "output", "name": "bob", "amount": 1000000000, "asset": "BTM"},
+            {"type": "output", "name": "alice", "amount": 4000000000, "asset": "BTM"}
+          ]
+        }
+      ],
+      "post_states": [
+        {"name": "alice", "asset": "BTM", "amount": 39250000000},
+        {"name": "bob", "asset": "BTM", "amount": 43250000000}
+      ],
+      "append": 0
+    }
+  ]
+}
diff --git a/test/testdata/wallet_tests/wt_invalid_txs.json b/test/testdata/wallet_tests/wt_invalid_txs.json
new file mode 100644 (file)
index 0000000..3d35b91
--- /dev/null
@@ -0,0 +1,113 @@
+{
+  "keys": [
+    {"name": "alice", "password": "alice"},
+    {"name": "bob", "password": "bob"},
+    {"name": "default", "password": "default"}
+  ],
+  "accounts": [
+    {"name": "alice", "keys": ["alice"], "quorum": 1},
+    {"name": "bob", "keys": ["bob"], "quorum": 1},
+    {"name": "default", "keys": ["default"], "quorum": 1}
+  ],
+  "blocks": [
+    {
+      "coinbase_account": "alice",
+      "post_states": [
+        {"name": "alice", "asset": "BTM", "amount": 41250000000}
+      ],
+      "append": 6
+    },
+    {
+      "coinbase_account": "bob",
+      "transactions": [
+        {
+          "passwords": ["alice"],
+          "inputs": [
+            {"type": "spend_account", "name": "alice", "amount": 1000000000, "asset": "BTM"},
+            {"type": "issue", "name": "alice", "amount": 100, "asset": "GOLD"}
+          ],
+          "outputs": [
+            {"type": "output", "name": "alice", "amount": 100, "asset": "GOLD"}
+          ]
+        }
+      ],
+      "post_states": [
+        {"name": "alice", "asset": "BTM", "amount": 40250000000},
+        {"name": "alice", "asset": "GOLD", "amount": 100},
+        {"name": "bob", "asset": "BTM", "amount": 42250000000}
+      ],
+      "append": 0
+    },
+    {
+      "coinbase_account": "default",
+      "transactions": [
+        {
+          "passwords": ["alice"],
+          "inputs": [
+            {"type": "spend_account", "name": "alice", "amount": 700000000000, "asset": "BTM"}
+          ],
+          "outputs": [
+            {"type": "output", "name": "alice", "amount": 400000000000, "asset": "BTM"},
+            {"type": "output", "name": "bob", "amount": 299000000000, "asset": "BTM"}
+          ]
+        },
+        {
+          "passwords": ["alice"],
+          "inputs": [
+            {"type": "spend_account", "name": "alice", "amount": 1000000000, "asset": "BTM"},
+            {"type": "spend_account", "name": "alice", "amount": 100, "asset": "GOLD"}
+          ],
+          "outputs": [
+            {"type": "retire", "name": "alice", "amount": 100, "asset": "GOLD"}
+          ]
+        }
+      ],
+      "post_states": [
+        {"name": "alice", "asset": "BTM", "amount": 39250000000},
+        {"name": "alice", "asset": "GOLD", "amount": 0},
+        {"name": "bob", "asset": "BTM", "amount": 42250000000}
+      ],
+      "append": 0
+    },
+    {
+      "coinbase_account": "default",
+      "transactions": [
+        {
+          "passwords": ["alice"],
+          "inputs": [
+            {"type": "spend_account", "name": "alice", "amount": 1000000000, "asset": "BTM"},
+            {"type": "spend_account", "name": "alice", "amount": 20, "asset": "GOLD"}
+          ],
+          "outputs": [
+            {"type": "output", "name": "bob", "amount": 20, "asset": "GOLD"}
+          ]
+        }
+      ],
+      "post_states": [
+        {"name": "alice", "asset": "BTM", "amount": 39250000000},
+        {"name": "alice", "asset": "GOLD", "amount": 0},
+        {"name": "bob", "asset": "BTM", "amount": 42250000000},
+        {"name": "bob", "asset": "GOLD", "amount": 0}
+      ],
+      "append": 0
+    },
+    {
+      "coinbase_account": "default",
+      "transactions": [
+        {
+          "passwords": ["alice"],
+          "inputs": [
+            {"type": "spend_account", "name": "alice", "amount": 1000000000, "asset": "BTM"}
+          ]
+        }
+      ],
+      "post_states": [
+        {"name": "alice", "asset": "BTM", "amount": 39250000000},
+        {"name": "alice", "asset": "GOLD", "amount": 0},
+        {"name": "bob", "asset": "BTM", "amount": 42250000000},
+        {"name": "bob", "asset": "GOLD", "amount": 0}
+      ],
+      "append": 0
+    }
+  ]
+}
diff --git a/test/testdata/wallet_tests/wt_multi_sig_asset.json b/test/testdata/wallet_tests/wt_multi_sig_asset.json
new file mode 100644 (file)
index 0000000..6b8a18d
--- /dev/null
@@ -0,0 +1,91 @@
+{
+  "keys": [
+    {"name": "alice", "password": "alice"},
+    {"name": "bob", "password": "bob"}
+  ],
+  "accounts": [
+    {"name": "alice", "keys": ["alice"], "quorum": 1},
+    {"name": "bob", "keys": ["bob"], "quorum": 1},
+    {"name": "multi-sig", "keys": ["alice", "bob"], "quorum": 2}
+  ],
+  "blocks": [
+    {
+      "coinbase_account": "alice",
+      "post_states": [
+        {"name": "alice", "asset": "BTM", "amount": 41250000000}
+      ],
+      "append": 6
+    },
+    {
+      "coinbase_account": "bob",
+      "transactions": [
+        {
+          "passwords": ["alice"],
+          "inputs": [
+            {"type": "spend_account", "name": "alice", "amount": 10000000000, "asset": "BTM"}
+          ],
+          "outputs": [
+            {"type": "output", "name": "bob", "amount": 4000000000, "asset": "BTM"},
+            {"type": "output", "name": "multi-sig", "amount": 5000000000, "asset": "BTM"}
+          ]
+        }
+      ],
+      "post_states": [
+        {"name": "alice", "asset": "BTM", "amount": 31250000000},
+        {"name": "bob", "asset": "BTM", "amount": 46250000000},
+        {"name": "multi-sig", "asset": "BTM", "amount": 5000000000}
+      ],
+      "append": 6
+    },
+    {
+      "coinbase_account": "bob",
+      "transactions": [
+        {
+          "passwords": ["bob", "alice"],
+          "inputs": [
+            {"type": "spend_account", "name": "multi-sig", "amount": 1000000000, "asset": "BTM"},
+            {"type": "issue", "name": "multi-sig", "amount": 100, "asset": "GOLD"}
+          ],
+          "outputs": [
+            {"type": "output", "name": "multi-sig", "amount": 100, "asset": "GOLD"}
+          ]
+        }
+      ],
+      "post_states": [
+        {"name": "alice", "asset": "BTM", "amount": 31250000000},
+        {"name": "multi-sig", "asset": "GOLD", "amount": 100},
+        {"name": "multi-sig", "asset": "BTM", "amount": 4000000000},
+        {"name": "bob", "asset": "BTM", "amount": 88500000000}
+      ],
+      "append": 6
+    },
+    {
+      "coinbase_account": "multi-sig",
+      "transactions": [
+        {
+          "passwords": ["alice", "bob"],
+          "inputs": [
+            {"type": "spend_account", "name": "multi-sig", "amount": 1000000000, "asset": "BTM"},
+            {"type": "spend_account", "name": "multi-sig", "amount": 40, "asset": "GOLD"},
+            {"type": "spend_account", "name": "bob", "amount": 10000000000, "asset": "BTM"},
+            {"type": "spend_account", "name": "alice", "amount": 10000000000, "asset": "BTM"}
+          ],
+          "outputs": [
+            {"type": "output", "name": "alice", "amount": 20, "asset": "GOLD"},
+            {"type": "output", "name": "bob", "amount": 20, "asset": "GOLD"},
+            {"type": "output", "name": "multi-sig", "amount": 20000000000, "asset": "BTM"}
+          ]
+        }
+      ],
+      "post_states": [
+        {"name": "multi-sig", "asset": "BTM", "amount": 65250000000},
+        {"name": "multi-sig", "asset": "GOLD", "amount": 60},
+        {"name": "alice", "asset": "BTM", "amount": 21250000000},
+        {"name": "alice", "asset": "GOLD", "amount": 20},
+        {"name": "bob", "asset": "BTM", "amount": 78500000000},
+        {"name": "bob", "asset": "GOLD", "amount": 20}
+      ],
+      "append": 0
+    }
+  ]
+}
diff --git a/test/testdata/wallet_tests/wt_retire_asset.json b/test/testdata/wallet_tests/wt_retire_asset.json
new file mode 100644 (file)
index 0000000..4eb5b7b
--- /dev/null
@@ -0,0 +1,115 @@
+{
+  "keys": [
+    {"name": "alice", "password": "alice"},
+    {"name": "bob", "password": "bob"}
+  ],
+  "accounts": [
+    {"name": "alice", "keys": ["alice"], "quorum": 1},
+    {"name": "bob", "keys": ["bob"], "quorum": 1}
+  ],
+  "blocks": [
+    {
+      "coinbase_account": "alice",
+      "post_states": [
+        {"name": "alice", "asset": "BTM", "amount": 41250000000}
+      ],
+      "append": 6
+    },
+    {
+      "coinbase_account": "bob",
+      "transactions": [
+        {
+          "passwords": ["alice"],
+          "inputs": [
+            {"type": "spend_account", "name": "alice", "amount": 6000000000, "asset": "BTM"}
+          ],
+          "outputs": [
+            {"type": "output", "name": "bob", "amount": 1000000000, "asset": "BTM"},
+            {"type": "output", "name": "alice", "amount": 4000000000, "asset": "BTM"}
+          ]
+        }
+      ],
+      "post_states": [
+        {"name": "alice", "asset": "BTM", "amount": 39250000000},
+        {"name": "bob", "asset": "BTM", "amount": 43250000000}
+      ],
+      "append": 6
+    },
+    {
+      "coinbase_account": "bob",
+      "transactions": [
+        {
+          "passwords": ["alice"],
+          "inputs": [
+            {"type": "spend_account", "name": "alice", "amount": 1000000000, "asset": "BTM"},
+            {"type": "issue", "name": "alice", "amount": 100, "asset": "GOLD"}
+          ],
+          "outputs": [
+            {"type": "output", "name": "alice", "amount": 100, "asset": "GOLD"}
+          ]
+        }
+      ],
+      "post_states": [
+        {"name": "alice", "asset": "BTM", "amount": 38250000000},
+        {"name": "alice", "asset": "GOLD", "amount": 100},
+        {"name": "bob", "asset": "BTM", "amount": 85500000000}
+      ],
+      "append": 6
+    },
+    {
+      "coinbase_account": "bob",
+      "transactions": [
+        {
+          "passwords": ["bob"],
+          "inputs": [
+            {"type": "spend_account", "name": "bob", "amount": 6000000000, "asset": "BTM"}
+          ],
+          "outputs": [
+            {"type": "output", "name": "alice", "amount": 1000000000, "asset": "BTM"},
+            {"type": "output", "name": "bob", "amount": 4000000000, "asset": "BTM"}
+          ]
+        },
+        {
+          "passwords": ["alice", "bob"],
+          "inputs": [
+            {"type": "spend_account", "name": "bob", "amount": 2000000000, "asset": "BTM"},
+            {"type": "spend_account", "name": "alice", "amount": 50, "asset": "GOLD"}
+          ],
+          "outputs": [
+            {"type": "output", "name": "bob", "amount": 50, "asset": "GOLD"},
+            {"type": "output", "name": "alice", "amount": 1000000000, "asset": "BTM"}
+          ]
+        }
+      ],
+      "post_states": [
+        {"name": "alice", "asset": "BTM", "amount": 40250000000},
+        {"name": "alice", "asset": "GOLD", "amount": 50},
+        {"name": "bob", "asset": "BTM", "amount": 124750000000},
+        {"name": "bob", "asset": "GOLD", "amount": 50}
+      ],
+      "append": 0
+    },
+    {
+      "coinbase_account": "alice",
+      "transactions": [
+        {
+          "passwords": ["alice"],
+          "inputs": [
+            {"type": "spend_account", "name": "alice", "amount": 1000000000, "asset": "BTM"},
+            {"type": "spend_account", "name": "alice", "amount": 20, "asset": "GOLD"}
+          ],
+          "outputs": [
+            {"type": "retire", "name": "alice", "amount": 20, "asset": "GOLD"}
+          ]
+        }
+      ],
+      "post_states": [
+        {"name": "alice", "asset": "BTM", "amount": 81500000000},
+        {"name": "alice", "asset": "GOLD", "amount": 30},
+        {"name": "bob", "asset": "BTM", "amount": 124750000000},
+        {"name": "bob", "asset": "GOLD", "amount": 50}
+      ],
+      "append": 0
+    }
+  ]
+}
diff --git a/test/testdata/wallet_tests/wt_rollback.json b/test/testdata/wallet_tests/wt_rollback.json
new file mode 100644 (file)
index 0000000..2f72d34
--- /dev/null
@@ -0,0 +1,92 @@
+{
+  "rollback_to": 20,
+  "keys": [
+    {"name": "alice", "password": "alice"},
+    {"name": "bob", "password": "bob"}
+  ],
+  "accounts": [
+    {"name": "alice", "keys": ["alice"], "quorum": 1},
+    {"name": "bob", "keys": ["bob"], "quorum": 1},
+    {"name": "multi-sig", "keys": ["alice", "bob"], "quorum": 2}
+  ],
+  "blocks": [
+    {
+      "coinbase_account": "alice",
+      "post_states": [
+        {"name": "alice", "asset": "BTM", "amount": 41250000000}
+      ],
+      "append": 6
+    },
+    {
+      "coinbase_account": "bob",
+      "transactions": [
+        {
+          "passwords": ["alice"],
+          "inputs": [
+            {"type": "spend_account", "name": "alice", "amount": 10000000000, "asset": "BTM"}
+          ],
+          "outputs": [
+            {"type": "output", "name": "bob", "amount": 4000000000, "asset": "BTM"},
+            {"type": "output", "name": "multi-sig", "amount": 5000000000, "asset": "BTM"}
+          ]
+        }
+      ],
+      "post_states": [
+        {"name": "alice", "asset": "BTM", "amount": 31250000000},
+        {"name": "bob", "asset": "BTM", "amount": 46250000000},
+        {"name": "multi-sig", "asset": "BTM", "amount": 5000000000}
+      ],
+      "append": 6
+    },
+    {
+      "coinbase_account": "bob",
+      "transactions": [
+        {
+          "passwords": ["bob", "alice"],
+          "inputs": [
+            {"type": "spend_account", "name": "multi-sig", "amount": 1000000000, "asset": "BTM"},
+            {"type": "issue", "name": "multi-sig", "amount": 100, "asset": "GOLD"}
+          ],
+          "outputs": [
+            {"type": "output", "name": "multi-sig", "amount": 100, "asset": "GOLD"}
+          ]
+        }
+      ],
+      "post_states": [
+        {"name": "alice", "asset": "BTM", "amount": 31250000000},
+        {"name": "multi-sig", "asset": "GOLD", "amount": 100},
+        {"name": "multi-sig", "asset": "BTM", "amount": 4000000000},
+        {"name": "bob", "asset": "BTM", "amount": 88500000000}
+      ],
+      "append": 6
+    },
+    {
+      "coinbase_account": "multi-sig",
+      "transactions": [
+        {
+          "passwords": ["alice", "bob"],
+          "inputs": [
+            {"type": "spend_account", "name": "multi-sig", "amount": 1000000000, "asset": "BTM"},
+            {"type": "spend_account", "name": "multi-sig", "amount": 40, "asset": "GOLD"},
+            {"type": "spend_account", "name": "bob", "amount": 10000000000, "asset": "BTM"},
+            {"type": "spend_account", "name": "alice", "amount": 10000000000, "asset": "BTM"}
+          ],
+          "outputs": [
+            {"type": "output", "name": "alice", "amount": 20, "asset": "GOLD"},
+            {"type": "output", "name": "bob", "amount": 20, "asset": "GOLD"},
+            {"type": "output", "name": "multi-sig", "amount": 20000000000, "asset": "BTM"}
+          ]
+        }
+      ],
+      "post_states": [
+        {"name": "multi-sig", "asset": "BTM", "amount": 65250000000},
+        {"name": "multi-sig", "asset": "GOLD", "amount": 60},
+        {"name": "alice", "asset": "BTM", "amount": 21250000000},
+        {"name": "alice", "asset": "GOLD", "amount": 20},
+        {"name": "bob", "asset": "BTM", "amount": 78500000000},
+        {"name": "bob", "asset": "GOLD", "amount": 20}
+      ],
+      "append": 0
+    }
+  ]
+}
diff --git a/test/tx_test.go b/test/tx_test.go
new file mode 100644 (file)
index 0000000..b5761a4
--- /dev/null
@@ -0,0 +1,253 @@
+// +build functional
+
+package test
+
+import (
+       "encoding/json"
+       "fmt"
+       "io/ioutil"
+       "os"
+       "testing"
+
+       dbm "github.com/tendermint/tmlibs/db"
+
+       "github.com/bytom/account"
+       "github.com/bytom/asset"
+       "github.com/bytom/blockchain/pseudohsm"
+       "github.com/bytom/consensus"
+       "github.com/bytom/protocol/bc"
+       "github.com/bytom/protocol/bc/types"
+       "github.com/bytom/protocol/validation"
+       "github.com/bytom/protocol/vm"
+)
+
+type TxTestConfig struct {
+       Keys         []*keyInfo       `json:"keys"`
+       Accounts     []*accountInfo   `json:"accounts"`
+       Transactions []*ttTransaction `json:"transactions"`
+}
+
+func (cfg *TxTestConfig) Run() error {
+       dirPath, err := ioutil.TempDir(".", "pseudo_hsm")
+       if err != nil {
+               return err
+       }
+       defer os.RemoveAll(dirPath)
+       hsm, err := pseudohsm.New(dirPath)
+       if err != nil {
+               return err
+       }
+
+       chainDB := dbm.NewDB("chain_db", "leveldb", "chain_db")
+       defer os.RemoveAll("chain_db")
+       chain, _, _, _ := MockChain(chainDB)
+       txTestDB := dbm.NewDB("tx_test_db", "leveldb", "tx_test_db")
+       defer os.RemoveAll("tx_test_db")
+       accountManager := account.NewManager(txTestDB, chain)
+       assets := asset.NewRegistry(txTestDB, chain)
+
+       generator := NewTxGenerator(accountManager, assets, hsm)
+       for _, key := range cfg.Keys {
+               if err := generator.createKey(key.Name, key.Password); err != nil {
+                       return err
+               }
+       }
+
+       for _, acc := range cfg.Accounts {
+               if err := generator.createAccount(acc.Name, acc.Keys, acc.Quorum); err != nil {
+                       return err
+               }
+       }
+
+       block := &bc.Block{
+               BlockHeader: &bc.BlockHeader{
+                       Height:  1,
+                       Version: 1,
+               },
+       }
+       for _, t := range cfg.Transactions {
+               tx, err := t.create(generator)
+               if err != nil {
+                       return err
+               }
+
+               tx.TxData.Version = t.Version
+               tx.Tx = types.MapTx(&tx.TxData)
+               status, err := validation.ValidateTx(tx.Tx, block)
+               result := err == nil
+               if result != t.Valid {
+                       return fmt.Errorf("tx %s validate failed, expected: %t, have: %t", t.Describe, t.Valid, result)
+               }
+               if status == nil {
+                       continue
+               }
+
+               gasOnlyTx := false
+               if err != nil && status.GasVaild {
+                       gasOnlyTx = true
+               }
+               if gasOnlyTx != t.GasOnly {
+                       return fmt.Errorf("gas only tx %s validate failed", t.Describe)
+               }
+               if result && t.TxFee != status.BTMValue {
+                       return fmt.Errorf("gas used dismatch, expected: %d, have: %d", t.TxFee, status.BTMValue)
+               }
+       }
+       return nil
+}
+
+type ttTransaction struct {
+       wtTransaction
+       Describe string `json:"describe"`
+       Version  uint64 `json:"version"`
+       Valid    bool   `json:"valid"`
+       GasOnly  bool   `json:"gas_only"`
+       TxFee    uint64 `json:"tx_fee"`
+}
+
+// UnmarshalJSON unmarshal transaction with default version 1
+func (t *ttTransaction) UnmarshalJSON(data []byte) error {
+       type typeAlias ttTransaction
+       tx := &typeAlias{
+               Version: 1,
+       }
+
+       err := json.Unmarshal(data, tx)
+       if err != nil {
+               return err
+       }
+       *t = ttTransaction(*tx)
+       return nil
+}
+
+func (t *ttTransaction) create(g *TxGenerator) (*types.Tx, error) {
+       g.Reset()
+       for _, input := range t.Inputs {
+               switch input.Type {
+               case "spend_account":
+                       utxo, err := g.mockUtxo(input.AccountAlias, input.AssetAlias, input.Amount)
+                       if err != nil {
+                               return nil, err
+                       }
+                       if err := g.AddTxInputFromUtxo(utxo, input.AccountAlias); err != nil {
+                               return nil, err
+                       }
+               case "issue":
+                       _, err := g.createAsset(input.AccountAlias, input.AssetAlias)
+                       if err != nil {
+                               return nil, err
+                       }
+                       if err := g.AddIssuanceInput(input.AssetAlias, input.Amount); err != nil {
+                               return nil, err
+                       }
+               }
+       }
+
+       for _, output := range t.Outputs {
+               switch output.Type {
+               case "output":
+                       if err := g.AddTxOutput(output.AccountAlias, output.AssetAlias, output.Amount); err != nil {
+                               return nil, err
+                       }
+               case "retire":
+                       if err := g.AddRetirement(output.AssetAlias, output.Amount); err != nil {
+                               return nil, err
+                       }
+               }
+       }
+       return g.Sign(t.Passwords)
+}
+
+func TestTx(t *testing.T) {
+       walk(t, txTestDir, func(t *testing.T, name string, test *TxTestConfig) {
+               if err := test.Run(); err != nil {
+                       t.Fatal(err)
+               }
+       })
+}
+
+func TestCoinbaseMature(t *testing.T) {
+       db := dbm.NewDB("test_coinbase_mature_db", "leveldb", "test_coinbase_mature_db")
+       defer os.RemoveAll("test_coinbase_mature_db")
+       chain, _, _, _ := MockChain(db)
+
+       defaultCtrlProg := []byte{byte(vm.OP_TRUE)}
+       height := chain.BestBlockHeight()
+       block, err := chain.GetBlockByHeight(height)
+       if err != nil {
+               t.Fatal(err)
+       }
+
+       tx, err := CreateTxFromTx(block.Transactions[0], 0, 1000000000, defaultCtrlProg)
+       if err != nil {
+               t.Fatal(err)
+       }
+
+       txs := []*types.Tx{tx}
+       matureHeight := chain.BestBlockHeight() + consensus.CoinbasePendingBlockNumber
+       currentHeight := chain.BestBlockHeight()
+       for h := currentHeight + 1; h < matureHeight; h++ {
+               block, err := NewBlock(chain, txs, defaultCtrlProg)
+               if err != nil {
+                       t.Fatal(err)
+               }
+               if err := SolveAndUpdate(chain, block); err == nil {
+                       t.Fatal("spent immature coinbase output success")
+               }
+               block, err = NewBlock(chain, nil, defaultCtrlProg)
+               if err != nil {
+                       t.Fatal(err)
+               }
+               if err := SolveAndUpdate(chain, block); err != nil {
+                       t.Fatal(err)
+               }
+       }
+
+       block, err = NewBlock(chain, txs, defaultCtrlProg)
+       if err != nil {
+               t.Fatal(err)
+       }
+       if err := SolveAndUpdate(chain, block); err != nil {
+               t.Fatalf("spent mature coinbase output failed: %s", err)
+       }
+}
+
+func TestCoinbaseTx(t *testing.T) {
+       db := dbm.NewDB("test_coinbase_tx_db", "leveldb", "test_coinbase_tx_db")
+       defer os.RemoveAll("test_coinbase_tx_db")
+       chain, _, _, _ := MockChain(db)
+
+       defaultCtrlProg := []byte{byte(vm.OP_TRUE)}
+       if err := AppendBlocks(chain, 1); err != nil {
+               t.Fatal(err)
+       }
+
+       block, err := chain.GetBlockByHeight(chain.BestBlockHeight())
+       if err != nil {
+               t.Fatal(err)
+       }
+
+       tx, err := CreateTxFromTx(block.Transactions[0], 0, 1000000000, defaultCtrlProg)
+       if err != nil {
+               t.Fatal(err)
+       }
+
+       block, err = NewBlock(chain, []*types.Tx{tx}, defaultCtrlProg)
+       if err != nil {
+               t.Fatal(err)
+       }
+
+       coinbaseTx, err := CreateCoinbaseTx(defaultCtrlProg, block.Height, 100000)
+       if err != nil {
+               t.Fatal(err)
+       }
+
+       if err := ReplaceCoinbase(block, coinbaseTx); err != nil {
+               t.Fatal(err)
+       }
+
+       err = SolveAndUpdate(chain, block)
+       if err == nil {
+               t.Fatalf("invalid coinbase tx validate success")
+       }
+}
diff --git a/test/tx_test_util.go b/test/tx_test_util.go
new file mode 100644 (file)
index 0000000..4444a9d
--- /dev/null
@@ -0,0 +1,406 @@
+package test
+
+import (
+       "crypto/rand"
+       "encoding/json"
+       "fmt"
+       "time"
+
+       "github.com/tendermint/tmlibs/db"
+
+       "github.com/bytom/account"
+       "github.com/bytom/asset"
+       "github.com/bytom/blockchain/pseudohsm"
+       "github.com/bytom/blockchain/signers"
+       "github.com/bytom/blockchain/txbuilder"
+       "github.com/bytom/common"
+       "github.com/bytom/consensus"
+       "github.com/bytom/crypto/ed25519/chainkd"
+       "github.com/bytom/crypto/sha3pool"
+       "github.com/bytom/errors"
+       "github.com/bytom/protocol/bc"
+       "github.com/bytom/protocol/bc/types"
+       "github.com/bytom/protocol/vm"
+       "github.com/bytom/protocol/vm/vmutil"
+)
+
+// TxGenerator used to generate new tx
+type TxGenerator struct {
+       Builder        *txbuilder.TemplateBuilder
+       AccountManager *account.Manager
+       Assets         *asset.Registry
+       Hsm            *pseudohsm.HSM
+}
+
+// NewTxGenerator create a TxGenerator
+func NewTxGenerator(accountManager *account.Manager, assets *asset.Registry, hsm *pseudohsm.HSM) *TxGenerator {
+       return &TxGenerator{
+               Builder:        txbuilder.NewBuilder(time.Now()),
+               AccountManager: accountManager,
+               Assets:         assets,
+               Hsm:            hsm,
+       }
+}
+
+// Reset reset transaction builder, used to create a new tx
+func (g *TxGenerator) Reset() {
+       g.Builder = txbuilder.NewBuilder(time.Now())
+}
+
+func (g *TxGenerator) createKey(alias string, auth string) error {
+       _, err := g.Hsm.XCreate(alias, auth)
+       return err
+}
+
+func (g *TxGenerator) getPubkey(keyAlias string) *chainkd.XPub {
+       pubKeys := g.Hsm.ListKeys()
+       for i, key := range pubKeys {
+               if key.Alias == keyAlias {
+                       return &pubKeys[i].XPub
+               }
+       }
+       return nil
+}
+
+func (g *TxGenerator) createAccount(name string, keys []string, quorum int) error {
+       xpubs := []chainkd.XPub{}
+       for _, alias := range keys {
+               xpub := g.getPubkey(alias)
+               if xpub == nil {
+                       return fmt.Errorf("can't find pubkey for %s", alias)
+               }
+               xpubs = append(xpubs, *xpub)
+       }
+       _, err := g.AccountManager.Create(nil, xpubs, quorum, name, nil)
+       return err
+}
+
+func (g *TxGenerator) createAsset(accountAlias string, assetAlias string) (*asset.Asset, error) {
+       acc, err := g.AccountManager.FindByAlias(nil, accountAlias)
+       if err != nil {
+               return nil, err
+       }
+       return g.Assets.Define(acc.XPubs, len(acc.XPubs), nil, assetAlias, nil)
+}
+
+func (g *TxGenerator) mockUtxo(accountAlias, assetAlias string, amount uint64) (*account.UTXO, error) {
+       ctrlProg, err := g.createControlProgram(accountAlias, false)
+       if err != nil {
+               return nil, err
+       }
+
+       assetAmount, err := g.assetAmount(assetAlias, amount)
+       if err != nil {
+               return nil, err
+       }
+       utxo := &account.UTXO{
+               OutputID:            bc.Hash{V0: 1},
+               SourceID:            bc.Hash{V0: 1},
+               AssetID:             *assetAmount.AssetId,
+               Amount:              assetAmount.Amount,
+               SourcePos:           0,
+               ControlProgram:      ctrlProg.ControlProgram,
+               ControlProgramIndex: ctrlProg.KeyIndex,
+               AccountID:           ctrlProg.AccountID,
+               Address:             ctrlProg.Address,
+               ValidHeight:         0,
+       }
+       return utxo, nil
+}
+
+func (g *TxGenerator) assetAmount(assetAlias string, amount uint64) (*bc.AssetAmount, error) {
+       if assetAlias == "BTM" {
+               a := &bc.AssetAmount{
+                       Amount:  amount,
+                       AssetId: consensus.BTMAssetID,
+               }
+               return a, nil
+       }
+
+       asset, err := g.Assets.FindByAlias(nil, assetAlias)
+       if err != nil {
+               return nil, err
+       }
+       return &bc.AssetAmount{
+               Amount:  amount,
+               AssetId: &asset.AssetID,
+       }, nil
+}
+
+func (g *TxGenerator) createControlProgram(accountAlias string, change bool) (*account.CtrlProgram, error) {
+       acc, err := g.AccountManager.FindByAlias(nil, accountAlias)
+       if err != nil {
+               return nil, err
+       }
+       return g.AccountManager.CreateAddress(nil, acc.ID, change)
+}
+
+// AddSpendInput add a spend input
+func (g *TxGenerator) AddSpendInput(accountAlias, assetAlias string, amount uint64) error {
+       assetAmount, err := g.assetAmount(assetAlias, amount)
+       if err != nil {
+               return err
+       }
+
+       acc, err := g.AccountManager.FindByAlias(nil, accountAlias)
+       if err != nil {
+               return err
+       }
+
+       reqAction := make(map[string]interface{})
+       reqAction["account_id"] = acc.ID
+       reqAction["amount"] = amount
+       reqAction["asset_id"] = assetAmount.AssetId.String()
+       data, err := json.Marshal(reqAction)
+       if err != nil {
+               return err
+       }
+
+       spendAction, err := g.AccountManager.DecodeSpendAction(data)
+       if err != nil {
+               return err
+       }
+       return spendAction.Build(nil, g.Builder)
+}
+
+// AddTxInput add a tx input and signing instruction
+func (g *TxGenerator) AddTxInput(txInput *types.TxInput, signInstruction *txbuilder.SigningInstruction) error {
+       return g.Builder.AddInput(txInput, signInstruction)
+}
+
+// AddTxInputFromUtxo add a tx input which spent the utxo
+func (g *TxGenerator) AddTxInputFromUtxo(utxo *account.UTXO, accountAlias string) error {
+       acc, err := g.AccountManager.FindByAlias(nil, accountAlias)
+       if err != nil {
+               return err
+       }
+
+       txInput, signInst, err := account.UtxoToInputs(acc.Signer, utxo)
+       if err != nil {
+               return err
+       }
+       return g.AddTxInput(txInput, signInst)
+}
+
+// AddIssuanceInput add a issue input
+func (g *TxGenerator) AddIssuanceInput(assetAlias string, amount uint64) error {
+       asset, err := g.Assets.FindByAlias(nil, assetAlias)
+       if err != nil {
+               return err
+       }
+
+       var nonce [8]byte
+       _, err = rand.Read(nonce[:])
+       if err != nil {
+               return err
+       }
+       issuanceInput := types.NewIssuanceInput(nonce[:], amount, asset.IssuanceProgram, nil, asset.RawDefinitionByte)
+       signInstruction := &txbuilder.SigningInstruction{}
+       path := signers.Path(asset.Signer, signers.AssetKeySpace)
+       signInstruction.AddRawWitnessKeys(asset.Signer.XPubs, path, asset.Signer.Quorum)
+       g.Builder.RestrictMinTime(time.Now())
+       return g.Builder.AddInput(issuanceInput, signInstruction)
+}
+
+// AddTxOutput add a tx output
+func (g *TxGenerator) AddTxOutput(accountAlias, assetAlias string, amount uint64) error {
+       assetAmount, err := g.assetAmount(assetAlias, uint64(amount))
+       if err != nil {
+               return err
+       }
+       controlProgram, err := g.createControlProgram(accountAlias, false)
+       if err != nil {
+               return err
+       }
+       out := types.NewTxOutput(*assetAmount.AssetId, assetAmount.Amount, controlProgram.ControlProgram)
+       return g.Builder.AddOutput(out)
+}
+
+// AddRetirement add a retirement output
+func (g *TxGenerator) AddRetirement(assetAlias string, amount uint64) error {
+       assetAmount, err := g.assetAmount(assetAlias, uint64(amount))
+       if err != nil {
+               return err
+       }
+       retirementProgram := []byte{byte(vm.OP_FAIL)}
+       out := types.NewTxOutput(*assetAmount.AssetId, assetAmount.Amount, retirementProgram)
+       return g.Builder.AddOutput(out)
+}
+
+// Sign used to sign tx
+func (g *TxGenerator) Sign(passwords []string) (*types.Tx, error) {
+       tpl, _, err := g.Builder.Build()
+       if err != nil {
+               return nil, err
+       }
+
+       txSerialized, err := tpl.Transaction.MarshalText()
+       if err != nil {
+               return nil, err
+       }
+
+       tpl.Transaction.Tx.SerializedSize = uint64(len(txSerialized))
+       tpl.Transaction.TxData.SerializedSize = uint64(len(txSerialized))
+       for _, password := range passwords {
+               _, err = MockSign(tpl, g.Hsm, password)
+               if err != nil {
+                       return nil, err
+               }
+       }
+       return tpl.Transaction, nil
+}
+
+func txFee(tx *types.Tx) uint64 {
+       if len(tx.Inputs) == 1 && tx.Inputs[0].InputType() == types.CoinbaseInputType {
+               return 0
+       }
+
+       inputSum := uint64(0)
+       outputSum := uint64(0)
+       for _, input := range tx.Inputs {
+               if input.AssetID() == *consensus.BTMAssetID {
+                       inputSum += input.Amount()
+               }
+       }
+
+       for _, output := range tx.Outputs {
+               if *output.AssetId == *consensus.BTMAssetID {
+                       outputSum += output.Amount
+               }
+       }
+       return inputSum - outputSum
+}
+
+// CreateSpendInput create SpendInput which spent the output from tx
+func CreateSpendInput(tx *types.Tx, outputIndex uint64) (*types.SpendInput, error) {
+       outputID := tx.ResultIds[outputIndex]
+       output, ok := tx.Entries[*outputID].(*bc.Output)
+       if !ok {
+               return nil, fmt.Errorf("retirement can't be spent")
+       }
+
+       sc := types.SpendCommitment{
+               AssetAmount:    *output.Source.Value,
+               SourceID:       *output.Source.Ref,
+               SourcePosition: output.Ordinal,
+               VMVersion:      vmVersion,
+               ControlProgram: output.ControlProgram.Code,
+       }
+       return &types.SpendInput{
+               SpendCommitment: sc,
+       }, nil
+}
+
+// SignInstructionFor read CtrlProgram from db, construct SignInstruction for SpendInput
+func SignInstructionFor(input *types.SpendInput, db db.DB, signer *signers.Signer) (*txbuilder.SigningInstruction, error) {
+       cp := account.CtrlProgram{}
+       var hash [32]byte
+       sha3pool.Sum256(hash[:], input.ControlProgram)
+       bytes := db.Get(account.CPKey(hash))
+       if bytes == nil {
+               return nil, fmt.Errorf("can't find CtrlProgram for the SpendInput")
+       }
+
+       err := json.Unmarshal(bytes, &cp)
+       if err != nil {
+               return nil, err
+       }
+
+       sigInst := &txbuilder.SigningInstruction{}
+       if signer == nil {
+               return sigInst, nil
+       }
+
+       // FIXME: code duplicate with account/builder.go
+       path := signers.Path(signer, signers.AccountKeySpace, cp.KeyIndex)
+       if cp.Address == "" {
+               sigInst.AddWitnessKeys(signer.XPubs, path, signer.Quorum)
+               return sigInst, nil
+       }
+
+       address, err := common.DecodeAddress(cp.Address, &consensus.MainNetParams)
+       if err != nil {
+               return nil, err
+       }
+
+       switch address.(type) {
+       case *common.AddressWitnessPubKeyHash:
+               sigInst.AddRawWitnessKeys(signer.XPubs, path, signer.Quorum)
+               derivedXPubs := chainkd.DeriveXPubs(signer.XPubs, path)
+               derivedPK := derivedXPubs[0].PublicKey()
+               sigInst.WitnessComponents = append(sigInst.WitnessComponents, txbuilder.DataWitness([]byte(derivedPK)))
+
+       case *common.AddressWitnessScriptHash:
+               sigInst.AddRawWitnessKeys(signer.XPubs, path, signer.Quorum)
+               path := signers.Path(signer, signers.AccountKeySpace, cp.KeyIndex)
+               derivedXPubs := chainkd.DeriveXPubs(signer.XPubs, path)
+               derivedPKs := chainkd.XPubKeys(derivedXPubs)
+               script, err := vmutil.P2SPMultiSigProgram(derivedPKs, signer.Quorum)
+               if err != nil {
+                       return nil, err
+               }
+               sigInst.WitnessComponents = append(sigInst.WitnessComponents, txbuilder.DataWitness(script))
+
+       default:
+               return nil, errors.New("unsupport address type")
+       }
+
+       return sigInst, nil
+}
+
+// CreateCoinbaseTx create coinbase tx at block height
+func CreateCoinbaseTx(controlProgram []byte, height, txsFee uint64) (*types.Tx, error) {
+       coinbaseValue := consensus.BlockSubsidy(height) + txsFee
+       builder := txbuilder.NewBuilder(time.Now())
+       if err := builder.AddInput(types.NewCoinbaseInput([]byte(string(height))), &txbuilder.SigningInstruction{}); err != nil {
+               return nil, err
+       }
+       if err := builder.AddOutput(types.NewTxOutput(*consensus.BTMAssetID, coinbaseValue, controlProgram)); err != nil {
+               return nil, err
+       }
+
+       tpl, _, err := builder.Build()
+       if err != nil {
+               return nil, err
+       }
+
+       txSerialized, err := tpl.Transaction.MarshalText()
+       if err != nil {
+               return nil, err
+       }
+
+       tpl.Transaction.Tx.SerializedSize = uint64(len(txSerialized))
+       tpl.Transaction.TxData.SerializedSize = uint64(len(txSerialized))
+       return tpl.Transaction, nil
+}
+
+// CreateTxFromTx create a tx spent the output in outputIndex at baseTx
+func CreateTxFromTx(baseTx *types.Tx, outputIndex uint64, outputAmount uint64, ctrlProgram []byte) (*types.Tx, error) {
+       spendInput, err := CreateSpendInput(baseTx, outputIndex)
+       if err != nil {
+               return nil, err
+       }
+
+       txInput := &types.TxInput{
+               AssetVersion: assetVersion,
+               TypedInput:   spendInput,
+       }
+       output := types.NewTxOutput(*consensus.BTMAssetID, outputAmount, ctrlProgram)
+       builder := txbuilder.NewBuilder(time.Now())
+       builder.AddInput(txInput, &txbuilder.SigningInstruction{})
+       builder.AddOutput(output)
+
+       tpl, _, err := builder.Build()
+       if err != nil {
+               return nil, err
+       }
+
+       txSerialized, err := tpl.Transaction.MarshalText()
+       if err != nil {
+               return nil, err
+       }
+
+       tpl.Transaction.Tx.SerializedSize = uint64(len(txSerialized))
+       tpl.Transaction.TxData.SerializedSize = uint64(len(txSerialized))
+       return tpl.Transaction, nil
+}
index 5191c7e..d2036ba 100644 (file)
@@ -18,21 +18,27 @@ import (
        "github.com/bytom/protocol/vm"
 )
 
-// Mock transaction pool
+const (
+       vmVersion    = 1
+       blockVersion = 1
+       assetVersion = 1
+       maxNonce     = ^uint64(0)
+)
+
+// MockTxPool mock transaction pool
 func MockTxPool() *protocol.TxPool {
        return protocol.NewTxPool()
 }
 
-func MockChain(testDB dbm.DB) (*protocol.Chain, error) {
+// MockChain mock chain with genesis block
+func MockChain(testDB dbm.DB) (*protocol.Chain, *leveldb.Store, *protocol.TxPool, error) {
        store := leveldb.NewStore(testDB)
        txPool := MockTxPool()
        chain, err := protocol.NewChain(store, txPool)
-       if err != nil {
-               return nil, err
-       }
-       return chain, nil
+       return chain, store, txPool, err
 }
 
+// MockUTXO mock a utxo
 func MockUTXO(controlProg *account.CtrlProgram) *account.UTXO {
        utxo := &account.UTXO{}
        utxo.OutputID = bc.Hash{V0: 1}
@@ -47,6 +53,7 @@ func MockUTXO(controlProg *account.CtrlProgram) *account.UTXO {
        return utxo
 }
 
+// MockTx mock a tx
 func MockTx(utxo *account.UTXO, testAccount *account.Account) (*txbuilder.Template, *types.TxData, error) {
        txInput, sigInst, err := account.UtxoToInputs(testAccount.Signer, utxo)
        if err != nil {
@@ -60,6 +67,7 @@ func MockTx(utxo *account.UTXO, testAccount *account.Account) (*txbuilder.Templa
        return b.Build()
 }
 
+// MockSign sign a tx
 func MockSign(tpl *txbuilder.Template, hsm *pseudohsm.HSM, password string) (bool, error) {
        err := txbuilder.Sign(nil, tpl, nil, password, func(_ context.Context, xpub chainkd.XPub, path [][]byte, data [32]byte, password string) ([]byte, error) {
                return hsm.XSign(xpub, path, data[:], password)
@@ -70,7 +78,7 @@ func MockSign(tpl *txbuilder.Template, hsm *pseudohsm.HSM, password string) (boo
        return txbuilder.SignProgress(tpl), nil
 }
 
-// Mock block
+// MockBlock mock a block
 func MockBlock() *bc.Block {
        return &bc.Block{
                BlockHeader: &bc.BlockHeader{Height: 1},
diff --git a/test/wallet_test.go b/test/wallet_test.go
new file mode 100644 (file)
index 0000000..2dbb3ce
--- /dev/null
@@ -0,0 +1,15 @@
+// +build functional
+
+package test
+
+import (
+       "testing"
+)
+
+func TestWallet(t *testing.T) {
+       walk(t, walletTestDir, func(t *testing.T, name string, test *walletTestConfig) {
+               if err := test.Run(); err != nil {
+                       t.Fatal(err)
+               }
+       })
+}
diff --git a/test/wallet_test_util.go b/test/wallet_test_util.go
new file mode 100644 (file)
index 0000000..910dc50
--- /dev/null
@@ -0,0 +1,328 @@
+package test
+
+import (
+       "fmt"
+       "io/ioutil"
+       "os"
+       "reflect"
+
+       dbm "github.com/tendermint/tmlibs/db"
+
+       "github.com/bytom/account"
+       "github.com/bytom/asset"
+       "github.com/bytom/blockchain/pseudohsm"
+       "github.com/bytom/crypto/ed25519/chainkd"
+       "github.com/bytom/protocol"
+       "github.com/bytom/protocol/bc/types"
+       w "github.com/bytom/wallet"
+)
+
+type walletTestConfig struct {
+       Keys       []*keyInfo     `json:"keys"`
+       Accounts   []*accountInfo `json:"accounts"`
+       Blocks     []*wtBlock     `json:"blocks"`
+       RollbackTo uint64         `json:"rollback_to"`
+}
+
+type keyInfo struct {
+       Name     string `json:"name"`
+       Password string `json:"password"`
+}
+
+type accountInfo struct {
+       Name   string   `json:"name"`
+       Keys   []string `json:"keys"`
+       Quorum int      `json:"quorum"`
+}
+
+type wtBlock struct {
+       CoinbaseAccount string            `json:"coinbase_account"`
+       Transactions    []*wtTransaction  `json:"transactions"`
+       PostStates      []*accountBalance `json:"post_states"`
+       Append          uint64            `json:"append"`
+}
+
+func (b *wtBlock) create(ctx *walletTestContext) (*types.Block, error) {
+       transactions := make([]*types.Tx, 0, len(b.Transactions))
+       for _, t := range b.Transactions {
+               tx, err := t.create(ctx)
+               if err != nil {
+                       continue
+               }
+               transactions = append(transactions, tx)
+       }
+       return ctx.newBlock(transactions, b.CoinbaseAccount)
+}
+
+func (b *wtBlock) verifyPostStates(ctx *walletTestContext) error {
+       for _, state := range b.PostStates {
+               balance, err := ctx.getBalance(state.AccountAlias, state.AssetAlias)
+               if err != nil {
+                       return err
+               }
+
+               if balance != state.Amount {
+                       return fmt.Errorf("AccountAlias: %s, AssetAlias: %s, expected: %d, have: %d", state.AccountAlias, state.AssetAlias, state.Amount, balance)
+               }
+       }
+       return nil
+}
+
+type wtTransaction struct {
+       Passwords []string  `json:"passwords"`
+       Inputs    []*action `json:"inputs"`
+       Outputs   []*action `json:"outputs"`
+}
+
+// create signed transaction
+func (t *wtTransaction) create(ctx *walletTestContext) (*types.Tx, error) {
+       generator := NewTxGenerator(ctx.Wallet.AccountMgr, ctx.Wallet.AssetReg, ctx.Wallet.Hsm)
+       for _, input := range t.Inputs {
+               switch input.Type {
+               case "spend_account":
+                       if err := generator.AddSpendInput(input.AccountAlias, input.AssetAlias, input.Amount); err != nil {
+                               return nil, err
+                       }
+               case "issue":
+                       _, err := ctx.createAsset(input.AccountAlias, input.AssetAlias)
+                       if err != nil {
+                               return nil, err
+                       }
+                       if err := generator.AddIssuanceInput(input.AssetAlias, input.Amount); err != nil {
+                               return nil, err
+                       }
+               }
+       }
+
+       for _, output := range t.Outputs {
+               switch output.Type {
+               case "output":
+                       if err := generator.AddTxOutput(output.AccountAlias, output.AssetAlias, output.Amount); err != nil {
+                               return nil, err
+                       }
+               case "retire":
+                       if err := generator.AddRetirement(output.AssetAlias, output.Amount); err != nil {
+                               return nil, err
+                       }
+               }
+       }
+       return generator.Sign(t.Passwords)
+}
+
+type action struct {
+       Type         string `json:"type"`
+       AccountAlias string `json:"name"`
+       AssetAlias   string `json:"asset"`
+       Amount       uint64 `json:"amount"`
+}
+
+type accountBalance struct {
+       AssetAlias   string `json:"asset"`
+       AccountAlias string `json:"name"`
+       Amount       uint64 `json:"amount"`
+}
+
+type walletTestContext struct {
+       Wallet *w.Wallet
+       Chain  *protocol.Chain
+}
+
+func (ctx *walletTestContext) createControlProgram(accountName string, change bool) (*account.CtrlProgram, error) {
+       acc, err := ctx.Wallet.AccountMgr.FindByAlias(nil, accountName)
+       if err != nil {
+               return nil, err
+       }
+
+       return ctx.Wallet.AccountMgr.CreateAddress(nil, acc.ID, change)
+}
+
+func (ctx *walletTestContext) getPubkey(keyAlias string) *chainkd.XPub {
+       pubKeys := ctx.Wallet.Hsm.ListKeys()
+       for i, key := range pubKeys {
+               if key.Alias == keyAlias {
+                       return &pubKeys[i].XPub
+               }
+       }
+       return nil
+}
+
+func (ctx *walletTestContext) createAsset(accountAlias string, assetAlias string) (*asset.Asset, error) {
+       acc, err := ctx.Wallet.AccountMgr.FindByAlias(nil, accountAlias)
+       if err != nil {
+               return nil, err
+       }
+       return ctx.Wallet.AssetReg.Define(acc.XPubs, len(acc.XPubs), nil, assetAlias, nil)
+}
+
+func (ctx *walletTestContext) newBlock(txs []*types.Tx, coinbaseAccount string) (*types.Block, error) {
+       controlProgram, err := ctx.createControlProgram(coinbaseAccount, true)
+       if err != nil {
+               return nil, err
+       }
+       return NewBlock(ctx.Chain, txs, controlProgram.ControlProgram)
+}
+
+func (ctx *walletTestContext) createKey(name string, password string) error {
+       _, err := ctx.Wallet.Hsm.XCreate(name, password)
+       return err
+}
+
+func (ctx *walletTestContext) createAccount(name string, keys []string, quorum int) error {
+       xpubs := []chainkd.XPub{}
+       for _, alias := range keys {
+               xpub := ctx.getPubkey(alias)
+               if xpub == nil {
+                       return fmt.Errorf("can't find pubkey for %s", alias)
+               }
+               xpubs = append(xpubs, *xpub)
+       }
+       _, err := ctx.Wallet.AccountMgr.Create(nil, xpubs, quorum, name, nil)
+       return err
+}
+
+func (ctx *walletTestContext) update(block *types.Block) error {
+       if err := SolveAndUpdate(ctx.Chain, block); err != nil {
+               return err
+       }
+       if err := ctx.Wallet.AttachBlock(block); err != nil {
+               return err
+       }
+       return nil
+}
+
+func (ctx *walletTestContext) getBalance(accountAlias string, assetAlias string) (uint64, error) {
+       balances := ctx.Wallet.GetAccountBalances("")
+       for _, balance := range balances {
+               if balance.Alias == accountAlias && balance.AssetAlias == assetAlias {
+                       return balance.Amount, nil
+               }
+       }
+       return 0, nil
+}
+
+func (ctx *walletTestContext) getAccBalances() map[string]map[string]uint64 {
+       accBalances := make(map[string]map[string]uint64)
+       balances := ctx.Wallet.GetAccountBalances("")
+       for _, balance := range balances {
+               if accBalance, ok := accBalances[balance.Alias]; ok {
+                       if _, ok := accBalance[balance.AssetAlias]; ok {
+                               accBalance[balance.AssetAlias] += balance.Amount
+                               continue
+                       }
+                       accBalance[balance.AssetAlias] = balance.Amount
+                       continue
+               }
+               accBalances[balance.Alias] = map[string]uint64{balance.AssetAlias: balance.Amount}
+       }
+       return accBalances
+}
+
+func (ctx *walletTestContext) getDetachedBlocks(height uint64) ([]*types.Block, error) {
+       currentHeight := ctx.Chain.BestBlockHeight()
+       detachedBlocks := make([]*types.Block, 0, currentHeight-height)
+       for i := currentHeight; i > height; i-- {
+               block, err := ctx.Chain.GetBlockByHeight(i)
+               if err != nil {
+                       return detachedBlocks, err
+               }
+               detachedBlocks = append(detachedBlocks, block)
+       }
+       return detachedBlocks, nil
+}
+
+func (ctx *walletTestContext) validateRollback(oldAccBalances map[string]map[string]uint64) error {
+       accBalances := ctx.getAccBalances()
+       if reflect.DeepEqual(oldAccBalances, accBalances) {
+               return nil
+       }
+       return fmt.Errorf("different account balances after rollback")
+}
+
+func (cfg *walletTestConfig) Run() error {
+       dirPath, err := ioutil.TempDir(".", "pseudo_hsm")
+       if err != nil {
+               return err
+       }
+       defer os.RemoveAll(dirPath)
+       hsm, err := pseudohsm.New(dirPath)
+       if err != nil {
+               return err
+       }
+
+       db := dbm.NewDB("wallet_test_db", "leveldb", "wallet_test_db")
+       defer os.RemoveAll("wallet_test_db")
+       chain, _, _, _ := MockChain(db)
+       walletDB := dbm.NewDB("wallet", "leveldb", "wallet_db")
+       defer os.RemoveAll("wallet_db")
+       accountManager := account.NewManager(walletDB, chain)
+       assets := asset.NewRegistry(walletDB, chain)
+       wallet, err := w.NewWallet(walletDB, accountManager, assets, hsm, chain)
+       if err != nil {
+               return err
+       }
+       ctx := &walletTestContext{
+               Wallet: wallet,
+               Chain:  chain,
+       }
+
+       for _, key := range cfg.Keys {
+               if err := ctx.createKey(key.Name, key.Password); err != nil {
+                       return err
+               }
+       }
+
+       for _, acc := range cfg.Accounts {
+               if err := ctx.createAccount(acc.Name, acc.Keys, acc.Quorum); err != nil {
+                       return err
+               }
+       }
+
+       var accBalances map[string]map[string]uint64
+       var rollbackBlock *types.Block
+       for _, blk := range cfg.Blocks {
+               block, err := blk.create(ctx)
+               if err != nil {
+                       return err
+               }
+               if err := ctx.update(block); err != nil {
+                       return err
+               }
+               if err := blk.verifyPostStates(ctx); err != nil {
+                       return err
+               }
+               if block.Height <= cfg.RollbackTo && cfg.RollbackTo <= block.Height+blk.Append {
+                       accBalances = ctx.getAccBalances()
+                       rollbackBlock = block
+               }
+               if err := AppendBlocks(ctx.Chain, blk.Append); err != nil {
+                       return err
+               }
+       }
+
+       if rollbackBlock == nil {
+               return nil
+       }
+
+       // rollback and validate
+       detachedBlocks, err := ctx.getDetachedBlocks(rollbackBlock.Height)
+       if err != nil {
+               return err
+       }
+
+       forkedChain, err := declChain("forked_chain", ctx.Chain, rollbackBlock.Height, ctx.Chain.BestBlockHeight()+1)
+       defer os.RemoveAll("forked_chain")
+       if err != nil {
+               return err
+       }
+
+       if err := merge(forkedChain, ctx.Chain); err != nil {
+               return err
+       }
+
+       for _, block := range detachedBlocks {
+               if err := ctx.Wallet.DetachBlock(block); err != nil {
+                       return err
+               }
+       }
+       return ctx.validateRollback(accBalances)
+}
index e4c0515..fce7f9d 100644 (file)
@@ -94,7 +94,7 @@ func (w *Wallet) loadWalletInfo() error {
        if err != nil {
                return err
        }
-       return w.attachBlock(block)
+       return w.AttachBlock(block)
 }
 
 func (w *Wallet) commitWalletInfo(batch db.Batch) error {
@@ -138,7 +138,8 @@ func (w *Wallet) getImportKeyFlag() bool {
        return false
 }
 
-func (w *Wallet) attachBlock(block *types.Block) error {
+// AttachBlock attach a new block
+func (w *Wallet) AttachBlock(block *types.Block) error {
        if block.PreviousBlockHash != w.status.WorkHash {
                log.Warn("wallet skip attachBlock due to status hash not equal to previous hash")
                return nil
@@ -163,7 +164,8 @@ func (w *Wallet) attachBlock(block *types.Block) error {
        return w.commitWalletInfo(storeBatch)
 }
 
-func (w *Wallet) detachBlock(block *types.Block) error {
+// DetachBlock detach a block and rollback state
+func (w *Wallet) DetachBlock(block *types.Block) error {
        blockHash := block.Hash()
        txStatus, err := w.chain.GetTransactionStatus(&blockHash)
        if err != nil {
@@ -197,7 +199,7 @@ func (w *Wallet) walletUpdater() {
                                return
                        }
 
-                       if err := w.detachBlock(block); err != nil {
+                       if err := w.DetachBlock(block); err != nil {
                                log.WithField("err", err).Error("walletUpdater detachBlock")
                                return
                        }
@@ -209,7 +211,7 @@ func (w *Wallet) walletUpdater() {
                        continue
                }
 
-               if err := w.attachBlock(block); err != nil {
+               if err := w.AttachBlock(block); err != nil {
                        log.WithField("err", err).Error("walletUpdater stop")
                        return
                }
index 13bf241..8a6d218 100644 (file)
@@ -81,7 +81,7 @@ func TestWalletUpdate(t *testing.T) {
        txStatus := bc.NewTransactionStatus()
        store.SaveBlock(block, txStatus)
 
-       err = w.attachBlock(block)
+       err = w.AttachBlock(block)
        if err != nil {
                t.Fatal(err)
        }