code.vegaprotocol.io/vega@v0.79.0/core/blockchain/nullchain/replay.go (about)

     1  // Copyright (C) 2023 Gobalsky Labs Limited
     2  //
     3  // This program is free software: you can redistribute it and/or modify
     4  // it under the terms of the GNU Affero General Public License as
     5  // published by the Free Software Foundation, either version 3 of the
     6  // License, or (at your option) any later version.
     7  //
     8  // This program is distributed in the hope that it will be useful,
     9  // but WITHOUT ANY WARRANTY; without even the implied warranty of
    10  // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    11  // GNU Affero General Public License for more details.
    12  //
    13  // You should have received a copy of the GNU Affero General Public License
    14  // along with this program.  If not, see <http://www.gnu.org/licenses/>.
    15  
    16  package nullchain
    17  
    18  import (
    19  	"bufio"
    20  	"bytes"
    21  	"context"
    22  	"encoding/hex"
    23  	"encoding/json"
    24  	"errors"
    25  	"fmt"
    26  	"io"
    27  	"os"
    28  	"strconv"
    29  	"time"
    30  
    31  	"code.vegaprotocol.io/vega/core/blockchain"
    32  	vgcrypto "code.vegaprotocol.io/vega/libs/crypto"
    33  	"code.vegaprotocol.io/vega/logging"
    34  
    35  	abci "github.com/cometbft/cometbft/abci/types"
    36  )
    37  
    38  var ErrReplayFileIsRequired = errors.New("replay-file is required when replay/record is enabled")
    39  
    40  type blockData struct {
    41  	Height  int64    `json:"height"`
    42  	Time    int64    `json:"time"`
    43  	Txs     [][]byte `json:"txns"`
    44  	AppHash []byte   `json:"appHash"`
    45  }
    46  
    47  type Replayer struct {
    48  	log     *logging.Logger
    49  	app     ApplicationService
    50  	rFile   *os.File
    51  	current *blockData
    52  	stop    chan struct{}
    53  }
    54  
    55  func NewNullChainReplayer(app ApplicationService, cfg blockchain.ReplayConfig, log *logging.Logger) (*Replayer, error) {
    56  	if cfg.ReplayFile == "" {
    57  		return nil, ErrReplayFileIsRequired
    58  	}
    59  
    60  	flags := os.O_RDWR | os.O_CREATE
    61  	if !cfg.Replay {
    62  		// not replaying so make sure the file is empty before we start recording
    63  		flags |= os.O_TRUNC
    64  	}
    65  	f, err := os.OpenFile(cfg.ReplayFile, flags, 0o600)
    66  	if err != nil {
    67  		return nil, fmt.Errorf("failed to open replay file %s: %w", cfg.ReplayFile, err)
    68  	}
    69  
    70  	return &Replayer{
    71  		app:   app,
    72  		rFile: f,
    73  		log:   log,
    74  		stop:  make(chan struct{}, 1),
    75  	}, nil
    76  }
    77  
    78  func (r *Replayer) InitChain(req abci.RequestInitChain) (*abci.ResponseInitChain, error) {
    79  	return r.app.InitChain(context.Background(), &req)
    80  }
    81  
    82  func (r *Replayer) Stop() error {
    83  	r.stop <- struct{}{}
    84  	close(r.stop)
    85  	return r.rFile.Close()
    86  }
    87  
    88  // startBlock saves in memory all the transactions in the block, we do not write until saveBlock us called
    89  // with a potential appHash.
    90  func (r *Replayer) startBlock(height, now int64, txs [][]byte) {
    91  	r.current = &blockData{
    92  		Height: height,
    93  		Time:   now,
    94  	}
    95  	r.current.Txs = append(r.current.Txs, txs...)
    96  }
    97  
    98  // saveBlock writes to the replay file the details of the current block adding the appHash to it.
    99  // If a panic occurred appHash may be empty.
   100  func (r *Replayer) saveBlock(appHash []byte) {
   101  	r.current.AppHash = appHash
   102  	if err := r.write(); err != nil {
   103  		r.log.Panic("unable to write block to file", logging.Int64("block-height", r.current.Height))
   104  	}
   105  	r.current = nil
   106  }
   107  
   108  func readLine(r *bufio.Reader) ([]byte, error) {
   109  	line := []byte{}
   110  	for {
   111  		l, more, err := r.ReadLine()
   112  		if err != nil {
   113  			return nil, err
   114  		}
   115  
   116  		line = append(line, l...)
   117  		if !more {
   118  			return line, nil
   119  		}
   120  	}
   121  }
   122  
   123  // replayChain sends all the recorded per-block transactions into the protocol returning the block-height and block-time it reached
   124  // appHeight is the block-height the application will process next, any blocks less than this will not be replayed.
   125  func (r *Replayer) replayChain(appHeight int64) (int64, time.Time, error) {
   126  	var replayedHeight int64
   127  	var replayedTime time.Time
   128  
   129  	s := bufio.NewReader(r.rFile)
   130  	for {
   131  		line, err := readLine(s)
   132  		if err == io.EOF {
   133  			break
   134  		}
   135  
   136  		if err != nil {
   137  			return replayedHeight, replayedTime, err
   138  		}
   139  
   140  		select {
   141  		case <-r.stop:
   142  			r.log.Info("core is shutting down, nullchain replaying stopped", logging.Int64("block-height", replayedHeight))
   143  			return replayedHeight, replayedTime, nil
   144  		default:
   145  		}
   146  		var data blockData
   147  		if err := json.Unmarshal(line, &data); err != nil {
   148  			return replayedHeight, replayedTime, err
   149  		}
   150  
   151  		replayedHeight = data.Height
   152  		replayedTime = time.Unix(0, data.Time)
   153  
   154  		if data.Height < appHeight {
   155  			// skip because we've loaded from a snapshot at a block higher than this
   156  			continue
   157  		}
   158  
   159  		r.log.Info("replaying block", logging.Int64("height", data.Height), logging.Int("ntxns", len(data.Txs)))
   160  		resp, _ := r.app.FinalizeBlock(context.Background(), &abci.RequestFinalizeBlock{
   161  			Height: data.Height,
   162  			Time:   time.Unix(0, data.Time),
   163  			Hash:   vgcrypto.Hash([]byte(strconv.FormatInt(data.Height+data.Time, 10))),
   164  			Txs:    data.Txs,
   165  		})
   166  
   167  		r.app.Commit(context.Background(), &abci.RequestCommit{})
   168  
   169  		if len(data.AppHash) == 0 {
   170  			// we've replayed a block which when recorded must have panicked so we do not have a apphash
   171  			// somehow we've made it through this time, maybe someone is testing a fix so we skip the hash check and log it as strange
   172  			r.log.Error("app-hash missing from block data -- a block with a panic is working now?")
   173  			continue
   174  		}
   175  
   176  		if !bytes.Equal(data.AppHash, resp.AppHash) {
   177  			return replayedHeight, replayedTime, fmt.Errorf("appHash mismatch on replay, expected %s got %s", hex.EncodeToString(data.AppHash), hex.EncodeToString(resp.AppHash))
   178  		}
   179  	}
   180  
   181  	if replayedHeight < appHeight-1 {
   182  		return replayedHeight, replayedTime, fmt.Errorf("replay data missing, replay store up to height %d, but app-height is %d", replayedHeight, appHeight)
   183  	}
   184  
   185  	return replayedHeight, replayedTime, nil
   186  }
   187  
   188  func (r *Replayer) write() error {
   189  	b, err := json.Marshal(r.current)
   190  	if err != nil {
   191  		return fmt.Errorf("unable to record block %d: %w", r.current.Height, err)
   192  	}
   193  
   194  	// write each marshalled json block on a new line, its crude, but lets worry about perf if perf becomes a problem.
   195  	r.rFile.Write(b)
   196  	r.rFile.Write([]byte("\n"))
   197  	return nil
   198  }