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  }