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 }