benchmark:
go test -bench $(PACKAGES)
+functional-tests:
+ @go test -v -timeout=30m -tags=functional ./test
+
.PHONY: all target release-all clean test benchmark
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)
}
}
}
--- /dev/null
+// +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")
+ }
+}
--- /dev/null
+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
+}
--- /dev/null
+// +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)
+ }
+ })
+}
--- /dev/null
+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
+}
--- /dev/null
+package test
+
+import (
+ "encoding/json"
+ "fmt"
+ "io"
+ "io/ioutil"
+ "os"
+ "path/filepath"
+ "reflect"
+ "strings"
+ "testing"
+)
+
+var (
+ baseDir = filepath.Join(".", "testdata")
+ walletTestDir = filepath.Join(baseDir, "wallet_tests")
+ chainTestDir = filepath.Join(baseDir, "chain_tests")
+ txTestDir = filepath.Join(baseDir, "tx_tests")
+)
+
+func readJSON(reader io.Reader, value interface{}) error {
+ data, err := ioutil.ReadAll(reader)
+ if err != nil {
+ return fmt.Errorf("error reading JSON file: %v", err)
+ }
+ if err = json.Unmarshal(data, &value); err != nil {
+ if syntaxerr, ok := err.(*json.SyntaxError); ok {
+ line := findLine(data, syntaxerr.Offset)
+ return fmt.Errorf("JSON syntax error at line %v: %v", line, err)
+ }
+ return err
+ }
+ return nil
+}
+
+func readJSONFile(fn string, value interface{}) error {
+ file, err := os.Open(fn)
+ if err != nil {
+ return err
+ }
+ defer file.Close()
+
+ if err := readJSON(file, value); err != nil {
+ return fmt.Errorf("%s in file %s", err.Error(), fn)
+ }
+ return nil
+}
+
+// findLine returns the line number for the given offset into data.
+func findLine(data []byte, offset int64) (line int) {
+ line = 1
+ for i, r := range string(data) {
+ if int64(i) >= offset {
+ return
+ }
+ if r == '\n' {
+ line++
+ }
+ }
+ return
+}
+
+// walk invokes its runTest argument for all subtests in the given directory.
+//
+// runTest should be a function of type func(t *testing.T, name string, x <TestType>),
+// where TestType is the type of the test contained in test files.
+func walk(t *testing.T, dir string, runTest interface{}) {
+ // Walk the directory.
+ dirinfo, err := os.Stat(dir)
+ if os.IsNotExist(err) || !dirinfo.IsDir() {
+ fmt.Fprintf(os.Stderr, "can't find test files in %s\n", dir)
+ t.Skip("missing test files")
+ }
+ err = filepath.Walk(dir, func(path string, info os.FileInfo, err error) error {
+ name := filepath.ToSlash(strings.TrimPrefix(path, dir+string(filepath.Separator)))
+ if info.IsDir() {
+ return nil
+ }
+ if filepath.Ext(path) == ".json" {
+ t.Run(name, func(t *testing.T) { runTestFile(t, path, name, runTest) })
+ }
+ return nil
+ })
+ if err != nil {
+ t.Fatal(err)
+ }
+}
+
+func runTestFile(t *testing.T, path, name string, runTest interface{}) {
+ m := makeMapFromTestFunc(runTest)
+ if err := readJSONFile(path, m.Addr().Interface()); err != nil {
+ t.Fatal(err)
+ }
+ runTestFunc(runTest, t, name, m)
+}
+
+func makeMapFromTestFunc(f interface{}) reflect.Value {
+ stringT := reflect.TypeOf("")
+ testingT := reflect.TypeOf((*testing.T)(nil))
+ ftyp := reflect.TypeOf(f)
+ if ftyp.Kind() != reflect.Func || ftyp.NumIn() != 3 || ftyp.NumOut() != 0 || ftyp.In(0) != testingT || ftyp.In(1) != stringT {
+ panic(fmt.Sprintf("bad test function type: want func(*testing.T, string, <TestType>), have %s", ftyp))
+ }
+ testType := ftyp.In(2)
+ mp := reflect.New(testType)
+ return mp.Elem()
+}
+
+func runTestFunc(runTest interface{}, t *testing.T, name string, m reflect.Value) {
+ reflect.ValueOf(runTest).Call([]reflect.Value{
+ reflect.ValueOf(t),
+ reflect.ValueOf(name),
+ m,
+ })
+}
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)
}
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)
}
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)
}
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)
}
--- /dev/null
+// +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")
+ }
+}
--- /dev/null
+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)
+}
--- /dev/null
+{
+ "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
--- /dev/null
+{
+ "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
+ }
+ ]
+}
--- /dev/null
+{
+ "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
--- /dev/null
+{
+ "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
--- /dev/null
+{
+ "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
--- /dev/null
+{
+ "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
+ }
+ ]
+}
--- /dev/null
+{
+ "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
+ }
+ ]
+}
--- /dev/null
+{
+ "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
+ }
+ ]
+}
--- /dev/null
+{
+ "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
+ }
+ ]
+}
--- /dev/null
+{
+ "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
+ }
+ ]
+}
--- /dev/null
+{
+ "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
+ }
+ ]
+}
--- /dev/null
+// +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")
+ }
+}
--- /dev/null
+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
+}
"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}
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 {
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)
return txbuilder.SignProgress(tpl), nil
}
-// Mock block
+// MockBlock mock a block
func MockBlock() *bc.Block {
return &bc.Block{
BlockHeader: &bc.BlockHeader{Height: 1},
--- /dev/null
+// +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)
+ }
+ })
+}
--- /dev/null
+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)
+}
if err != nil {
return err
}
- return w.attachBlock(block)
+ return w.AttachBlock(block)
}
func (w *Wallet) commitWalletInfo(batch db.Batch) error {
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
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 {
return
}
- if err := w.detachBlock(block); err != nil {
+ if err := w.DetachBlock(block); err != nil {
log.WithField("err", err).Error("walletUpdater detachBlock")
return
}
continue
}
- if err := w.attachBlock(block); err != nil {
+ if err := w.AttachBlock(block); err != nil {
log.WithField("err", err).Error("walletUpdater stop")
return
}
txStatus := bc.NewTransactionStatus()
store.SaveBlock(block, txStatus)
- err = w.attachBlock(block)
+ err = w.AttachBlock(block)
if err != nil {
t.Fatal(err)
}