Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Add btclightclient events and hooks #53

Merged
merged 16 commits into from
Jul 11, 2022
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)
Comment on lines 68 to +71
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This handling of the "previous" doesn't look right. The comment says "if the starting key has not been set" but there is no if to check it, GetTip is called every time, and prevHeaderHeight is set to be that of the tip. Then, if the key is not 0 (is that the right check to do?) we retrieve the header, but not the corresponding height - that stays the height of the tip.

Can we either:

  • delay retrieving the height until we know which header we are dealing with, or
  • return the "info" struct which has the height, so there's no chance to mix it up?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Working on that on the refactoring PR for #57

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)
Comment on lines +130 to +131
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
// tipHeight + 1 - len(addedToMainChain) -> height of the highest common ancestor
addedHeight := tipHeight - uint64(len(addedToMainChain)) + 1 + uint64(idx)
addedHeight := hcaHeight + 1 + uint64(idx)

Since it's already available as a variable, this is much easier to grok.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This will entirely be removed on the PR that resolves #57

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
}
Comment on lines +145 to +161
Copy link
Contributor

@aakoshh aakoshh Jul 11, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We really need to rationalise how we handle headers. The wire format is that - a wire format. Since we already store the height when we insert the data into the store, there's little excuse for not using it to our advantage.

I see this retrieves the whole mainchain, which is much more than what we needed if just used the height of the header we are interested in. Based on that knowledge we would know exactly how far we have to traverse back on the main chain: if we are beyond that height, and we have not reached the header in question, it's not on the main chain.

So, a query like func MainChainDepth(blockHash: BTCBlockHash) -> uint64 can express what we need:

  • Retrieve the header; if it can't be found, return -1 (or an error)
  • Retrieve the tip
  • Traverse the main chain up to tip.height - header.height + 1 deep (should be an input to a variant of GetMainChain)
  • Check that the last block is the one we are looking for, if so, return the depth, otherwise -1 (0 would be the tip itself I suppose, there are 0 blocks building on it)

The caller can project the "at least k-deep" query by getting the depth and comparing.

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
Comment on lines +290 to +291
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This iteration is not guaranteed to return a path to the ancestor.

Once again, it would be much better to work with the "info" versions that have height, so you could quit the iteration early if you are past the point where you have a chance of meeting the ancestor.

If I ask the impossible, e.g. by mixing up the order of child and ancestor, or asking with a non-existent ancestor, or passing blocks which are on different forks, this query will dutifully iterate all the way back to the current oldest block and return the whole path to a block which is not the ancestor.

Ideally it should:

  • Panic if the height of the ancestor is not less than the height of the child.
  • Abandon the iteration if we went lower than the height of the ancestor without finding it and return an empty array.
  • Check that the last block in the array has the ancestor as its parent, otherwise return empty array. (That means we reached the root without visiting the ancestor; maybe they share the height).

}

// 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())
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

They height is in the key, but ideally we should just store the "info" as the Value so we can put more stuff in it than what we can retrieve from the key. Or you can pass the header and the height to the function.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yep, working on that for #57

stop := fn(btcdHeader)
if stop {
break
}
}
}
Loading