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 }