code.vegaprotocol.io/vega@v0.79.0/core/blockchain/nullchain/nullchain_test.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_test
    17  
    18  import (
    19  	"context"
    20  	"fmt"
    21  	"path"
    22  	"path/filepath"
    23  	"testing"
    24  	"time"
    25  
    26  	"code.vegaprotocol.io/vega/core/blockchain"
    27  	"code.vegaprotocol.io/vega/core/blockchain/nullchain"
    28  	"code.vegaprotocol.io/vega/core/blockchain/nullchain/mocks"
    29  	"code.vegaprotocol.io/vega/libs/config/encoding"
    30  	vgfs "code.vegaprotocol.io/vega/libs/fs"
    31  	vgrand "code.vegaprotocol.io/vega/libs/rand"
    32  	"code.vegaprotocol.io/vega/logging"
    33  
    34  	abci "github.com/cometbft/cometbft/abci/types"
    35  	"github.com/golang/mock/gomock"
    36  	"github.com/stretchr/testify/assert"
    37  	"github.com/stretchr/testify/require"
    38  )
    39  
    40  const (
    41  	chainID     = "somechainid"
    42  	genesisTime = "2021-11-25T10:22:23.03277423Z"
    43  )
    44  
    45  func TestNullChain(t *testing.T) {
    46  	t.Run("test basics", testBasics)
    47  	t.Run("test transactions create block", testTransactionsCreateBlock)
    48  	t.Run("test timeforwarding creates blocks", testTimeForwardingCreatesBlocks)
    49  	t.Run("test timeforwarding less than a block does nothing", testTimeForwardingLessThanABlockDoesNothing)
    50  	t.Run("test timeforwarding request conversion", testTimeForwardingRequestConversion)
    51  	t.Run("test replay from genesis", testReplayFromGenesis)
    52  	t.Run("test replay with snapshot restore", testReplayWithSnapshotRestore)
    53  	t.Run("test replay with a block that panics", testReplayPanicBlock)
    54  }
    55  
    56  func testBasics(t *testing.T) {
    57  	ctx := context.Background()
    58  	testChain := getTestNullChain(t, 2, time.Second)
    59  	defer testChain.ctrl.Finish()
    60  
    61  	// Check genesis time from genesis file has filtered through
    62  	gt, _ := time.Parse(time.RFC3339Nano, genesisTime)
    63  	getGT, err := testChain.chain.GetGenesisTime(ctx)
    64  	assert.NoError(t, err)
    65  	assert.Equal(t, gt, getGT)
    66  
    67  	// Check chainID time from genesis file has filtered through
    68  	id, err := testChain.chain.GetChainID(ctx)
    69  	assert.NoError(t, err)
    70  	assert.Equal(t, chainID, id)
    71  }
    72  
    73  func testTransactionsCreateBlock(t *testing.T) {
    74  	ctx := context.Background()
    75  	testChain := getTestNullChain(t, 2, time.Second)
    76  	defer testChain.ctrl.Finish()
    77  
    78  	// Expected BeginBlock to be called with time shuffled forward by a block
    79  	now, _ := testChain.chain.GetGenesisTime(ctx)
    80  
    81  	// One round of block processing calls
    82  	testChain.app.EXPECT().FinalizeBlock(gomock.Any(), gomock.Any()).Do(func(_ context.Context, rr *abci.RequestFinalizeBlock) {
    83  		require.Equal(t, now, rr.Time)
    84  		require.Equal(t, int64(1), rr.Height)
    85  	}).Times(1)
    86  	testChain.app.EXPECT().Commit(gomock.Any(), gomock.Any()).Times(1)
    87  	// Send in three transactions, two gets delivered in the block, one left over
    88  	testChain.chain.SendTransactionSync(ctx, []byte(vgrand.RandomStr(5)))
    89  	testChain.chain.SendTransactionSync(ctx, []byte(vgrand.RandomStr(5)))
    90  	testChain.chain.SendTransactionSync(ctx, []byte(vgrand.RandomStr(5)))
    91  
    92  	count, err := testChain.chain.GetUnconfirmedTxCount(ctx)
    93  	assert.NoError(t, err)
    94  	assert.Equal(t, 1, count)
    95  }
    96  
    97  func testTimeForwardingCreatesBlocks(t *testing.T) {
    98  	ctx := context.Background()
    99  	testChain := getTestNullChain(t, 10, 2*time.Second)
   100  	defer testChain.ctrl.Finish()
   101  
   102  	// each block is 2 seconds (we should snap back to 10 blocks)
   103  	step := 21 * time.Second
   104  	now, _ := testChain.chain.GetGenesisTime(ctx)
   105  	beginBlockTime := now
   106  	height := 0
   107  
   108  	// Fill in a partial blocks worth of transactions
   109  	testChain.chain.SendTransactionSync(ctx, []byte(vgrand.RandomStr(5)))
   110  	testChain.chain.SendTransactionSync(ctx, []byte(vgrand.RandomStr(5)))
   111  	testChain.chain.SendTransactionSync(ctx, []byte(vgrand.RandomStr(5)))
   112  
   113  	// One round of block processing calls
   114  
   115  	testChain.app.EXPECT().FinalizeBlock(gomock.Any(), gomock.Any()).Times(10).Do(func(_ context.Context, r *abci.RequestFinalizeBlock) {
   116  		beginBlockTime = r.Time
   117  		height = int(r.Height)
   118  	})
   119  	testChain.app.EXPECT().Commit(gomock.Any(), gomock.Any()).Times(10)
   120  
   121  	testChain.chain.ForwardTime(step)
   122  	assert.True(t, beginBlockTime.Equal(now.Add(18*time.Second))) // the start of the next block will take us to +20 seconds
   123  	assert.Equal(t, 10, height)
   124  
   125  	count, err := testChain.chain.GetUnconfirmedTxCount(ctx)
   126  	assert.NoError(t, err)
   127  	assert.Equal(t, 0, count)
   128  }
   129  
   130  func testTimeForwardingLessThanABlockDoesNothing(t *testing.T) {
   131  	ctx := context.Background()
   132  	testChain := getTestNullChain(t, 10, 2*time.Second)
   133  	defer testChain.ctrl.Finish()
   134  
   135  	// half a block duration
   136  	step := time.Second
   137  
   138  	// Fill in a partial blocks worth of transactions
   139  	testChain.chain.SendTransactionSync(ctx, []byte(vgrand.RandomStr(5)))
   140  	testChain.chain.SendTransactionSync(ctx, []byte(vgrand.RandomStr(5)))
   141  	testChain.chain.SendTransactionSync(ctx, []byte(vgrand.RandomStr(5)))
   142  
   143  	testChain.chain.ForwardTime(step)
   144  
   145  	count, err := testChain.chain.GetUnconfirmedTxCount(ctx)
   146  	assert.NoError(t, err)
   147  	assert.Equal(t, 3, count)
   148  }
   149  
   150  func testTimeForwardingRequestConversion(t *testing.T) {
   151  	now := time.Time{}
   152  
   153  	// Bad input
   154  	_, err := nullchain.RequestToDuration("nonsense", now)
   155  	assert.Error(t, err)
   156  
   157  	// Valid duration
   158  	d, err := nullchain.RequestToDuration("1m10s", now)
   159  	assert.NoError(t, err)
   160  	assert.Equal(t, d, time.Minute+(10*time.Second))
   161  
   162  	// backwards duration
   163  	_, err = nullchain.RequestToDuration("-1m10s", now)
   164  	assert.Error(t, err)
   165  	// Valid datetime
   166  	forward := now.Add(time.Minute)
   167  	d, err = nullchain.RequestToDuration(forward.Format(time.RFC3339), now)
   168  	assert.NoError(t, err)
   169  	assert.Equal(t, time.Minute, d)
   170  
   171  	// backwards in datetime
   172  	forward = now.Add(-time.Hour)
   173  	_, err = nullchain.RequestToDuration(forward.Format(time.RFC3339), now)
   174  	assert.Error(t, err)
   175  }
   176  
   177  func testReplayWithSnapshotRestore(t *testing.T) {
   178  	ctx := context.Background()
   179  	rplFile := path.Join(t.TempDir(), "rfile")
   180  	testChain := getTestUnstartedNullChain(t, 2, time.Second, &blockchain.ReplayConfig{Record: true, Replay: true, ReplayFile: rplFile})
   181  	defer testChain.ctrl.Finish()
   182  
   183  	generateChain(t, testChain, 15)
   184  	testChain.chain.Stop()
   185  
   186  	// pretend the protocol restores to block height 10
   187  	restoredBlockTime := time.Unix(10000, 15)
   188  	restoreBlockHeight := int64(10)
   189  	testChain.app.EXPECT().Info(gomock.Any(), gomock.Any()).Times(1).Return(
   190  		&abci.ResponseInfo{
   191  			LastBlockHeight: restoreBlockHeight,
   192  		}, nil,
   193  	)
   194  
   195  	// we'll replay 5 blocks
   196  	testChain.app.EXPECT().FinalizeBlock(gomock.Any(), gomock.Any()).Times(5)
   197  	testChain.app.EXPECT().Commit(gomock.Any(), gomock.Any()).Times(5)
   198  	testChain.ts.EXPECT().GetTimeNow().Times(1).Return(restoredBlockTime)
   199  
   200  	// start the nullchain from a snapshot
   201  	err := testChain.chain.StartChain()
   202  	require.NoError(t, err)
   203  
   204  	// continue the chain and check we're at the right block height and stuff
   205  	// the next begin block should be at block height 16 (restored to 10, replayed 5, starting the next)
   206  	req := &abci.RequestFinalizeBlock{}
   207  	testChain.app.EXPECT().FinalizeBlock(gomock.Any(), gomock.Any()).DoAndReturn(func(_ context.Context, r *abci.RequestFinalizeBlock) (*abci.ResponseFinalizeBlock, error) {
   208  		req = r
   209  		return &abci.ResponseFinalizeBlock{}, nil
   210  	}).AnyTimes()
   211  	testChain.app.EXPECT().Commit(gomock.Any(), gomock.Any()).Times(1)
   212  
   213  	// fill the block
   214  	testChain.chain.SendTransactionSync(ctx, []byte(vgrand.RandomStr(5)))
   215  	testChain.chain.SendTransactionSync(ctx, []byte(vgrand.RandomStr(5)))
   216  
   217  	genesis, err := testChain.chain.GetGenesisTime(ctx)
   218  	require.NoError(t, err)
   219  	require.Equal(t, int64(16), req.Height)
   220  	require.Equal(t, genesis.Add(15*time.Second).UnixNano(), req.Time.UnixNano())
   221  }
   222  
   223  func testReplayFromGenesis(t *testing.T) {
   224  	// replay file
   225  	rplFile := path.Join(t.TempDir(), "rfile")
   226  	testChain := getTestUnstartedNullChain(t, 2, time.Second, &blockchain.ReplayConfig{Record: true, ReplayFile: rplFile})
   227  	defer testChain.ctrl.Finish()
   228  
   229  	generateChain(t, testChain, 15)
   230  	testChain.chain.Stop()
   231  
   232  	newChain := getTestUnstartedNullChain(t, 2, time.Second, &blockchain.ReplayConfig{Replay: true, ReplayFile: rplFile})
   233  	defer newChain.ctrl.Finish()
   234  
   235  	// protocol is starting from 0
   236  	newChain.app.EXPECT().Info(gomock.Any(), gomock.Any()).Times(1).Return(
   237  		&abci.ResponseInfo{
   238  			LastBlockHeight: 0,
   239  		}, nil,
   240  	)
   241  
   242  	// we'll replay 15 blocks
   243  	newChain.app.EXPECT().InitChain(gomock.Any(), gomock.Any()).Times(1)
   244  	newChain.app.EXPECT().FinalizeBlock(gomock.Any(), gomock.Any()).Times(15).Return(&abci.ResponseFinalizeBlock{}, nil)
   245  	newChain.app.EXPECT().Commit(gomock.Any(), gomock.Any()).Times(15)
   246  
   247  	// start the nullchain from genesis
   248  	err := newChain.chain.StartChain()
   249  	require.NoError(t, err)
   250  }
   251  
   252  func testReplayPanicBlock(t *testing.T) {
   253  	ctx := context.Background()
   254  	// replay file
   255  	rplFile := path.Join(t.TempDir(), "rfile")
   256  	testChain := getTestUnstartedNullChain(t, 2, time.Second, &blockchain.ReplayConfig{Record: true, ReplayFile: rplFile})
   257  	defer testChain.ctrl.Finish()
   258  
   259  	generateChain(t, testChain, 5)
   260  
   261  	// send in a single transaction that works
   262  
   263  	testChain.app.EXPECT().FinalizeBlock(gomock.Any(), gomock.Any()).Do(func(_ context.Context, rr *abci.RequestFinalizeBlock) {
   264  		panic("ah panic processing transaction")
   265  	}).Times(1)
   266  	testChain.chain.SendTransactionSync(ctx, []byte(vgrand.RandomStr(5)))
   267  
   268  	require.Panics(t, func() {
   269  		testChain.chain.SendTransactionSync(ctx, []byte(vgrand.RandomStr(5)))
   270  	})
   271  
   272  	// now stop the nullchain so we save the unfinished block
   273  	testChain.chain.Stop()
   274  
   275  	// replay the chain
   276  	newChain := getTestUnstartedNullChain(t, 2, time.Second, &blockchain.ReplayConfig{Replay: true, ReplayFile: rplFile})
   277  	defer newChain.ctrl.Finish()
   278  
   279  	// protocol is starting from 0
   280  	newChain.app.EXPECT().Info(gomock.Any(), gomock.Any()).Times(1).Return(
   281  		&abci.ResponseInfo{
   282  			LastBlockHeight: 0,
   283  		}, nil,
   284  	)
   285  
   286  	// we'll replay 5 full blocks, and process the 6th "panic" block ready to start the 7th
   287  	newChain.app.EXPECT().InitChain(gomock.Any(), gomock.Any()).Times(1)
   288  	newChain.app.EXPECT().FinalizeBlock(gomock.Any(), gomock.Any()).Times(6)
   289  	newChain.app.EXPECT().Commit(gomock.Any(), gomock.Any()).Times(6)
   290  
   291  	// start the nullchain from genesis
   292  	err := newChain.chain.StartChain()
   293  	require.NoError(t, err)
   294  }
   295  
   296  type testNullBlockChain struct {
   297  	chain *nullchain.NullBlockchain
   298  	ctrl  *gomock.Controller
   299  	app   *mocks.MockApplicationService
   300  	ts    *mocks.MockTimeService
   301  	cfg   blockchain.NullChainConfig
   302  }
   303  
   304  func getTestUnstartedNullChain(t *testing.T, txnPerBlock uint64, d time.Duration, rplCfg *blockchain.ReplayConfig) *testNullBlockChain {
   305  	t.Helper()
   306  
   307  	ctrl := gomock.NewController(t)
   308  
   309  	app := mocks.NewMockApplicationService(ctrl)
   310  	ts := mocks.NewMockTimeService(ctrl)
   311  
   312  	cfg := blockchain.NewDefaultNullChainConfig()
   313  	cfg.GenesisFile = newGenesisFile(t)
   314  	cfg.BlockDuration = encoding.Duration{Duration: d}
   315  	cfg.TransactionsPerBlock = txnPerBlock
   316  	cfg.Level = encoding.LogLevel{Level: logging.DebugLevel}
   317  	if rplCfg != nil {
   318  		cfg.Replay = *rplCfg
   319  	}
   320  
   321  	n := nullchain.NewClient(logging.NewTestLogger(), cfg, ts)
   322  	n.SetABCIApp(app)
   323  	require.NotNil(t, n)
   324  
   325  	app.EXPECT().PrepareProposal(gomock.Any(), gomock.Any()).DoAndReturn(
   326  		func(_ context.Context, req *abci.RequestPrepareProposal) (*abci.ResponsePrepareProposal, error) {
   327  			ret := &abci.ResponsePrepareProposal{
   328  				Txs: req.Txs,
   329  			}
   330  			return ret, nil
   331  		},
   332  	).AnyTimes()
   333  
   334  	return &testNullBlockChain{
   335  		chain: n,
   336  		ctrl:  ctrl,
   337  		app:   app,
   338  		ts:    ts,
   339  		cfg:   cfg,
   340  	}
   341  }
   342  
   343  func getTestNullChain(t *testing.T, txnPerBlock uint64, d time.Duration) *testNullBlockChain {
   344  	t.Helper()
   345  	nc := getTestUnstartedNullChain(t, txnPerBlock, d, nil)
   346  
   347  	nc.app.EXPECT().Info(gomock.Any(), gomock.Any()).Times(1).Return(&abci.ResponseInfo{}, nil)
   348  	nc.app.EXPECT().InitChain(gomock.Any(), gomock.Any()).Times(1)
   349  
   350  	err := nc.chain.StartChain()
   351  	require.NoError(t, err)
   352  
   353  	return nc
   354  }
   355  
   356  func newGenesisFile(t *testing.T) string {
   357  	t.Helper()
   358  	data := fmt.Sprintf("{ \"genesis_time\": \"%s\",\"chain_id\": \"%s\", \"app_state\": { \"validators\": {}}}", genesisTime, chainID)
   359  
   360  	filePath := filepath.Join(t.TempDir(), "genesis.json")
   361  	if err := vgfs.WriteFile(filePath, []byte(data)); err != nil {
   362  		t.Fatalf("couldn't write file: %v", err)
   363  	}
   364  	return filePath
   365  }
   366  
   367  // generateChain start the nullblockchain and generates random chain data until it reaches the given block height.
   368  func generateChain(t *testing.T, nc *testNullBlockChain, height int) {
   369  	t.Helper()
   370  
   371  	nTxns := int(nc.cfg.TransactionsPerBlock) * height
   372  
   373  	ctx := context.Background()
   374  	nc.app.EXPECT().InitChain(gomock.Any(), gomock.Any()).Times(1)
   375  	nc.app.EXPECT().FinalizeBlock(gomock.Any(), gomock.Any()).Times(height).Return(&abci.ResponseFinalizeBlock{}, nil)
   376  	nc.app.EXPECT().Commit(gomock.Any(), gomock.Any()).Times(height)
   377  	nc.app.EXPECT().Info(gomock.Any(), gomock.Any()).Times(1).Return(
   378  		&abci.ResponseInfo{
   379  			LastBlockHeight: 0,
   380  		}, nil,
   381  	)
   382  
   383  	// start the nullchain
   384  	err := nc.chain.StartChain()
   385  	require.NoError(t, err)
   386  
   387  	// send in enough transactions to fill the required blocks
   388  
   389  	for i := 0; i < nTxns; i++ {
   390  		nc.chain.SendTransactionSync(ctx, []byte(vgrand.RandomStr(5)))
   391  	}
   392  }