8 "github.com/golang/protobuf/proto"
9 "github.com/vapor/blockchain/txbuilder"
10 "github.com/vapor/consensus"
11 "github.com/vapor/database"
12 dbm "github.com/vapor/database/leveldb"
13 "github.com/vapor/database/storage"
14 "github.com/vapor/protocol"
15 "github.com/vapor/protocol/bc"
16 "github.com/vapor/protocol/bc/types"
17 "github.com/vapor/protocol/vm"
20 const utxoPrefix = "UT:"
22 type chainTestContext struct {
28 func (ctx *chainTestContext) validateStatus(block *types.Block) error {
29 // validate in mainchain
30 if !ctx.Chain.InMainChain(block.Hash()) {
31 return fmt.Errorf("block %d is not in mainchain", block.Height)
34 // validate chain status and saved block
35 bestBlockHeader := ctx.Chain.BestBlockHeader()
36 chainBlock, err := ctx.Chain.GetBlockByHeight(block.Height)
41 blockHash := block.Hash()
42 if bestBlockHeader.Hash() != blockHash || chainBlock.Hash() != blockHash {
43 return fmt.Errorf("chain status error")
47 txStatus, err := ctx.Chain.GetTransactionStatus(&blockHash)
52 txStatusMerkleRoot, err := types.TxStatusMerkleRoot(txStatus.VerifyStatus)
57 if txStatusMerkleRoot != block.TransactionStatusHash {
58 return fmt.Errorf("tx status error")
63 func (ctx *chainTestContext) validateExecution(block *types.Block) error {
64 for _, tx := range block.Transactions {
65 for _, mainchainOutputID := range tx.MainchainOutputIDs {
66 utxoEntry, _ := ctx.Store.GetUtxo(&mainchainOutputID)
70 if utxoEntry.Type != storage.CrosschainUTXOType {
71 return fmt.Errorf("found non-mainchain utxo entry")
74 return fmt.Errorf("utxo entry status should be spent")
78 for _, spentOutputID := range tx.SpentOutputIDs {
79 utxoEntry, _ := ctx.Store.GetUtxo(&spentOutputID)
83 if utxoEntry.Type == storage.NormalUTXOType {
84 return fmt.Errorf("found normal utxo entry")
87 return fmt.Errorf("utxo entry status should be spent")
91 for _, outputID := range tx.ResultIds {
92 utxoEntry, _ := ctx.Store.GetUtxo(outputID)
93 if utxoEntry == nil && isSpent(outputID, block) {
96 if utxoEntry.BlockHeight != block.Height {
97 return fmt.Errorf("block height error, expected: %d, have: %d", block.Height, utxoEntry.BlockHeight)
100 return fmt.Errorf("utxo entry status should not be spent")
107 func (ctx *chainTestContext) getUtxoEntries() map[string]*storage.UtxoEntry {
108 utxoEntries := make(map[string]*storage.UtxoEntry)
109 iter := ctx.DB.IteratorPrefix([]byte(utxoPrefix))
113 utxoEntry := storage.UtxoEntry{}
114 if err := proto.Unmarshal(iter.Value(), &utxoEntry); err != nil {
117 key := string(iter.Key())
118 utxoEntries[key] = &utxoEntry
123 func (ctx *chainTestContext) validateRollback(utxoEntries map[string]*storage.UtxoEntry) error {
124 newUtxoEntries := ctx.getUtxoEntries()
125 for key := range utxoEntries {
126 entry, ok := newUtxoEntries[key]
128 return fmt.Errorf("can't find utxo after rollback")
130 if entry.Spent != utxoEntries[key].Spent {
131 return fmt.Errorf("utxo status dismatch after rollback")
137 type chainTestConfig struct {
138 RollbackTo uint64 `json:"rollback_to"`
139 Blocks []*ctBlock `json:"blocks"`
142 type ctBlock struct {
143 Transactions []*ctTransaction `json:"transactions"`
144 Append uint64 `json:"append"`
145 Invalid bool `json:"invalid"`
148 func (b *ctBlock) createBlock(ctx *chainTestContext) (*types.Block, error) {
149 txs := make([]*types.Tx, 0, len(b.Transactions))
150 for _, t := range b.Transactions {
151 tx, err := t.createTransaction(ctx, txs)
155 txs = append(txs, tx)
157 return NewBlock(ctx.Chain, txs, []byte{byte(vm.OP_TRUE)})
160 type ctTransaction struct {
161 Inputs []*ctInput `json:"inputs"`
162 Outputs []uint64 `json:"outputs"`
165 type ctInput struct {
166 Height uint64 `json:"height"`
167 TxIndex uint64 `json:"tx_index"`
168 OutputIndex uint64 `json:"output_index"`
171 func (input *ctInput) createTxInput(ctx *chainTestContext) (*types.TxInput, error) {
172 block, err := ctx.Chain.GetBlockByHeight(input.Height)
177 spendInput, err := CreateSpendInput(block.Transactions[input.TxIndex], input.OutputIndex)
182 return &types.TxInput{
183 AssetVersion: assetVersion,
184 TypedInput: spendInput,
188 // create tx input spent previous tx output in the same block
189 func (input *ctInput) createDependencyTxInput(txs []*types.Tx) (*types.TxInput, error) {
190 // sub 1 because of coinbase tx is not included in txs
191 spendInput, err := CreateSpendInput(txs[input.TxIndex-1], input.OutputIndex)
196 return &types.TxInput{
197 AssetVersion: assetVersion,
198 TypedInput: spendInput,
202 func (t *ctTransaction) createTransaction(ctx *chainTestContext, txs []*types.Tx) (*types.Tx, error) {
203 builder := txbuilder.NewBuilder(time.Now())
204 sigInst := &txbuilder.SigningInstruction{}
205 currentHeight := ctx.Chain.BestBlockHeight()
206 for _, input := range t.Inputs {
207 var txInput *types.TxInput
209 if input.Height == currentHeight+1 {
210 txInput, err = input.createDependencyTxInput(txs)
212 txInput, err = input.createTxInput(ctx)
217 err = builder.AddInput(txInput, sigInst)
223 for _, amount := range t.Outputs {
224 output := types.NewIntraChainOutput(*consensus.BTMAssetID, amount, []byte{byte(vm.OP_TRUE)})
225 if err := builder.AddOutput(output); err != nil {
230 tpl, _, err := builder.Build()
235 txSerialized, err := tpl.Transaction.MarshalText()
240 tpl.Transaction.Tx.SerializedSize = uint64(len(txSerialized))
241 tpl.Transaction.TxData.SerializedSize = uint64(len(txSerialized))
242 return tpl.Transaction, err
245 func (cfg *chainTestConfig) Run() error {
246 db := dbm.NewDB("chain_test_db", "leveldb", "chain_test_db")
247 defer os.RemoveAll("chain_test_db")
248 chain, store, _, err := MockChain(db)
252 ctx := &chainTestContext{
258 var utxoEntries map[string]*storage.UtxoEntry
259 var rollbackBlock *types.Block
260 for _, blk := range cfg.Blocks {
261 block, err := blk.createBlock(ctx)
265 _, err = ctx.Chain.ProcessBlock(block)
266 if err != nil && blk.Invalid {
272 if err := ctx.validateStatus(block); err != nil {
275 if err := ctx.validateExecution(block); err != nil {
278 if block.Height <= cfg.RollbackTo && cfg.RollbackTo <= block.Height+blk.Append {
279 utxoEntries = ctx.getUtxoEntries()
280 rollbackBlock = block
282 if err := AppendBlocks(ctx.Chain, blk.Append); err != nil {
287 if rollbackBlock == nil {
291 // rollback and validate
292 forkedChain, err := declChain("forked_chain", ctx.Chain, rollbackBlock.Height, ctx.Chain.BestBlockHeight()+1)
293 defer os.RemoveAll("forked_chain")
298 if err := merge(forkedChain, ctx.Chain); err != nil {
301 return ctx.validateRollback(utxoEntries)
304 // if the output(hash) was spent in block
305 func isSpent(hash *bc.Hash, block *types.Block) bool {
306 for _, tx := range block.Transactions {
307 for _, spendOutputID := range tx.SpentOutputIDs {
308 if spendOutputID == *hash {