-
Notifications
You must be signed in to change notification settings - Fork 170
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
zoneconcierge/epoching: proof that a header is in an epoch #248
Changes from 4 commits
e1c89f9
d0992cf
f4c7035
e7ef783
22cadbe
fb5ba91
f946baf
2b475c4
2d8c22d
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Large diffs are not rendered by default.
Original file line number | Diff line number | Diff line change | ||||
---|---|---|---|---|---|---|
@@ -0,0 +1,126 @@ | ||||||
package keeper | ||||||
|
||||||
import ( | ||||||
"crypto/sha256" | ||||||
"fmt" | ||||||
|
||||||
"github.com/babylonchain/babylon/x/epoching/types" | ||||||
"github.com/cosmos/cosmos-sdk/store/prefix" | ||||||
sdk "github.com/cosmos/cosmos-sdk/types" | ||||||
sdkerrors "github.com/cosmos/cosmos-sdk/types/errors" | ||||||
"github.com/tendermint/tendermint/crypto/merkle" | ||||||
tmcrypto "github.com/tendermint/tendermint/proto/tendermint/crypto" | ||||||
) | ||||||
|
||||||
func (k Keeper) setAppHash(ctx sdk.Context, height uint64, appHash []byte) { | ||||||
store := k.appHashStore(ctx) | ||||||
heightBytes := sdk.Uint64ToBigEndian(height) | ||||||
store.Set(heightBytes, appHash) | ||||||
} | ||||||
|
||||||
// GetAppHash gets the AppHash of the header at the given height | ||||||
func (k Keeper) GetAppHash(ctx sdk.Context, height uint64) ([]byte, error) { | ||||||
store := k.appHashStore(ctx) | ||||||
heightBytes := sdk.Uint64ToBigEndian(height) | ||||||
appHash := store.Get(heightBytes) | ||||||
if appHash == nil { | ||||||
return nil, sdkerrors.Wrapf(types.ErrInvalidHeight, "height %d is now known in DB yet", height) | ||||||
} | ||||||
return appHash, nil | ||||||
} | ||||||
|
||||||
// RecordAppHash stores the AppHash of the current header to KVStore | ||||||
func (k Keeper) RecordAppHash(ctx sdk.Context) { | ||||||
header := ctx.BlockHeader() | ||||||
height := uint64(header.Height) | ||||||
k.setAppHash(ctx, height, header.AppHash) | ||||||
} | ||||||
|
||||||
// GetAllAppHashsForEpoch fetches all AppHashs in the given epoch | ||||||
func (k Keeper) GetAllAppHashsForEpoch(ctx sdk.Context, epochNumber uint64) ([][]byte, error) { | ||||||
epoch, err := k.GetHistoricalEpoch(ctx, epochNumber) | ||||||
if err != nil { | ||||||
return nil, err | ||||||
} | ||||||
// if this epoch is the most recent AND has not ended, then we cannot get all AppHashs for this epoch | ||||||
if k.GetEpoch(ctx).EpochNumber == epoch.EpochNumber && !epoch.IsLastBlock(ctx) { | ||||||
return nil, sdkerrors.Wrapf(types.ErrInvalidHeight, "GetAllAppHashsForEpoch can only be invoked when this epoch has ended") | ||||||
} | ||||||
|
||||||
// fetch each AppHash in this epoch | ||||||
appHashs := [][]byte{} | ||||||
for i := epoch.FirstBlockHeight; i <= uint64(epoch.LastBlockHeader.Height); i++ { | ||||||
appHash, err := k.GetAppHash(ctx, i) | ||||||
if err != nil { | ||||||
return nil, err | ||||||
} | ||||||
appHashs = append(appHashs, appHash) | ||||||
} | ||||||
|
||||||
return appHashs, nil | ||||||
} | ||||||
|
||||||
// RecordAppHashRoot calculates the Merkle root of all AppHashs in the current epoch, and stores it to epoch metadata | ||||||
func (k Keeper) RecordAppHashRoot(ctx sdk.Context) error { | ||||||
epoch := k.GetEpoch(ctx) | ||||||
appHashs, err := k.GetAllAppHashsForEpoch(ctx, epoch.EpochNumber) | ||||||
if err != nil { | ||||||
return err | ||||||
} | ||||||
appHashRoot := merkle.HashFromByteSlices(appHashs) | ||||||
epoch.AppHashRoot = appHashRoot | ||||||
k.setEpochInfo(ctx, epoch.EpochNumber, epoch) | ||||||
return nil | ||||||
} | ||||||
|
||||||
// ProveAppHashInEpoch generates a proof that the given appHash is in a given epoch | ||||||
func (k Keeper) ProveAppHashInEpoch(ctx sdk.Context, height uint64, epochNumber uint64) (*tmcrypto.Proof, error) { | ||||||
// ensure height is inside this epoch | ||||||
epoch, err := k.GetHistoricalEpoch(ctx, epochNumber) | ||||||
if err != nil { | ||||||
return nil, err | ||||||
} | ||||||
if height < epoch.FirstBlockHeight || uint64(epoch.LastBlockHeader.Height) < height { | ||||||
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
Same thing but seems more intuitive. To be cleaner, maybe we can add a method to 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. The |
||||||
return nil, sdkerrors.Wrapf(types.ErrInvalidHeight, "the given height %d is not in epoch %d (interval [%d, %d])", height, epoch.EpochNumber, epoch.FirstBlockHeight, uint64(epoch.LastBlockHeader.Height)) | ||||||
} | ||||||
|
||||||
// calculate index of this height in this epoch | ||||||
idx := height - epoch.FirstBlockHeight | ||||||
|
||||||
// fetch all AppHashs, calculate Merkle tree and proof | ||||||
appHashs, err := k.GetAllAppHashsForEpoch(ctx, epochNumber) | ||||||
if err != nil { | ||||||
return nil, err | ||||||
} | ||||||
_, proofs := merkle.ProofsFromByteSlices(appHashs) | ||||||
|
||||||
return proofs[idx].ToProto(), nil | ||||||
} | ||||||
|
||||||
// VerifyAppHashInclusion verifies whether the given appHash is in the Merkle tree w.r.t. the appHashRoot | ||||||
func VerifyAppHashInclusion(appHash []byte, appHashRoot []byte, proof *tmcrypto.Proof) error { | ||||||
if len(appHash) != sha256.Size { | ||||||
return fmt.Errorf("appHash with length %d is not a Sha256 hash", len(appHash)) | ||||||
} | ||||||
if len(appHashRoot) != sha256.Size { | ||||||
return fmt.Errorf("appHash with length %d is not a Sha256 hash", len(appHashRoot)) | ||||||
} | ||||||
if proof == nil { | ||||||
return fmt.Errorf("proof is nil") | ||||||
} | ||||||
|
||||||
unwrappedProof, err := merkle.ProofFromProto(proof) | ||||||
if err != nil { | ||||||
return err | ||||||
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
Just to be more informative |
||||||
} | ||||||
return unwrappedProof.Verify(appHashRoot, appHash) | ||||||
} | ||||||
|
||||||
// appHashStore returns the KVStore for the AppHash of each header | ||||||
// prefix: AppHashKey | ||||||
// key: height | ||||||
// value: AppHash in bytes | ||||||
func (k Keeper) appHashStore(ctx sdk.Context) prefix.Store { | ||||||
store := ctx.KVStore(k.storeKey) | ||||||
return prefix.NewStore(store, types.AppHashKey) | ||||||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,58 @@ | ||
package keeper_test | ||
|
||
import ( | ||
"math/rand" | ||
"testing" | ||
|
||
"github.com/babylonchain/babylon/testutil/datagen" | ||
"github.com/babylonchain/babylon/x/epoching/keeper" | ||
"github.com/babylonchain/babylon/x/epoching/testepoching" | ||
"github.com/babylonchain/babylon/x/epoching/types" | ||
"github.com/stretchr/testify/require" | ||
) | ||
|
||
func FuzzAppHashChain(f *testing.F) { | ||
datagen.AddRandomSeedsToFuzzer(f, 10) | ||
|
||
f.Fuzz(func(t *testing.T, seed int64) { | ||
rand.Seed(seed) | ||
|
||
helper := testepoching.NewHelper(t) | ||
ctx, k := helper.Ctx, helper.EpochingKeeper | ||
// ensure that the epoch info is correct at the genesis | ||
epoch := k.GetEpoch(ctx) | ||
require.Equal(t, epoch.EpochNumber, uint64(0)) | ||
require.Equal(t, epoch.FirstBlockHeight, uint64(0)) | ||
|
||
// set a random epoch interval | ||
epochInterval := rand.Uint64()%100 + 2 // the epoch interval should at at least 2 | ||
k.SetParams(ctx, types.Params{ | ||
EpochInterval: epochInterval, | ||
}) | ||
|
||
// reach the end of the 1st epoch | ||
expectedHeight := epochInterval | ||
expectedAppHashs := [][]byte{} | ||
for i := uint64(0); i < expectedHeight; i++ { | ||
ctx = helper.GenAndApplyEmptyBlock() | ||
expectedAppHashs = append(expectedAppHashs, ctx.BlockHeader().AppHash) | ||
} | ||
// ensure epoch number is 1 | ||
epoch = k.GetEpoch(ctx) | ||
require.Equal(t, uint64(1), epoch.EpochNumber) | ||
|
||
// ensure appHashs are same as expectedAppHashs | ||
appHashs, err := k.GetAllAppHashsForEpoch(ctx, epoch.EpochNumber) | ||
require.NoError(t, err) | ||
require.Equal(t, expectedAppHashs, appHashs) | ||
|
||
// ensure prover and verifier are correct | ||
randomHeightInEpoch := uint64(rand.Intn(int(expectedHeight)) + 1) | ||
randomAppHash, err := k.GetAppHash(ctx, randomHeightInEpoch) | ||
require.NoError(t, err) | ||
proof, err := k.ProveAppHashInEpoch(ctx, randomHeightInEpoch, epoch.EpochNumber) | ||
require.NoError(t, err) | ||
err = keeper.VerifyAppHashInclusion(randomAppHash, epoch.AppHashRoot, proof) | ||
require.NoError(t, err) | ||
}) | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -4,6 +4,7 @@ import ( | |
"github.com/babylonchain/babylon/x/epoching/types" | ||
"github.com/cosmos/cosmos-sdk/store/prefix" | ||
sdk "github.com/cosmos/cosmos-sdk/types" | ||
sdkerrors "github.com/cosmos/cosmos-sdk/types/errors" | ||
) | ||
|
||
const ( | ||
|
@@ -75,15 +76,15 @@ func (k Keeper) GetHistoricalEpoch(ctx sdk.Context, epochNumber uint64) (*types. | |
return epoch, err | ||
} | ||
|
||
func (k Keeper) RecordLastBlockHeader(ctx sdk.Context) *types.Epoch { | ||
func (k Keeper) RecordLastBlockHeader(ctx sdk.Context) error { | ||
epoch := k.GetEpoch(ctx) | ||
if !epoch.IsLastBlock(ctx) { | ||
panic("RecordLastBlockHeader can only be invoked at the last block of an epoch") | ||
return sdkerrors.Wrapf(types.ErrInvalidHeight, "RecordLastBlockHeader can only be invoked at the last block of an epoch") | ||
} | ||
Comment on lines
83
to
86
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. As the name implies, the caller should ensure that it is this is the last block of the epoch. So maybe we don't need to check it here again. We can add a note about calling this function tho. 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. Hmm yeah such a contract can do the job as well, but I feel checking it here again can be a better precaution for misuses. Now constructing the full epoch metadata becomes quite cryptic with the following steps:
So I feel it might be better to impose as many restrictions to these functions as possible. |
||
header := ctx.BlockHeader() | ||
epoch.LastBlockHeader = &header | ||
k.setEpochInfo(ctx, epoch.EpochNumber, epoch) | ||
return epoch | ||
return nil | ||
} | ||
|
||
// RecordSealerHeaderForPrevEpoch records the sealer header for the previous epoch, | ||
|
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.
It seems that the epoch info is stored twice in the two functions. Is it possible to combine the two into one like
RecordEpochInfo
?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.
Yep that's a good idea. Merged them to
RecordLastHeaderAndAppHashRoot
so that we only write to DB once 👍