8 dbm "github.com/tendermint/tmlibs/db"
10 "github.com/golang/protobuf/proto"
11 "github.com/vapor/blockchain/txbuilder"
12 "github.com/vapor/consensus"
13 "github.com/vapor/database/leveldb"
14 "github.com/vapor/database/storage"
15 "github.com/vapor/protocol"
16 "github.com/vapor/protocol/bc"
17 "github.com/vapor/protocol/bc/types"
18 "github.com/vapor/protocol/vm"
21 const utxoPrefix = "UT:"
23 type chainTestContext struct {
29 func (ctx *chainTestContext) validateStatus(block *types.Block) error {
30 // validate in mainchain
31 if !ctx.Chain.InMainChain(block.Hash()) {
32 return fmt.Errorf("block %d is not in mainchain", block.Height)
35 // validate chain status and saved block
36 bestBlockHeader := ctx.Chain.BestBlockHeader()
37 chainBlock, err := ctx.Chain.GetBlockByHeight(block.Height)
42 blockHash := block.Hash()
43 if bestBlockHeader.Hash() != blockHash || chainBlock.Hash() != blockHash {
44 return fmt.Errorf("chain status error")
48 txStatus, err := ctx.Chain.GetTransactionStatus(&blockHash)
53 txStatusMerkleRoot, err := types.TxStatusMerkleRoot(txStatus.VerifyStatus)
58 if txStatusMerkleRoot != block.TransactionStatusHash {
59 return fmt.Errorf("tx status error")
64 func (ctx *chainTestContext) validateExecution(block *types.Block) error {
65 for _, tx := range block.Transactions {
66 for _, spentOutputID := range tx.SpentOutputIDs {
67 utxoEntry, _ := ctx.Store.GetUtxo(&spentOutputID)
71 if !utxoEntry.IsCoinBase {
72 return fmt.Errorf("found non-coinbase spent utxo entry")
75 return fmt.Errorf("utxo entry status should be spent")
79 for _, outputID := range tx.ResultIds {
80 utxoEntry, _ := ctx.Store.GetUtxo(outputID)
81 if utxoEntry == nil && isSpent(outputID, block) {
84 if utxoEntry.BlockHeight != block.Height {
85 return fmt.Errorf("block height error, expected: %d, have: %d", block.Height, utxoEntry.BlockHeight)
88 return fmt.Errorf("utxo entry status should not be spent")
95 func (ctx *chainTestContext) getUtxoEntries() map[string]*storage.UtxoEntry {
96 utxoEntries := make(map[string]*storage.UtxoEntry)
97 iter := ctx.DB.IteratorPrefix([]byte(utxoPrefix))
101 utxoEntry := storage.UtxoEntry{}
102 if err := proto.Unmarshal(iter.Value(), &utxoEntry); err != nil {
105 key := string(iter.Key())
106 utxoEntries[key] = &utxoEntry
111 func (ctx *chainTestContext) validateRollback(utxoEntries map[string]*storage.UtxoEntry) error {
112 newUtxoEntries := ctx.getUtxoEntries()
113 for key := range utxoEntries {
114 entry, ok := newUtxoEntries[key]
116 return fmt.Errorf("can't find utxo after rollback")
118 if entry.Spent != utxoEntries[key].Spent {
119 return fmt.Errorf("utxo status dismatch after rollback")
125 type chainTestConfig struct {
126 RollbackTo uint64 `json:"rollback_to"`
127 Blocks []*ctBlock `json:"blocks"`
130 type ctBlock struct {
131 Transactions []*ctTransaction `json:"transactions"`
132 Append uint64 `json:"append"`
133 Invalid bool `json:"invalid"`
136 func (b *ctBlock) createBlock(ctx *chainTestContext) (*types.Block, error) {
137 txs := make([]*types.Tx, 0, len(b.Transactions))
138 for _, t := range b.Transactions {
139 tx, err := t.createTransaction(ctx, txs)
143 txs = append(txs, tx)
145 return NewBlock(ctx.Chain, txs, []byte{byte(vm.OP_TRUE)})
148 type ctTransaction struct {
149 Inputs []*ctInput `json:"inputs"`
150 Outputs []uint64 `json:"outputs"`
153 type ctInput struct {
154 Height uint64 `json:"height"`
155 TxIndex uint64 `json:"tx_index"`
156 OutputIndex uint64 `json:"output_index"`
159 func (input *ctInput) createTxInput(ctx *chainTestContext) (*types.TxInput, error) {
160 block, err := ctx.Chain.GetBlockByHeight(input.Height)
165 spendInput, err := CreateSpendInput(block.Transactions[input.TxIndex], input.OutputIndex)
170 return &types.TxInput{
171 AssetVersion: assetVersion,
172 TypedInput: spendInput,
176 // create tx input spent previous tx output in the same block
177 func (input *ctInput) createDependencyTxInput(txs []*types.Tx) (*types.TxInput, error) {
178 // sub 1 because of coinbase tx is not included in txs
179 spendInput, err := CreateSpendInput(txs[input.TxIndex-1], input.OutputIndex)
184 return &types.TxInput{
185 AssetVersion: assetVersion,
186 TypedInput: spendInput,
190 func (t *ctTransaction) createTransaction(ctx *chainTestContext, txs []*types.Tx) (*types.Tx, error) {
191 builder := txbuilder.NewBuilder(time.Now())
192 sigInst := &txbuilder.SigningInstruction{}
193 currentHeight := ctx.Chain.BestBlockHeight()
194 for _, input := range t.Inputs {
195 var txInput *types.TxInput
197 if input.Height == currentHeight+1 {
198 txInput, err = input.createDependencyTxInput(txs)
200 txInput, err = input.createTxInput(ctx)
205 err = builder.AddInput(txInput, sigInst)
211 for _, amount := range t.Outputs {
212 output := types.NewTxOutput(*consensus.BTMAssetID, amount, []byte{byte(vm.OP_TRUE)})
213 if err := builder.AddOutput(output); err != nil {
218 tpl, _, err := builder.Build()
223 txSerialized, err := tpl.Transaction.MarshalText()
228 tpl.Transaction.Tx.SerializedSize = uint64(len(txSerialized))
229 tpl.Transaction.TxData.SerializedSize = uint64(len(txSerialized))
230 return tpl.Transaction, err
233 func (cfg *chainTestConfig) Run() error {
234 db := dbm.NewDB("chain_test_db", "leveldb", "chain_test_db")
235 defer os.RemoveAll("chain_test_db")
236 chain, store, _, err := MockChain(db)
240 ctx := &chainTestContext{
246 var utxoEntries map[string]*storage.UtxoEntry
247 var rollbackBlock *types.Block
248 for _, blk := range cfg.Blocks {
249 block, err := blk.createBlock(ctx)
253 err = SolveAndUpdate(ctx.Chain, block)
254 if err != nil && blk.Invalid {
260 if err := ctx.validateStatus(block); err != nil {
263 if err := ctx.validateExecution(block); err != nil {
266 if block.Height <= cfg.RollbackTo && cfg.RollbackTo <= block.Height+blk.Append {
267 utxoEntries = ctx.getUtxoEntries()
268 rollbackBlock = block
270 if err := AppendBlocks(ctx.Chain, blk.Append); err != nil {
275 if rollbackBlock == nil {
279 // rollback and validate
280 forkedChain, err := declChain("forked_chain", ctx.Chain, rollbackBlock.Height, ctx.Chain.BestBlockHeight()+1)
281 defer os.RemoveAll("forked_chain")
286 if err := merge(forkedChain, ctx.Chain); err != nil {
289 return ctx.validateRollback(utxoEntries)
292 // if the output(hash) was spent in block
293 func isSpent(hash *bc.Hash, block *types.Block) bool {
294 for _, tx := range block.Transactions {
295 for _, spendOutputID := range tx.SpentOutputIDs {
296 if spendOutputID == *hash {