bitbucket.org/number571/tendermint@v0.8.14/store/store_test.go (about)

     1  package store
     2  
     3  import (
     4  	"fmt"
     5  	"os"
     6  	"runtime/debug"
     7  	"strings"
     8  	"testing"
     9  	"time"
    10  
    11  	"github.com/stretchr/testify/assert"
    12  	"github.com/stretchr/testify/require"
    13  	dbm "github.com/tendermint/tm-db"
    14  
    15  	cfg "bitbucket.org/number571/tendermint/config"
    16  	"bitbucket.org/number571/tendermint/crypto"
    17  	"bitbucket.org/number571/tendermint/libs/log"
    18  	tmrand "bitbucket.org/number571/tendermint/libs/rand"
    19  	tmtime "bitbucket.org/number571/tendermint/libs/time"
    20  	sm "bitbucket.org/number571/tendermint/state"
    21  	"bitbucket.org/number571/tendermint/state/test/factory"
    22  	"bitbucket.org/number571/tendermint/types"
    23  	"bitbucket.org/number571/tendermint/version"
    24  )
    25  
    26  // A cleanupFunc cleans up any config / test files created for a particular
    27  // test.
    28  type cleanupFunc func()
    29  
    30  // make a Commit with a single vote containing just the height and a timestamp
    31  func makeTestCommit(height int64, timestamp time.Time) *types.Commit {
    32  	commitSigs := []types.CommitSig{{
    33  		BlockIDFlag:      types.BlockIDFlagCommit,
    34  		ValidatorAddress: tmrand.Bytes(crypto.AddressSize),
    35  		Timestamp:        timestamp,
    36  		Signature:        []byte("Signature"),
    37  	}}
    38  	return types.NewCommit(
    39  		height,
    40  		0,
    41  		types.BlockID{
    42  			Hash:          crypto.CRandBytes(32),
    43  			PartSetHeader: types.PartSetHeader{Hash: crypto.CRandBytes(32), Total: 2},
    44  		},
    45  		commitSigs)
    46  }
    47  
    48  func makeStateAndBlockStore(logger log.Logger) (sm.State, *BlockStore, cleanupFunc) {
    49  	config := cfg.ResetTestRoot("blockchain_reactor_test")
    50  	blockDB := dbm.NewMemDB()
    51  	state, err := sm.MakeGenesisStateFromFile(config.GenesisFile())
    52  	if err != nil {
    53  		panic(fmt.Errorf("error constructing state from genesis file: %w", err))
    54  	}
    55  	return state, NewBlockStore(blockDB), func() { os.RemoveAll(config.RootDir) }
    56  }
    57  
    58  func freshBlockStore() (*BlockStore, dbm.DB) {
    59  	db := dbm.NewMemDB()
    60  	return NewBlockStore(db), db
    61  }
    62  
    63  var (
    64  	state       sm.State
    65  	block       *types.Block
    66  	partSet     *types.PartSet
    67  	part1       *types.Part
    68  	part2       *types.Part
    69  	seenCommit1 *types.Commit
    70  )
    71  
    72  func TestMain(m *testing.M) {
    73  	var cleanup cleanupFunc
    74  	state, _, cleanup = makeStateAndBlockStore(log.NewNopLogger())
    75  	block = factory.MakeBlock(state, 1, new(types.Commit))
    76  	partSet = block.MakePartSet(2)
    77  	part1 = partSet.GetPart(0)
    78  	part2 = partSet.GetPart(1)
    79  	seenCommit1 = makeTestCommit(10, tmtime.Now())
    80  	code := m.Run()
    81  	cleanup()
    82  	os.Exit(code)
    83  }
    84  
    85  // TODO: This test should be simplified ...
    86  func TestBlockStoreSaveLoadBlock(t *testing.T) {
    87  	state, bs, cleanup := makeStateAndBlockStore(log.NewNopLogger())
    88  	defer cleanup()
    89  	require.Equal(t, bs.Base(), int64(0), "initially the base should be zero")
    90  	require.Equal(t, bs.Height(), int64(0), "initially the height should be zero")
    91  
    92  	// check there are no blocks at various heights
    93  	noBlockHeights := []int64{0, -1, 100, 1000, 2}
    94  	for i, height := range noBlockHeights {
    95  		if g := bs.LoadBlock(height); g != nil {
    96  			t.Errorf("#%d: height(%d) got a block; want nil", i, height)
    97  		}
    98  	}
    99  
   100  	// save a block
   101  	block := factory.MakeBlock(state, bs.Height()+1, new(types.Commit))
   102  	validPartSet := block.MakePartSet(2)
   103  	seenCommit := makeTestCommit(10, tmtime.Now())
   104  	bs.SaveBlock(block, partSet, seenCommit)
   105  	require.EqualValues(t, 1, bs.Base(), "expecting the new height to be changed")
   106  	require.EqualValues(t, block.Header.Height, bs.Height(), "expecting the new height to be changed")
   107  
   108  	incompletePartSet := types.NewPartSetFromHeader(types.PartSetHeader{Total: 2})
   109  	uncontiguousPartSet := types.NewPartSetFromHeader(types.PartSetHeader{Total: 0})
   110  	_, err := uncontiguousPartSet.AddPart(part2)
   111  	require.Error(t, err)
   112  
   113  	header1 := types.Header{
   114  		Version:         version.Consensus{Block: version.BlockProtocol},
   115  		Height:          1,
   116  		ChainID:         "block_test",
   117  		Time:            tmtime.Now(),
   118  		ProposerAddress: tmrand.Bytes(crypto.AddressSize),
   119  	}
   120  
   121  	// End of setup, test data
   122  	commitAtH10 := makeTestCommit(10, tmtime.Now())
   123  	tuples := []struct {
   124  		block      *types.Block
   125  		parts      *types.PartSet
   126  		seenCommit *types.Commit
   127  		wantPanic  string
   128  		wantErr    bool
   129  
   130  		corruptBlockInDB      bool
   131  		corruptCommitInDB     bool
   132  		corruptSeenCommitInDB bool
   133  		eraseCommitInDB       bool
   134  		eraseSeenCommitInDB   bool
   135  	}{
   136  		{
   137  			block:      newBlock(header1, commitAtH10),
   138  			parts:      validPartSet,
   139  			seenCommit: seenCommit1,
   140  		},
   141  
   142  		{
   143  			block:     nil,
   144  			wantPanic: "only save a non-nil block",
   145  		},
   146  
   147  		{
   148  			block: newBlock( // New block at height 5 in empty block store is fine
   149  				types.Header{
   150  					Version:         version.Consensus{Block: version.BlockProtocol},
   151  					Height:          5,
   152  					ChainID:         "block_test",
   153  					Time:            tmtime.Now(),
   154  					ProposerAddress: tmrand.Bytes(crypto.AddressSize)},
   155  				makeTestCommit(5, tmtime.Now()),
   156  			),
   157  			parts:      validPartSet,
   158  			seenCommit: makeTestCommit(5, tmtime.Now()),
   159  		},
   160  
   161  		{
   162  			block:     newBlock(header1, commitAtH10),
   163  			parts:     incompletePartSet,
   164  			wantPanic: "only save complete block", // incomplete parts
   165  		},
   166  
   167  		{
   168  			block:             newBlock(header1, commitAtH10),
   169  			parts:             validPartSet,
   170  			seenCommit:        seenCommit1,
   171  			corruptCommitInDB: true, // Corrupt the DB's commit entry
   172  			wantPanic:         "error reading block commit",
   173  		},
   174  
   175  		{
   176  			block:            newBlock(header1, commitAtH10),
   177  			parts:            validPartSet,
   178  			seenCommit:       seenCommit1,
   179  			wantPanic:        "unmarshal to tmproto.BlockMeta",
   180  			corruptBlockInDB: true, // Corrupt the DB's block entry
   181  		},
   182  
   183  		{
   184  			block:      newBlock(header1, commitAtH10),
   185  			parts:      validPartSet,
   186  			seenCommit: seenCommit1,
   187  
   188  			// Expecting no error and we want a nil back
   189  			eraseSeenCommitInDB: true,
   190  		},
   191  
   192  		{
   193  			block:      newBlock(header1, commitAtH10),
   194  			parts:      validPartSet,
   195  			seenCommit: seenCommit1,
   196  
   197  			corruptSeenCommitInDB: true,
   198  			wantPanic:             "error reading block seen commit",
   199  		},
   200  
   201  		{
   202  			block:      newBlock(header1, commitAtH10),
   203  			parts:      validPartSet,
   204  			seenCommit: seenCommit1,
   205  
   206  			// Expecting no error and we want a nil back
   207  			eraseCommitInDB: true,
   208  		},
   209  	}
   210  
   211  	type quad struct {
   212  		block  *types.Block
   213  		commit *types.Commit
   214  		meta   *types.BlockMeta
   215  
   216  		seenCommit *types.Commit
   217  	}
   218  
   219  	for i, tuple := range tuples {
   220  		tuple := tuple
   221  		bs, db := freshBlockStore()
   222  		// SaveBlock
   223  		res, err, panicErr := doFn(func() (interface{}, error) {
   224  			bs.SaveBlock(tuple.block, tuple.parts, tuple.seenCommit)
   225  			if tuple.block == nil {
   226  				return nil, nil
   227  			}
   228  
   229  			if tuple.corruptBlockInDB {
   230  				err := db.Set(blockMetaKey(tuple.block.Height), []byte("block-bogus"))
   231  				require.NoError(t, err)
   232  			}
   233  			bBlock := bs.LoadBlock(tuple.block.Height)
   234  			bBlockMeta := bs.LoadBlockMeta(tuple.block.Height)
   235  
   236  			if tuple.eraseSeenCommitInDB {
   237  				err := db.Delete(seenCommitKey(tuple.block.Height))
   238  				require.NoError(t, err)
   239  			}
   240  			if tuple.corruptSeenCommitInDB {
   241  				err := db.Set(seenCommitKey(tuple.block.Height), []byte("bogus-seen-commit"))
   242  				require.NoError(t, err)
   243  			}
   244  			bSeenCommit := bs.LoadSeenCommit(tuple.block.Height)
   245  
   246  			commitHeight := tuple.block.Height - 1
   247  			if tuple.eraseCommitInDB {
   248  				err := db.Delete(blockCommitKey(commitHeight))
   249  				require.NoError(t, err)
   250  			}
   251  			if tuple.corruptCommitInDB {
   252  				err := db.Set(blockCommitKey(commitHeight), []byte("foo-bogus"))
   253  				require.NoError(t, err)
   254  			}
   255  			bCommit := bs.LoadBlockCommit(commitHeight)
   256  			return &quad{block: bBlock, seenCommit: bSeenCommit, commit: bCommit,
   257  				meta: bBlockMeta}, nil
   258  		})
   259  
   260  		if subStr := tuple.wantPanic; subStr != "" {
   261  			if panicErr == nil {
   262  				t.Errorf("#%d: want a non-nil panic", i)
   263  			} else if got := fmt.Sprintf("%#v", panicErr); !strings.Contains(got, subStr) {
   264  				t.Errorf("#%d:\n\tgotErr: %q\nwant substring: %q", i, got, subStr)
   265  			}
   266  			continue
   267  		}
   268  
   269  		if tuple.wantErr {
   270  			if err == nil {
   271  				t.Errorf("#%d: got nil error", i)
   272  			}
   273  			continue
   274  		}
   275  
   276  		assert.Nil(t, panicErr, "#%d: unexpected panic", i)
   277  		assert.Nil(t, err, "#%d: expecting a non-nil error", i)
   278  		qua, ok := res.(*quad)
   279  		if !ok || qua == nil {
   280  			t.Errorf("#%d: got nil quad back; gotType=%T", i, res)
   281  			continue
   282  		}
   283  		if tuple.eraseSeenCommitInDB {
   284  			assert.Nil(t, qua.seenCommit,
   285  				"erased the seenCommit in the DB hence we should get back a nil seenCommit")
   286  		}
   287  		if tuple.eraseCommitInDB {
   288  			assert.Nil(t, qua.commit,
   289  				"erased the commit in the DB hence we should get back a nil commit")
   290  		}
   291  	}
   292  }
   293  
   294  func TestLoadBaseMeta(t *testing.T) {
   295  	config := cfg.ResetTestRoot("blockchain_reactor_test")
   296  	defer os.RemoveAll(config.RootDir)
   297  	state, err := sm.MakeGenesisStateFromFile(config.GenesisFile())
   298  	require.NoError(t, err)
   299  	bs := NewBlockStore(dbm.NewMemDB())
   300  
   301  	for h := int64(1); h <= 10; h++ {
   302  		block := factory.MakeBlock(state, h, new(types.Commit))
   303  		partSet := block.MakePartSet(2)
   304  		seenCommit := makeTestCommit(h, tmtime.Now())
   305  		bs.SaveBlock(block, partSet, seenCommit)
   306  	}
   307  
   308  	pruned, err := bs.PruneBlocks(4)
   309  	require.NoError(t, err)
   310  	assert.EqualValues(t, 3, pruned)
   311  
   312  	baseBlock := bs.LoadBaseMeta()
   313  	assert.EqualValues(t, 4, baseBlock.Header.Height)
   314  	assert.EqualValues(t, 4, bs.Base())
   315  }
   316  
   317  func TestLoadBlockPart(t *testing.T) {
   318  	bs, db := freshBlockStore()
   319  	height, index := int64(10), 1
   320  	loadPart := func() (interface{}, error) {
   321  		part := bs.LoadBlockPart(height, index)
   322  		return part, nil
   323  	}
   324  
   325  	// Initially no contents.
   326  	// 1. Requesting for a non-existent block shouldn't fail
   327  	res, _, panicErr := doFn(loadPart)
   328  	require.Nil(t, panicErr, "a non-existent block part shouldn't cause a panic")
   329  	require.Nil(t, res, "a non-existent block part should return nil")
   330  
   331  	// 2. Next save a corrupted block then try to load it
   332  	err := db.Set(blockPartKey(height, index), []byte("Tendermint"))
   333  	require.NoError(t, err)
   334  	res, _, panicErr = doFn(loadPart)
   335  	require.NotNil(t, panicErr, "expecting a non-nil panic")
   336  	require.Contains(t, panicErr.Error(), "unmarshal to tmproto.Part failed")
   337  
   338  	// 3. A good block serialized and saved to the DB should be retrievable
   339  	pb1, err := part1.ToProto()
   340  	require.NoError(t, err)
   341  	err = db.Set(blockPartKey(height, index), mustEncode(pb1))
   342  	require.NoError(t, err)
   343  	gotPart, _, panicErr := doFn(loadPart)
   344  	require.Nil(t, panicErr, "an existent and proper block should not panic")
   345  	require.Nil(t, res, "a properly saved block should return a proper block")
   346  	require.Equal(t, gotPart.(*types.Part), part1,
   347  		"expecting successful retrieval of previously saved block")
   348  }
   349  
   350  func TestPruneBlocks(t *testing.T) {
   351  	config := cfg.ResetTestRoot("blockchain_reactor_test")
   352  	defer os.RemoveAll(config.RootDir)
   353  	state, err := sm.MakeGenesisStateFromFile(config.GenesisFile())
   354  	require.NoError(t, err)
   355  	db := dbm.NewMemDB()
   356  	bs := NewBlockStore(db)
   357  	assert.EqualValues(t, 0, bs.Base())
   358  	assert.EqualValues(t, 0, bs.Height())
   359  	assert.EqualValues(t, 0, bs.Size())
   360  
   361  	_, err = bs.PruneBlocks(0)
   362  	require.Error(t, err)
   363  
   364  	// make more than 1000 blocks, to test batch deletions
   365  	for h := int64(1); h <= 1500; h++ {
   366  		block := factory.MakeBlock(state, h, new(types.Commit))
   367  		partSet := block.MakePartSet(2)
   368  		seenCommit := makeTestCommit(h, tmtime.Now())
   369  		bs.SaveBlock(block, partSet, seenCommit)
   370  	}
   371  
   372  	assert.EqualValues(t, 1, bs.Base())
   373  	assert.EqualValues(t, 1500, bs.Height())
   374  	assert.EqualValues(t, 1500, bs.Size())
   375  
   376  	prunedBlock := bs.LoadBlock(1199)
   377  
   378  	// Check that basic pruning works
   379  	pruned, err := bs.PruneBlocks(1200)
   380  	require.NoError(t, err)
   381  	assert.EqualValues(t, 1199, pruned)
   382  	assert.EqualValues(t, 1200, bs.Base())
   383  	assert.EqualValues(t, 1500, bs.Height())
   384  	assert.EqualValues(t, 301, bs.Size())
   385  
   386  	require.NotNil(t, bs.LoadBlock(1200))
   387  	require.Nil(t, bs.LoadBlock(1199))
   388  	require.Nil(t, bs.LoadBlockByHash(prunedBlock.Hash()))
   389  	require.Nil(t, bs.LoadBlockCommit(1199))
   390  	require.Nil(t, bs.LoadBlockMeta(1199))
   391  	require.Nil(t, bs.LoadBlockPart(1199, 1))
   392  
   393  	for i := int64(1); i < 1200; i++ {
   394  		require.Nil(t, bs.LoadBlock(i))
   395  	}
   396  	for i := int64(1200); i <= 1500; i++ {
   397  		require.NotNil(t, bs.LoadBlock(i))
   398  	}
   399  
   400  	// Pruning below the current base should not error
   401  	_, err = bs.PruneBlocks(1199)
   402  	require.NoError(t, err)
   403  
   404  	// Pruning to the current base should work
   405  	pruned, err = bs.PruneBlocks(1200)
   406  	require.NoError(t, err)
   407  	assert.EqualValues(t, 0, pruned)
   408  
   409  	// Pruning again should work
   410  	pruned, err = bs.PruneBlocks(1300)
   411  	require.NoError(t, err)
   412  	assert.EqualValues(t, 100, pruned)
   413  	assert.EqualValues(t, 1300, bs.Base())
   414  
   415  	// Pruning beyond the current height should error
   416  	_, err = bs.PruneBlocks(1501)
   417  	require.Error(t, err)
   418  
   419  	// Pruning to the current height should work
   420  	pruned, err = bs.PruneBlocks(1500)
   421  	require.NoError(t, err)
   422  	assert.EqualValues(t, 200, pruned)
   423  	assert.Nil(t, bs.LoadBlock(1499))
   424  	assert.NotNil(t, bs.LoadBlock(1500))
   425  	assert.Nil(t, bs.LoadBlock(1501))
   426  }
   427  
   428  func TestLoadBlockMeta(t *testing.T) {
   429  	bs, db := freshBlockStore()
   430  	height := int64(10)
   431  	loadMeta := func() (interface{}, error) {
   432  		meta := bs.LoadBlockMeta(height)
   433  		return meta, nil
   434  	}
   435  
   436  	// Initially no contents.
   437  	// 1. Requesting for a non-existent blockMeta shouldn't fail
   438  	res, _, panicErr := doFn(loadMeta)
   439  	require.Nil(t, panicErr, "a non-existent blockMeta shouldn't cause a panic")
   440  	require.Nil(t, res, "a non-existent blockMeta should return nil")
   441  
   442  	// 2. Next save a corrupted blockMeta then try to load it
   443  	err := db.Set(blockMetaKey(height), []byte("Tendermint-Meta"))
   444  	require.NoError(t, err)
   445  	res, _, panicErr = doFn(loadMeta)
   446  	require.NotNil(t, panicErr, "expecting a non-nil panic")
   447  	require.Contains(t, panicErr.Error(), "unmarshal to tmproto.BlockMeta")
   448  
   449  	// 3. A good blockMeta serialized and saved to the DB should be retrievable
   450  	meta := &types.BlockMeta{Header: types.Header{
   451  		Version: version.Consensus{
   452  			Block: version.BlockProtocol, App: 0}, Height: 1, ProposerAddress: tmrand.Bytes(crypto.AddressSize)}}
   453  	pbm := meta.ToProto()
   454  	err = db.Set(blockMetaKey(height), mustEncode(pbm))
   455  	require.NoError(t, err)
   456  	gotMeta, _, panicErr := doFn(loadMeta)
   457  	require.Nil(t, panicErr, "an existent and proper block should not panic")
   458  	require.Nil(t, res, "a properly saved blockMeta should return a proper blocMeta ")
   459  	pbmeta := meta.ToProto()
   460  	if gmeta, ok := gotMeta.(*types.BlockMeta); ok {
   461  		pbgotMeta := gmeta.ToProto()
   462  		require.Equal(t, mustEncode(pbmeta), mustEncode(pbgotMeta),
   463  			"expecting successful retrieval of previously saved blockMeta")
   464  	}
   465  }
   466  
   467  func TestBlockFetchAtHeight(t *testing.T) {
   468  	state, bs, cleanup := makeStateAndBlockStore(log.NewNopLogger())
   469  	defer cleanup()
   470  	require.Equal(t, bs.Height(), int64(0), "initially the height should be zero")
   471  	block := factory.MakeBlock(state, bs.Height()+1, new(types.Commit))
   472  
   473  	partSet := block.MakePartSet(2)
   474  	seenCommit := makeTestCommit(10, tmtime.Now())
   475  	bs.SaveBlock(block, partSet, seenCommit)
   476  	require.Equal(t, bs.Height(), block.Header.Height, "expecting the new height to be changed")
   477  
   478  	blockAtHeight := bs.LoadBlock(bs.Height())
   479  	b1, err := block.ToProto()
   480  	require.NoError(t, err)
   481  	b2, err := blockAtHeight.ToProto()
   482  	require.NoError(t, err)
   483  	bz1 := mustEncode(b1)
   484  	bz2 := mustEncode(b2)
   485  	require.Equal(t, bz1, bz2)
   486  	require.Equal(t, block.Hash(), blockAtHeight.Hash(),
   487  		"expecting a successful load of the last saved block")
   488  
   489  	blockAtHeightPlus1 := bs.LoadBlock(bs.Height() + 1)
   490  	require.Nil(t, blockAtHeightPlus1, "expecting an unsuccessful load of Height()+1")
   491  	blockAtHeightPlus2 := bs.LoadBlock(bs.Height() + 2)
   492  	require.Nil(t, blockAtHeightPlus2, "expecting an unsuccessful load of Height()+2")
   493  }
   494  
   495  func TestSeenAndCanonicalCommit(t *testing.T) {
   496  	bs, _ := freshBlockStore()
   497  	height := int64(2)
   498  	loadCommit := func() (interface{}, error) {
   499  		meta := bs.LoadSeenCommit(height)
   500  		return meta, nil
   501  	}
   502  
   503  	// Initially no contents.
   504  	// 1. Requesting for a non-existent blockMeta shouldn't fail
   505  	res, _, panicErr := doFn(loadCommit)
   506  	require.Nil(t, panicErr, "a non-existent blockMeta shouldn't cause a panic")
   507  	require.Nil(t, res, "a non-existent blockMeta should return nil")
   508  
   509  	// produce a few blocks and check that the correct seen and cannoncial commits
   510  	// are persisted.
   511  	for h := int64(3); h <= 5; h++ {
   512  		c1 := bs.LoadSeenCommit(h)
   513  		require.Nil(t, c1)
   514  		c2 := bs.LoadBlockCommit(h - 1)
   515  		require.Nil(t, c2)
   516  		blockCommit := makeTestCommit(h-1, tmtime.Now())
   517  		block := factory.MakeBlock(state, h, blockCommit)
   518  		partSet := block.MakePartSet(2)
   519  		seenCommit := makeTestCommit(h, tmtime.Now())
   520  		bs.SaveBlock(block, partSet, seenCommit)
   521  		c3 := bs.LoadSeenCommit(h)
   522  		require.Equal(t, seenCommit.Hash(), c3.Hash())
   523  		// the previous seen commit should be removed
   524  		c4 := bs.LoadSeenCommit(h - 1)
   525  		require.Nil(t, c4)
   526  		c5 := bs.LoadBlockCommit(h)
   527  		require.Nil(t, c5)
   528  		c6 := bs.LoadBlockCommit(h - 1)
   529  		require.Equal(t, blockCommit.Hash(), c6.Hash())
   530  	}
   531  
   532  }
   533  
   534  func doFn(fn func() (interface{}, error)) (res interface{}, err error, panicErr error) {
   535  	defer func() {
   536  		if r := recover(); r != nil {
   537  			switch e := r.(type) {
   538  			case error:
   539  				panicErr = e
   540  			case string:
   541  				panicErr = fmt.Errorf("%s", e)
   542  			default:
   543  				if st, ok := r.(fmt.Stringer); ok {
   544  					panicErr = fmt.Errorf("%s", st)
   545  				} else {
   546  					panicErr = fmt.Errorf("%s", debug.Stack())
   547  				}
   548  			}
   549  		}
   550  	}()
   551  
   552  	res, err = fn()
   553  	return res, err, panicErr
   554  }
   555  
   556  func newBlock(hdr types.Header, lastCommit *types.Commit) *types.Block {
   557  	return &types.Block{
   558  		Header:     hdr,
   559  		LastCommit: lastCommit,
   560  	}
   561  }