From 5d457f699d2aeddf249c5ad489023f2df93a4d02 Mon Sep 17 00:00:00 2001 From: Thomas van Dam Date: Mon, 13 Jan 2025 14:43:31 +0100 Subject: [PATCH] feat(batching): add double batch signing evidence submission Closes: #459 --- app/app.go | 9 +- proto/sedachain/batching/v1/evidence.proto | 40 ++ x/batching/client/cli/tx.go | 132 +++++ x/batching/keeper/endblock.go | 14 +- x/batching/keeper/evidence.go | 216 +++++++ x/batching/keeper/evidence_test.go | 440 ++++++++++++++ x/batching/keeper/integration_test.go | 62 +- x/batching/keeper/keeper.go | 3 + x/batching/keeper/keeper_test.go | 2 +- x/batching/module.go | 10 +- x/batching/types/batch.go | 25 + x/batching/types/codec.go | 14 + x/batching/types/events.go | 14 + x/batching/types/evidence.go | 87 +++ x/batching/types/evidence.pb.go | 655 +++++++++++++++++++++ x/batching/types/evidence_test.go | 214 +++++++ x/batching/types/expected_keepers.go | 11 + x/tally/keeper/integration_test.go | 1 + 18 files changed, 1929 insertions(+), 20 deletions(-) create mode 100644 proto/sedachain/batching/v1/evidence.proto create mode 100644 x/batching/client/cli/tx.go create mode 100644 x/batching/keeper/evidence.go create mode 100644 x/batching/keeper/evidence_test.go create mode 100644 x/batching/types/batch.go create mode 100644 x/batching/types/events.go create mode 100644 x/batching/types/evidence.go create mode 100644 x/batching/types/evidence.pb.go create mode 100644 x/batching/types/evidence_test.go diff --git a/app/app.go b/app/app.go index 992f41d6..80afb423 100644 --- a/app/app.go +++ b/app/app.go @@ -579,7 +579,7 @@ func NewApp( app.AccountKeeper.AddressCodec(), runtime.ProvideCometInfoService(), ) - // If evidence needs to be handled for the app, set routes in router here and seal + // Evidence routes and router are set after instantiating SEDA keepers. app.EvidenceKeeper = *evidenceKeeper wasmDir := filepath.Join(homePath, wasmDirectory) @@ -672,6 +672,7 @@ func NewApp( appCodec, runtime.NewKVStoreService(keys[batchingtypes.StoreKey]), app.StakingKeeper, + app.SlashingKeeper, app.WasmStorageKeeper, app.PubKeyKeeper, contractKeeper, @@ -689,6 +690,12 @@ func NewApp( authtypes.NewModuleAddress(govtypes.ModuleName).String(), ) + // Create evidence router, add batching evidence route, seal it, and set it in the keeper. + evidenceRouter := evidencetypes.NewRouter() + evidenceRouter.AddRoute(batchingtypes.RouteBatchDoubleSign, batchingkeeper.NewBatchDoubleSignHandler(app.BatchingKeeper)) + evidenceRouter.Seal() + app.EvidenceKeeper.SetRouter(evidenceRouter) + /* =================================================== */ /* TRANSFER STACK */ /* =================================================== */ diff --git a/proto/sedachain/batching/v1/evidence.proto b/proto/sedachain/batching/v1/evidence.proto new file mode 100644 index 00000000..05dab2af --- /dev/null +++ b/proto/sedachain/batching/v1/evidence.proto @@ -0,0 +1,40 @@ +syntax = "proto3"; +package sedachain.batching.v1; + +import "amino/amino.proto"; +import "cosmos_proto/cosmos.proto"; + +option go_package = "github.com/sedaprotocol/seda-chain/x/batching/types"; + +// BatchDoubleSign implements the Evidence interface and defines evidence of +// double signing a batch for a given proving scheme. +message BatchDoubleSign { + option (amino.name) = "sedachain/DoubleSign"; + + // batch_number is the number of the batch that the validator double signed. + uint64 batch_number = 1; + + // block_height is the height of the block which includes the batch that the + // validator double signed. + int64 block_height = 2; + + // operator_address is the operator address of the validator committing the + // double signing. + string operator_address = 3 + [ (cosmos_proto.scalar) = "cosmos.AddressString" ]; + + // validator_root is the hex-encoded root of the validator merkle tree. + string validator_root = 4; + + // data_result_root is the hex-encoded root of the data result merkle tree. + string data_result_root = 5; + + // proving_metadata_hash is the hex-encoded hash of the proving metadata. + string proving_metadata_hash = 6; + + // signature is the hex-encoded signature of the validator. + string signature = 7; + + // proving_scheme_index is the SEDA key index of the proving scheme. + uint32 proving_scheme_index = 8; +} diff --git a/x/batching/client/cli/tx.go b/x/batching/client/cli/tx.go new file mode 100644 index 00000000..799e2a31 --- /dev/null +++ b/x/batching/client/cli/tx.go @@ -0,0 +1,132 @@ +package cli + +import ( + "fmt" + "strconv" + + "github.com/spf13/cobra" + + evidencetypes "cosmossdk.io/x/evidence/types" + + "github.com/cosmos/cosmos-sdk/client" + "github.com/cosmos/cosmos-sdk/client/flags" + "github.com/cosmos/cosmos-sdk/client/tx" + codectypes "github.com/cosmos/cosmos-sdk/codec/types" + sdk "github.com/cosmos/cosmos-sdk/types" + + "github.com/sedaprotocol/seda-chain/app/utils" + "github.com/sedaprotocol/seda-chain/x/batching/types" +) + +const ( + // FlagProvingScheme defines a flag to specify the proving scheme. + FlagProvingScheme = "proving-scheme" +) + +// GetTxCmd returns the CLI transaction commands for this module +func GetTxCmd() *cobra.Command { + cmd := &cobra.Command{ + Use: types.ModuleName, + Short: fmt.Sprintf("%s transactions subcommands", types.ModuleName), + SuggestionsMinimumDistance: 2, + RunE: client.ValidateCmd, + } + cmd.AddCommand( + SubmitBatchDoubleSign(), + ) + return cmd +} + +func SubmitBatchDoubleSign() *cobra.Command { + cmd := &cobra.Command{ + Use: "submit-double-sign [batch_height] [block_height] [operator_address] [validator_root] [data_result_root] [proving_metadata_hash] [signature]", + Short: "Submit evidence of a validator double signing a batch", + Args: cobra.ExactArgs(7), + RunE: func(cmd *cobra.Command, args []string) error { + clientCtx, err := client.GetClientTxContext(cmd) + if err != nil { + return err + } + + submitter := clientCtx.GetFromAddress().String() + if submitter == "" { + return fmt.Errorf("set the from address using --from flag") + } + + batchNumber, err := strconv.ParseUint(args[0], 10, 64) + if err != nil { + return fmt.Errorf("invalid batch number: %s", args[0]) + } + + blockHeight, err := strconv.ParseInt(args[1], 10, 64) + if err != nil { + return fmt.Errorf("invalid block height: %s", args[1]) + } + + operatorAddr := args[2] + valAddr := sdk.ValAddress(operatorAddr) + if valAddr.Empty() { + return fmt.Errorf("invalid operator address: %s", args[2]) + } + + validatorRoot := args[3] + if validatorRoot == "" { + return fmt.Errorf("invalid validator root: %s", args[3]) + } + + dataResultRoot := args[4] + if dataResultRoot == "" { + return fmt.Errorf("invalid data result root: %s", args[4]) + } + + provingMetadataHash := args[5] + if provingMetadataHash == "" { + return fmt.Errorf("invalid proving metadata hash: %s", args[5]) + } + + signature := args[6] + if signature == "" { + return fmt.Errorf("invalid signature: %s", args[6]) + } + + // It's easier to use a uint64 as it's the return type of the strconv.ParseUint function + var provingSchemeIndex uint64 + provingSchemeInput, _ := cmd.Flags().GetString(FlagProvingScheme) + if provingSchemeInput != "" { + provingSchemeIndex, err = strconv.ParseUint(provingSchemeInput, 10, 32) + if err != nil || provingSchemeIndex != uint64(utils.SEDAKeyIndexSecp256k1) { + return fmt.Errorf("invalid proving scheme index: %s", provingSchemeInput) + } + } else { + provingSchemeIndex = uint64(utils.SEDAKeyIndexSecp256k1) + } + + evidence := &types.BatchDoubleSign{ + BatchNumber: batchNumber, + BlockHeight: blockHeight, + OperatorAddress: operatorAddr, + ValidatorRoot: validatorRoot, + DataResultRoot: dataResultRoot, + ProvingMetadataHash: provingMetadataHash, + Signature: signature, + ProvingSchemeIndex: uint32(provingSchemeIndex), + } + + evidencePacked, err := codectypes.NewAnyWithValue(evidence) + if err != nil { + return err + } + + msg := &evidencetypes.MsgSubmitEvidence{ + Submitter: submitter, + Evidence: evidencePacked, + } + + return tx.GenerateOrBroadcastTxCLI(clientCtx, cmd.Flags(), msg) + }, + } + + cmd.Flags().String(FlagProvingScheme, "0", fmt.Sprintf("proving scheme index [%d]", utils.SEDAKeyIndexSecp256k1)) + flags.AddTxFlagsToCmd(cmd) + return cmd +} diff --git a/x/batching/keeper/endblock.go b/x/batching/keeper/endblock.go index f9e00454..f524cc82 100644 --- a/x/batching/keeper/endblock.go +++ b/x/batching/keeper/endblock.go @@ -110,19 +110,7 @@ func (k Keeper) ConstructBatch(ctx sdk.Context) (types.Batch, types.DataResultTr provingMetaDataHash = hasher.Sum(nil) } - // Compute the batch ID, which is defined as - // keccak256(batch_number, block_height, validator_root, results_root, proving_metadata_hash) - var hashContent []byte - hashContent = binary.BigEndian.AppendUint64(hashContent, newBatchNum) - //nolint:gosec // G115: We shouldn't get negative block heights anyway. - hashContent = binary.BigEndian.AppendUint64(hashContent, uint64(ctx.BlockHeight())) - hashContent = append(hashContent, valRoot...) - hashContent = append(hashContent, superRoot...) - hashContent = append(hashContent, provingMetaDataHash...) - - hasher := sha3.NewLegacyKeccak256() - hasher.Write(hashContent) - batchID := hasher.Sum(nil) + batchID := types.ComputeBatchID(newBatchNum, ctx.BlockHeight(), valRoot, superRoot, provingMetaDataHash) return types.Batch{ BatchNumber: newBatchNum, diff --git a/x/batching/keeper/evidence.go b/x/batching/keeper/evidence.go new file mode 100644 index 00000000..2da47512 --- /dev/null +++ b/x/batching/keeper/evidence.go @@ -0,0 +1,216 @@ +package keeper + +import ( + "bytes" + "context" + "encoding/hex" + "fmt" + + "github.com/ethereum/go-ethereum/crypto" + + "cosmossdk.io/x/evidence/exported" + evidencetypes "cosmossdk.io/x/evidence/types" + + sdk "github.com/cosmos/cosmos-sdk/types" + stakingtypes "github.com/cosmos/cosmos-sdk/x/staking/types" + + "github.com/sedaprotocol/seda-chain/app/utils" + "github.com/sedaprotocol/seda-chain/x/batching/types" +) + +func NewBatchDoubleSignHandler(keeper Keeper) func(ctx context.Context, evidence exported.Evidence) error { + return func(ctx context.Context, evidence exported.Evidence) error { + return keeper.handleEvidence(ctx, evidence.(*types.BatchDoubleSign)) + } +} + +func (k *Keeper) handleEvidence(ctx context.Context, evidence *types.BatchDoubleSign) error { + sdkCtx := sdk.UnwrapSDKContext(ctx) + + sdkCtx.Logger().Info("received batch double sign evidence", "batch number", evidence.BatchNumber, "operator address", evidence.OperatorAddress, "result root", evidence.DataResultRoot, "validator root", evidence.ValidatorRoot, "proving metadata hash", evidence.ProvingMetadataHash, "proving scheme index", evidence.ProvingSchemeIndex) + + // Validate that a batch exists for the given batch height. + batch, err := k.GetBatchByBatchNumber(ctx, evidence.BatchNumber) + if err != nil { + return err + } + + // Validate the signed batch is different from what was recorded on chain. + fraudulentBatchID, err := evidence.GetBatchID() + if err != nil { + return err + } + + if bytes.Equal(batch.BatchId, fraudulentBatchID) { + return fmt.Errorf("batch IDs are the same") + } + + // Currently we only support secp256k1 signatures so no need to check which proving scheme the validator used. + signatureAddr, err := k.recoverEthAddressFromSecp256k1Signature(fraudulentBatchID, evidence.Signature) + if err != nil { + return err + } + + // Retrieve the validator entry from the previous batch, as they might have changed their public key in the + // fraudulent batch. + validatorEthAddr, err := k.getEthAddressForBatch(ctx, evidence.BatchNumber-1, evidence.OperatorAddress) + if err != nil { + return err + } + + // If the recovered address matches the validator entry they have committed a double sign. + if !bytes.Equal(validatorEthAddr, signatureAddr) { + return fmt.Errorf("recovered address does not match validator entry. Recorded: %s, Got: %s", hex.EncodeToString(validatorEthAddr), hex.EncodeToString(signatureAddr)) + } + + sdkCtx.Logger().Info("confirmed double batch sign", "validator", evidence.OperatorAddress, "batch number", evidence.BatchNumber, "block height", evidence.BlockHeight) + + // Reject evidence if the double-sign is too old. Evidence is considered stale + // if the difference in number of blocks is greater than the allowed + // parameter defined. + // NOTE: The default max age is way larger than the default historical entries + // we should check how CometBFT valdiates the power attached to block level evidence + // and rely on that instead of the staking keeper. + infractionHeight := batch.BlockHeight + ageBlocks := sdkCtx.BlockHeader().Height - infractionHeight + cp := sdkCtx.ConsensusParams() + if cp.Evidence != nil { + if ageBlocks > cp.Evidence.MaxAgeNumBlocks { + sdkCtx.Logger().Info( + "ignored double batch sign; evidence too old", + "validator", evidence.OperatorAddress, + "infraction_height", infractionHeight, + "max_age_num_blocks", cp.Evidence.MaxAgeNumBlocks, + ) + return nil + } + } + + // We need to get the validator voting power at the time of the double sign. + validator, err := k.getValidatorAtHeight(ctx, batch.BlockHeight, evidence.OperatorAddress) + if err != nil { + return err + } + + consAddr, err := validator.GetConsAddr() + if err != nil { + return err + } + + if k.slashingKeeper.IsTombstoned(ctx, consAddr) { + sdkCtx.Logger().Info("validator already tombstoned", "validator", evidence.OperatorAddress) + return nil + } + + slashFractionDoubleSign, err := k.slashingKeeper.SlashFractionDoubleSign(ctx) + if err != nil { + return err + } + + // We use the staking keeper instead of the slashing keeper so we can emit the correct events. + validatorPower := validator.GetConsensusPower(sdk.DefaultPowerReduction) + coinsBurned, err := k.stakingKeeper.Slash( + ctx, + consAddr, + batch.BlockHeight, + validatorPower, + slashFractionDoubleSign, + ) + if err != nil { + return err + } + + err = k.jailAndTombstone(ctx, consAddr, validator.IsJailed()) + if err != nil { + return err + } + + sdkCtx.EventManager().EmitEvent( + sdk.NewEvent( + types.EventTypeSlash, + sdk.NewAttribute(types.AttributeOperatorAddress, evidence.OperatorAddress), + sdk.NewAttribute(types.AttributePower, fmt.Sprintf("%d", validatorPower)), + sdk.NewAttribute(types.AttributeReason, types.AttributeValueBatchDoubleSign), + sdk.NewAttribute(types.AttributeBurnedCoins, coinsBurned.String()), + sdk.NewAttribute(types.AttributeBatchNumber, fmt.Sprintf("%d", evidence.BatchNumber)), + sdk.NewAttribute(types.AttributeProvingScheme, fmt.Sprintf("%d", evidence.ProvingSchemeIndex)), + ), + ) + + return nil +} + +func (k *Keeper) recoverEthAddressFromSecp256k1Signature(batchID []byte, signature string) ([]byte, error) { + signatureBytes, err := hex.DecodeString(signature) + if err != nil { + return nil, err + } + + signaturePubkey, err := crypto.Ecrecover(batchID, signatureBytes) + if err != nil { + return nil, err + } + + signatureAddr, err := utils.PubKeyToEthAddress(signaturePubkey) + if err != nil { + return nil, err + } + + return signatureAddr, nil +} + +func (k *Keeper) getEthAddressForBatch(ctx context.Context, batchNumber uint64, operatorAddr string) ([]byte, error) { + operatorAddrBytes, err := k.validatorAddressCodec.StringToBytes(operatorAddr) + if err != nil { + return nil, err + } + + validatorEntry, err := k.GetValidatorTreeEntry(ctx, batchNumber, operatorAddrBytes) + if err != nil { + return nil, err + } + + return validatorEntry.EthAddress, nil +} + +// Retrieves the validator as it was at a given height, provided the height is within the historical info window. +func (k *Keeper) getValidatorAtHeight(ctx context.Context, height int64, operatorAddr string) (validator stakingtypes.Validator, err error) { + historicalInfo, err := k.stakingKeeper.GetHistoricalInfo(ctx, height) + if err != nil { + return validator, err + } + + for _, val := range historicalInfo.Valset { + if val.OperatorAddress == operatorAddr { + validator = val + break + } + } + + if validator.OperatorAddress != operatorAddr { + return validator, fmt.Errorf("validator not found") + } + + return validator, nil +} + +func (k *Keeper) jailAndTombstone(ctx context.Context, consAddr []byte, jailed bool) error { + if !jailed { + err := k.slashingKeeper.Jail(ctx, consAddr) + if err != nil { + return err + } + } + + err := k.slashingKeeper.JailUntil(ctx, consAddr, evidencetypes.DoubleSignJailEndTime) + if err != nil { + return err + } + + err = k.slashingKeeper.Tombstone(ctx, consAddr) + if err != nil { + return err + } + + return nil +} diff --git a/x/batching/keeper/evidence_test.go b/x/batching/keeper/evidence_test.go new file mode 100644 index 00000000..1db57560 --- /dev/null +++ b/x/batching/keeper/evidence_test.go @@ -0,0 +1,440 @@ +package keeper_test + +import ( + "encoding/hex" + "fmt" + "testing" + + sdkstakingtypes "github.com/cosmos/cosmos-sdk/x/staking/types" + "github.com/stretchr/testify/require" + + "cosmossdk.io/collections" + sdkmath "cosmossdk.io/math" + cmtproto "github.com/cometbft/cometbft/proto/tendermint/types" + "github.com/ethereum/go-ethereum/crypto" + + "github.com/sedaprotocol/seda-chain/x/batching/keeper" + "github.com/sedaprotocol/seda-chain/x/batching/types" +) + +func TestHandleEvidence(t *testing.T) { + f := initFixture(t) + + valAddrs, privKeys, validators := generateFirstBatch(t, f, 10) + + fraudAddr := valAddrs[0] + fraudPrivKey := privKeys[0] + fraudulentValidator := validators[0] + + // Keep track of all validator tokens before the evidence is handled + validatorTokensBefore := make(map[string]sdkmath.Int, len(validators)) + for _, validator := range validators { + validatorTokensBefore[validator.OperatorAddress] = validator.GetTokens() + } + + // Store the legitimate batch for which the validator will be double signing + doubleSignBatchNumber := uint64(2) + doubleSignBlockHeight := int64(4) + + batchToDoubleSign := types.Batch{ + BatchId: []byte("batch2"), + BatchNumber: doubleSignBatchNumber, + BlockHeight: doubleSignBlockHeight, + } + err := f.batchingKeeper.SetNewBatch(f.Context(), batchToDoubleSign, types.DataResultTreeEntries{}, []types.ValidatorTreeEntry{}) + require.NoError(t, err) + + f.stakingKeeper.SetHistoricalInfo(f.Context(), doubleSignBlockHeight, &sdkstakingtypes.HistoricalInfo{ + Valset: validators, + }) + + // Create evidence for a fraudulent batch + evidence := &types.BatchDoubleSign{ + BatchNumber: doubleSignBatchNumber, + BlockHeight: doubleSignBlockHeight, + OperatorAddress: fraudAddr.String(), + DataResultRoot: "6027c97e8b0588f86a9e140d73a31af5ee0d37b93ff0f2f54f5305d0f2ea3fd9", + ValidatorRoot: "2306d94cc69db8435c56294ff7f27cf3a7d042f8965e2d76f38c63a616a937b0", + ProvingMetadataHash: "0000000000000000000000000000000000000000000000000000000000000000", + ProvingSchemeIndex: 0, + } + + // Sign fraudulent batch + fraudulentBatchID, err := evidence.GetBatchID() + require.NoError(t, err) + signature, err := crypto.Sign(fraudulentBatchID, fraudPrivKey) + require.NoError(t, err) + + evidence.Signature = hex.EncodeToString(signature) + + // Test handling evidence + handler := keeper.NewBatchDoubleSignHandler(f.batchingKeeper) + err = handler(f.Context().WithBlockHeight(doubleSignBlockHeight), evidence) + require.NoError(t, err) + + // Verify validator was slashed, jailed, and tombstoned + fraudulentValidatorAfter, err := f.stakingKeeper.GetValidator(f.Context(), fraudAddr) + require.NoError(t, err) + require.True(t, fraudulentValidatorAfter.IsJailed()) + tokensAfter := fraudulentValidatorAfter.GetTokens() + require.True(t, tokensAfter.LT(fraudulentValidator.GetTokens())) + consAddr, err := fraudulentValidatorAfter.GetConsAddr() + require.NoError(t, err) + require.True(t, f.slashingKeeper.IsTombstoned(f.Context(), consAddr)) + + // Verify all other validators are unaffected + for _, valAddr := range valAddrs[1:] { + validatorAfter, err := f.stakingKeeper.GetValidator(f.Context(), valAddr) + require.NoError(t, err) + require.False(t, validatorAfter.IsJailed()) + require.Equal(t, validatorAfter.GetTokens(), validatorTokensBefore[validatorAfter.OperatorAddress]) + } +} + +func TestHandleEvidence_DifferentBlockHeight(t *testing.T) { + f := initFixture(t) + valAddrs, privKeys, validators := generateFirstBatch(t, f, 1) + + fraudAddr := valAddrs[0] + fraudPrivKey := privKeys[0] + + // Keep track of all validator tokens before the evidence is handled + validatorTokensBefore := make(map[string]sdkmath.Int, len(validators)) + for _, validator := range validators { + validatorTokensBefore[validator.OperatorAddress] = validator.GetTokens() + } + + // Store the legitimate batch for which the validator will be double signing + doubleSignBatchNumber := uint64(2) + doubleSignBlockHeight := int64(4) + + batchToDoubleSign := types.Batch{ + BatchId: []byte("batch2"), + BatchNumber: doubleSignBatchNumber, + BlockHeight: doubleSignBlockHeight, + } + err := f.batchingKeeper.SetNewBatch(f.Context(), batchToDoubleSign, types.DataResultTreeEntries{}, []types.ValidatorTreeEntry{}) + require.NoError(t, err) + + f.stakingKeeper.SetHistoricalInfo(f.Context(), doubleSignBlockHeight, &sdkstakingtypes.HistoricalInfo{ + Valset: validators, + }) + + // Create evidence for a fraudulent batch + evidence := &types.BatchDoubleSign{ + BatchNumber: doubleSignBatchNumber, + BlockHeight: doubleSignBlockHeight + 100, + OperatorAddress: fraudAddr.String(), + DataResultRoot: "6027c97e8b0588f86a9e140d73a31af5ee0d37b93ff0f2f54f5305d0f2ea3fd9", + ValidatorRoot: "2306d94cc69db8435c56294ff7f27cf3a7d042f8965e2d76f38c63a616a937b0", + ProvingMetadataHash: "0000000000000000000000000000000000000000000000000000000000000000", + ProvingSchemeIndex: 0, + } + + // Sign fraudulent batch + fraudulentBatchID, err := evidence.GetBatchID() + require.NoError(t, err) + signature, err := crypto.Sign(fraudulentBatchID, fraudPrivKey) + require.NoError(t, err) + + evidence.Signature = hex.EncodeToString(signature) + + // Test handling evidence + handler := keeper.NewBatchDoubleSignHandler(f.batchingKeeper) + err = handler(f.Context().WithBlockHeight(doubleSignBlockHeight), evidence) + require.NoError(t, err) + + fraudulentValidator, err := f.stakingKeeper.GetValidator(f.Context(), fraudAddr) + require.NoError(t, err) + require.True(t, fraudulentValidator.IsJailed()) + consAddr, err := fraudulentValidator.GetConsAddr() + require.NoError(t, err) + require.True(t, f.slashingKeeper.IsTombstoned(f.Context(), consAddr)) +} + +func TestHandleEvidence_FutureBatch(t *testing.T) { + f := initFixture(t) + _, _, _ = generateFirstBatch(t, f, 1) + + evidence := &types.BatchDoubleSign{ + BatchNumber: 999, // Non-existent batch + } + + handler := keeper.NewBatchDoubleSignHandler(f.batchingKeeper) + + err := handler(f.Context(), evidence) + require.ErrorIs(t, err, collections.ErrNotFound) +} + +func TestHandleEvidence_InvalidBatchID(t *testing.T) { + f := initFixture(t) + _, _, _ = generateFirstBatch(t, f, 1) + + evidence := &types.BatchDoubleSign{ + BatchNumber: 1, + BlockHeight: 1, + DataResultRoot: "x027c97e8b0588f86a9e140d73a31af5ee0d37b93ff0f2f54f5305d0f2ea3fd9", + ValidatorRoot: "y2306d94cc69db8435c56294ff7f27cf3a7d042f8965e2d76f38c63a616a937b0", + ProvingMetadataHash: "z000000000000000000000000000000000000000000000000000000000000000", + } + + handler := keeper.NewBatchDoubleSignHandler(f.batchingKeeper) + err := handler(f.Context(), evidence) + require.ErrorIs(t, err, hex.InvalidByteError('y')) +} + +func TestHandleEvidence_LegitBatchID(t *testing.T) { + f := initFixture(t) + + valAddrs, privKeys, _ := generateFirstBatch(t, f, 1) + + notFraudAddr := valAddrs[0] + notFraudPrivKey := privKeys[0] + + // Create evidence for a legitimate batch + evidence := &types.BatchDoubleSign{ + BatchNumber: 2, + BlockHeight: 2, + OperatorAddress: notFraudAddr.String(), + DataResultRoot: "6027c97e8b0588f86a9e140d73a31af5ee0d37b93ff0f2f54f5305d0f2ea3fd9", + ValidatorRoot: "2306d94cc69db8435c56294ff7f27cf3a7d042f8965e2d76f38c63a616a937b0", + ProvingMetadataHash: "0000000000000000000000000000000000000000000000000000000000000000", + ProvingSchemeIndex: 0, + } + + legitBatchID, err := evidence.GetBatchID() + require.NoError(t, err) + + err = f.batchingKeeper.SetNewBatch(f.Context(), types.Batch{ + BatchId: legitBatchID, + BatchNumber: 2, + BlockHeight: 2, + }, types.DataResultTreeEntries{}, []types.ValidatorTreeEntry{}) + require.NoError(t, err) + + // Sign legitimate batch + signature, err := crypto.Sign(legitBatchID, notFraudPrivKey) + require.NoError(t, err) + evidence.Signature = hex.EncodeToString(signature) + + handler := keeper.NewBatchDoubleSignHandler(f.batchingKeeper) + err = handler(f.Context().WithBlockHeight(2), evidence) + require.Equal(t, err, fmt.Errorf("batch IDs are the same")) +} + +func TestHandleEvidence_InvalidSignature(t *testing.T) { + f := initFixture(t) + _, _, _ = generateFirstBatch(t, f, 1) + + testcases := []struct { + name string + signature string + errorString string + }{ + {name: "invalid signature length", signature: "", errorString: "invalid signature length"}, + {name: "invalid signature hex", signature: "x1234567890", errorString: "invalid byte"}, + } + + for _, tc := range testcases { + t.Run(tc.name, func(t *testing.T) { + evidence := &types.BatchDoubleSign{ + BatchNumber: 1, + BlockHeight: 1, + DataResultRoot: "6027c97e8b0588f86a9e140d73a31af5ee0d37b93ff0f2f54f5305d0f2ea3fd9", + ValidatorRoot: "2306d94cc69db8435c56294ff7f27cf3a7d042f8965e2d76f38c63a616a937b0", + ProvingMetadataHash: "0000000000000000000000000000000000000000000000000000000000000000", + ProvingSchemeIndex: 0, + Signature: tc.signature, + } + + handler := keeper.NewBatchDoubleSignHandler(f.batchingKeeper) + err := handler(f.Context(), evidence) + require.ErrorContains(t, err, tc.errorString) + }) + } +} + +func TestHandleEvidence_DifferentPrivateKey(t *testing.T) { + f := initFixture(t) + + valAddrs, privKeys, validators := generateFirstBatch(t, f, 2) + + fraudAddr := valAddrs[0] + fraudPrivKey := privKeys[1] + + // Store the legitimate batch for which the validator will be double signing + doubleSignBatchNumber := uint64(2) + doubleSignBlockHeight := int64(4) + + batchToDoubleSign := types.Batch{ + BatchId: []byte("batch2"), + BatchNumber: doubleSignBatchNumber, + BlockHeight: doubleSignBlockHeight, + } + err := f.batchingKeeper.SetNewBatch(f.Context(), batchToDoubleSign, types.DataResultTreeEntries{}, []types.ValidatorTreeEntry{}) + require.NoError(t, err) + + f.stakingKeeper.SetHistoricalInfo(f.Context(), doubleSignBlockHeight, &sdkstakingtypes.HistoricalInfo{ + Valset: validators, + }) + + // Create evidence for a fraudulent batch + evidence := &types.BatchDoubleSign{ + BatchNumber: doubleSignBatchNumber, + BlockHeight: doubleSignBlockHeight, + OperatorAddress: fraudAddr.String(), + DataResultRoot: "6027c97e8b0588f86a9e140d73a31af5ee0d37b93ff0f2f54f5305d0f2ea3fd9", + ValidatorRoot: "2306d94cc69db8435c56294ff7f27cf3a7d042f8965e2d76f38c63a616a937b0", + ProvingMetadataHash: "0000000000000000000000000000000000000000000000000000000000000000", + ProvingSchemeIndex: 0, + } + + // Sign fraudulent batch with a different private key + fraudulentBatchID, err := evidence.GetBatchID() + require.NoError(t, err) + signature, err := crypto.Sign(fraudulentBatchID, fraudPrivKey) + require.NoError(t, err) + + evidence.Signature = hex.EncodeToString(signature) + + // Test handling evidence + handler := keeper.NewBatchDoubleSignHandler(f.batchingKeeper) + err = handler(f.Context().WithBlockHeight(doubleSignBlockHeight), evidence) + require.ErrorContains(t, err, "recovered address does not match validator entry") +} + +func TestHandleEvidence_StaleEvidence(t *testing.T) { + f := initFixture(t) + + valAddrs, privKeys, validators := generateFirstBatch(t, f, 1) + + fraudAddr := valAddrs[0] + fraudPrivKey := privKeys[0] + + // Store the legitimate batch for which the validator will be double signing + doubleSignBatchNumber := uint64(2) + doubleSignBlockHeight := int64(4) + + batchToDoubleSign := types.Batch{ + BatchId: []byte("batch2"), + BatchNumber: doubleSignBatchNumber, + BlockHeight: doubleSignBlockHeight, + } + err := f.batchingKeeper.SetNewBatch(f.Context(), batchToDoubleSign, types.DataResultTreeEntries{}, []types.ValidatorTreeEntry{}) + require.NoError(t, err) + + f.stakingKeeper.SetHistoricalInfo(f.Context(), doubleSignBlockHeight, &sdkstakingtypes.HistoricalInfo{ + Valset: validators, + }) + + // Create evidence for a fraudulent batch + evidence := &types.BatchDoubleSign{ + BatchNumber: doubleSignBatchNumber, + BlockHeight: doubleSignBlockHeight, + OperatorAddress: fraudAddr.String(), + DataResultRoot: "6027c97e8b0588f86a9e140d73a31af5ee0d37b93ff0f2f54f5305d0f2ea3fd9", + ValidatorRoot: "2306d94cc69db8435c56294ff7f27cf3a7d042f8965e2d76f38c63a616a937b0", + ProvingMetadataHash: "0000000000000000000000000000000000000000000000000000000000000000", + ProvingSchemeIndex: 0, + } + + // Sign fraudulent batch + fraudulentBatchID, err := evidence.GetBatchID() + require.NoError(t, err) + signature, err := crypto.Sign(fraudulentBatchID, fraudPrivKey) + require.NoError(t, err) + + evidence.Signature = hex.EncodeToString(signature) + + consParams := f.Context().ConsensusParams() + consParams.Evidence = &cmtproto.EvidenceParams{ + MaxAgeNumBlocks: 1, + } + + // Test handling evidence + handler := keeper.NewBatchDoubleSignHandler(f.batchingKeeper) + err = handler(f.Context().WithBlockHeight(doubleSignBlockHeight+10).WithConsensusParams(consParams), evidence) + require.NoError(t, err) + + // Verify all validators are unaffected + for _, valAddr := range valAddrs { + validator, err := f.stakingKeeper.GetValidator(f.Context(), valAddr) + require.NoError(t, err) + require.False(t, validator.IsJailed()) + } + + // Test the same input with a block height that is within the max age + err = handler(f.Context().WithBlockHeight(doubleSignBlockHeight).WithConsensusParams(consParams), evidence) + require.NoError(t, err) + + fraudulentValidator, err := f.stakingKeeper.GetValidator(f.Context(), fraudAddr) + require.NoError(t, err) + require.True(t, fraudulentValidator.IsJailed()) + + // Verify all other validators are unaffected + for _, valAddr := range valAddrs[1:] { + validator, err := f.stakingKeeper.GetValidator(f.Context(), valAddr) + require.NoError(t, err) + require.False(t, validator.IsJailed()) + } +} + +func TestHandleEvidence_TombstonedValidator(t *testing.T) { + f := initFixture(t) + + valAddrs, privKeys, validators := generateFirstBatch(t, f, 1) + + fraudAddr := valAddrs[0] + fraudPrivKey := privKeys[0] + fraudulentValidator := validators[0] + + // Store the legitimate batch for which the validator will be double signing + doubleSignBatchNumber := uint64(2) + doubleSignBlockHeight := int64(4) + + batchToDoubleSign := types.Batch{ + BatchId: []byte("batch2"), + BatchNumber: doubleSignBatchNumber, + BlockHeight: doubleSignBlockHeight, + } + err := f.batchingKeeper.SetNewBatch(f.Context(), batchToDoubleSign, types.DataResultTreeEntries{}, []types.ValidatorTreeEntry{}) + require.NoError(t, err) + + f.stakingKeeper.SetHistoricalInfo(f.Context(), doubleSignBlockHeight, &sdkstakingtypes.HistoricalInfo{ + Valset: validators, + }) + + // Create evidence for a fraudulent batch + evidence := &types.BatchDoubleSign{ + BatchNumber: doubleSignBatchNumber, + BlockHeight: doubleSignBlockHeight, + OperatorAddress: fraudAddr.String(), + DataResultRoot: "6027c97e8b0588f86a9e140d73a31af5ee0d37b93ff0f2f54f5305d0f2ea3fd9", + ValidatorRoot: "2306d94cc69db8435c56294ff7f27cf3a7d042f8965e2d76f38c63a616a937b0", + ProvingMetadataHash: "0000000000000000000000000000000000000000000000000000000000000000", + ProvingSchemeIndex: 0, + } + + // Sign fraudulent batch + fraudulentBatchID, err := evidence.GetBatchID() + require.NoError(t, err) + signature, err := crypto.Sign(fraudulentBatchID, fraudPrivKey) + require.NoError(t, err) + + evidence.Signature = hex.EncodeToString(signature) + + // Tombstone the validator before handling evidence + consAddr, err := fraudulentValidator.GetConsAddr() + require.NoError(t, err) + f.slashingKeeper.Tombstone(f.Context(), consAddr) + + // Test handling evidence + handler := keeper.NewBatchDoubleSignHandler(f.batchingKeeper) + err = handler(f.Context().WithBlockHeight(doubleSignBlockHeight), evidence) + require.NoError(t, err) + + // Verify the validator was not slashed + fraudulentValidatorAfter, err := f.stakingKeeper.GetValidator(f.Context(), fraudAddr) + require.NoError(t, err) + require.Equal(t, fraudulentValidatorAfter.GetTokens(), fraudulentValidator.GetTokens()) +} diff --git a/x/batching/keeper/integration_test.go b/x/batching/keeper/integration_test.go index cad665e1..6a6777b1 100644 --- a/x/batching/keeper/integration_test.go +++ b/x/batching/keeper/integration_test.go @@ -2,6 +2,7 @@ package keeper_test import ( "bytes" + "crypto/ecdsa" "testing" "time" @@ -11,12 +12,15 @@ import ( cmtproto "github.com/cometbft/cometbft/proto/tendermint/types" + sdkmath "cosmossdk.io/math" wasmkeeper "github.com/CosmWasm/wasmd/x/wasm/keeper" wasmtypes "github.com/CosmWasm/wasmd/x/wasm/types" "cosmossdk.io/core/appmodule" "cosmossdk.io/log" storetypes "cosmossdk.io/store/types" + "github.com/ethereum/go-ethereum/crypto" + "github.com/sedaprotocol/seda-chain/app/utils" "github.com/cosmos/cosmos-sdk/codec" addresscodec "github.com/cosmos/cosmos-sdk/codec/address" @@ -81,6 +85,7 @@ type fixture struct { accountKeeper authkeeper.AccountKeeper bankKeeper bankkeeper.Keeper stakingKeeper stakingkeeper.Keeper + slashingKeeper slashingkeeper.Keeper contractKeeper wasmkeeper.PermissionedKeeper wasmKeeper wasmkeeper.Keeper wasmStorageKeeper wasmstoragekeeper.Keeper @@ -98,7 +103,7 @@ func initFixture(tb testing.TB) *fixture { keys := storetypes.NewKVStoreKeys( authtypes.StoreKey, banktypes.StoreKey, sdkstakingtypes.StoreKey, wasmstoragetypes.StoreKey, - wasmtypes.StoreKey, pubkeytypes.StoreKey, tallytypes.StoreKey, types.StoreKey, + wasmtypes.StoreKey, pubkeytypes.StoreKey, tallytypes.StoreKey, types.StoreKey, slashingtypes.StoreKey, ) cdc := moduletestutil.MakeTestEncodingConfig(auth.AppModuleBasic{}, bank.AppModuleBasic{}, wasmstorage.AppModuleBasic{}).Codec @@ -167,6 +172,11 @@ func initFixture(tb testing.TB) *fixture { authtypes.NewModuleAddress(govtypes.ModuleName).String(), ) + err = slashingKeeper.SetParams(ctx, slashingtypes.Params{ + SlashFractionDoubleSign: sdkmath.LegacyNewDec(1).Quo(sdkmath.LegacyNewDec(20)), + }) + require.NoError(tb, err) + // x/wasm wasmKeeper := wasmkeeper.NewKeeper( cdc, @@ -213,6 +223,7 @@ func initFixture(tb testing.TB) *fixture { cdc, runtime.NewKVStoreService(keys[types.StoreKey]), stakingKeeper, + slashingKeeper, wasmStorageKeeper, pubKeyKeeper, contractKeeper, @@ -254,6 +265,7 @@ func initFixture(tb testing.TB) *fixture { accountKeeper: accountKeeper, bankKeeper: bankKeeper, stakingKeeper: *stakingKeeper, + slashingKeeper: slashingKeeper, contractKeeper: *contractKeeper, wasmKeeper: wasmKeeper, wasmStorageKeeper: *wasmStorageKeeper, @@ -264,3 +276,51 @@ func initFixture(tb testing.TB) *fixture { logBuf: buf, } } + +func generateFirstBatch(t *testing.T, f *fixture, numValidators int) ([]sdk.ValAddress, []*ecdsa.PrivateKey, []sdkstakingtypes.Validator) { + valAddrs, _, _ := addBatchSigningValidators(t, f, numValidators) + + // Create private keys for all validators and add them to the first batch + privKeys := make([]*ecdsa.PrivateKey, numValidators) + validatorAddrs := make([]sdk.ValAddress, numValidators) + validators := make([]sdkstakingtypes.Validator, numValidators) + validatorEntries := make([]types.ValidatorTreeEntry, numValidators) + for i := 0; i < numValidators; i++ { + privKey, err := crypto.GenerateKey() + require.NoError(t, err) + privKeys[i] = privKey + + validatorAddr := sdk.ValAddress(valAddrs[i]) + validatorAddrs[i] = validatorAddr + + pubKeyBytes := crypto.FromECDSAPub(&privKey.PublicKey) + ethAddr, err := utils.PubKeyToEthAddress(pubKeyBytes) + require.NoError(t, err) + validatorEntries[i] = types.ValidatorTreeEntry{ + ValidatorAddress: validatorAddr, + EthAddress: ethAddr, + } + + validator, err := f.stakingKeeper.GetValidator(f.Context(), validatorAddr) + require.NoError(t, err) + validators[i] = validator + + consAddr, err := validator.GetConsAddr() + require.NoError(t, err) + + f.slashingKeeper.SetValidatorSigningInfo(f.Context(), consAddr, slashingtypes.ValidatorSigningInfo{ + StartHeight: 1, + }) + } + + // Generate a first batch and set it alongside the validators eth addresses + batch := types.Batch{ + BatchId: []byte("batch1"), + BatchNumber: 1, + BlockHeight: 1, + } + err := f.batchingKeeper.SetNewBatch(f.Context(), batch, types.DataResultTreeEntries{}, validatorEntries) + require.NoError(t, err) + + return validatorAddrs, privKeys, validators +} diff --git a/x/batching/keeper/keeper.go b/x/batching/keeper/keeper.go index 1724e048..04367710 100644 --- a/x/batching/keeper/keeper.go +++ b/x/batching/keeper/keeper.go @@ -19,6 +19,7 @@ import ( type Keeper struct { stakingKeeper types.StakingKeeper + slashingKeeper types.SlashingKeeper wasmStorageKeeper types.WasmStorageKeeper pubKeyKeeper types.PubKeyKeeper wasmKeeper wasmtypes.ContractOpsKeeper @@ -39,6 +40,7 @@ func NewKeeper( cdc codec.BinaryCodec, storeService storetypes.KVStoreService, sk types.StakingKeeper, + slk types.SlashingKeeper, wsk types.WasmStorageKeeper, pkk types.PubKeyKeeper, wk wasmtypes.ContractOpsKeeper, @@ -49,6 +51,7 @@ func NewKeeper( k := Keeper{ stakingKeeper: sk, + slashingKeeper: slk, wasmStorageKeeper: wsk, pubKeyKeeper: pkk, wasmKeeper: wk, diff --git a/x/batching/keeper/keeper_test.go b/x/batching/keeper/keeper_test.go index c97a9f10..56fcceb8 100644 --- a/x/batching/keeper/keeper_test.go +++ b/x/batching/keeper/keeper_test.go @@ -60,7 +60,7 @@ func setupKeeper(t *testing.T) (*keeper.Keeper, moduletestutil.TestEncodingConfi ctx := testCtx.Ctx encCfg := moduletestutil.MakeTestEncodingConfig(batching.AppModuleBasic{}) - keeper := keeper.NewKeeper(encCfg.Codec, runtime.NewKVStoreService(key), nil, nil, nil, nil, nil, nil) + keeper := keeper.NewKeeper(encCfg.Codec, runtime.NewKVStoreService(key), nil, nil, nil, nil, nil, nil, nil) return &keeper, encCfg, ctx } diff --git a/x/batching/module.go b/x/batching/module.go index 63703e7a..a6f2cab5 100644 --- a/x/batching/module.go +++ b/x/batching/module.go @@ -36,10 +36,10 @@ var ( // AppModuleBasic implements the AppModuleBasic interface that defines the independent methods a Cosmos SDK module needs to implement. type AppModuleBasic struct { - cdc codec.BinaryCodec + cdc codec.Codec } -func NewAppModuleBasic(cdc codec.BinaryCodec) AppModuleBasic { +func NewAppModuleBasic(cdc codec.Codec) AppModuleBasic { return AppModuleBasic{cdc: cdc} } @@ -60,7 +60,9 @@ func (AppModuleBasic) RegisterLegacyAminoCodec(cdc *codec.LegacyAmino) { } // RegisterInterfaces registers a module's interface types and their concrete implementations as proto.Message -func (a AppModuleBasic) RegisterInterfaces(_ cdctypes.InterfaceRegistry) {} +func (a AppModuleBasic) RegisterInterfaces(reg cdctypes.InterfaceRegistry) { + types.RegisterInterfaces(reg) +} // DefaultGenesis returns a default GenesisState for the module, marshaled to json.RawMessage. The default GenesisState need to be defined by the module developer and is primarily used for testing func (AppModuleBasic) DefaultGenesis(cdc codec.JSONCodec) json.RawMessage { @@ -85,7 +87,7 @@ func (AppModuleBasic) RegisterGRPCGatewayRoutes(clientCtx client.Context, mux *r // GetTxCmd returns the root Tx command for the module. The subcommands of this root command are used by end-users to generate new transactions containing messages defined in the module func (a AppModuleBasic) GetTxCmd() *cobra.Command { - return nil + return cli.GetTxCmd() } // GetQueryCmd returns the root query command for the module. The subcommands of this root command are used by end-users to generate new queries to the subset of the state defined by the module diff --git a/x/batching/types/batch.go b/x/batching/types/batch.go new file mode 100644 index 00000000..ca3f8d52 --- /dev/null +++ b/x/batching/types/batch.go @@ -0,0 +1,25 @@ +package types + +import ( + "encoding/binary" + + "golang.org/x/crypto/sha3" +) + +// Computes the batch ID, which is defined as +// keccak256(batch_number, block_height, validator_root, results_root, proving_metadata_hash) +func ComputeBatchID(batchNumber uint64, blockHeight int64, validatorRoot []byte, dataResultRoot []byte, provingMetadataHash []byte) []byte { + var hashContent []byte + hashContent = binary.BigEndian.AppendUint64(hashContent, batchNumber) + //nolint:gosec // G115: We shouldn't get negative block heights anyway. + hashContent = binary.BigEndian.AppendUint64(hashContent, uint64(blockHeight)) + hashContent = append(hashContent, validatorRoot...) + hashContent = append(hashContent, dataResultRoot...) + hashContent = append(hashContent, provingMetadataHash...) + + hasher := sha3.NewLegacyKeccak256() + hasher.Write(hashContent) + batchID := hasher.Sum(nil) + + return batchID +} diff --git a/x/batching/types/codec.go b/x/batching/types/codec.go index 4e38b061..6467df48 100644 --- a/x/batching/types/codec.go +++ b/x/batching/types/codec.go @@ -1,8 +1,11 @@ package types import ( + "cosmossdk.io/x/evidence/exported" + "github.com/cosmos/cosmos-sdk/codec" cdctypes "github.com/cosmos/cosmos-sdk/codec/types" + sdk "github.com/cosmos/cosmos-sdk/types" ) func RegisterCodec(_ *codec.LegacyAmino) { @@ -12,3 +15,14 @@ var ( Amino = codec.NewLegacyAmino() ModuleCdc = codec.NewProtoCodec(cdctypes.NewInterfaceRegistry()) ) + +func RegisterInterfaces(registry cdctypes.InterfaceRegistry) { + registry.RegisterImplementations((*sdk.Msg)(nil), + &BatchDoubleSign{}, + ) + + registry.RegisterImplementations( + (*exported.Evidence)(nil), + &BatchDoubleSign{}, + ) +} diff --git a/x/batching/types/events.go b/x/batching/types/events.go new file mode 100644 index 00000000..47751421 --- /dev/null +++ b/x/batching/types/events.go @@ -0,0 +1,14 @@ +package types + +const ( + EventTypeSlash = "slash_double_batch_sign" + + AttributeOperatorAddress = "operator_address" + AttributePower = "power" + AttributeReason = "reason" + AttributeBurnedCoins = "burned_coins" + AttributeBatchNumber = "batch_height" + AttributeProvingScheme = "proving_scheme" + + AttributeValueBatchDoubleSign = "batch_double_sign" +) diff --git a/x/batching/types/evidence.go b/x/batching/types/evidence.go new file mode 100644 index 00000000..497d0cb8 --- /dev/null +++ b/x/batching/types/evidence.go @@ -0,0 +1,87 @@ +package types + +import ( + "encoding/hex" + "fmt" + + "github.com/cometbft/cometbft/crypto/tmhash" + + "cosmossdk.io/x/evidence/exported" + + "github.com/sedaprotocol/seda-chain/app/utils" +) + +// Evidence type constants +const RouteBatchDoubleSign = "batchdoublesign" + +var _ exported.Evidence = &BatchDoubleSign{} + +// Route returns the Evidence Handler route for a BatchDoubleSign type. +func (e *BatchDoubleSign) Route() string { return RouteBatchDoubleSign } + +// Hash returns the hash of a BatchDoubleSign object. +func (e *BatchDoubleSign) Hash() []byte { + bz, err := e.Marshal() + if err != nil { + panic(err) + } + return tmhash.Sum(bz) +} + +// ValidateBasic performs basic stateless validation checks on a BatchDoubleSign object. +func (e *BatchDoubleSign) ValidateBasic() error { + if e.BatchNumber <= 1 { + return fmt.Errorf("batch number must be greater than 1") + } + + if e.BlockHeight < 1 { + return fmt.Errorf("invalid block height: %d", e.BlockHeight) + } + + if e.OperatorAddress == "" { + return fmt.Errorf("invalid operator address: %s", e.OperatorAddress) + } + + if e.ValidatorRoot == "" { + return fmt.Errorf("invalid validator root: %s", e.ValidatorRoot) + } + + if e.DataResultRoot == "" { + return fmt.Errorf("invalid data result root: %s", e.DataResultRoot) + } + + if e.ProvingMetadataHash == "" { + return fmt.Errorf("invalid proving metadata hash: %s", e.ProvingMetadataHash) + } + + // Currently we only support secp256k1 signatures for batch signing + if e.ProvingSchemeIndex != uint32(utils.SEDAKeyIndexSecp256k1) { + return fmt.Errorf("invalid proving scheme index: %d", e.ProvingSchemeIndex) + } + + return nil +} + +func (e *BatchDoubleSign) GetBatchID() ([]byte, error) { + validatorRoot, err := hex.DecodeString(e.ValidatorRoot) + if err != nil { + return nil, err + } + + dataResultRoot, err := hex.DecodeString(e.DataResultRoot) + if err != nil { + return nil, err + } + + provingMetadataHash, err := hex.DecodeString(e.ProvingMetadataHash) + if err != nil { + return nil, err + } + + return ComputeBatchID(e.BatchNumber, e.BlockHeight, validatorRoot, dataResultRoot, provingMetadataHash), nil +} + +// GetHeight returns the height at time of the BatchDoubleSign infraction. +func (e *BatchDoubleSign) GetHeight() int64 { + return e.BlockHeight +} diff --git a/x/batching/types/evidence.pb.go b/x/batching/types/evidence.pb.go new file mode 100644 index 00000000..09c520eb --- /dev/null +++ b/x/batching/types/evidence.pb.go @@ -0,0 +1,655 @@ +// Code generated by protoc-gen-gogo. DO NOT EDIT. +// source: sedachain/batching/v1/evidence.proto + +package types + +import ( + fmt "fmt" + _ "github.com/cosmos/cosmos-proto" + _ "github.com/cosmos/cosmos-sdk/types/tx/amino" + proto "github.com/cosmos/gogoproto/proto" + io "io" + math "math" + math_bits "math/bits" +) + +// Reference imports to suppress errors if they are not otherwise used. +var _ = proto.Marshal +var _ = fmt.Errorf +var _ = math.Inf + +// This is a compile-time assertion to ensure that this generated file +// is compatible with the proto package it is being compiled against. +// A compilation error at this line likely means your copy of the +// proto package needs to be updated. +const _ = proto.GoGoProtoPackageIsVersion3 // please upgrade the proto package + +// BatchDoubleSign implements the Evidence interface and defines evidence of +// double signing a batch for a given proving scheme. +type BatchDoubleSign struct { + // batch_number is the number of the batch that the validator double signed. + BatchNumber uint64 `protobuf:"varint,1,opt,name=batch_number,json=batchNumber,proto3" json:"batch_number,omitempty"` + // block_height is the height of the block which includes the batch that the + // validator double signed. + BlockHeight int64 `protobuf:"varint,2,opt,name=block_height,json=blockHeight,proto3" json:"block_height,omitempty"` + // operator_address is the operator address of the validator committing the + // double signing. + OperatorAddress string `protobuf:"bytes,3,opt,name=operator_address,json=operatorAddress,proto3" json:"operator_address,omitempty"` + // validator_root is the hex-encoded root of the validator merkle tree. + ValidatorRoot string `protobuf:"bytes,4,opt,name=validator_root,json=validatorRoot,proto3" json:"validator_root,omitempty"` + // data_result_root is the hex-encoded root of the data result merkle tree. + DataResultRoot string `protobuf:"bytes,5,opt,name=data_result_root,json=dataResultRoot,proto3" json:"data_result_root,omitempty"` + // proving_metadata_hash is the hex-encoded hash of the proving metadata. + ProvingMetadataHash string `protobuf:"bytes,6,opt,name=proving_metadata_hash,json=provingMetadataHash,proto3" json:"proving_metadata_hash,omitempty"` + // signature is the hex-encoded signature of the validator. + Signature string `protobuf:"bytes,7,opt,name=signature,proto3" json:"signature,omitempty"` + // proving_scheme_index is the SEDA key index of the proving scheme. + ProvingSchemeIndex uint32 `protobuf:"varint,8,opt,name=proving_scheme_index,json=provingSchemeIndex,proto3" json:"proving_scheme_index,omitempty"` +} + +func (m *BatchDoubleSign) Reset() { *m = BatchDoubleSign{} } +func (m *BatchDoubleSign) String() string { return proto.CompactTextString(m) } +func (*BatchDoubleSign) ProtoMessage() {} +func (*BatchDoubleSign) Descriptor() ([]byte, []int) { + return fileDescriptor_54e6b11c86e4ba23, []int{0} +} +func (m *BatchDoubleSign) XXX_Unmarshal(b []byte) error { + return m.Unmarshal(b) +} +func (m *BatchDoubleSign) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { + if deterministic { + return xxx_messageInfo_BatchDoubleSign.Marshal(b, m, deterministic) + } else { + b = b[:cap(b)] + n, err := m.MarshalToSizedBuffer(b) + if err != nil { + return nil, err + } + return b[:n], nil + } +} +func (m *BatchDoubleSign) XXX_Merge(src proto.Message) { + xxx_messageInfo_BatchDoubleSign.Merge(m, src) +} +func (m *BatchDoubleSign) XXX_Size() int { + return m.Size() +} +func (m *BatchDoubleSign) XXX_DiscardUnknown() { + xxx_messageInfo_BatchDoubleSign.DiscardUnknown(m) +} + +var xxx_messageInfo_BatchDoubleSign proto.InternalMessageInfo + +func (m *BatchDoubleSign) GetBatchNumber() uint64 { + if m != nil { + return m.BatchNumber + } + return 0 +} + +func (m *BatchDoubleSign) GetBlockHeight() int64 { + if m != nil { + return m.BlockHeight + } + return 0 +} + +func (m *BatchDoubleSign) GetOperatorAddress() string { + if m != nil { + return m.OperatorAddress + } + return "" +} + +func (m *BatchDoubleSign) GetValidatorRoot() string { + if m != nil { + return m.ValidatorRoot + } + return "" +} + +func (m *BatchDoubleSign) GetDataResultRoot() string { + if m != nil { + return m.DataResultRoot + } + return "" +} + +func (m *BatchDoubleSign) GetProvingMetadataHash() string { + if m != nil { + return m.ProvingMetadataHash + } + return "" +} + +func (m *BatchDoubleSign) GetSignature() string { + if m != nil { + return m.Signature + } + return "" +} + +func (m *BatchDoubleSign) GetProvingSchemeIndex() uint32 { + if m != nil { + return m.ProvingSchemeIndex + } + return 0 +} + +func init() { + proto.RegisterType((*BatchDoubleSign)(nil), "sedachain.batching.v1.BatchDoubleSign") +} + +func init() { + proto.RegisterFile("sedachain/batching/v1/evidence.proto", fileDescriptor_54e6b11c86e4ba23) +} + +var fileDescriptor_54e6b11c86e4ba23 = []byte{ + // 415 bytes of a gzipped FileDescriptorProto + 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0x44, 0x92, 0xbf, 0x6e, 0xd4, 0x40, + 0x10, 0xc6, 0xcf, 0x5c, 0x08, 0x64, 0x43, 0xfe, 0x60, 0x2e, 0x92, 0x13, 0x21, 0xeb, 0x40, 0x20, + 0x59, 0x48, 0x39, 0x13, 0xd2, 0xd1, 0x71, 0x50, 0x84, 0x22, 0x14, 0xbe, 0x8e, 0xc6, 0x5a, 0xaf, + 0x47, 0xde, 0x15, 0xf6, 0xce, 0x69, 0x77, 0x6d, 0x85, 0x57, 0x40, 0x14, 0x3c, 0x0a, 0x05, 0x0f, + 0x41, 0x19, 0x51, 0x51, 0xa2, 0xbb, 0x82, 0xd7, 0x88, 0x3c, 0xf6, 0x9d, 0x1b, 0x4b, 0xf3, 0xfb, + 0x7e, 0xf3, 0xc9, 0xf2, 0x98, 0xbd, 0xb0, 0x90, 0x73, 0x21, 0xb9, 0xd2, 0x71, 0xc6, 0x9d, 0x90, + 0x4a, 0x17, 0x71, 0x73, 0x11, 0x43, 0xa3, 0x72, 0xd0, 0x02, 0x66, 0x4b, 0x83, 0x0e, 0xfd, 0x93, + 0xad, 0x35, 0xdb, 0x58, 0xb3, 0xe6, 0xe2, 0xec, 0x31, 0xaf, 0x94, 0xc6, 0x98, 0x9e, 0x9d, 0x79, + 0x76, 0x2a, 0xd0, 0x56, 0x68, 0x53, 0x9a, 0xe2, 0x6e, 0xe8, 0xa2, 0xe7, 0xdf, 0xc7, 0xec, 0x68, + 0xde, 0x6e, 0x7f, 0xc0, 0x3a, 0x2b, 0x61, 0xa1, 0x0a, 0xed, 0x3f, 0x63, 0x8f, 0xa8, 0x30, 0xd5, + 0x75, 0x95, 0x81, 0x09, 0xbc, 0xa9, 0x17, 0xed, 0x24, 0xfb, 0xc4, 0x3e, 0x11, 0x22, 0xa5, 0x44, + 0xf1, 0x25, 0x95, 0xa0, 0x0a, 0xe9, 0x82, 0x7b, 0x53, 0x2f, 0x1a, 0x27, 0xfb, 0xc4, 0xae, 0x08, + 0xf9, 0xef, 0xd9, 0x31, 0x2e, 0xc1, 0x70, 0x87, 0x26, 0xe5, 0x79, 0x6e, 0xc0, 0xda, 0x60, 0x3c, + 0xf5, 0xa2, 0xbd, 0x79, 0xf0, 0xe7, 0xd7, 0xf9, 0xa4, 0x7f, 0x8b, 0x77, 0x5d, 0xb2, 0x70, 0x46, + 0xe9, 0x22, 0x39, 0xda, 0x6c, 0xf4, 0xd8, 0x7f, 0xc9, 0x0e, 0x1b, 0x5e, 0xaa, 0x9c, 0x5a, 0x0c, + 0xa2, 0x0b, 0x76, 0xda, 0x8a, 0xe4, 0x60, 0x4b, 0x13, 0x44, 0xe7, 0x47, 0xec, 0x38, 0xe7, 0x8e, + 0xa7, 0x06, 0x6c, 0x5d, 0xba, 0x4e, 0xbc, 0x4f, 0xe2, 0x61, 0xcb, 0x13, 0xc2, 0x64, 0xbe, 0x61, + 0x27, 0x4b, 0x83, 0x8d, 0xd2, 0x45, 0x5a, 0x81, 0xe3, 0xb4, 0x25, 0xb9, 0x95, 0xc1, 0x2e, 0xe9, + 0x4f, 0xfa, 0xf0, 0xba, 0xcf, 0xae, 0xb8, 0x95, 0xfe, 0x53, 0xb6, 0x67, 0x55, 0xa1, 0xb9, 0xab, + 0x0d, 0x04, 0x0f, 0xc8, 0x1b, 0x80, 0xff, 0x9a, 0x4d, 0x36, 0x8d, 0x56, 0x48, 0xa8, 0x20, 0x55, + 0x3a, 0x87, 0x9b, 0xe0, 0xe1, 0xd4, 0x8b, 0x0e, 0x12, 0xbf, 0xcf, 0x16, 0x14, 0x7d, 0x6c, 0x93, + 0xb7, 0xa7, 0xdf, 0xfe, 0xff, 0x7c, 0x35, 0x19, 0x6e, 0x3c, 0x7c, 0xfa, 0xf9, 0xf5, 0xef, 0x55, + 0xe8, 0xdd, 0xae, 0x42, 0xef, 0xdf, 0x2a, 0xf4, 0x7e, 0xac, 0xc3, 0xd1, 0xed, 0x3a, 0x1c, 0xfd, + 0x5d, 0x87, 0xa3, 0xcf, 0x97, 0x85, 0x72, 0xb2, 0xce, 0x66, 0x02, 0xab, 0xb8, 0x5d, 0xa5, 0xf3, + 0x09, 0x2c, 0x69, 0x38, 0xef, 0x8a, 0x6e, 0x86, 0xdf, 0xc5, 0x7d, 0x5d, 0x82, 0xcd, 0x76, 0xc9, + 0xba, 0xbc, 0x0b, 0x00, 0x00, 0xff, 0xff, 0x83, 0xef, 0xfd, 0xb0, 0x51, 0x02, 0x00, 0x00, +} + +func (m *BatchDoubleSign) Marshal() (dAtA []byte, err error) { + size := m.Size() + dAtA = make([]byte, size) + n, err := m.MarshalToSizedBuffer(dAtA[:size]) + if err != nil { + return nil, err + } + return dAtA[:n], nil +} + +func (m *BatchDoubleSign) MarshalTo(dAtA []byte) (int, error) { + size := m.Size() + return m.MarshalToSizedBuffer(dAtA[:size]) +} + +func (m *BatchDoubleSign) MarshalToSizedBuffer(dAtA []byte) (int, error) { + i := len(dAtA) + _ = i + var l int + _ = l + if m.ProvingSchemeIndex != 0 { + i = encodeVarintEvidence(dAtA, i, uint64(m.ProvingSchemeIndex)) + i-- + dAtA[i] = 0x40 + } + if len(m.Signature) > 0 { + i -= len(m.Signature) + copy(dAtA[i:], m.Signature) + i = encodeVarintEvidence(dAtA, i, uint64(len(m.Signature))) + i-- + dAtA[i] = 0x3a + } + if len(m.ProvingMetadataHash) > 0 { + i -= len(m.ProvingMetadataHash) + copy(dAtA[i:], m.ProvingMetadataHash) + i = encodeVarintEvidence(dAtA, i, uint64(len(m.ProvingMetadataHash))) + i-- + dAtA[i] = 0x32 + } + if len(m.DataResultRoot) > 0 { + i -= len(m.DataResultRoot) + copy(dAtA[i:], m.DataResultRoot) + i = encodeVarintEvidence(dAtA, i, uint64(len(m.DataResultRoot))) + i-- + dAtA[i] = 0x2a + } + if len(m.ValidatorRoot) > 0 { + i -= len(m.ValidatorRoot) + copy(dAtA[i:], m.ValidatorRoot) + i = encodeVarintEvidence(dAtA, i, uint64(len(m.ValidatorRoot))) + i-- + dAtA[i] = 0x22 + } + if len(m.OperatorAddress) > 0 { + i -= len(m.OperatorAddress) + copy(dAtA[i:], m.OperatorAddress) + i = encodeVarintEvidence(dAtA, i, uint64(len(m.OperatorAddress))) + i-- + dAtA[i] = 0x1a + } + if m.BlockHeight != 0 { + i = encodeVarintEvidence(dAtA, i, uint64(m.BlockHeight)) + i-- + dAtA[i] = 0x10 + } + if m.BatchNumber != 0 { + i = encodeVarintEvidence(dAtA, i, uint64(m.BatchNumber)) + i-- + dAtA[i] = 0x8 + } + return len(dAtA) - i, nil +} + +func encodeVarintEvidence(dAtA []byte, offset int, v uint64) int { + offset -= sovEvidence(v) + base := offset + for v >= 1<<7 { + dAtA[offset] = uint8(v&0x7f | 0x80) + v >>= 7 + offset++ + } + dAtA[offset] = uint8(v) + return base +} +func (m *BatchDoubleSign) Size() (n int) { + if m == nil { + return 0 + } + var l int + _ = l + if m.BatchNumber != 0 { + n += 1 + sovEvidence(uint64(m.BatchNumber)) + } + if m.BlockHeight != 0 { + n += 1 + sovEvidence(uint64(m.BlockHeight)) + } + l = len(m.OperatorAddress) + if l > 0 { + n += 1 + l + sovEvidence(uint64(l)) + } + l = len(m.ValidatorRoot) + if l > 0 { + n += 1 + l + sovEvidence(uint64(l)) + } + l = len(m.DataResultRoot) + if l > 0 { + n += 1 + l + sovEvidence(uint64(l)) + } + l = len(m.ProvingMetadataHash) + if l > 0 { + n += 1 + l + sovEvidence(uint64(l)) + } + l = len(m.Signature) + if l > 0 { + n += 1 + l + sovEvidence(uint64(l)) + } + if m.ProvingSchemeIndex != 0 { + n += 1 + sovEvidence(uint64(m.ProvingSchemeIndex)) + } + return n +} + +func sovEvidence(x uint64) (n int) { + return (math_bits.Len64(x|1) + 6) / 7 +} +func sozEvidence(x uint64) (n int) { + return sovEvidence(uint64((x << 1) ^ uint64((int64(x) >> 63)))) +} +func (m *BatchDoubleSign) Unmarshal(dAtA []byte) error { + l := len(dAtA) + iNdEx := 0 + for iNdEx < l { + preIndex := iNdEx + var wire uint64 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowEvidence + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + wire |= uint64(b&0x7F) << shift + if b < 0x80 { + break + } + } + fieldNum := int32(wire >> 3) + wireType := int(wire & 0x7) + if wireType == 4 { + return fmt.Errorf("proto: BatchDoubleSign: wiretype end group for non-group") + } + if fieldNum <= 0 { + return fmt.Errorf("proto: BatchDoubleSign: illegal tag %d (wire type %d)", fieldNum, wire) + } + switch fieldNum { + case 1: + if wireType != 0 { + return fmt.Errorf("proto: wrong wireType = %d for field BatchNumber", wireType) + } + m.BatchNumber = 0 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowEvidence + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + m.BatchNumber |= uint64(b&0x7F) << shift + if b < 0x80 { + break + } + } + case 2: + if wireType != 0 { + return fmt.Errorf("proto: wrong wireType = %d for field BlockHeight", wireType) + } + m.BlockHeight = 0 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowEvidence + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + m.BlockHeight |= int64(b&0x7F) << shift + if b < 0x80 { + break + } + } + case 3: + if wireType != 2 { + return fmt.Errorf("proto: wrong wireType = %d for field OperatorAddress", wireType) + } + var stringLen uint64 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowEvidence + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + stringLen |= uint64(b&0x7F) << shift + if b < 0x80 { + break + } + } + intStringLen := int(stringLen) + if intStringLen < 0 { + return ErrInvalidLengthEvidence + } + postIndex := iNdEx + intStringLen + if postIndex < 0 { + return ErrInvalidLengthEvidence + } + if postIndex > l { + return io.ErrUnexpectedEOF + } + m.OperatorAddress = string(dAtA[iNdEx:postIndex]) + iNdEx = postIndex + case 4: + if wireType != 2 { + return fmt.Errorf("proto: wrong wireType = %d for field ValidatorRoot", wireType) + } + var stringLen uint64 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowEvidence + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + stringLen |= uint64(b&0x7F) << shift + if b < 0x80 { + break + } + } + intStringLen := int(stringLen) + if intStringLen < 0 { + return ErrInvalidLengthEvidence + } + postIndex := iNdEx + intStringLen + if postIndex < 0 { + return ErrInvalidLengthEvidence + } + if postIndex > l { + return io.ErrUnexpectedEOF + } + m.ValidatorRoot = string(dAtA[iNdEx:postIndex]) + iNdEx = postIndex + case 5: + if wireType != 2 { + return fmt.Errorf("proto: wrong wireType = %d for field DataResultRoot", wireType) + } + var stringLen uint64 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowEvidence + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + stringLen |= uint64(b&0x7F) << shift + if b < 0x80 { + break + } + } + intStringLen := int(stringLen) + if intStringLen < 0 { + return ErrInvalidLengthEvidence + } + postIndex := iNdEx + intStringLen + if postIndex < 0 { + return ErrInvalidLengthEvidence + } + if postIndex > l { + return io.ErrUnexpectedEOF + } + m.DataResultRoot = string(dAtA[iNdEx:postIndex]) + iNdEx = postIndex + case 6: + if wireType != 2 { + return fmt.Errorf("proto: wrong wireType = %d for field ProvingMetadataHash", wireType) + } + var stringLen uint64 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowEvidence + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + stringLen |= uint64(b&0x7F) << shift + if b < 0x80 { + break + } + } + intStringLen := int(stringLen) + if intStringLen < 0 { + return ErrInvalidLengthEvidence + } + postIndex := iNdEx + intStringLen + if postIndex < 0 { + return ErrInvalidLengthEvidence + } + if postIndex > l { + return io.ErrUnexpectedEOF + } + m.ProvingMetadataHash = string(dAtA[iNdEx:postIndex]) + iNdEx = postIndex + case 7: + if wireType != 2 { + return fmt.Errorf("proto: wrong wireType = %d for field Signature", wireType) + } + var stringLen uint64 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowEvidence + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + stringLen |= uint64(b&0x7F) << shift + if b < 0x80 { + break + } + } + intStringLen := int(stringLen) + if intStringLen < 0 { + return ErrInvalidLengthEvidence + } + postIndex := iNdEx + intStringLen + if postIndex < 0 { + return ErrInvalidLengthEvidence + } + if postIndex > l { + return io.ErrUnexpectedEOF + } + m.Signature = string(dAtA[iNdEx:postIndex]) + iNdEx = postIndex + case 8: + if wireType != 0 { + return fmt.Errorf("proto: wrong wireType = %d for field ProvingSchemeIndex", wireType) + } + m.ProvingSchemeIndex = 0 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowEvidence + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + m.ProvingSchemeIndex |= uint32(b&0x7F) << shift + if b < 0x80 { + break + } + } + default: + iNdEx = preIndex + skippy, err := skipEvidence(dAtA[iNdEx:]) + if err != nil { + return err + } + if (skippy < 0) || (iNdEx+skippy) < 0 { + return ErrInvalidLengthEvidence + } + if (iNdEx + skippy) > l { + return io.ErrUnexpectedEOF + } + iNdEx += skippy + } + } + + if iNdEx > l { + return io.ErrUnexpectedEOF + } + return nil +} +func skipEvidence(dAtA []byte) (n int, err error) { + l := len(dAtA) + iNdEx := 0 + depth := 0 + for iNdEx < l { + var wire uint64 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return 0, ErrIntOverflowEvidence + } + if iNdEx >= l { + return 0, io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + wire |= (uint64(b) & 0x7F) << shift + if b < 0x80 { + break + } + } + wireType := int(wire & 0x7) + switch wireType { + case 0: + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return 0, ErrIntOverflowEvidence + } + if iNdEx >= l { + return 0, io.ErrUnexpectedEOF + } + iNdEx++ + if dAtA[iNdEx-1] < 0x80 { + break + } + } + case 1: + iNdEx += 8 + case 2: + var length int + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return 0, ErrIntOverflowEvidence + } + if iNdEx >= l { + return 0, io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + length |= (int(b) & 0x7F) << shift + if b < 0x80 { + break + } + } + if length < 0 { + return 0, ErrInvalidLengthEvidence + } + iNdEx += length + case 3: + depth++ + case 4: + if depth == 0 { + return 0, ErrUnexpectedEndOfGroupEvidence + } + depth-- + case 5: + iNdEx += 4 + default: + return 0, fmt.Errorf("proto: illegal wireType %d", wireType) + } + if iNdEx < 0 { + return 0, ErrInvalidLengthEvidence + } + if depth == 0 { + return iNdEx, nil + } + } + return 0, io.ErrUnexpectedEOF +} + +var ( + ErrInvalidLengthEvidence = fmt.Errorf("proto: negative length found during unmarshaling") + ErrIntOverflowEvidence = fmt.Errorf("proto: integer overflow") + ErrUnexpectedEndOfGroupEvidence = fmt.Errorf("proto: unexpected end of group") +) diff --git a/x/batching/types/evidence_test.go b/x/batching/types/evidence_test.go new file mode 100644 index 00000000..78567a0f --- /dev/null +++ b/x/batching/types/evidence_test.go @@ -0,0 +1,214 @@ +package types + +import ( + "encoding/hex" + "fmt" + "testing" + + "github.com/stretchr/testify/require" +) + +func TestEvidenceValidateBasic(t *testing.T) { + tests := []struct { + name string + evidence *BatchDoubleSign + wantErr error + }{ + { + name: "Happy path", + evidence: &BatchDoubleSign{ + BatchNumber: 2, + BlockHeight: 48, + OperatorAddress: "sedavaloper1easwjglg0l6s5qrnlhqd25l2x0h5gy7law835s", + ValidatorRoot: "6027c97e8b0588f86a9e140d73a31af5ee0d37b93ff0f2f54f5305d0f2ea3fd9", + DataResultRoot: "2306d94cc69db8435c56294ff7f27cf3a7d042f8965e2d76f38c63a616a937b0", + ProvingMetadataHash: "0000000000000000000000000000000000000000000000000000000000000000", + }, + wantErr: nil, + }, + { + name: "Invalid batch number", + evidence: &BatchDoubleSign{ + BatchNumber: 0, + BlockHeight: 48, + OperatorAddress: "sedavaloper1easwjglg0l6s5qrnlhqd25l2x0h5gy7law835s", + ValidatorRoot: "6027c97e8b0588f86a9e140d73a31af5ee0d37b93ff0f2f54f5305d0f2ea3fd9", + DataResultRoot: "2306d94cc69db8435c56294ff7f27cf3a7d042f8965e2d76f38c63a616a937b0", + ProvingMetadataHash: "0000000000000000000000000000000000000000000000000000000000000000", + }, + wantErr: fmt.Errorf("batch number must be greater than 1"), + }, + { + name: "Invalid block height", + evidence: &BatchDoubleSign{ + BatchNumber: 2, + BlockHeight: -100, + OperatorAddress: "sedavaloper1easwjglg0l6s5qrnlhqd25l2x0h5gy7law835s", + ValidatorRoot: "6027c97e8b0588f86a9e140d73a31af5ee0d37b93ff0f2f54f5305d0f2ea3fd9", + DataResultRoot: "2306d94cc69db8435c56294ff7f27cf3a7d042f8965e2d76f38c63a616a937b0", + ProvingMetadataHash: "0000000000000000000000000000000000000000000000000000000000000000", + }, + wantErr: fmt.Errorf("invalid block height: -100"), + }, + { + name: "Invalid validator operator address", + evidence: &BatchDoubleSign{ + BatchNumber: 2, + BlockHeight: 48, + OperatorAddress: "", + ValidatorRoot: "6027c97e8b0588f86a9e140d73a31af5ee0d37b93ff0f2f54f5305d0f2ea3fd9", + DataResultRoot: "2306d94cc69db8435c56294ff7f27cf3a7d042f8965e2d76f38c63a616a937b0", + ProvingMetadataHash: "0000000000000000000000000000000000000000000000000000000000000000", + }, + wantErr: fmt.Errorf("invalid operator address: "), + }, + { + name: "Invalid validator root", + evidence: &BatchDoubleSign{ + BatchNumber: 2, + BlockHeight: 48, + OperatorAddress: "sedavaloper1easwjglg0l6s5qrnlhqd25l2x0h5gy7law835s", + ValidatorRoot: "", + DataResultRoot: "2306d94cc69db8435c56294ff7f27cf3a7d042f8965e2d76f38c63a616a937b0", + ProvingMetadataHash: "0000000000000000000000000000000000000000000000000000000000000000", + }, + wantErr: fmt.Errorf("invalid validator root: "), + }, + { + name: "Invalid data result root", + evidence: &BatchDoubleSign{ + BatchNumber: 2, + BlockHeight: 48, + OperatorAddress: "sedavaloper1easwjglg0l6s5qrnlhqd25l2x0h5gy7law835s", + ValidatorRoot: "6027c97e8b0588f86a9e140d73a31af5ee0d37b93ff0f2f54f5305d0f2ea3fd9", + DataResultRoot: "", + ProvingMetadataHash: "0000000000000000000000000000000000000000000000000000000000000000", + }, + wantErr: fmt.Errorf("invalid data result root: "), + }, + { + name: "Invalid proving metadata hash", + evidence: &BatchDoubleSign{ + BatchNumber: 2, + BlockHeight: 48, + OperatorAddress: "sedavaloper1easwjglg0l6s5qrnlhqd25l2x0h5gy7law835s", + ValidatorRoot: "6027c97e8b0588f86a9e140d73a31af5ee0d37b93ff0f2f54f5305d0f2ea3fd9", + DataResultRoot: "2306d94cc69db8435c56294ff7f27cf3a7d042f8965e2d76f38c63a616a937b0", + ProvingMetadataHash: "", + }, + wantErr: fmt.Errorf("invalid proving metadata hash: "), + }, + { + name: "Invalid proving scheme index", + evidence: &BatchDoubleSign{ + BatchNumber: 2, + BlockHeight: 48, + OperatorAddress: "sedavaloper1easwjglg0l6s5qrnlhqd25l2x0h5gy7law835s", + ValidatorRoot: "6027c97e8b0588f86a9e140d73a31af5ee0d37b93ff0f2f54f5305d0f2ea3fd9", + DataResultRoot: "2306d94cc69db8435c56294ff7f27cf3a7d042f8965e2d76f38c63a616a937b0", + ProvingMetadataHash: "0000000000000000000000000000000000000000000000000000000000000000", + // Seems unlikely that this will ever be used + ProvingSchemeIndex: 999999, + }, + wantErr: fmt.Errorf("invalid proving scheme index: 999999"), + }, + } + for _, tt := range tests { + t.Run(tt.name, func(ti *testing.T) { + err := tt.evidence.ValidateBasic() + if tt.wantErr != nil { + require.Error(ti, err) + require.Equal(ti, tt.wantErr, err) + } else { + require.NoError(ti, err) + } + }) + } +} + +func TestEvidenceBatchID(t *testing.T) { + tests := []struct { + name string + evidence *BatchDoubleSign + wantBatchID string + wantErr error + }{ + { + name: "Happy path", + evidence: &BatchDoubleSign{ + BatchNumber: 2, + BlockHeight: 48, + OperatorAddress: "sedavaloper1easwjglg0l6s5qrnlhqd25l2x0h5gy7law835s", + ValidatorRoot: "6027c97e8b0588f86a9e140d73a31af5ee0d37b93ff0f2f54f5305d0f2ea3fd9", + DataResultRoot: "2306d94cc69db8435c56294ff7f27cf3a7d042f8965e2d76f38c63a616a937b0", + ProvingMetadataHash: "0000000000000000000000000000000000000000000000000000000000000000", + }, + wantBatchID: "7e263196439561a12d3488a44302605ca27df1ecc4d5e07e42f3630a8435ae88", + wantErr: nil, + }, + { + name: "Invalid validator root", + evidence: &BatchDoubleSign{ + BatchNumber: 2, + BlockHeight: 48, + OperatorAddress: "sedavaloper1easwjglg0l6s5qrnlhqd25l2x0h5gy7law835s", + ValidatorRoot: "g027c97e8b0588f86a9e140d73a31af5ee0d37b93ff0f2f54f5305d0f2ea3fd9", + DataResultRoot: "2306d94cc69db8435c56294ff7f27cf3a7d042f8965e2d76f38c63a616a937b0", + ProvingMetadataHash: "0000000000000000000000000000000000000000000000000000000000000000", + }, + wantErr: hex.InvalidByteError('g'), + }, + { + name: "Invalid data result root", + evidence: &BatchDoubleSign{ + BatchNumber: 2, + BlockHeight: 48, + OperatorAddress: "sedavaloper1easwjglg0l6s5qrnlhqd25l2x0h5gy7law835s", + ValidatorRoot: "6027c97e8b0588f86a9e140d73a31af5ee0d37b93ff0f2f54f5305d0f2ea3fd9", + DataResultRoot: "z306d94cc69db8435c56294ff7f27cf3a7d042f8965e2d76f38c63a616a937b0", + ProvingMetadataHash: "0000000000000000000000000000000000000000000000000000000000000000", + }, + wantErr: hex.InvalidByteError('z'), + }, + { + name: "Invalid proving metadata hash", + evidence: &BatchDoubleSign{ + BatchNumber: 2, + BlockHeight: 48, + OperatorAddress: "sedavaloper1easwjglg0l6s5qrnlhqd25l2x0h5gy7law835s", + ValidatorRoot: "6027c97e8b0588f86a9e140d73a31af5ee0d37b93ff0f2f54f5305d0f2ea3fd9", + DataResultRoot: "2306d94cc69db8435c56294ff7f27cf3a7d042f8965e2d76f38c63a616a937b0", + ProvingMetadataHash: "p000000000000000000000000000000000000000000000000000000000000000", + }, + wantErr: hex.InvalidByteError('p'), + }, + } + for _, tt := range tests { + t.Run(tt.name, func(ti *testing.T) { + batchID, err := tt.evidence.GetBatchID() + if tt.wantErr != nil { + require.Error(ti, err) + require.Equal(ti, tt.wantErr, err) + } else { + require.NoError(ti, err) + require.Equal(ti, tt.wantBatchID, hex.EncodeToString(batchID)) + } + }) + } +} + +// Since the evidence module relies of the hash of the evidence to be unique we add this test +// to be notified if the hash changes. +func TestEvidenceHash(t *testing.T) { + evidence := &BatchDoubleSign{ + BatchNumber: 2, + BlockHeight: 48, + OperatorAddress: "sedavaloper1easwjglg0l6s5qrnlhqd25l2x0h5gy7law835s", + ValidatorRoot: "6027c97e8b0588f86a9e140d73a31af5ee0d37b93ff0f2f54f5305d0f2ea3fd9", + DataResultRoot: "2306d94cc69db8435c56294ff7f27cf3a7d042f8965e2d76f38c63a616a937b0", + ProvingMetadataHash: "0000000000000000000000000000000000000000000000000000000000000000", + } + hash := evidence.Hash() + + require.Equal(t, "986aa7a4d4c0213cc7a2ca53807af3a79abdaf47ab55c36d96067776721cd5e2", hex.EncodeToString(hash), "If this test fails it means that old evidence could be resubmitted!") +} diff --git a/x/batching/types/expected_keepers.go b/x/batching/types/expected_keepers.go index 5aafac48..4740a854 100644 --- a/x/batching/types/expected_keepers.go +++ b/x/batching/types/expected_keepers.go @@ -2,6 +2,7 @@ package types import ( "context" + "time" abci "github.com/cometbft/cometbft/abci/types" @@ -13,11 +14,21 @@ import ( "github.com/sedaprotocol/seda-chain/app/utils" ) +type SlashingKeeper interface { + IsTombstoned(ctx context.Context, consAddr sdk.ConsAddress) bool + SlashFractionDoubleSign(ctx context.Context) (math.LegacyDec, error) + JailUntil(ctx context.Context, consAddr sdk.ConsAddress, jailTime time.Time) error + Jail(ctx context.Context, consAddr sdk.ConsAddress) error + Tombstone(ctx context.Context, consAddr sdk.ConsAddress) error +} + type StakingKeeper interface { GetBondedValidatorsByPower(ctx context.Context) ([]stakingtypes.Validator, error) GetValidatorUpdates(ctx context.Context) ([]abci.ValidatorUpdate, error) IterateLastValidatorPowers(ctx context.Context, handler func(operator sdk.ValAddress, power int64) (stop bool)) error GetLastTotalPower(ctx context.Context) (math.Int, error) + GetHistoricalInfo(ctx context.Context, height int64) (stakingtypes.HistoricalInfo, error) + Slash(ctx context.Context, consAddr sdk.ConsAddress, infractionHeight, power int64, slashFactor math.LegacyDec) (math.Int, error) } type WasmStorageKeeper interface { diff --git a/x/tally/keeper/integration_test.go b/x/tally/keeper/integration_test.go index c07d8402..77e16ed5 100644 --- a/x/tally/keeper/integration_test.go +++ b/x/tally/keeper/integration_test.go @@ -199,6 +199,7 @@ func initFixture(t testing.TB) *fixture { cdc, runtime.NewKVStoreService(keys[batchingtypes.StoreKey]), stakingKeeper, + slashingKeeper, wasmStorageKeeper, pubKeyKeeper, contractKeeper,