From: WangYifu Date: Thu, 12 Apr 2018 01:54:39 +0000 (-0500) Subject: Functional tests (#543) X-Git-Tag: v1.0.5~157 X-Git-Url: http://git.osdn.net/view?a=commitdiff_plain;h=9203b8f246d1abd5a825e700d015443594c90529;p=bytom%2Fbytom.git Functional tests (#543) * 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 --- diff --git a/Makefile b/Makefile index 7571c958..7e20043e 100644 --- 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 diff --git a/protocol/validation/tx_test.go b/protocol/validation/tx_test.go index dd2efb57..2c780ffe 100644 --- a/protocol/validation/tx_test.go +++ b/protocol/validation/tx_test.go @@ -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 index 00000000..6d1d31cf --- /dev/null +++ b/test/block_test.go @@ -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 index 00000000..0c839a8c --- /dev/null +++ b/test/block_test_util.go @@ -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 index 00000000..052c3575 --- /dev/null +++ b/test/chain_test.go @@ -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 index 00000000..f9eed1fe --- /dev/null +++ b/test/chain_test_util.go @@ -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 index 00000000..e6e0c73c --- /dev/null +++ b/test/init_test.go @@ -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 ), +// 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, ), 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, + }) +} diff --git a/test/integration/standard_transaction_test.go b/test/integration/standard_transaction_test.go index 707a3513..25e195f3 100644 --- a/test/integration/standard_transaction_test.go +++ b/test/integration/standard_transaction_test.go @@ -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) } diff --git a/test/performance/mining_test.go b/test/performance/mining_test.go index b58335af..5aee53fa 100644 --- a/test/performance/mining_test.go +++ b/test/performance/mining_test.go @@ -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 index 00000000..de509b87 --- /dev/null +++ b/test/protocol_test.go @@ -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 index 00000000..43c34535 --- /dev/null +++ b/test/protocol_test_util.go @@ -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 index 00000000..d756e858 --- /dev/null +++ b/test/testdata/chain_tests/ct_dependency_tx.json @@ -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 index 00000000..4bcc29e4 --- /dev/null +++ b/test/testdata/chain_tests/ct_double_spend.json @@ -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 index 00000000..282bc259 --- /dev/null +++ b/test/testdata/chain_tests/ct_normal.json @@ -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 index 00000000..cb775527 --- /dev/null +++ b/test/testdata/chain_tests/ct_rollback.json @@ -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 index 00000000..5417ce2b --- /dev/null +++ b/test/testdata/tx_tests/tx_tests.json @@ -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 index 00000000..5024a8b3 --- /dev/null +++ b/test/testdata/wallet_tests/wt_asset.json @@ -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 index 00000000..12994451 --- /dev/null +++ b/test/testdata/wallet_tests/wt_btm.json @@ -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 index 00000000..3d35b911 --- /dev/null +++ b/test/testdata/wallet_tests/wt_invalid_txs.json @@ -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 index 00000000..6b8a18d7 --- /dev/null +++ b/test/testdata/wallet_tests/wt_multi_sig_asset.json @@ -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 index 00000000..4eb5b7bc --- /dev/null +++ b/test/testdata/wallet_tests/wt_retire_asset.json @@ -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 index 00000000..2f72d345 --- /dev/null +++ b/test/testdata/wallet_tests/wt_rollback.json @@ -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 index 00000000..b5761a45 --- /dev/null +++ b/test/tx_test.go @@ -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 index 00000000..4444a9d1 --- /dev/null +++ b/test/tx_test_util.go @@ -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 +} diff --git a/test/util.go b/test/util.go index 5191c7e2..d2036bac 100644 --- a/test/util.go +++ b/test/util.go @@ -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 index 00000000..2dbb3ced --- /dev/null +++ b/test/wallet_test.go @@ -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 index 00000000..910dc505 --- /dev/null +++ b/test/wallet_test_util.go @@ -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) +} diff --git a/wallet/wallet.go b/wallet/wallet.go index e4c05159..fce7f9d3 100644 --- a/wallet/wallet.go +++ b/wallet/wallet.go @@ -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 } diff --git a/wallet/wallet_test.go b/wallet/wallet_test.go index 13bf2419..8a6d218e 100644 --- a/wallet/wallet_test.go +++ b/wallet/wallet_test.go @@ -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) }