-
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: query inclusion proofs and add them to ProofEpochSealed
#243
Changes from 12 commits
4050aeb
d297ba5
b0efb50
eddfbdb
3755d2d
4f3a5ba
ed02aa9
00f9d73
8b29e13
df20622
a0e86ea
adddfb9
65b2fc9
44ba32e
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 | ||||
---|---|---|---|---|---|---|
|
@@ -8,8 +8,45 @@ import ( | |||||
epochingtypes "github.com/babylonchain/babylon/x/epoching/types" | ||||||
"github.com/babylonchain/babylon/x/zoneconcierge/types" | ||||||
sdk "github.com/cosmos/cosmos-sdk/types" | ||||||
sdkerrors "github.com/cosmos/cosmos-sdk/types/errors" | ||||||
tmcrypto "github.com/tendermint/tendermint/proto/tendermint/crypto" | ||||||
) | ||||||
|
||||||
func getEpochInfoKey(epochNumber uint64) []byte { | ||||||
epochInfoKey := epochingtypes.EpochInfoKey | ||||||
epochInfoKey = append(epochInfoKey, sdk.Uint64ToBigEndian(epochNumber)...) | ||||||
return epochInfoKey | ||||||
} | ||||||
|
||||||
func (k Keeper) ProveEpochInfo(ctx sdk.Context, epoch *epochingtypes.Epoch) (*tmcrypto.ProofOps, error) { | ||||||
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. It seems that |
||||||
epochInfoKey := getEpochInfoKey(epoch.EpochNumber) | ||||||
_, _, proof, err := k.QueryStore(epochingtypes.StoreKey, epochInfoKey, epoch.SealerHeader.Height) | ||||||
if err != nil { | ||||||
return nil, err | ||||||
} | ||||||
|
||||||
return proof, nil | ||||||
} | ||||||
|
||||||
func getValSetKey(epochNumber uint64) []byte { | ||||||
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. Same here |
||||||
valSetKey := checkpointingtypes.ValidatorBlsKeySetPrefix | ||||||
valSetKey = append(valSetKey, sdk.Uint64ToBigEndian(epochNumber)...) | ||||||
return valSetKey | ||||||
} | ||||||
|
||||||
func (k Keeper) ProveValSet(ctx sdk.Context, epoch *epochingtypes.Epoch) (*tmcrypto.ProofOps, error) { | ||||||
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. Same here, |
||||||
valSetKey := getValSetKey(epoch.EpochNumber) | ||||||
_, _, proof, err := k.QueryStore(epochingtypes.StoreKey, valSetKey, epoch.SealerHeader.Height) | ||||||
if err != nil { | ||||||
return nil, err | ||||||
} | ||||||
return proof, nil | ||||||
} | ||||||
|
||||||
// ProveEpochSealed proves an epoch has been sealed, i.e., | ||||||
// - the epoch's validator set has a valid multisig over the sealer header | ||||||
// - the epoch's validator set is committed to the sealer header's last_commit_hash | ||||||
// - the epoch's metadata is committed to the sealer header's last_commit_hash | ||||||
func (k Keeper) ProveEpochSealed(ctx sdk.Context, epochNumber uint64) (*types.ProofEpochSealed, error) { | ||||||
var ( | ||||||
proof *types.ProofEpochSealed = &types.ProofEpochSealed{} | ||||||
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
|
||||||
|
@@ -22,9 +59,23 @@ func (k Keeper) ProveEpochSealed(ctx sdk.Context, epochNumber uint64) (*types.Pr | |||||
return nil, err | ||||||
} | ||||||
|
||||||
// TODO: proof of inclusion for epoch metadata in sealer header | ||||||
// get sealer header and the query height | ||||||
epoch, err := k.epochingKeeper.GetHistoricalEpoch(ctx, epochNumber) | ||||||
if err != nil { | ||||||
return nil, err | ||||||
} | ||||||
|
||||||
// TODO: proof of inclusion for validator set in sealer header | ||||||
// proof of inclusion for epoch metadata in sealer header | ||||||
proof.ProofEpochInfo, err = k.ProveEpochInfo(ctx, epoch) | ||||||
if err != nil { | ||||||
return nil, err | ||||||
} | ||||||
|
||||||
// proof of inclusion for validator set in sealer header | ||||||
proof.ProofEpochValSet, err = k.ProveValSet(ctx, epoch) | ||||||
if err != nil { | ||||||
return nil, err | ||||||
} | ||||||
|
||||||
return proof, nil | ||||||
} | ||||||
|
@@ -55,9 +106,6 @@ func VerifyEpochSealed(epoch *epochingtypes.Epoch, rawCkpt *checkpointingtypes.R | |||||
return err | ||||||
} | ||||||
|
||||||
// TODO: Ensure The epoch medatata is committed to the app_hash of the sealer header | ||||||
// TODO: Ensure The validator set is committed to the app_hash of the sealer header | ||||||
|
||||||
// ensure epoch number is same in epoch and rawCkpt | ||||||
if epoch.EpochNumber != rawCkpt.EpochNum { | ||||||
return fmt.Errorf("epoch.EpochNumber (%d) is not equal to rawCkpt.EpochNum (%d)", epoch.EpochNumber, rawCkpt.EpochNum) | ||||||
|
@@ -97,5 +145,26 @@ func VerifyEpochSealed(epoch *epochingtypes.Epoch, rawCkpt *checkpointingtypes.R | |||||
return fmt.Errorf("BLS signature does not match the public key") | ||||||
} | ||||||
|
||||||
// get the Merkle root, i.e., the AppHash of the sealer header | ||||||
root := epoch.SealerHeader.AppHash | ||||||
|
||||||
// Ensure The epoch medatata is committed to the app_hash of the sealer header | ||||||
epochBytes, err := epoch.Marshal() | ||||||
if err != nil { | ||||||
return err | ||||||
} | ||||||
if err := VerifyStore(root, epochingtypes.StoreKey, getEpochInfoKey(epoch.EpochNumber), epochBytes, proof.ProofEpochInfo); err != nil { | ||||||
return sdkerrors.Wrapf(types.ErrInvalidMerkleProof, "invalid inclusion proof for epoch metadata: %w", err) | ||||||
} | ||||||
|
||||||
// Ensure The validator set is committed to the app_hash of the sealer header | ||||||
valSetBytes, err := valSet.Marshal() | ||||||
if err != nil { | ||||||
return err | ||||||
} | ||||||
if err := VerifyStore(root, checkpointingtypes.StoreKey, getValSetKey(epoch.EpochNumber), valSetBytes, proof.ProofEpochValSet); err != nil { | ||||||
return sdkerrors.Wrapf(types.ErrInvalidMerkleProof, "invalid inclusion proof for validator set: %w", err) | ||||||
} | ||||||
|
||||||
return nil | ||||||
} |
Original file line number | Diff line number | Diff line change | ||||
---|---|---|---|---|---|---|
|
@@ -11,6 +11,7 @@ import ( | |||||
zckeeper "github.com/babylonchain/babylon/x/zoneconcierge/keeper" | ||||||
zctypes "github.com/babylonchain/babylon/x/zoneconcierge/types" | ||||||
"github.com/boljen/go-bitmap" | ||||||
sdkerrors "github.com/cosmos/cosmos-sdk/types/errors" | ||||||
"github.com/golang/mock/gomock" | ||||||
"github.com/stretchr/testify/require" | ||||||
) | ||||||
|
@@ -33,14 +34,11 @@ func signBLSWithBitmap(blsSKs []bls12381.PrivateKey, bm bitmap.Bitmap, msg []byt | |||||
// 3. Generate a BLS multisig with >1/3 random validators of the validator set | ||||||
// 4. Generate a checkpoint based on the above validator subset and the above sealer header | ||||||
// 5. Execute ProveEpochSealed where the mocked checkpointing keeper produces the above validator set | ||||||
// 6. (TODO: simulate blocks within these epochs, and generate inclusion proofs for valset and epoch metadata) | ||||||
// 7. Execute VerifyEpochSealed with above epoch, checkpoint and proof, and assert the outcome to be true | ||||||
// 6. Execute VerifyEpochSealed with above epoch, checkpoint and proof, and assert the outcome to be true | ||||||
// | ||||||
// Tested property: proof is valid only when | ||||||
// - BLS sig in proof is valid | ||||||
// - TODO: BLS val set has a valid inclusion proof | ||||||
// - TODO: epoch metadata has a valid inclusion proof | ||||||
func FuzzProofEpochSealed(f *testing.F) { | ||||||
func FuzzProofEpochSealed_BLSSig(f *testing.F) { | ||||||
datagen.AddRandomSeedsToFuzzer(f, 10) | ||||||
|
||||||
f.Fuzz(func(t *testing.T, seed int64) { | ||||||
|
@@ -80,8 +78,12 @@ func FuzzProofEpochSealed(f *testing.F) { | |||||
// mock checkpointing keeper that produces the expected validator set | ||||||
checkpointingKeeper := zctypes.NewMockCheckpointingKeeper(ctrl) | ||||||
checkpointingKeeper.EXPECT().GetBLSPubKeySet(gomock.Any(), gomock.Eq(epoch.EpochNumber)).Return(valSet.ValSet, nil).AnyTimes() | ||||||
// mock epoching keeper | ||||||
epochingKeeper := zctypes.NewMockEpochingKeeper(ctrl) | ||||||
epochingKeeper.EXPECT().GetEpoch(gomock.Any()).Return(epoch).AnyTimes() | ||||||
epochingKeeper.EXPECT().GetHistoricalEpoch(gomock.Any(), gomock.Eq(epoch.EpochNumber)).Return(epoch, nil).AnyTimes() | ||||||
// create zcKeeper and ctx | ||||||
zcKeeper, ctx := testkeeper.ZoneConciergeKeeper(t, checkpointingKeeper, nil, nil, nil) | ||||||
zcKeeper, ctx := testkeeper.ZoneConciergeKeeper(t, checkpointingKeeper, nil, epochingKeeper, nil) | ||||||
|
||||||
// prove | ||||||
proof, err := zcKeeper.ProveEpochSealed(ctx, epoch.EpochNumber) | ||||||
|
@@ -92,9 +94,30 @@ func FuzzProofEpochSealed(f *testing.F) { | |||||
if numSubSet <= numVals*1/3 { // BLS sig does not reach a quorum | ||||||
require.LessOrEqual(t, subsetPower, uint64(numVals*1/3)) | ||||||
require.Error(t, err) | ||||||
require.False(t, sdkerrors.IsOf(zctypes.ErrInvalidMerkleProof, 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
|
||||||
} else { // BLS sig has a valid quorum | ||||||
require.Greater(t, subsetPower, valSet.GetTotalPower()*1/3) | ||||||
require.NoError(t, err) | ||||||
require.True(t, sdkerrors.IsOf(zctypes.ErrInvalidMerkleProof, 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
|
||||||
} | ||||||
}) | ||||||
} | ||||||
|
||||||
// - TODO: epoch metadata has a valid inclusion proof | ||||||
func FuzzProofEpochSealed_Epoch(f *testing.F) { | ||||||
datagen.AddRandomSeedsToFuzzer(f, 10) | ||||||
|
||||||
f.Fuzz(func(t *testing.T, seed int64) { | ||||||
rand.Seed(seed) | ||||||
|
||||||
}) | ||||||
} | ||||||
|
||||||
// - TODO: BLS val set has a valid inclusion proof | ||||||
func FuzzProofEpochSealed_ValSet(f *testing.F) { | ||||||
datagen.AddRandomSeedsToFuzzer(f, 10) | ||||||
|
||||||
f.Fuzz(func(t *testing.T, seed int64) { | ||||||
rand.Seed(seed) | ||||||
|
||||||
}) | ||||||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,52 @@ | ||
package keeper | ||
|
||
import ( | ||
"fmt" | ||
|
||
"github.com/cosmos/cosmos-sdk/store/rootmulti" | ||
abci "github.com/tendermint/tendermint/abci/types" | ||
"github.com/tendermint/tendermint/crypto/merkle" | ||
tmcrypto "github.com/tendermint/tendermint/proto/tendermint/crypto" | ||
) | ||
|
||
// QueryStore queries a KV pair in the KVStore, where | ||
// - moduleStoreKey is the store key of a module, e.g., zctypes.StoreKey | ||
// - key is the key of the queried KV pair, including the prefix, e.g., zctypes.EpochChainInfoKey || chainID in the chain info store | ||
// and returns | ||
// - key of this KV pair | ||
// - value of this KV pair | ||
// - Merkle proof of this KV pair | ||
// - error | ||
// (adapted from https://github.com/cosmos/cosmos-sdk/blob/v0.46.6/baseapp/abci.go#L774-L795) | ||
func (k Keeper) QueryStore(moduleStoreKey string, key []byte, queryHeight int64) ([]byte, []byte, *tmcrypto.ProofOps, error) { | ||
// construct the query path for ABCI query | ||
// since we are querying the DB directly, the path will not need prefix "/store" as done in ABCIQuery | ||
// Instead, it will be formed as "/<moduleStoreKey>/key", e.g., "/epoching/key" | ||
path := fmt.Sprintf("/%s/key", moduleStoreKey) | ||
|
||
// query the KV with Merkle proof | ||
resp := k.storeQuerier.Query(abci.RequestQuery{ | ||
Path: path, | ||
Data: key, | ||
Height: queryHeight, | ||
Prove: true, | ||
}) | ||
if resp.Code != 0 { | ||
return nil, nil, nil, fmt.Errorf("query (with path %s) failed with response: %v", path, resp) | ||
} | ||
|
||
return resp.Key, resp.Value, resp.ProofOps, nil | ||
} | ||
|
||
// VerifyStore verifies whether a KV pair is committed to the Merkle root, with the assistance of a Merkle proof | ||
// (adapted from https://github.com/cosmos/cosmos-sdk/blob/v0.46.6/store/rootmulti/proof_test.go) | ||
func VerifyStore(root []byte, moduleStoreKey string, key []byte, value []byte, proof *tmcrypto.ProofOps) error { | ||
prt := rootmulti.DefaultProofRuntime() | ||
|
||
keypath := merkle.KeyPath{} | ||
keypath = keypath.AppendKey([]byte(moduleStoreKey), merkle.KeyEncodingURL) | ||
keypath = keypath.AppendKey(key, merkle.KeyEncodingURL) | ||
keypathStr := keypath.String() | ||
|
||
return prt.VerifyAbsence(proof, root, keypathStr) // TODO: verify value rather than just existence | ||
} |
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.
Would it be cleaner if we move this function to
x/zoneconcierge/types/keys.go
?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 is possible. However, my concern is that if we do this, then the function has to be public to any module that imports
zoneconciergetypes
. This might be too open. I thus chose to make this function and the belowgetValSetKey
as private functions. If other modules underzoneconcierge
need to use them in the future, I will move them tox/zoneconcierge/types/keys.go
. 👍