-
Notifications
You must be signed in to change notification settings - Fork 165
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
Changes from all commits
b1233ff
6c58337
d90a4be
973aad0
4122827
d97a563
ea4d700
44d8c3d
a1a6aac
bd2a571
e472254
7abbb26
29df7cd
3a4edab
f764d2b
ff8aaaf
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
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; | ||
} |
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) | ||
} | ||
} |
Original file line number | Diff line number | Diff line change | ||||||
---|---|---|---|---|---|---|---|---|
|
@@ -17,6 +17,7 @@ type ( | |||||||
cdc codec.BinaryCodec | ||||||||
storeKey sdk.StoreKey | ||||||||
memKey sdk.StoreKey | ||||||||
hooks types.BTCLightClientHooks | ||||||||
paramstore paramtypes.Subspace | ||||||||
} | ||||||||
) | ||||||||
|
@@ -37,6 +38,7 @@ func NewKeeper( | |||||||
cdc: cdc, | ||||||||
storeKey: storeKey, | ||||||||
memKey: memKey, | ||||||||
hooks: nil, | ||||||||
paramstore: ps, | ||||||||
} | ||||||||
} | ||||||||
|
@@ -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() | ||||||||
|
@@ -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") | ||||||||
|
@@ -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
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
Since it's already available as a variable, this is much easier to grok. There was a problem hiding this comment. Choose a reason for hiding this commentThe 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
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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
The caller can project the "at least k-deep" query by getting the depth and comparing. |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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() { | ||
|
@@ -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
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 Ideally it should:
|
||
} | ||
|
||
// 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 | ||
|
@@ -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() { | ||
|
@@ -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()) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yep, working on that for #57 |
||
stop := fn(btcdHeader) | ||
if stop { | ||
break | ||
} | ||
} | ||
} |
There was a problem hiding this comment.
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, andprevHeaderHeight
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:
There was a problem hiding this comment.
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