Skip to content

Commit

Permalink
feat: Add btclightclient events and hooks (#53)
Browse files Browse the repository at this point in the history
  • Loading branch information
vitsalis authored Jul 11, 2022
1 parent b4d26d4 commit e625af4
Show file tree
Hide file tree
Showing 17 changed files with 940 additions and 95 deletions.
4 changes: 3 additions & 1 deletion app/app.go
Original file line number Diff line number Diff line change
Expand Up @@ -330,7 +330,9 @@ func NewBabylonApp(
epochingKeeper.SetMsgServiceRouter(app.BaseApp.MsgServiceRouter())
app.EpochingKeeper = epochingKeeper

app.BTCLightClientKeeper = *btclightclientkeeper.NewKeeper(appCodec, keys[btclightclienttypes.StoreKey], keys[btclightclienttypes.MemStoreKey], app.GetSubspace(btclightclienttypes.ModuleName))
btclightclientKeeper := *btclightclientkeeper.NewKeeper(appCodec, keys[btclightclienttypes.StoreKey], keys[btclightclienttypes.MemStoreKey], app.GetSubspace(btclightclienttypes.ModuleName))
btclightclientKeeper.SetHooks(btclightclienttypes.NewMultiBTCLightClientHooks())
app.BTCLightClientKeeper = btclightclientKeeper

// TODO for now use mocks, as soon as Checkpoining and lightClient will have correct interfaces
// change to correct implementations
Expand Down
3 changes: 2 additions & 1 deletion proto/babylon/btclightclient/btclightclient.proto
Original file line number Diff line number Diff line change
Expand Up @@ -14,12 +14,13 @@ message BaseBTCHeader {
uint64 height = 2;
}

message HeaderInfo {
message BTCHeaderInfo {
bytes header = 1 [
(gogoproto.customtype) = "github.com/babylonchain/babylon/types.BTCHeaderBytes"
];
bytes hash = 2 [
(gogoproto.customtype) = "github.com/babylonchain/babylon/types.BTCHeaderHashBytes"
];
uint64 height = 3;
}

23 changes: 23 additions & 0 deletions proto/babylon/btclightclient/event.proto
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
syntax = "proto3";
package babylon.btclightclient.v1;

import "gogoproto/gogo.proto";
import "babylon/btclightclient/btclightclient.proto";

option go_package = "github.com/babylonchain/babylon/x/btclightclient/types";

// The header included in the event is the block in the history
// of the current mainchain to which we are rolling back to.
// In other words, there is one rollback event emitted per re-org, to the
// greatest common ancestor of the old and the new fork.
message EventBTCRollBack {
BTCHeaderInfo header = 1;
}

// EventBTCRollForward is emitted on Msg/InsertHeader
// The header included in the event is the one the main chain is extended with.
// In the event of a reorg, each block on the new fork that comes after
// the greatest common ancestor will have a corresponding roll forward event.
message EventBTCRollForward {
BTCHeaderInfo header = 1;
}
2 changes: 1 addition & 1 deletion proto/babylon/btclightclient/query.proto
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@ message QueryMainChainRequest {

// QueryMainChainResponse is response type for the Query/MainChain RPC method.
message QueryMainChainResponse {
repeated HeaderInfo headers = 1;
repeated BTCHeaderInfo headers = 1;

cosmos.base.query.v1beta1.PageResponse pagination = 2;
}
13 changes: 10 additions & 3 deletions x/btclightclient/keeper/grpc_query.go
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,11 @@ func (k Keeper) MainChain(ctx context.Context, req *types.QueryMainChainRequest)
}
// If a starting key has not been set, then the first header is the tip
prevHeader := k.HeadersState(sdkCtx).GetTip()
prevHeaderHash := prevHeader.BlockHash()
prevHeaderHeight, err := k.HeadersState(sdkCtx).GetHeaderHeight(&prevHeaderHash)
if err != nil {
panic("Maintained header does not have a height")
}
// Otherwise, retrieve the header from the key
if len(req.Pagination.Key) != 0 {
headerHash, err := bbl.NewBTCHeaderHashBytesFromBytes(req.Pagination.Key)
Expand All @@ -82,8 +87,9 @@ func (k Keeper) MainChain(ctx context.Context, req *types.QueryMainChainRequest)
return &types.QueryMainChainResponse{}, nil
}

var headers []*types.HeaderInfo
headerInfo := types.NewHeaderInfo(prevHeader)
var headers []*types.BTCHeaderInfo
currentHeight := prevHeaderHeight
headerInfo := types.NewBTCHeaderInfo(prevHeader, prevHeaderHeight)
headers = append(headers, headerInfo)
store := prefix.NewStore(k.HeadersState(sdkCtx).headers, types.HeadersObjectPrefix)

Expand All @@ -94,8 +100,9 @@ func (k Keeper) MainChain(ctx context.Context, req *types.QueryMainChainRequest)
btcdHeader := blockHeaderFromStoredBytes(value)
// If the previous block extends this block, then this block is part of the main chain
if prevHeader.PrevBlock.String() == btcdHeader.BlockHash().String() {
currentHeight -= 1
prevHeader = btcdHeader
headers = append(headers, types.NewHeaderInfo(btcdHeader))
headers = append(headers, types.NewBTCHeaderInfo(btcdHeader, currentHeight))
}
}
return true, nil
Expand Down
23 changes: 23 additions & 0 deletions x/btclightclient/keeper/hooks.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package keeper

import (
"github.com/babylonchain/babylon/x/btclightclient/types"
sdk "github.com/cosmos/cosmos-sdk/types"
)

// Implements BTCLightClientHooks interface
var _ types.BTCLightClientHooks = Keeper{}

// AfterBTCRollBack - call hook if registered
func (k Keeper) AfterBTCRollBack(ctx sdk.Context, headerInfo *types.BTCHeaderInfo) {
if k.hooks != nil {
k.hooks.AfterBTCRollBack(ctx, headerInfo)
}
}

// AfterBTCRollForward - call hook if registered
func (k Keeper) AfterBTCRollForward(ctx sdk.Context, headerInfo *types.BTCHeaderInfo) {
if k.hooks != nil {
k.hooks.AfterBTCRollBack(ctx, headerInfo)
}
}
86 changes: 84 additions & 2 deletions x/btclightclient/keeper/keeper.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ type (
cdc codec.BinaryCodec
storeKey sdk.StoreKey
memKey sdk.StoreKey
hooks types.BTCLightClientHooks
paramstore paramtypes.Subspace
}
)
Expand All @@ -37,6 +38,7 @@ func NewKeeper(
cdc: cdc,
storeKey: storeKey,
memKey: memKey,
hooks: nil,
paramstore: ps,
}
}
Expand All @@ -45,6 +47,16 @@ func (k Keeper) Logger(ctx sdk.Context) log.Logger {
return ctx.Logger().With("module", fmt.Sprintf("x/%s", types.ModuleName))
}

// SetHooks sets the btclightclient hooks
func (k *Keeper) SetHooks(bh types.BTCLightClientHooks) *Keeper {
if k.hooks != nil {
panic("cannot set btclightclient hooks twice")
}
k.hooks = bh

return k
}

// InsertHeader inserts a btcd header into the header state
func (k Keeper) InsertHeader(ctx sdk.Context, header *wire.BlockHeader) error {
headerHash := header.BlockHash()
Expand All @@ -58,7 +70,7 @@ func (k Keeper) InsertHeader(ctx sdk.Context, header *wire.BlockHeader) error {
return types.ErrHeaderParentDoesNotExist.Wrap("parent for provided hash is not maintained")
}

height, err := k.HeadersState(ctx).GetHeaderHeight(&header.PrevBlock)
parentHeight, err := k.HeadersState(ctx).GetHeaderHeight(&header.PrevBlock)
if err != nil {
// Height should always exist if the previous checks have passed
panic("Height for parent is not maintained")
Expand All @@ -73,7 +85,77 @@ func (k Keeper) InsertHeader(ctx sdk.Context, header *wire.BlockHeader) error {
headerWork := types.CalcWork(header)
cumulativeWork := types.CumulativeWork(headerWork, parentWork)

previousTip := k.HeadersState(ctx).GetTip()
// Create the header
k.HeadersState(ctx).CreateHeader(header, height+1, cumulativeWork)
k.HeadersState(ctx).CreateHeader(header, parentHeight+1, cumulativeWork)

// Get the new tip
currentTip := k.HeadersState(ctx).GetTip()

// Variable maintaining the headers that have been added to the main chain
var addedToMainChain []*wire.BlockHeader

// The tip has changed, we need to send events
if !sameBlock(currentTip, previousTip) {
if !sameBlock(currentTip, header) {
panic("The tip was updated but with a different header than the one provided")
}
tipHeight := parentHeight + 1
// Get the highest common ancestor between the new tip and the old tip
// There are two cases:
// 1. The new tip extends the old tip
// - The highest common ancestor is the old tip
// - No need to send a roll-back event
// 2. There has been a chain re-org
// - Need to send a roll-back event
var hca *wire.BlockHeader
var hcaHeight uint64
if isParent(currentTip, previousTip) {
hca = previousTip
hcaHeight = parentHeight
} else {
hca := k.HeadersState(ctx).GetHighestCommonAncestor(previousTip, currentTip)
hcaHash := hca.BlockHash()
hcaHeight, err = k.HeadersState(ctx).GetHeaderHeight(&hcaHash)
if err != nil {
panic("Height for maintained header not available in storage")
}
// chain re-org: trigger a roll-back event to the highest common ancestor
k.triggerRollBack(ctx, hca, hcaHeight)
}
// Find the newly added headers to the main chain
addedToMainChain = k.HeadersState(ctx).GetInOrderAncestorsUntil(currentTip, hca)
// Iterate through the added headers and trigger a roll-forward event
for idx, added := range addedToMainChain {
// tipHeight + 1 - len(addedToMainChain) -> height of the highest common ancestor
addedHeight := tipHeight - uint64(len(addedToMainChain)) + 1 + uint64(idx)
k.triggerRollForward(ctx, added, addedHeight)
}
}

return nil
}

// BlockHeight returns the height of the provided header
func (k Keeper) BlockHeight(ctx sdk.Context, header *wire.BlockHeader) (uint64, error) {
headerHash := header.BlockHash()
return k.HeadersState(ctx).GetHeaderHeight(&headerHash)
}

// HeaderKDeep returns true if a header is at least k-deep on the main chain
func (k Keeper) HeaderKDeep(ctx sdk.Context, header *wire.BlockHeader, depth uint64) bool {
// TODO: optimize to not traverse the entire mainchain by storing the height along with the header
mainchain := k.HeadersState(ctx).GetMainChain()
if depth > uint64(len(mainchain)) {
return false
}
// k-deep -> k headers built on top of the BTC header
// Discard the first `depth` headers
kDeepMainChain := mainchain[depth:]
for _, mainChainHeader := range kDeepMainChain {
if sameBlock(header, mainChainHeader) {
return true
}
}
return false
}
115 changes: 104 additions & 11 deletions x/btclightclient/keeper/state.go
Original file line number Diff line number Diff line change
Expand Up @@ -173,21 +173,16 @@ func (s HeadersState) GetHeadersByHeight(height uint64, f func(*wire.BlockHeader

// GetDescendingHeaders returns a collection of descending headers according to their height
func (s HeadersState) GetDescendingHeaders() []*wire.BlockHeader {
// Get the prefix store for the (height, hash) -> header collection
store := prefix.NewStore(s.headers, types.HeadersObjectPrefix)
// Iterate it in reverse in order to get highest heights first
// TODO: need to verify this assumption
iter := store.ReverseIterator(nil, nil)
defer iter.Close()

var headers []*wire.BlockHeader
for ; iter.Valid(); iter.Next() {
headers = append(headers, blockHeaderFromStoredBytes(iter.Value()))
}
s.iterateReverseHeaders(func(header *wire.BlockHeader) bool {
headers = append(headers, header)
return false
})
return headers
}

// GetMainChain returns the current canonical chain as a collection of block headers
// starting from the tip and ending on the base header
func (s HeadersState) GetMainChain() []*wire.BlockHeader {
// If there is no tip, there is no base header
if !s.TipExists() {
Expand Down Expand Up @@ -215,6 +210,87 @@ func (s HeadersState) GetMainChain() []*wire.BlockHeader {
return chain
}

// GetHighestCommonAncestor traverses the ancestors of both headers
// to identify the common ancestor with the highest height
func (s HeadersState) GetHighestCommonAncestor(header1 *wire.BlockHeader, header2 *wire.BlockHeader) *wire.BlockHeader {
// The algorithm works as follows:
// 1. Initialize a hashmap hash -> bool denoting whether the hash
// of an ancestor of either header1 or header2 has been encountered
// 2. Maintain ancestor1 and ancestor2 as variables that point
// to the current ancestor hash of the header1 and header2 parameters
// 3. Whenever a node is encountered with a hash that is equal to ancestor{1,2},
// update the ancestor{1,2} variables.
// 4. If ancestor1 or ancestor2 is set to the hash table,
// then that's the hash of the earliest ancestor
// 5. Using the hash of the heighest ancestor wait until we get the header bytes
// in order to avoid an extra access.
if isParent(header1, header2) {
return header2
}
if isParent(header2, header1) {
return header1
}
ancestor1 := header1.BlockHash()
ancestor2 := header2.BlockHash()
var encountered map[string]bool
encountered[ancestor1.String()] = true
encountered[ancestor2.String()] = true
var found *chainhash.Hash = nil

var resHeader *wire.BlockHeader = nil

s.iterateReverseHeaders(func(btcdHeader *wire.BlockHeader) bool {
// During iteration, we will encounter an ancestor for which its header hash
// has been set on the hash map.
// However, we do not have the entry yet, so we set the found flag to that hash
// and when we encounter it during iteration we return it.
if found != nil && sameHash(*found, btcdHeader.BlockHash()) {
resHeader = btcdHeader
return true
} else {
if ancestor1 == btcdHeader.BlockHash() {
ancestor1 = btcdHeader.PrevBlock
if encountered[ancestor1.String()] {
found = &ancestor1
}
encountered[ancestor1.String()] = true
}
if ancestor2 == btcdHeader.BlockHash() {
ancestor2 = btcdHeader.PrevBlock
if encountered[ancestor2.String()] {
found = &ancestor2
}
encountered[ancestor2.String()] = true
}
}
return false
})
return resHeader
}

// GetInOrderAncestorsUntil returns the list of nodes starting from the child and ending with the block *before* the `ancestor`.
func (s HeadersState) GetInOrderAncestorsUntil(child *wire.BlockHeader, ancestor *wire.BlockHeader) []*wire.BlockHeader {
currentHeader := child

var ancestors []*wire.BlockHeader
ancestors = append(ancestors, child)
if isParent(child, ancestor) {
return ancestors
}
s.iterateReverseHeaders(func(header *wire.BlockHeader) bool {
if header.BlockHash() == ancestor.BlockHash() {
return true
}
if header.BlockHash().String() == currentHeader.PrevBlock.String() {
currentHeader = header
ancestors = append(ancestors, header)
}
return false
})

return ancestors
}

// HeaderExists Check whether a hash is maintained in storage
func (s HeadersState) HeaderExists(hash *chainhash.Hash) bool {
// Get the prefix store for the hash->height collection
Expand All @@ -235,7 +311,7 @@ func (s HeadersState) TipExists() bool {
return s.tip.Has(tipKey)
}

// updateLongestChain checks whether the tip should be updated and acts accordingly
// updateLongestChain checks whether the tip should be updated and returns true if it does
func (s HeadersState) updateLongestChain(header *wire.BlockHeader, cumulativeWork *big.Int) {
// If there is no existing tip, then the header is set as the tip
if !s.TipExists() {
Expand All @@ -259,3 +335,20 @@ func (s HeadersState) updateLongestChain(header *wire.BlockHeader, cumulativeWor
s.CreateTip(header)
}
}

func (s HeadersState) iterateReverseHeaders(fn func(*wire.BlockHeader) bool) {
// Get the prefix store for the (height, hash) -> header collection
store := prefix.NewStore(s.headers, types.HeadersObjectPrefix)
// Iterate it in reverse in order to get highest heights first
// TODO: need to verify this assumption
iter := store.ReverseIterator(nil, nil)
defer iter.Close()

for ; iter.Valid(); iter.Next() {
btcdHeader := blockHeaderFromStoredBytes(iter.Value())
stop := fn(btcdHeader)
if stop {
break
}
}
}
Loading

0 comments on commit e625af4

Please sign in to comment.