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

zoneconcierge: query inclusion proofs and add them to ProofEpochSealed #243

Merged
merged 14 commits into from
Dec 13, 2022
8 changes: 8 additions & 0 deletions app/app.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ import (
servertypes "github.com/cosmos/cosmos-sdk/server/types"
"github.com/cosmos/cosmos-sdk/testutil/testdata"
sdk "github.com/cosmos/cosmos-sdk/types"
sdkerrors "github.com/cosmos/cosmos-sdk/types/errors"
"github.com/cosmos/cosmos-sdk/types/module"
"github.com/cosmos/cosmos-sdk/version"
"github.com/cosmos/cosmos-sdk/x/auth"
Expand Down Expand Up @@ -407,10 +408,16 @@ func NewBabylonApp(
scopedIBCKeeper,
)

// create Tendermint client
tmClient, err := client.NewClientFromNode(privSigner.ClientCtx.NodeURI) // create a Tendermint client for ZoneConcierge
if err != nil {
panic(fmt.Errorf("couldn't get client from nodeURI %s: %w", privSigner.ClientCtx.NodeURI, err))
}
// create querier for KVStore
storeQuerier, ok := app.CommitMultiStore().(sdk.Queryable)
if !ok {
panic(sdkerrors.Wrap(sdkerrors.ErrUnknownRequest, "multistore doesn't support queries"))
}
zcKeeper := zckeeper.NewKeeper(
appCodec,
keys[zctypes.StoreKey],
Expand All @@ -425,6 +432,7 @@ func NewBabylonApp(
nil, // BTCCheckpoint is set later (TODO: figure out a proper way for this)
epochingKeeper,
tmClient,
storeQuerier,
scopedZoneConciergeKeeper,
)

Expand Down
15 changes: 15 additions & 0 deletions testutil/keeper/zoneconcierge.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,9 @@ import (
channeltypes "github.com/cosmos/ibc-go/v5/modules/core/04-channel/types"
ibcexported "github.com/cosmos/ibc-go/v5/modules/core/exported"
"github.com/stretchr/testify/require"
abci "github.com/tendermint/tendermint/abci/types"
"github.com/tendermint/tendermint/libs/log"
tmcrypto "github.com/tendermint/tendermint/proto/tendermint/crypto"
tmproto "github.com/tendermint/tendermint/proto/tendermint/types"
tmdb "github.com/tendermint/tm-db"
)
Expand Down Expand Up @@ -48,6 +50,18 @@ func (zoneconciergePortKeeper) BindPort(ctx sdk.Context, portID string) *capabil
return &capabilitytypes.Capability{}
}

type zoneconciergeStoreQuerier struct{}

func (zoneconciergeStoreQuerier) Query(req abci.RequestQuery) abci.ResponseQuery {
return abci.ResponseQuery{
ProofOps: &tmcrypto.ProofOps{
Ops: []tmcrypto.ProofOp{
tmcrypto.ProofOp{},
},
},
}
}

func ZoneConciergeKeeper(t testing.TB, checkpointingKeeper types.CheckpointingKeeper, btccKeeper types.BtcCheckpointKeeper, epochingKeeper types.EpochingKeeper, tmClient types.TMClient) (*keeper.Keeper, sdk.Context) {
logger := log.NewNopLogger()

Expand Down Expand Up @@ -84,6 +98,7 @@ func ZoneConciergeKeeper(t testing.TB, checkpointingKeeper types.CheckpointingKe
btccKeeper,
epochingKeeper,
tmClient,
zoneconciergeStoreQuerier{},
capabilityKeeper.ScopeToModule("ZoneconciergeScopedKeeper"),
)

Expand Down
17 changes: 8 additions & 9 deletions x/zoneconcierge/keeper/grpc_query_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@ import (
testkeeper "github.com/babylonchain/babylon/testutil/keeper"
btcctypes "github.com/babylonchain/babylon/x/btccheckpoint/types"
checkpointingtypes "github.com/babylonchain/babylon/x/checkpointing/types"
epochingtypes "github.com/babylonchain/babylon/x/epoching/types"
zctypes "github.com/babylonchain/babylon/x/zoneconcierge/types"
"github.com/golang/mock/gomock"
"github.com/stretchr/testify/require"
Expand Down Expand Up @@ -97,12 +96,12 @@ func FuzzFinalizedChainInfo(f *testing.F) {
czChainID := string(datagen.GenRandomByteArray(czChainIDLen))

// simulate the scenario that a random epoch has ended and finalised
epochNum := datagen.RandomInt(10)
epoch := datagen.GenRandomEpoch()

// mock checkpointing keeper
// TODO: tests with a set of validators
checkpointingKeeper := zctypes.NewMockCheckpointingKeeper(ctrl)
checkpointingKeeper.EXPECT().GetBLSPubKeySet(gomock.Any(), gomock.Eq(epochNum)).Return([]*checkpointingtypes.ValidatorWithBlsKey{}, nil).AnyTimes()
checkpointingKeeper.EXPECT().GetBLSPubKeySet(gomock.Any(), gomock.Eq(epoch.EpochNumber)).Return([]*checkpointingtypes.ValidatorWithBlsKey{}, nil).AnyTimes()
// mock btccheckpoint keeper
// TODO: test with BTCSpvProofs
btccKeeper := zctypes.NewMockBtcCheckpointKeeper(ctrl)
Expand All @@ -111,15 +110,15 @@ func FuzzFinalizedChainInfo(f *testing.F) {
{Key: []*btcctypes.TransactionKey{}},
},
Status: btcctypes.Finalized,
RawCheckpoint: datagen.RandomRawCheckpointDataForEpoch(epochNum).ExpectedOpReturn,
RawCheckpoint: datagen.RandomRawCheckpointDataForEpoch(epoch.EpochNumber).ExpectedOpReturn,
}
btccKeeper.EXPECT().GetEpochData(gomock.Any(), gomock.Eq(epochNum)).Return(mockEpochData).AnyTimes()
btccKeeper.EXPECT().GetEpochData(gomock.Any(), gomock.Eq(epoch.EpochNumber)).Return(mockEpochData).AnyTimes()
mockSubmissionData := &btcctypes.SubmissionData{TxsInfo: []*btcctypes.TransactionInfo{}}
btccKeeper.EXPECT().GetSubmissionData(gomock.Any(), gomock.Any()).Return(mockSubmissionData).AnyTimes()
// mock epoching keeper
epochingKeeper := zctypes.NewMockEpochingKeeper(ctrl)
epochingKeeper.EXPECT().GetEpoch(gomock.Any()).Return(&epochingtypes.Epoch{EpochNumber: epochNum}).AnyTimes()
epochingKeeper.EXPECT().GetHistoricalEpoch(gomock.Any(), gomock.Eq(epochNum)).Return(&epochingtypes.Epoch{EpochNumber: epochNum}, nil).AnyTimes()
epochingKeeper.EXPECT().GetEpoch(gomock.Any()).Return(epoch).AnyTimes()
epochingKeeper.EXPECT().GetHistoricalEpoch(gomock.Any(), gomock.Eq(epoch.EpochNumber)).Return(epoch, nil).AnyTimes()
// mock Tendermint client
// TODO: integration tests with Tendermint
tmClient := zctypes.NewMockTMClient(ctrl)
Expand All @@ -136,8 +135,8 @@ func FuzzFinalizedChainInfo(f *testing.F) {
numForkHeaders := datagen.RandomInt(10) + 1
SimulateHeadersAndForksViaHook(ctx, hooks, czChainID, numHeaders, numForkHeaders)

hooks.AfterEpochEnds(ctx, epochNum)
hooks.AfterRawCheckpointFinalized(ctx, epochNum)
hooks.AfterEpochEnds(ctx, epoch.EpochNumber)
hooks.AfterRawCheckpointFinalized(ctx, epoch.EpochNumber)

// check if the chain info of this epoch is recorded or not
resp, err := zcKeeper.FinalizedChainInfo(ctx, &zctypes.QueryFinalizedChainInfoRequest{ChainId: czChainID, Prove: true})
Expand Down
3 changes: 3 additions & 0 deletions x/zoneconcierge/keeper/keeper.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ type (
btccKeeper types.BtcCheckpointKeeper
epochingKeeper types.EpochingKeeper
tmClient types.TMClient
storeQuerier sdk.Queryable
scopedKeeper types.ScopedKeeper
}
)
Expand All @@ -46,6 +47,7 @@ func NewKeeper(
btccKeeper types.BtcCheckpointKeeper,
epochingKeeper types.EpochingKeeper,
tmClient types.TMClient,
storeQuerier sdk.Queryable,
scopedKeeper types.ScopedKeeper,
) *Keeper {
// set KeyTable if it has not already been set
Expand All @@ -67,6 +69,7 @@ func NewKeeper(
btccKeeper: btccKeeper,
epochingKeeper: epochingKeeper,
tmClient: tmClient,
storeQuerier: storeQuerier,
scopedKeeper: scopedKeeper,
}
}
Expand Down
79 changes: 74 additions & 5 deletions x/zoneconcierge/keeper/proof_epoch_sealed.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Copy link
Contributor

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?

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 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 below getValSetKey as private functions. If other modules under zoneconcierge need to use them in the future, I will move them to x/zoneconcierge/types/keys.go. 👍

epochInfoKey := epochingtypes.EpochInfoKey
epochInfoKey = append(epochInfoKey, sdk.Uint64ToBigEndian(epochNumber)...)
return epochInfoKey
}

func (k Keeper) ProveEpochInfo(ctx sdk.Context, epoch *epochingtypes.Epoch) (*tmcrypto.ProofOps, error) {
Copy link
Contributor

Choose a reason for hiding this comment

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

It seems that ctx is not used. Do we still want to keep it?

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 {
Copy link
Contributor

Choose a reason for hiding this comment

The 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) {
Copy link
Contributor

Choose a reason for hiding this comment

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

Same here, ctx not used

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{}
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
proof *types.ProofEpochSealed = &types.ProofEpochSealed{}
proof = &types.ProofEpochSealed{}

Expand All @@ -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
}
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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
}
37 changes: 30 additions & 7 deletions x/zoneconcierge/keeper/proof_epoch_sealed_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
)
Expand All @@ -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) {
Expand Down Expand Up @@ -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)
Expand All @@ -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))
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
require.False(t, sdkerrors.IsOf(zctypes.ErrInvalidMerkleProof, err))
require.NotErrorIs(t, err, zctypes.ErrInvalidMerkleProof)

} 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))
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
require.True(t, sdkerrors.IsOf(zctypes.ErrInvalidMerkleProof, err))
require.ErrorIs(t, err, zctypes.ErrInvalidMerkleProof)

}
})
}

// - 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)

})
}
52 changes: 52 additions & 0 deletions x/zoneconcierge/keeper/query_kvstore.go
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
}
Loading