code.vegaprotocol.io/vega@v0.79.0/core/blockchain/nullchain/nullchain.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 "context" 20 "encoding/base64" 21 "encoding/json" 22 "errors" 23 "net/http" 24 "strconv" 25 "sync" 26 "sync/atomic" 27 "time" 28 29 "code.vegaprotocol.io/vega/core/blockchain" 30 vgcrypto "code.vegaprotocol.io/vega/libs/crypto" 31 vgfs "code.vegaprotocol.io/vega/libs/fs" 32 vgrand "code.vegaprotocol.io/vega/libs/rand" 33 "code.vegaprotocol.io/vega/logging" 34 35 abci "github.com/cometbft/cometbft/abci/types" 36 "github.com/cometbft/cometbft/crypto/tmhash" 37 "github.com/cometbft/cometbft/p2p" 38 "github.com/cometbft/cometbft/proto/tendermint/crypto" 39 tmctypes "github.com/cometbft/cometbft/rpc/core/types" 40 tmtypes "github.com/cometbft/cometbft/types" 41 ) 42 43 const namedLogger = "nullchain" 44 45 var ( 46 ErrNotImplemented = errors.New("not implemented for nullblockchain") 47 ErrChainReplaying = errors.New("nullblockchain is replaying") 48 ErrGenesisFileRequired = errors.New("--blockchain.nullchain.genesis-file is required") 49 ) 50 51 //go:generate go run github.com/golang/mock/mockgen -destination mocks/mocks.go -package mocks code.vegaprotocol.io/vega/core/blockchain/nullchain TimeService,ApplicationService 52 type TimeService interface { 53 GetTimeNow() time.Time 54 } 55 56 type ApplicationService interface { 57 InitChain(context.Context, *abci.RequestInitChain) (*abci.ResponseInitChain, error) 58 PrepareProposal(_ context.Context, req *abci.RequestPrepareProposal) (*abci.ResponsePrepareProposal, error) 59 FinalizeBlock(context.Context, *abci.RequestFinalizeBlock) (*abci.ResponseFinalizeBlock, error) 60 Commit(context.Context, *abci.RequestCommit) (*abci.ResponseCommit, error) 61 Info(context.Context, *abci.RequestInfo) (*abci.ResponseInfo, error) 62 } 63 64 // nullGenesis is a subset of a tendermint genesis file, just the bits we need to run the nullblockchain. 65 type nullGenesis struct { 66 GenesisTime *time.Time `json:"genesis_time"` 67 ChainID string `json:"chain_id"` 68 Appstate json.RawMessage `json:"app_state"` 69 } 70 71 type NullBlockchain struct { 72 log *logging.Logger 73 cfg blockchain.NullChainConfig 74 app ApplicationService 75 timeService TimeService 76 srv *http.Server 77 genesis nullGenesis 78 blockDuration time.Duration 79 transactionsPerBlock uint64 80 81 now time.Time 82 blockHeight int64 83 pending [][]byte 84 85 mu sync.Mutex 86 replaying atomic.Bool 87 replayer *Replayer 88 } 89 90 func NewClient( 91 log *logging.Logger, 92 cfg blockchain.NullChainConfig, 93 timeService TimeService, 94 ) *NullBlockchain { 95 // setup logger 96 log = log.Named(namedLogger) 97 log.SetLevel(cfg.Level.Get()) 98 99 n := &NullBlockchain{ 100 log: log, 101 cfg: cfg, 102 timeService: timeService, 103 transactionsPerBlock: cfg.TransactionsPerBlock, 104 blockDuration: cfg.BlockDuration.Duration, 105 blockHeight: 1, 106 pending: make([][]byte, 0), 107 } 108 109 return n 110 } 111 112 func (n *NullBlockchain) SetABCIApp(app ApplicationService) { 113 n.app = app 114 } 115 116 // ReloadConf update the internal configuration. 117 func (n *NullBlockchain) ReloadConf(cfg blockchain.Config) { 118 n.mu.Lock() 119 defer n.mu.Unlock() 120 121 n.log.Info("reloading configuration") 122 if n.log.GetLevel() != cfg.Level.Get() { 123 n.log.Info("updating log level", 124 logging.String("old", n.log.GetLevel().String()), 125 logging.String("new", cfg.Level.String()), 126 ) 127 n.log.SetLevel(cfg.Level.Get()) 128 } 129 130 n.blockDuration = cfg.Null.BlockDuration.Duration 131 n.transactionsPerBlock = cfg.Null.TransactionsPerBlock 132 } 133 134 func (n *NullBlockchain) StartChain() error { 135 if err := n.parseGenesis(); err != nil { 136 return err 137 } 138 139 if r, _ := n.app.Info(context.Background(), &abci.RequestInfo{}); r.LastBlockHeight > 0 { 140 n.log.Info("protocol loaded from snapshot", logging.Int64("height", r.LastBlockHeight)) 141 n.blockHeight = r.LastBlockHeight + 1 142 n.now = n.timeService.GetTimeNow().Add(n.blockDuration) 143 } else { 144 n.log.Info("initialising new chain", logging.String("chain-id", n.genesis.ChainID), logging.Time("chain-time", n.now)) 145 err := n.InitChain() 146 if err != nil { 147 return err 148 } 149 } 150 151 // not replaying or recording, proceed as normal 152 if !n.cfg.Replay.Record && !n.cfg.Replay.Replay { 153 return nil 154 } 155 156 r, err := NewNullChainReplayer(n.app, n.cfg.Replay, n.log) 157 if err != nil { 158 return err 159 } 160 n.replayer = r 161 162 if n.cfg.Replay.Replay { 163 n.log.Info("nullchain is replaying chain", logging.String("replay-file", n.cfg.Replay.ReplayFile)) 164 n.replaying.Store(true) 165 blockHeight, blockTime, err := r.replayChain(n.blockHeight) 166 if err != nil { 167 return err 168 } 169 n.replaying.Store(false) 170 171 n.log.Info("nullchain finished replaying chain", logging.Int64("block-height", blockHeight)) 172 if blockHeight != 0 { 173 // set the next height to where we replayed to 174 n.blockHeight = blockHeight + 1 175 n.now = blockTime.Add(n.blockDuration) 176 } 177 } 178 179 if n.cfg.Replay.Record { 180 n.log.Info("nullchain is recording chain data", logging.String("replay-file", n.cfg.Replay.ReplayFile)) 181 } 182 183 return nil 184 } 185 186 func (n *NullBlockchain) processBlock() { 187 if n.log.GetLevel() <= logging.DebugLevel { 188 n.log.Debugf("processing block %d with %d transactions", n.blockHeight, len(n.pending)) 189 } 190 191 // prepare it first 192 ctx := context.Background() 193 proposal, err := n.app.PrepareProposal(ctx, 194 &abci.RequestPrepareProposal{ 195 Height: n.blockHeight, 196 Time: n.now, 197 Txs: n.pending, 198 }) 199 if err != nil { 200 // core always returns nil so we are safe really 201 panic("nullchain cannot handle failure to prepare a proposal") 202 } 203 204 resp := &abci.ResponseFinalizeBlock{} 205 if n.replayer != nil && n.cfg.Replay.Record { 206 n.replayer.startBlock(n.blockHeight, n.now.UnixNano(), proposal.Txs) 207 defer func() { 208 n.replayer.saveBlock(resp.AppHash) 209 }() 210 } 211 212 resp, _ = n.app.FinalizeBlock(ctx, &abci.RequestFinalizeBlock{ 213 Height: n.blockHeight, 214 Time: n.now, 215 Hash: vgcrypto.Hash([]byte(strconv.FormatInt(n.blockHeight+n.now.UnixNano(), 10))), 216 Txs: proposal.Txs, 217 }) 218 n.pending = n.pending[:0] 219 n.app.Commit(ctx, &abci.RequestCommit{}) 220 221 // Increment time, blockheight, ready to start a new block 222 n.blockHeight++ 223 n.now = n.now.Add(n.blockDuration) 224 } 225 226 func (n *NullBlockchain) handleTransaction(tx []byte) { 227 n.mu.Lock() 228 defer n.mu.Unlock() 229 230 n.pending = append(n.pending, tx) 231 if n.log.GetLevel() <= logging.DebugLevel { 232 n.log.Debugf("transaction added to block: %d of %d", len(n.pending), n.transactionsPerBlock) 233 } 234 if len(n.pending) == int(n.transactionsPerBlock) { 235 n.processBlock() 236 } 237 } 238 239 // parseGenesis reads the Tendermint genesis file defined in the cfg and saves values needed to run the chain. 240 func (n *NullBlockchain) parseGenesis() error { 241 var ng nullGenesis 242 exists, err := vgfs.FileExists(n.cfg.GenesisFile) 243 if !exists || err != nil { 244 return ErrGenesisFileRequired 245 } 246 247 b, err := vgfs.ReadFile(n.cfg.GenesisFile) 248 if err != nil { 249 return err 250 } 251 252 err = json.Unmarshal(b, &ng) 253 if err != nil { 254 return err 255 } 256 257 n.now = time.Now() 258 if ng.GenesisTime != nil { 259 n.now = *ng.GenesisTime 260 } else { 261 // genesisTime not provided, just use now 262 ng.GenesisTime = &n.now 263 } 264 265 if len(ng.ChainID) == 0 { 266 // chainID not provided we'll just make one up 267 ng.ChainID = vgrand.RandomStr(12) 268 } 269 270 n.genesis = ng 271 return nil 272 } 273 274 // ForwardTime moves the chain time forward by the given duration, delivering any pending 275 // transaction and creating any extra empty blocks if time is stepped forward by more than 276 // a block duration. 277 func (n *NullBlockchain) ForwardTime(d time.Duration) { 278 n.log.Debugf("time-forwarding by %s", d) 279 280 nBlocks := d / n.blockDuration 281 if nBlocks == 0 { 282 n.log.Errorf("not a full block-duration, not moving time: %s < %s", d, n.blockDuration) 283 return 284 } 285 286 n.mu.Lock() 287 defer n.mu.Unlock() 288 for i := 0; i < int(nBlocks); i++ { 289 n.processBlock() 290 } 291 } 292 293 // InitChain processes the given genesis file setting the chain's time, and passing the 294 // appstate through to the processors InitChain. 295 func (n *NullBlockchain) InitChain() error { 296 // read appstate so that we can set the validators 297 appstate := struct { 298 Validators map[string]struct{} `json:"validators"` 299 }{} 300 301 if err := json.Unmarshal(n.genesis.Appstate, &appstate); err != nil { 302 return err 303 } 304 305 validators := make([]abci.ValidatorUpdate, 0, len(appstate.Validators)) 306 for k := range appstate.Validators { 307 pubKey, _ := base64.StdEncoding.DecodeString(k) 308 validators = append(validators, 309 abci.ValidatorUpdate{ 310 PubKey: crypto.PublicKey{ 311 Sum: &crypto.PublicKey_Ed25519{ 312 Ed25519: pubKey, 313 }, 314 }, 315 }, 316 ) 317 } 318 319 n.log.Debug("sending InitChain into core", 320 logging.String("chainID", n.genesis.ChainID), 321 logging.Int64("blockHeight", n.blockHeight), 322 logging.String("time", n.now.String()), 323 logging.Int("n_validators", len(validators)), 324 ) 325 n.app.InitChain(context.Background(), 326 &abci.RequestInitChain{ 327 Time: n.now, 328 ChainId: n.genesis.ChainID, 329 InitialHeight: n.blockHeight, 330 AppStateBytes: n.genesis.Appstate, 331 Validators: validators, 332 }, 333 ) 334 return nil 335 } 336 337 func (n *NullBlockchain) GetGenesisTime(context.Context) (time.Time, error) { 338 return *n.genesis.GenesisTime, nil 339 } 340 341 func (n *NullBlockchain) GetChainID(context.Context) (string, error) { 342 return n.genesis.ChainID, nil 343 } 344 345 func (n *NullBlockchain) GetStatus(context.Context) (*tmctypes.ResultStatus, error) { 346 return &tmctypes.ResultStatus{ 347 NodeInfo: p2p.DefaultNodeInfo{ 348 Version: "0.38.0", 349 }, 350 SyncInfo: tmctypes.SyncInfo{ 351 CatchingUp: n.replaying.Load(), 352 }, 353 }, nil 354 } 355 356 func (n *NullBlockchain) GetNetworkInfo(context.Context) (*tmctypes.ResultNetInfo, error) { 357 return &tmctypes.ResultNetInfo{ 358 Listening: true, 359 Listeners: []string{}, 360 NPeers: 0, 361 }, nil 362 } 363 364 func (n *NullBlockchain) GetUnconfirmedTxCount(context.Context) (int, error) { 365 n.mu.Lock() 366 defer n.mu.Unlock() 367 return len(n.pending), nil 368 } 369 370 func (n *NullBlockchain) Health(_ context.Context) (*tmctypes.ResultHealth, error) { 371 return &tmctypes.ResultHealth{}, nil 372 } 373 374 func (n *NullBlockchain) SendTransactionAsync(ctx context.Context, tx []byte) (*tmctypes.ResultBroadcastTx, error) { 375 if n.replaying.Load() { 376 return &tmctypes.ResultBroadcastTx{}, ErrChainReplaying 377 } 378 go func() { 379 n.handleTransaction(tx) 380 }() 381 return &tmctypes.ResultBroadcastTx{Hash: tmhash.Sum(tx)}, nil 382 } 383 384 func (n *NullBlockchain) CheckTransaction(ctx context.Context, tx []byte) (*tmctypes.ResultCheckTx, error) { 385 n.log.Error("not implemented") 386 return &tmctypes.ResultCheckTx{}, ErrNotImplemented 387 } 388 389 func (n *NullBlockchain) SendTransactionSync(ctx context.Context, tx []byte) (*tmctypes.ResultBroadcastTx, error) { 390 if n.replaying.Load() { 391 return &tmctypes.ResultBroadcastTx{}, ErrChainReplaying 392 } 393 n.handleTransaction(tx) 394 return &tmctypes.ResultBroadcastTx{Hash: tmhash.Sum(tx)}, nil 395 } 396 397 func (n *NullBlockchain) SendTransactionCommit(ctx context.Context, tx []byte) (*tmctypes.ResultBroadcastTxCommit, error) { 398 // I think its worth only implementing this if needed. With time-forwarding we already have 399 // control over when a block ends and gets committed, so I don't think its worth adding the 400 // the complexity of trying to keep track of tx deliveries here. 401 n.log.Error("not implemented") 402 return &tmctypes.ResultBroadcastTxCommit{Hash: tmhash.Sum(tx)}, ErrNotImplemented 403 } 404 405 func (n *NullBlockchain) Validators(_ context.Context, _ *int64) ([]*tmtypes.Validator, error) { 406 // TODO: if we are feeling brave we, could pretend to have a validator set and open 407 // up the nullblockchain to more code paths 408 return []*tmtypes.Validator{}, nil 409 } 410 411 func (n *NullBlockchain) GenesisValidators(_ context.Context) ([]*tmtypes.Validator, error) { 412 n.log.Error("not implemented") 413 return nil, ErrNotImplemented 414 } 415 416 func (n *NullBlockchain) Subscribe(context.Context, func(tmctypes.ResultEvent) error, ...string) error { 417 n.log.Error("not implemented") 418 return ErrNotImplemented 419 }