From f0c42674097786c12d0341b44b215ba12d41fa1b Mon Sep 17 00:00:00 2001 From: Alex Gartner Date: Mon, 30 Dec 2024 16:42:54 -0800 Subject: [PATCH 1/7] feat: add configurable signer latency correction (#3317) * feat: add configurable signer latency correction * coderabbit feedback * changelog --- changelog.md | 2 + cmd/zetae2e/local/local.go | 3 +- docs/cli/zetacored/cli.md | 57 +++--- docs/openapi/openapi.swagger.yaml | 5 + docs/spec/observer/messages.md | 2 +- e2e/e2etests/e2etests.go | 15 +- e2e/e2etests/test_operational_flags.go | 51 +++++- .../zetacore/observer/operational.proto | 8 + proto/zetachain/zetacore/observer/tx.proto | 2 +- testutil/sample/observer.go | 5 +- .../zetacore/observer/operational_pb.d.ts | 10 +- .../zetachain/zetacore/observer/tx_pb.d.ts | 2 +- .../client/cli/tx_update_operational_flags.go | 8 +- ...sg_server_update_operational_flags_test.go | 12 +- x/observer/types/errors.go | 22 ++- .../message_update_operational_flags_test.go | 16 +- x/observer/types/operational.go | 19 ++ x/observer/types/operational.pb.go | 97 ++++++++-- x/observer/types/operational_test.go | 33 +++- x/observer/types/tx.pb.go | 172 +++++++++--------- zetaclient/metrics/metrics.go | 7 + zetaclient/orchestrator/orchestrator.go | 61 +++++++ zetaclient/orchestrator/orchestrator_test.go | 54 ++++++ 23 files changed, 514 insertions(+), 149 deletions(-) diff --git a/changelog.md b/changelog.md index 8060dbd540..72d1ea2c59 100644 --- a/changelog.md +++ b/changelog.md @@ -5,6 +5,8 @@ ### Features * [3235](https://github.com/zeta-chain/node/pull/3235) - add /systemtime telemetry endpoint (zetaclient) +* [3317](https://github.com/zeta-chain/node/pull/3317) - add configurable signer latency correction (zetaclient) + ### Tests diff --git a/cmd/zetae2e/local/local.go b/cmd/zetae2e/local/local.go index 4385101a24..65b5fdcf44 100644 --- a/cmd/zetae2e/local/local.go +++ b/cmd/zetae2e/local/local.go @@ -362,7 +362,8 @@ func localE2ETest(cmd *cobra.Command, _ []string) { if testAdmin { eg.Go(adminTestRoutine(conf, deployerRunner, verbose, - e2etests.TestOperationalFlagsName, + e2etests.TestZetaclientSignerOffsetName, + e2etests.TestZetaclientRestartHeightName, e2etests.TestWhitelistERC20Name, e2etests.TestPauseZRC20Name, e2etests.TestUpdateBytecodeZRC20Name, diff --git a/docs/cli/zetacored/cli.md b/docs/cli/zetacored/cli.md index 56e293efe4..5f6d963e6f 100644 --- a/docs/cli/zetacored/cli.md +++ b/docs/cli/zetacored/cli.md @@ -13347,34 +13347,35 @@ zetacored tx observer update-operational-flags [flags] ### Options ``` - -a, --account-number uint The account number of the signing account (offline mode only) - --aux Generate aux signer data instead of sending a tx - -b, --broadcast-mode string Transaction broadcasting mode (sync|async) - --chain-id string The network chain ID - --dry-run ignore the --gas flag and perform a simulation of a transaction, but don't broadcast it (when enabled, the local Keybase is not accessible) - --fee-granter string Fee granter grants fees for the transaction - --fee-payer string Fee payer pays fees for the transaction instead of deducting from the signer - --fees string Fees to pay along with transaction; eg: 10uatom - --file string Path to a JSON file containing OperationalFlags - --from string Name or address of private key with which to sign - --gas string gas limit to set per-transaction; set to "auto" to calculate sufficient gas automatically. Note: "auto" option doesn't always report accurate results. Set a valid coin value to adjust the result. Can be used instead of "fees". (default 200000) - --gas-adjustment float adjustment factor to be multiplied against the estimate returned by the tx simulation; if the gas limit is set manually this flag is ignored (default 1) - --gas-prices string Gas prices in decimal format to determine the transaction fee (e.g. 0.1uatom) - --generate-only Build an unsigned transaction and write it to STDOUT (when enabled, the local Keybase only accessed when providing a key name) - -h, --help help for update-operational-flags - --keyring-backend string Select keyring's backend (os|file|kwallet|pass|test|memory) - --keyring-dir string The client Keyring directory; if omitted, the default 'home' directory will be used - --ledger Use a connected Ledger device - --node string [host]:[port] to tendermint rpc interface for this chain - --note string Note to add a description to the transaction (previously --memo) - --offline Offline mode (does not allow any online functionality) - -o, --output string Output format (text|json) - --restart-height int Height for a coordinated zetaclient restart - -s, --sequence uint The sequence number of the signing account (offline mode only) - --sign-mode string Choose sign mode (direct|amino-json|direct-aux), this is an advanced feature - --timeout-height uint Set a block timeout height to prevent the tx from being committed past a certain height - --tip string Tip is the amount that is going to be transferred to the fee payer on the target chain. This flag is only valid when used with --aux, and is ignored if the target chain didn't enable the TipDecorator - -y, --yes Skip tx broadcasting prompt confirmation + -a, --account-number uint The account number of the signing account (offline mode only) + --aux Generate aux signer data instead of sending a tx + -b, --broadcast-mode string Transaction broadcasting mode (sync|async) + --chain-id string The network chain ID + --dry-run ignore the --gas flag and perform a simulation of a transaction, but don't broadcast it (when enabled, the local Keybase is not accessible) + --fee-granter string Fee granter grants fees for the transaction + --fee-payer string Fee payer pays fees for the transaction instead of deducting from the signer + --fees string Fees to pay along with transaction; eg: 10uatom + --file string Path to a JSON file containing OperationalFlags + --from string Name or address of private key with which to sign + --gas string gas limit to set per-transaction; set to "auto" to calculate sufficient gas automatically. Note: "auto" option doesn't always report accurate results. Set a valid coin value to adjust the result. Can be used instead of "fees". (default 200000) + --gas-adjustment float adjustment factor to be multiplied against the estimate returned by the tx simulation; if the gas limit is set manually this flag is ignored (default 1) + --gas-prices string Gas prices in decimal format to determine the transaction fee (e.g. 0.1uatom) + --generate-only Build an unsigned transaction and write it to STDOUT (when enabled, the local Keybase only accessed when providing a key name) + -h, --help help for update-operational-flags + --keyring-backend string Select keyring's backend (os|file|kwallet|pass|test|memory) + --keyring-dir string The client Keyring directory; if omitted, the default 'home' directory will be used + --ledger Use a connected Ledger device + --node string [host]:[port] to tendermint rpc interface for this chain + --note string Note to add a description to the transaction (previously --memo) + --offline Offline mode (does not allow any online functionality) + -o, --output string Output format (text|json) + --restart-height int Height for a coordinated zetaclient restart + -s, --sequence uint The sequence number of the signing account (offline mode only) + --sign-mode string Choose sign mode (direct|amino-json|direct-aux), this is an advanced feature + --signer-block-time-offset duration Offset from the zetacore block time to initiate signing + --timeout-height uint Set a block timeout height to prevent the tx from being committed past a certain height + --tip string Tip is the amount that is going to be transferred to the fee payer on the target chain. This flag is only valid when used with --aux, and is ignored if the target chain didn't enable the TipDecorator + -y, --yes Skip tx broadcasting prompt confirmation ``` ### Options inherited from parent commands diff --git a/docs/openapi/openapi.swagger.yaml b/docs/openapi/openapi.swagger.yaml index 65f305da0f..694f839db6 100644 --- a/docs/openapi/openapi.swagger.yaml +++ b/docs/openapi/openapi.swagger.yaml @@ -58110,6 +58110,11 @@ definitions: description: |- Height for a coordinated zetaclient restart. Will be ignored if missed. + signer_block_time_offset: + type: string + description: |- + Offset from the zetacore block time to initiate signing. + Should be calculated and set based on max(zetaclient_core_block_latency). description: Flags for the top-level operation of zetaclient. observerPendingNonces: type: object diff --git a/docs/spec/observer/messages.md b/docs/spec/observer/messages.md index acceee29f2..0e194bed85 100644 --- a/docs/spec/observer/messages.md +++ b/docs/spec/observer/messages.md @@ -169,7 +169,7 @@ message MsgUpdateGasPriceIncreaseFlags { ```proto message MsgUpdateOperationalFlags { string creator = 1; - OperationalFlags operationalFlags = 2; + OperationalFlags operational_flags = 2; } ``` diff --git a/e2e/e2etests/e2etests.go b/e2e/e2etests/e2etests.go index cabc1e394f..1b9f3ef419 100644 --- a/e2e/e2etests/e2etests.go +++ b/e2e/e2etests/e2etests.go @@ -132,7 +132,8 @@ const ( TestMigrateERC20CustodyFundsName = "migrate_erc20_custody_funds" TestMigrateTSSName = "migrate_tss" TestSolanaWhitelistSPLName = "solana_whitelist_spl" - TestOperationalFlagsName = "operational_flags" + TestZetaclientRestartHeightName = "zetaclient_restart_height" + TestZetaclientSignerOffsetName = "zetaclient_signer_offset" /* Operational tests @@ -880,10 +881,16 @@ var AllE2ETests = []runner.E2ETest{ TestMigrateERC20CustodyFunds, ), runner.NewE2ETest( - TestOperationalFlagsName, - "operational flags functionality", + TestZetaclientRestartHeightName, + "zetaclient scheduled restart height", []runner.ArgDefinition{}, - TestOperationalFlags, + TestZetaclientRestartHeight, + ), + runner.NewE2ETest( + TestZetaclientSignerOffsetName, + "zetaclient signer offset", + []runner.ArgDefinition{}, + TestZetaclientSignerOffset, ), /* Special tests diff --git a/e2e/e2etests/test_operational_flags.go b/e2e/e2etests/test_operational_flags.go index 5084c38b58..0a3d0ee38c 100644 --- a/e2e/e2etests/test_operational_flags.go +++ b/e2e/e2etests/test_operational_flags.go @@ -11,11 +11,13 @@ import ( ) const ( - startTimestampMetricName = "zetaclient_last_start_timestamp_seconds" + startTimestampMetricName = "zetaclient_last_start_timestamp_seconds" + blockTimeLatencyMetricName = "zetaclient_core_block_latency" + blockTimeLatencySleepMetricName = "zetaclient_core_block_latency_sleep" ) -// TestOperationalFlags tests the functionality of operations flags. -func TestOperationalFlags(r *runner.E2ERunner, _ []string) { +// TestZetaclientRestartHeight tests scheduling a zetaclient restart via operational flags +func TestZetaclientRestartHeight(r *runner.E2ERunner, _ []string) { _, err := r.Clients.Zetacore.Observer.OperationalFlags( r.Ctx, &observertypes.QueryOperationalFlagsRequest{}, @@ -60,3 +62,46 @@ func TestOperationalFlags(r *runner.E2ERunner, _ []string) { require.Greater(r, currentStartTime, originalStartTime+1) } + +// TestZetaclientSignerOffset tests scheduling a zetaclient restart via operational flags +func TestZetaclientSignerOffset(r *runner.E2ERunner, _ []string) { + startBlockTimeLatencySleep, err := r.Clients.ZetaclientMetrics.FetchGauge(blockTimeLatencySleepMetricName) + require.NoError(r, err) + require.InDelta(r, 0, startBlockTimeLatencySleep, .01, "start block time latency should be 0") + + // get starting block time latency. + // we need to ensure it's not zero (if zetaclient just finished a restart) + var startBlockTimeLatency float64 + require.Eventually(r, func() bool { + startBlockTimeLatency, err = r.Clients.ZetaclientMetrics.FetchGauge(blockTimeLatencyMetricName) + require.NoError(r, err) + return startBlockTimeLatency > 1 + }, time.Second*15, time.Millisecond*100) + + desiredSignerBlockTimeOffset := time.Duration(startBlockTimeLatency*float64(time.Second)) + time.Millisecond*200 + + updateMsg := observertypes.NewMsgUpdateOperationalFlags( + r.ZetaTxServer.MustGetAccountAddressFromName(utils.OperationalPolicyName), + observertypes.OperationalFlags{ + SignerBlockTimeOffset: &desiredSignerBlockTimeOffset, + }, + ) + + _, err = r.ZetaTxServer.BroadcastTx(utils.OperationalPolicyName, updateMsg) + require.NoError(r, err) + + operationalFlagsRes, err := r.Clients.Zetacore.Observer.OperationalFlags( + r.Ctx, + &observertypes.QueryOperationalFlagsRequest{}, + ) + require.NoError(r, err) + require.InDelta(r, desiredSignerBlockTimeOffset, *(operationalFlagsRes.OperationalFlags.SignerBlockTimeOffset), .01) + + require.Eventually(r, func() bool { + blockTimeLatencySleep, err := r.Clients.ZetaclientMetrics.FetchGauge(blockTimeLatencySleepMetricName) + if err != nil { + return false + } + return blockTimeLatencySleep > .05 + }, time.Second*20, time.Second*1) +} diff --git a/proto/zetachain/zetacore/observer/operational.proto b/proto/zetachain/zetacore/observer/operational.proto index f94c7e7d11..62ff737074 100644 --- a/proto/zetachain/zetacore/observer/operational.proto +++ b/proto/zetachain/zetacore/observer/operational.proto @@ -1,6 +1,9 @@ syntax = "proto3"; package zetachain.zetacore.observer; +import "gogoproto/gogo.proto"; +import "google/protobuf/duration.proto"; + option go_package = "github.com/zeta-chain/node/x/observer/types"; // Flags for the top-level operation of zetaclient. @@ -8,4 +11,9 @@ message OperationalFlags { // Height for a coordinated zetaclient restart. // Will be ignored if missed. int64 restart_height = 1; + + // Offset from the zetacore block time to initiate signing. + // Should be calculated and set based on max(zetaclient_core_block_latency). + google.protobuf.Duration signer_block_time_offset = 2 + [ (gogoproto.stdduration) = true ]; } \ No newline at end of file diff --git a/proto/zetachain/zetacore/observer/tx.proto b/proto/zetachain/zetacore/observer/tx.proto index 0d4ecf275e..b3e1c1ab97 100644 --- a/proto/zetachain/zetacore/observer/tx.proto +++ b/proto/zetachain/zetacore/observer/tx.proto @@ -143,7 +143,7 @@ message MsgUpdateGasPriceIncreaseFlagsResponse {} message MsgUpdateOperationalFlags { string creator = 1; - OperationalFlags operationalFlags = 2 [ (gogoproto.nullable) = false ]; + OperationalFlags operational_flags = 2 [ (gogoproto.nullable) = false ]; } message MsgUpdateOperationalFlagsResponse {} diff --git a/testutil/sample/observer.go b/testutil/sample/observer.go index aa8e0d82c4..1a0ed8f2ba 100644 --- a/testutil/sample/observer.go +++ b/testutil/sample/observer.go @@ -4,6 +4,7 @@ import ( "fmt" "math/rand" "testing" + "time" "github.com/cosmos/cosmos-sdk/crypto/keys/ed25519" "github.com/cosmos/cosmos-sdk/testutil/testdata" @@ -14,6 +15,7 @@ import ( "github.com/zeta-chain/node/pkg/chains" "github.com/zeta-chain/node/pkg/cosmos" zetacrypto "github.com/zeta-chain/node/pkg/crypto" + "github.com/zeta-chain/node/pkg/ptr" "github.com/zeta-chain/node/x/observer/types" ) @@ -287,6 +289,7 @@ func GasPriceIncreaseFlags() types.GasPriceIncreaseFlags { func OperationalFlags() types.OperationalFlags { return types.OperationalFlags{ - RestartHeight: 1, + RestartHeight: 1, + SignerBlockTimeOffset: ptr.Ptr(time.Second), } } diff --git a/typescript/zetachain/zetacore/observer/operational_pb.d.ts b/typescript/zetachain/zetacore/observer/operational_pb.d.ts index 5bae51a4e0..c9fc213927 100644 --- a/typescript/zetachain/zetacore/observer/operational_pb.d.ts +++ b/typescript/zetachain/zetacore/observer/operational_pb.d.ts @@ -3,7 +3,7 @@ /* eslint-disable */ // @ts-nocheck -import type { BinaryReadOptions, FieldList, JsonReadOptions, JsonValue, PartialMessage, PlainMessage } from "@bufbuild/protobuf"; +import type { BinaryReadOptions, Duration, FieldList, JsonReadOptions, JsonValue, PartialMessage, PlainMessage } from "@bufbuild/protobuf"; import { Message, proto3 } from "@bufbuild/protobuf"; /** @@ -20,6 +20,14 @@ export declare class OperationalFlags extends Message { */ restartHeight: bigint; + /** + * Offset from the zetacore block time to initiate signing. + * Should be calculated and set based on max(zetaclient_core_block_latency). + * + * @generated from field: google.protobuf.Duration signer_block_time_offset = 2; + */ + signerBlockTimeOffset?: Duration; + constructor(data?: PartialMessage); static readonly runtime: typeof proto3; diff --git a/typescript/zetachain/zetacore/observer/tx_pb.d.ts b/typescript/zetachain/zetacore/observer/tx_pb.d.ts index 9afe867fdf..40ea5f9e8f 100644 --- a/typescript/zetachain/zetacore/observer/tx_pb.d.ts +++ b/typescript/zetachain/zetacore/observer/tx_pb.d.ts @@ -694,7 +694,7 @@ export declare class MsgUpdateOperationalFlags extends Message signerBlockTimeOffsetLimit { + return cosmoserrors.Wrapf(ErrOperationalFlagsSignerBlockTimeOffsetLimit, "(%s)", signerBlockTimeOffset) + } + } return nil } diff --git a/x/observer/types/operational.pb.go b/x/observer/types/operational.pb.go index b7af5711cf..e5fb9c3281 100644 --- a/x/observer/types/operational.pb.go +++ b/x/observer/types/operational.pb.go @@ -5,16 +5,21 @@ package types import ( fmt "fmt" + _ "github.com/cosmos/gogoproto/gogoproto" proto "github.com/cosmos/gogoproto/proto" + github_com_cosmos_gogoproto_types "github.com/cosmos/gogoproto/types" + _ "google.golang.org/protobuf/types/known/durationpb" io "io" math "math" math_bits "math/bits" + time "time" ) // Reference imports to suppress errors if they are not otherwise used. var _ = proto.Marshal var _ = fmt.Errorf var _ = math.Inf +var _ = time.Kitchen // This is a compile-time assertion to ensure that this generated file // is compatible with the proto package it is being compiled against. @@ -27,6 +32,9 @@ type OperationalFlags struct { // Height for a coordinated zetaclient restart. // Will be ignored if missed. RestartHeight int64 `protobuf:"varint,1,opt,name=restart_height,json=restartHeight,proto3" json:"restart_height,omitempty"` + // Offset from the zetacore block time to initiate signing. + // Should be calculated and set based on max(zetaclient_core_block_latency). + SignerBlockTimeOffset *time.Duration `protobuf:"bytes,2,opt,name=signer_block_time_offset,json=signerBlockTimeOffset,proto3,stdduration" json:"signer_block_time_offset,omitempty"` } func (m *OperationalFlags) Reset() { *m = OperationalFlags{} } @@ -69,6 +77,13 @@ func (m *OperationalFlags) GetRestartHeight() int64 { return 0 } +func (m *OperationalFlags) GetSignerBlockTimeOffset() *time.Duration { + if m != nil { + return m.SignerBlockTimeOffset + } + return nil +} + func init() { proto.RegisterType((*OperationalFlags)(nil), "zetachain.zetacore.observer.OperationalFlags") } @@ -78,19 +93,25 @@ func init() { } var fileDescriptor_ea3eed2ec55093b5 = []byte{ - // 184 bytes of a gzipped FileDescriptorProto - 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0xe2, 0xd2, 0xad, 0x4a, 0x2d, 0x49, - 0x4c, 0xce, 0x48, 0xcc, 0xcc, 0xd3, 0x07, 0xb3, 0xf2, 0x8b, 0x52, 0xf5, 0xf3, 0x93, 0x8a, 0x53, - 0x8b, 0xca, 0x52, 0x8b, 0xf4, 0xf3, 0x0b, 0x52, 0x8b, 0x12, 0x4b, 0x32, 0xf3, 0xf3, 0x12, 0x73, - 0xf4, 0x0a, 0x8a, 0xf2, 0x4b, 0xf2, 0x85, 0xa4, 0xe1, 0xca, 0xf5, 0x60, 0xca, 0xf5, 0x60, 0xca, - 0x95, 0x2c, 0xb9, 0x04, 0xfc, 0x11, 0x3a, 0xdc, 0x72, 0x12, 0xd3, 0x8b, 0x85, 0x54, 0xb9, 0xf8, - 0x8a, 0x52, 0x8b, 0x4b, 0x12, 0x8b, 0x4a, 0xe2, 0x33, 0x52, 0x33, 0xd3, 0x33, 0x4a, 0x24, 0x18, - 0x15, 0x18, 0x35, 0x98, 0x83, 0x78, 0xa1, 0xa2, 0x1e, 0x60, 0x41, 0x27, 0xd7, 0x13, 0x8f, 0xe4, - 0x18, 0x2f, 0x3c, 0x92, 0x63, 0x7c, 0xf0, 0x48, 0x8e, 0x71, 0xc2, 0x63, 0x39, 0x86, 0x0b, 0x8f, - 0xe5, 0x18, 0x6e, 0x3c, 0x96, 0x63, 0x88, 0xd2, 0x4e, 0xcf, 0x2c, 0xc9, 0x28, 0x4d, 0xd2, 0x4b, - 0xce, 0xcf, 0x05, 0xbb, 0x50, 0x17, 0xe2, 0xd8, 0xbc, 0xfc, 0x94, 0x54, 0xfd, 0x0a, 0x84, 0x53, - 0x4b, 0x2a, 0x0b, 0x52, 0x8b, 0x93, 0xd8, 0xc0, 0xae, 0x34, 0x06, 0x04, 0x00, 0x00, 0xff, 0xff, - 0xcf, 0xd8, 0x7d, 0x54, 0xd6, 0x00, 0x00, 0x00, + // 282 bytes of a gzipped FileDescriptorProto + 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0x44, 0x90, 0xc1, 0x4a, 0xec, 0x30, + 0x14, 0x86, 0x27, 0xf7, 0x8a, 0x8b, 0x8a, 0x22, 0x83, 0x42, 0x1d, 0x21, 0x0e, 0x82, 0x30, 0x20, + 0x93, 0x80, 0xbe, 0x41, 0x51, 0x71, 0x37, 0x30, 0xb8, 0x10, 0x37, 0x25, 0xed, 0x9c, 0xa6, 0xc1, + 0xb6, 0xa7, 0x24, 0xa9, 0xa8, 0x4f, 0x21, 0xb8, 0xf1, 0x91, 0x5c, 0xce, 0xd2, 0x9d, 0xd2, 0xbe, + 0x88, 0x4c, 0x32, 0x75, 0x76, 0x27, 0x27, 0xdf, 0x7f, 0x3e, 0xf8, 0x83, 0xe9, 0x2b, 0x58, 0x91, + 0xe6, 0x42, 0x55, 0xdc, 0x4d, 0xa8, 0x81, 0x63, 0x62, 0x40, 0x3f, 0x81, 0xe6, 0x58, 0x83, 0x16, + 0x56, 0x61, 0x25, 0x0a, 0x56, 0x6b, 0xb4, 0x38, 0x3c, 0xfe, 0xc3, 0x59, 0x8f, 0xb3, 0x1e, 0x1f, + 0x1d, 0x48, 0x94, 0xe8, 0x38, 0xbe, 0x9a, 0x7c, 0x64, 0x44, 0x25, 0xa2, 0x2c, 0x80, 0xbb, 0x57, + 0xd2, 0x64, 0x7c, 0xd1, 0xf8, 0xa3, 0xfe, 0xff, 0xf4, 0x9d, 0x04, 0xfb, 0xb3, 0x8d, 0xe8, 0xa6, + 0x10, 0xd2, 0x0c, 0xcf, 0x82, 0x3d, 0x0d, 0xc6, 0x0a, 0x6d, 0xe3, 0x1c, 0x94, 0xcc, 0x6d, 0x48, + 0xc6, 0x64, 0xf2, 0x7f, 0xbe, 0xbb, 0xde, 0xde, 0xba, 0xe5, 0xf0, 0x3e, 0x08, 0x8d, 0x92, 0x15, + 0xe8, 0x38, 0x29, 0x30, 0x7d, 0x8c, 0xad, 0x2a, 0x21, 0xc6, 0x2c, 0x33, 0x60, 0xc3, 0x7f, 0x63, + 0x32, 0xd9, 0xb9, 0x38, 0x62, 0x5e, 0xcf, 0x7a, 0x3d, 0xbb, 0x5a, 0xeb, 0xa3, 0xad, 0x8f, 0xef, + 0x13, 0x32, 0x3f, 0xf4, 0x07, 0xa2, 0x55, 0xfe, 0x4e, 0x95, 0x30, 0x73, 0xe9, 0xe8, 0xfa, 0xb3, + 0xa5, 0x64, 0xd9, 0x52, 0xf2, 0xd3, 0x52, 0xf2, 0xd6, 0xd1, 0xc1, 0xb2, 0xa3, 0x83, 0xaf, 0x8e, + 0x0e, 0x1e, 0xce, 0xa5, 0xb2, 0x79, 0x93, 0xb0, 0x14, 0x4b, 0x57, 0xd9, 0xd4, 0xb7, 0x57, 0xe1, + 0x02, 0xf8, 0xf3, 0xa6, 0x3b, 0xfb, 0x52, 0x83, 0x49, 0xb6, 0x9d, 0xf6, 0xf2, 0x37, 0x00, 0x00, + 0xff, 0xff, 0xf0, 0xe0, 0x3c, 0x60, 0x67, 0x01, 0x00, 0x00, } func (m *OperationalFlags) Marshal() (dAtA []byte, err error) { @@ -113,6 +134,16 @@ func (m *OperationalFlags) MarshalToSizedBuffer(dAtA []byte) (int, error) { _ = i var l int _ = l + if m.SignerBlockTimeOffset != nil { + n1, err1 := github_com_cosmos_gogoproto_types.StdDurationMarshalTo(*m.SignerBlockTimeOffset, dAtA[i-github_com_cosmos_gogoproto_types.SizeOfStdDuration(*m.SignerBlockTimeOffset):]) + if err1 != nil { + return 0, err1 + } + i -= n1 + i = encodeVarintOperational(dAtA, i, uint64(n1)) + i-- + dAtA[i] = 0x12 + } if m.RestartHeight != 0 { i = encodeVarintOperational(dAtA, i, uint64(m.RestartHeight)) i-- @@ -141,6 +172,10 @@ func (m *OperationalFlags) Size() (n int) { if m.RestartHeight != 0 { n += 1 + sovOperational(uint64(m.RestartHeight)) } + if m.SignerBlockTimeOffset != nil { + l = github_com_cosmos_gogoproto_types.SizeOfStdDuration(*m.SignerBlockTimeOffset) + n += 1 + l + sovOperational(uint64(l)) + } return n } @@ -198,6 +233,42 @@ func (m *OperationalFlags) Unmarshal(dAtA []byte) error { break } } + case 2: + if wireType != 2 { + return fmt.Errorf("proto: wrong wireType = %d for field SignerBlockTimeOffset", wireType) + } + var msglen int + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowOperational + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + msglen |= int(b&0x7F) << shift + if b < 0x80 { + break + } + } + if msglen < 0 { + return ErrInvalidLengthOperational + } + postIndex := iNdEx + msglen + if postIndex < 0 { + return ErrInvalidLengthOperational + } + if postIndex > l { + return io.ErrUnexpectedEOF + } + if m.SignerBlockTimeOffset == nil { + m.SignerBlockTimeOffset = new(time.Duration) + } + if err := github_com_cosmos_gogoproto_types.StdDurationUnmarshal(m.SignerBlockTimeOffset, dAtA[iNdEx:postIndex]); err != nil { + return err + } + iNdEx = postIndex default: iNdEx = preIndex skippy, err := skipOperational(dAtA[iNdEx:]) diff --git a/x/observer/types/operational_test.go b/x/observer/types/operational_test.go index 39ef06ef82..79c8caf0dc 100644 --- a/x/observer/types/operational_test.go +++ b/x/observer/types/operational_test.go @@ -2,8 +2,10 @@ package types_test import ( "testing" + "time" "github.com/stretchr/testify/require" + "github.com/zeta-chain/node/pkg/ptr" "github.com/zeta-chain/node/x/observer/types" ) @@ -14,18 +16,45 @@ func TestOperationalFlags_Validate(t *testing.T) { errContains string }{ { - name: "invalid operational flags", + name: "invalid restart height", of: types.OperationalFlags{ RestartHeight: -1, }, errContains: types.ErrOperationalFlagsRestartHeightNegative.Error(), }, { - name: "valid", + name: "valid restart height", of: types.OperationalFlags{ RestartHeight: 1, }, }, + { + name: "valid signer offset", + of: types.OperationalFlags{ + SignerBlockTimeOffset: ptr.Ptr(time.Second), + }, + }, + { + name: "negative signer offset", + of: types.OperationalFlags{ + SignerBlockTimeOffset: ptr.Ptr(-time.Second), + }, + errContains: types.ErrOperationalFlagsSignerBlockTimeOffsetNegative.Error(), + }, + { + name: "signer offset limit exceeded", + of: types.OperationalFlags{ + SignerBlockTimeOffset: ptr.Ptr(time.Minute), + }, + errContains: types.ErrOperationalFlagsSignerBlockTimeOffsetLimit.Error(), + }, + { + name: "all flags valid", + of: types.OperationalFlags{ + RestartHeight: 1, + SignerBlockTimeOffset: ptr.Ptr(time.Second), + }, + }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { diff --git a/x/observer/types/tx.pb.go b/x/observer/types/tx.pb.go index 7ea1ae4086..2481756e9e 100644 --- a/x/observer/types/tx.pb.go +++ b/x/observer/types/tx.pb.go @@ -1242,7 +1242,7 @@ var xxx_messageInfo_MsgUpdateGasPriceIncreaseFlagsResponse proto.InternalMessage type MsgUpdateOperationalFlags struct { Creator string `protobuf:"bytes,1,opt,name=creator,proto3" json:"creator,omitempty"` - OperationalFlags OperationalFlags `protobuf:"bytes,2,opt,name=operationalFlags,proto3" json:"operationalFlags"` + OperationalFlags OperationalFlags `protobuf:"bytes,2,opt,name=operational_flags,json=operationalFlags,proto3" json:"operational_flags"` } func (m *MsgUpdateOperationalFlags) Reset() { *m = MsgUpdateOperationalFlags{} } @@ -1362,91 +1362,91 @@ func init() { } var fileDescriptor_eda6e3b1d16a4021 = []byte{ - // 1332 bytes of a gzipped FileDescriptorProto - 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0xb4, 0x58, 0xcb, 0x6f, 0xdb, 0xc6, - 0x13, 0x36, 0x7f, 0xce, 0xc3, 0x1e, 0x5b, 0xb2, 0xc3, 0x9f, 0xe3, 0xd8, 0x74, 0xa3, 0xa6, 0x6c, - 0xe2, 0x28, 0x2f, 0x29, 0x56, 0x8a, 0x3e, 0x81, 0x02, 0x8e, 0x93, 0xd8, 0x6e, 0xea, 0x24, 0xa0, - 0xdc, 0xa0, 0xc8, 0x85, 0x58, 0x91, 0x6b, 0x8a, 0x35, 0xbd, 0x2b, 0x70, 0x29, 0x3b, 0x4e, 0x83, - 0x02, 0x3d, 0x16, 0xe8, 0x21, 0xa7, 0x9e, 0x0a, 0xf4, 0xde, 0xff, 0xa0, 0xf7, 0x1e, 0xd2, 0x43, - 0x81, 0x1c, 0x7b, 0x2a, 0x8a, 0xe4, 0xd4, 0xff, 0xa2, 0xe0, 0xee, 0x72, 0x25, 0x51, 0x32, 0x45, - 0xb9, 0xe8, 0x49, 0xe4, 0xec, 0xf7, 0xcd, 0x7c, 0xb3, 0x8f, 0x99, 0x15, 0xe1, 0xe2, 0x33, 0x1c, - 0x21, 0xa7, 0x89, 0x7c, 0x52, 0xe5, 0x4f, 0x34, 0xc4, 0x55, 0xda, 0x60, 0x38, 0xdc, 0xc7, 0x61, - 0x35, 0x7a, 0x5a, 0x69, 0x85, 0x34, 0xa2, 0xfa, 0x92, 0x42, 0x55, 0x12, 0x54, 0x25, 0x41, 0x19, - 0x73, 0x1e, 0xf5, 0x28, 0xc7, 0x55, 0xe3, 0x27, 0x41, 0x31, 0x2e, 0x67, 0x39, 0x6e, 0x04, 0x68, - 0x0f, 0x4b, 0x60, 0x2d, 0x0b, 0xe8, 0x84, 0x94, 0x31, 0x3e, 0x68, 0xef, 0x04, 0xc8, 0x63, 0x92, - 0x73, 0x35, 0x8b, 0x93, 0x3c, 0x48, 0x6c, 0x39, 0x0b, 0xdb, 0x42, 0x21, 0xda, 0x4b, 0xbc, 0xde, - 0xcc, 0x44, 0x62, 0xe2, 0xfa, 0xc4, 0xb3, 0x09, 0x25, 0x0e, 0x4e, 0x18, 0x97, 0x32, 0x67, 0x8f, - 0x25, 0xb0, 0x1b, 0x99, 0x72, 0x5b, 0x38, 0x44, 0x91, 0x4f, 0x09, 0x0a, 0x32, 0xb2, 0x6b, 0xed, - 0x7a, 0x55, 0x6e, 0x62, 0xf2, 0x67, 0x08, 0xb6, 0x15, 0x52, 0xba, 0xc3, 0xe4, 0x8f, 0xc0, 0x9a, - 0x7f, 0x6b, 0x70, 0x66, 0x8b, 0x79, 0x5f, 0xb4, 0x5c, 0x14, 0xe1, 0x87, 0x32, 0xbe, 0xbe, 0x00, - 0xa7, 0x9d, 0x10, 0xa3, 0x88, 0x86, 0x0b, 0xda, 0x05, 0xad, 0x3c, 0x69, 0x25, 0xaf, 0xfa, 0x4d, - 0x98, 0xa3, 0x81, 0x6b, 0x27, 0x4a, 0x6d, 0xe4, 0xba, 0x21, 0x66, 0x6c, 0xe1, 0x7f, 0x1c, 0xa6, - 0xd3, 0xc0, 0x4d, 0x9c, 0xac, 0x8a, 0x91, 0x98, 0x41, 0xf0, 0x41, 0x3f, 0x63, 0x5c, 0x30, 0x08, - 0x3e, 0x48, 0x33, 0x1e, 0x43, 0xa1, 0xcd, 0xf5, 0xd8, 0x21, 0x46, 0x8c, 0x92, 0x85, 0x13, 0x17, - 0xb4, 0x72, 0xb1, 0xb6, 0x52, 0xc9, 0xd8, 0x71, 0x95, 0xc4, 0x89, 0xc8, 0xc4, 0xe2, 0x44, 0x6b, - 0xba, 0xdd, 0xf5, 0x66, 0x2e, 0xc1, 0x62, 0x5f, 0xaa, 0x16, 0x66, 0x2d, 0x4a, 0x18, 0x36, 0x7f, - 0xd3, 0x40, 0xdf, 0x62, 0xde, 0x63, 0x1a, 0xe1, 0xdb, 0x01, 0x75, 0x76, 0x37, 0x30, 0x72, 0x33, - 0x67, 0x62, 0x11, 0x26, 0xc4, 0x26, 0xf4, 0x5d, 0x9e, 0xfd, 0xb8, 0x75, 0x9a, 0xbf, 0x6f, 0xba, - 0xfa, 0x79, 0x80, 0x46, 0xec, 0xc3, 0x6e, 0x22, 0xd6, 0xe4, 0x89, 0x4e, 0x5b, 0x93, 0xdc, 0xb2, - 0x81, 0x58, 0x53, 0x9f, 0x87, 0x53, 0x4d, 0xec, 0x7b, 0xcd, 0x88, 0x27, 0x36, 0x6e, 0xc9, 0x37, - 0x7d, 0x3d, 0xb6, 0xc7, 0x51, 0x17, 0x4e, 0x5e, 0xd0, 0xca, 0x53, 0xb5, 0x2b, 0x83, 0x12, 0x6e, - 0xed, 0x7a, 0x15, 0xb9, 0x82, 0x42, 0xe2, 0x1d, 0x14, 0xa1, 0xdb, 0x27, 0x5e, 0xfe, 0xf9, 0xf6, - 0x98, 0x25, 0xe9, 0xe6, 0x57, 0x60, 0xf4, 0xa7, 0x92, 0x64, 0xaa, 0x5f, 0x82, 0x62, 0x03, 0x05, - 0x01, 0x8d, 0x6c, 0x9e, 0x0a, 0x76, 0x79, 0x66, 0x13, 0x56, 0x41, 0x58, 0xd7, 0x84, 0x31, 0x86, - 0xed, 0xd3, 0x08, 0xdb, 0x3b, 0x3e, 0x41, 0x81, 0xff, 0x0c, 0x8b, 0x2c, 0x27, 0xac, 0x42, 0x6c, - 0xbd, 0x97, 0x18, 0xcd, 0xe7, 0x30, 0xa7, 0x26, 0x75, 0x2d, 0x96, 0xfa, 0x88, 0x1f, 0x9f, 0x8c, - 0x89, 0xfb, 0x0c, 0xa6, 0x9c, 0x0e, 0x90, 0x7b, 0x9d, 0xaa, 0x95, 0x33, 0x17, 0xb7, 0xcb, 0xb1, - 0xd5, 0x4d, 0x36, 0x4b, 0xf0, 0xd6, 0xa0, 0xe8, 0x6a, 0x55, 0xef, 0x73, 0x75, 0x16, 0xde, 0xa3, - 0xfb, 0x39, 0xd5, 0x1d, 0xbd, 0xac, 0x32, 0x58, 0x9f, 0x33, 0x15, 0xec, 0x57, 0x0d, 0x8a, 0x5b, - 0xcc, 0x5b, 0x75, 0xdd, 0x1c, 0x07, 0xe9, 0x0a, 0xcc, 0x1e, 0x71, 0x88, 0x66, 0x68, 0xea, 0x3c, - 0x7c, 0x0c, 0x8b, 0x7c, 0x4a, 0x02, 0x1f, 0x93, 0xc8, 0xf6, 0x42, 0x44, 0x22, 0x8c, 0xed, 0x56, - 0xbb, 0xb1, 0x8b, 0x0f, 0xe5, 0x31, 0x3a, 0xd7, 0x01, 0xac, 0x8b, 0xf1, 0x47, 0x7c, 0x58, 0x5f, - 0x81, 0xb3, 0xc8, 0x75, 0x6d, 0x42, 0x5d, 0x6c, 0x23, 0xc7, 0xa1, 0x6d, 0x12, 0xd9, 0x94, 0x04, - 0x87, 0x7c, 0xeb, 0x4d, 0x58, 0x3a, 0x72, 0xdd, 0x07, 0xd4, 0xc5, 0xab, 0x62, 0xe8, 0x21, 0x09, - 0x0e, 0xcd, 0x05, 0x98, 0xef, 0xcd, 0x42, 0x25, 0xf8, 0xbd, 0x06, 0xd3, 0x6a, 0x63, 0xa1, 0x3d, - 0x7c, 0xbc, 0xd3, 0xb1, 0x1e, 0x9f, 0x0e, 0xb4, 0x87, 0x6d, 0x9f, 0xec, 0x50, 0xae, 0x7f, 0xaa, - 0x66, 0x66, 0x2e, 0x3f, 0x0f, 0x26, 0xf7, 0xf8, 0x24, 0xe7, 0x6e, 0x92, 0x1d, 0x6a, 0xce, 0xf3, - 0xc5, 0x55, 0x6a, 0x94, 0xcc, 0x55, 0x98, 0x51, 0x9b, 0xe2, 0x3e, 0x3e, 0xf4, 0x30, 0xc9, 0x10, - 0x3a, 0x07, 0x27, 0xf9, 0xc9, 0x94, 0x2a, 0xc5, 0x8b, 0xb9, 0x08, 0xe7, 0x52, 0x2e, 0x94, 0xf7, - 0x1f, 0x35, 0xf8, 0x3f, 0xdf, 0x06, 0x0c, 0x47, 0x7c, 0x17, 0x3c, 0xe0, 0xd5, 0xff, 0x78, 0x73, - 0xb1, 0x0c, 0x33, 0x62, 0x88, 0xb7, 0x10, 0x3b, 0xa0, 0x07, 0x7c, 0x42, 0xc6, 0xad, 0x82, 0xa3, - 0x5c, 0x7f, 0x4e, 0x0f, 0xf4, 0x32, 0xcc, 0x76, 0xe3, 0x9a, 0xbe, 0xd7, 0x94, 0xc5, 0xa3, 0xd8, - 0x01, 0x6e, 0xf8, 0x5e, 0xd3, 0x3c, 0x0f, 0x4b, 0x03, 0xd4, 0x29, 0xf5, 0xbf, 0x68, 0x00, 0x72, - 0xd2, 0xb6, 0xeb, 0xf5, 0x0c, 0xd1, 0xe7, 0x01, 0x22, 0xc6, 0x92, 0x5d, 0x26, 0x76, 0xe6, 0x64, - 0xc4, 0x98, 0xdc, 0x57, 0xd7, 0x41, 0xdf, 0xe5, 0xf3, 0x62, 0xc7, 0xcb, 0x65, 0xcb, 0x7a, 0x26, - 0xb4, 0xcf, 0x8a, 0x91, 0x27, 0x38, 0x42, 0x1b, 0xa2, 0xb2, 0xdd, 0x81, 0x53, 0x2c, 0x42, 0x51, - 0x9b, 0xc9, 0x52, 0x7e, 0xfd, 0xa8, 0xca, 0x26, 0xfb, 0x98, 0x85, 0x1d, 0xec, 0xef, 0xe3, 0x3a, - 0xe7, 0x58, 0x92, 0x6b, 0x7e, 0xd7, 0x29, 0xd1, 0xdb, 0xf5, 0xfa, 0x7f, 0x53, 0xcf, 0x62, 0x98, - 0x4c, 0x8c, 0xb5, 0x1d, 0x27, 0x69, 0x54, 0x13, 0x56, 0x41, 0x58, 0xeb, 0xc2, 0x68, 0x1e, 0x40, - 0x61, 0x8b, 0x79, 0x77, 0x09, 0x6a, 0x04, 0x78, 0x6d, 0x6d, 0xfb, 0xcb, 0x8c, 0x99, 0xbc, 0x08, - 0x05, 0xcc, 0x71, 0x9b, 0xa4, 0x41, 0xdb, 0x44, 0xc5, 0xed, 0x31, 0xea, 0xcb, 0x50, 0x14, 0x86, - 0x87, 0xed, 0x48, 0xc0, 0x44, 0xdc, 0x94, 0xd5, 0x3c, 0x07, 0x67, 0x7b, 0x02, 0xab, 0x95, 0x7d, - 0xce, 0x8b, 0xcf, 0x1d, 0x9f, 0xe5, 0x90, 0xb4, 0x0c, 0x45, 0x57, 0x00, 0x7b, 0x35, 0xa5, 0xac, - 0x7a, 0x19, 0x66, 0xa4, 0x25, 0xa5, 0x2a, 0x6d, 0x96, 0x45, 0xa3, 0x2b, 0xba, 0xd2, 0xf5, 0xb3, - 0x06, 0x25, 0x75, 0x96, 0xd6, 0x11, 0x7b, 0x14, 0xfa, 0x0e, 0xde, 0x24, 0xb1, 0x14, 0x86, 0xef, - 0xc5, 0x17, 0xb8, 0x0c, 0xa1, 0x04, 0xce, 0x7a, 0x83, 0x28, 0xb2, 0x6b, 0xd4, 0x32, 0xcb, 0xc6, - 0xc0, 0x60, 0xb2, 0x8c, 0x0c, 0x76, 0x6b, 0x96, 0x61, 0x39, 0x5b, 0xab, 0x4a, 0xeb, 0x07, 0xad, - 0xfb, 0x36, 0xd1, 0xb9, 0xaf, 0x0d, 0xcb, 0xc8, 0x86, 0x59, 0x9a, 0x42, 0xcb, 0x64, 0x6e, 0x64, - 0xdf, 0x6f, 0x52, 0x24, 0x99, 0x47, 0x9f, 0x33, 0xf3, 0x5d, 0x78, 0xe7, 0x48, 0x5d, 0x89, 0xfa, - 0xda, 0xef, 0xd3, 0x30, 0xbe, 0xc5, 0x3c, 0x9d, 0xc2, 0x54, 0x77, 0xbb, 0xba, 0x96, 0x29, 0xa1, - 0xb7, 0x2b, 0x18, 0xb7, 0x46, 0x00, 0xab, 0xc3, 0xfa, 0x14, 0x8a, 0xa9, 0xbb, 0x66, 0x65, 0x98, - 0x9b, 0x5e, 0xbc, 0xf1, 0xfe, 0x68, 0x78, 0x15, 0xf9, 0x5b, 0x0d, 0xce, 0xf4, 0x5f, 0x53, 0x56, - 0xf2, 0x79, 0xeb, 0xa2, 0x18, 0x1f, 0x8d, 0x4c, 0xe9, 0xd1, 0xd0, 0x7f, 0x19, 0x19, 0xaa, 0xa1, - 0x8f, 0x32, 0x5c, 0xc3, 0x91, 0xb7, 0x14, 0xdd, 0x87, 0xc9, 0x4e, 0x03, 0xbf, 0x32, 0xcc, 0x8f, - 0x82, 0x1a, 0x2b, 0xb9, 0xa1, 0x2a, 0x54, 0x08, 0xd3, 0x3d, 0x5d, 0xf8, 0x7a, 0xbe, 0x99, 0x13, - 0x68, 0xe3, 0xbd, 0x51, 0xd0, 0x2a, 0xe6, 0xd7, 0x30, 0x93, 0xbe, 0xc3, 0x57, 0xf3, 0x29, 0x57, - 0x04, 0xe3, 0x83, 0x11, 0x09, 0x2a, 0xf8, 0x37, 0x30, 0xdb, 0x77, 0x2f, 0xb8, 0x39, 0x7c, 0xa9, - 0x7a, 0x19, 0xc6, 0x87, 0xa3, 0x32, 0x54, 0x7c, 0x07, 0x4e, 0x27, 0x9d, 0xfd, 0x72, 0x9e, 0x1c, - 0xb6, 0xeb, 0x75, 0xa3, 0x9a, 0x13, 0xa8, 0x82, 0x04, 0x00, 0x5d, 0x7d, 0xef, 0xea, 0x30, 0x7a, - 0x07, 0x6b, 0xd4, 0xf2, 0x63, 0x55, 0x34, 0x0a, 0x53, 0xdd, 0x3d, 0x6d, 0x68, 0x85, 0xea, 0x02, - 0x0f, 0xaf, 0x50, 0x03, 0xfa, 0x95, 0xfe, 0x93, 0x06, 0x4b, 0x59, 0xcd, 0xea, 0x93, 0x7c, 0xdb, - 0x72, 0x20, 0xd9, 0x58, 0xfb, 0x17, 0x64, 0xa5, 0xf0, 0x85, 0x06, 0xf3, 0x47, 0xf4, 0x9d, 0xbc, - 0xc5, 0x31, 0xc5, 0x33, 0x3e, 0x3d, 0x1e, 0x2f, 0x91, 0x74, 0xfb, 0xee, 0xcb, 0xd7, 0x25, 0xed, - 0xd5, 0xeb, 0x92, 0xf6, 0xd7, 0xeb, 0x92, 0xf6, 0xe2, 0x4d, 0x69, 0xec, 0xd5, 0x9b, 0xd2, 0xd8, - 0x1f, 0x6f, 0x4a, 0x63, 0x4f, 0xae, 0x79, 0x7e, 0xd4, 0x6c, 0x37, 0x2a, 0x0e, 0xdd, 0xe3, 0x5f, - 0x23, 0x6e, 0x88, 0x0f, 0x13, 0xf1, 0x1f, 0x92, 0xea, 0xd3, 0xae, 0x0f, 0x23, 0x87, 0x2d, 0xcc, - 0x1a, 0xa7, 0xf8, 0x47, 0x89, 0x5b, 0xff, 0x04, 0x00, 0x00, 0xff, 0xff, 0xa3, 0xa3, 0x3d, 0x21, - 0x82, 0x12, 0x00, 0x00, + // 1336 bytes of a gzipped FileDescriptorProto + 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0xb4, 0x58, 0xcd, 0x6f, 0xdc, 0x44, + 0x14, 0x8f, 0x49, 0x3f, 0x92, 0x97, 0xec, 0x26, 0x35, 0x69, 0x9a, 0x38, 0x74, 0x29, 0xa6, 0x4d, + 0xb7, 0x5f, 0xbb, 0xcd, 0x16, 0xf1, 0x29, 0x21, 0xa5, 0x69, 0x9b, 0x84, 0x92, 0xb6, 0xf2, 0x86, + 0x0a, 0xf5, 0x62, 0x66, 0xed, 0x89, 0xd7, 0xc4, 0x99, 0x59, 0x79, 0xbc, 0x49, 0x53, 0x2a, 0x24, + 0x8e, 0x48, 0x1c, 0x7a, 0xe3, 0x82, 0xc4, 0x9d, 0xff, 0x80, 0x3b, 0x87, 0x72, 0x40, 0xea, 0x91, + 0x13, 0x42, 0xed, 0x89, 0xff, 0x02, 0x79, 0x66, 0x3c, 0xeb, 0xfd, 0x88, 0xd7, 0x5b, 0xc4, 0x69, + 0xed, 0x37, 0xbf, 0xdf, 0x7b, 0xbf, 0x37, 0x1f, 0xef, 0xcd, 0x1a, 0xce, 0x3f, 0xc1, 0x11, 0x72, + 0x9a, 0xc8, 0x27, 0x55, 0xfe, 0x44, 0x43, 0x5c, 0xa5, 0x0d, 0x86, 0xc3, 0x7d, 0x1c, 0x56, 0xa3, + 0xc7, 0x95, 0x56, 0x48, 0x23, 0xaa, 0x2f, 0x29, 0x54, 0x25, 0x41, 0x55, 0x12, 0x94, 0x31, 0xe7, + 0x51, 0x8f, 0x72, 0x5c, 0x35, 0x7e, 0x12, 0x14, 0xe3, 0x62, 0x96, 0xe3, 0x46, 0x80, 0xf6, 0xb0, + 0x04, 0xd6, 0xb2, 0x80, 0x4e, 0x48, 0x19, 0xe3, 0x83, 0xf6, 0x4e, 0x80, 0x3c, 0x26, 0x39, 0x97, + 0xb3, 0x38, 0xc9, 0x83, 0xc4, 0x96, 0xb3, 0xb0, 0x2d, 0x14, 0xa2, 0xbd, 0xc4, 0xeb, 0xf5, 0x4c, + 0x24, 0x26, 0xae, 0x4f, 0x3c, 0x9b, 0x50, 0xe2, 0xe0, 0x84, 0x71, 0x21, 0x73, 0xf6, 0x58, 0x02, + 0xbb, 0x96, 0x29, 0xb7, 0x85, 0x43, 0x14, 0xf9, 0x94, 0xa0, 0x20, 0x23, 0xbb, 0xd6, 0xae, 0x57, + 0xe5, 0x26, 0x26, 0x7f, 0x86, 0x60, 0x5b, 0x21, 0xa5, 0x3b, 0x4c, 0xfe, 0x08, 0xac, 0xf9, 0x8f, + 0x06, 0xa7, 0xb6, 0x98, 0xf7, 0x45, 0xcb, 0x45, 0x11, 0xbe, 0x2f, 0xe3, 0xeb, 0x0b, 0x70, 0xd2, + 0x09, 0x31, 0x8a, 0x68, 0xb8, 0xa0, 0x9d, 0xd3, 0xca, 0x93, 0x56, 0xf2, 0xaa, 0x5f, 0x87, 0x39, + 0x1a, 0xb8, 0x76, 0xa2, 0xd4, 0x46, 0xae, 0x1b, 0x62, 0xc6, 0x16, 0xde, 0xe0, 0x30, 0x9d, 0x06, + 0x6e, 0xe2, 0x64, 0x55, 0x8c, 0xc4, 0x0c, 0x82, 0x0f, 0xfa, 0x19, 0xe3, 0x82, 0x41, 0xf0, 0x41, + 0x2f, 0xe3, 0x21, 0x14, 0xda, 0x5c, 0x8f, 0x1d, 0x62, 0xc4, 0x28, 0x59, 0x38, 0x76, 0x4e, 0x2b, + 0x17, 0x6b, 0x2b, 0x95, 0x8c, 0x1d, 0x57, 0x49, 0x9c, 0x88, 0x4c, 0x2c, 0x4e, 0xb4, 0xa6, 0xdb, + 0xa9, 0x37, 0x73, 0x09, 0x16, 0xfb, 0x52, 0xb5, 0x30, 0x6b, 0x51, 0xc2, 0xb0, 0xf9, 0xbb, 0x06, + 0xfa, 0x16, 0xf3, 0x1e, 0xd2, 0x08, 0xdf, 0x0c, 0xa8, 0xb3, 0xbb, 0x81, 0x91, 0x9b, 0x39, 0x13, + 0x8b, 0x30, 0x21, 0x36, 0xa1, 0xef, 0xf2, 0xec, 0xc7, 0xad, 0x93, 0xfc, 0x7d, 0xd3, 0xd5, 0xcf, + 0x02, 0x34, 0x62, 0x1f, 0x76, 0x13, 0xb1, 0x26, 0x4f, 0x74, 0xda, 0x9a, 0xe4, 0x96, 0x0d, 0xc4, + 0x9a, 0xfa, 0x3c, 0x9c, 0x68, 0x62, 0xdf, 0x6b, 0x46, 0x3c, 0xb1, 0x71, 0x4b, 0xbe, 0xe9, 0xeb, + 0xb1, 0x3d, 0x8e, 0xba, 0x70, 0xfc, 0x9c, 0x56, 0x9e, 0xaa, 0x5d, 0x1a, 0x94, 0x70, 0x6b, 0xd7, + 0xab, 0xc8, 0x15, 0x14, 0x12, 0x6f, 0xa1, 0x08, 0xdd, 0x3c, 0xf6, 0xfc, 0xaf, 0xb7, 0xc7, 0x2c, + 0x49, 0x37, 0xbf, 0x06, 0xa3, 0x3f, 0x95, 0x24, 0x53, 0xfd, 0x02, 0x14, 0x1b, 0x28, 0x08, 0x68, + 0x64, 0xf3, 0x54, 0xb0, 0xcb, 0x33, 0x9b, 0xb0, 0x0a, 0xc2, 0xba, 0x26, 0x8c, 0x31, 0x6c, 0x9f, + 0x46, 0xd8, 0xde, 0xf1, 0x09, 0x0a, 0xfc, 0x27, 0x58, 0x64, 0x39, 0x61, 0x15, 0x62, 0xeb, 0x9d, + 0xc4, 0x68, 0x3e, 0x85, 0x39, 0x35, 0xa9, 0x6b, 0xb1, 0xd4, 0x07, 0xfc, 0xf8, 0x64, 0x4c, 0xdc, + 0x67, 0x30, 0xe5, 0x74, 0x80, 0xdc, 0xeb, 0x54, 0xad, 0x9c, 0xb9, 0xb8, 0x29, 0xc7, 0x56, 0x9a, + 0x6c, 0x96, 0xe0, 0xad, 0x41, 0xd1, 0xd5, 0xaa, 0xde, 0xe5, 0xea, 0x2c, 0xbc, 0x47, 0xf7, 0x73, + 0xaa, 0x3b, 0x7a, 0x59, 0x65, 0xb0, 0x3e, 0x67, 0x2a, 0xd8, 0x6f, 0x1a, 0x14, 0xb7, 0x98, 0xb7, + 0xea, 0xba, 0x39, 0x0e, 0xd2, 0x25, 0x98, 0x3d, 0xe2, 0x10, 0xcd, 0xd0, 0x9e, 0xf3, 0xf0, 0x31, + 0x2c, 0xf2, 0x29, 0x09, 0x7c, 0x4c, 0x22, 0xdb, 0x0b, 0x11, 0x89, 0x30, 0xb6, 0x5b, 0xed, 0xc6, + 0x2e, 0x3e, 0x94, 0xc7, 0xe8, 0x4c, 0x07, 0xb0, 0x2e, 0xc6, 0x1f, 0xf0, 0x61, 0x7d, 0x05, 0x4e, + 0x23, 0xd7, 0xb5, 0x09, 0x75, 0xb1, 0x8d, 0x1c, 0x87, 0xb6, 0x49, 0x64, 0x53, 0x12, 0x1c, 0xf2, + 0xad, 0x37, 0x61, 0xe9, 0xc8, 0x75, 0xef, 0x51, 0x17, 0xaf, 0x8a, 0xa1, 0xfb, 0x24, 0x38, 0x34, + 0x17, 0x60, 0xbe, 0x3b, 0x0b, 0x95, 0xe0, 0x0f, 0x1a, 0x4c, 0xab, 0x8d, 0x85, 0xf6, 0xf0, 0xeb, + 0x9d, 0x8e, 0xf5, 0xf8, 0x74, 0xa0, 0x3d, 0x6c, 0xfb, 0x64, 0x87, 0x72, 0xfd, 0x53, 0x35, 0x33, + 0x73, 0xf9, 0x79, 0x30, 0xb9, 0xc7, 0x27, 0x39, 0x77, 0x93, 0xec, 0x50, 0x73, 0x9e, 0x2f, 0xae, + 0x52, 0xa3, 0x64, 0xae, 0xc2, 0x8c, 0xda, 0x14, 0x77, 0xf1, 0xa1, 0x87, 0x49, 0x86, 0xd0, 0x39, + 0x38, 0xce, 0x4f, 0xa6, 0x54, 0x29, 0x5e, 0xcc, 0x45, 0x38, 0xd3, 0xe3, 0x42, 0x79, 0xff, 0x49, + 0x83, 0x37, 0xf9, 0x36, 0x60, 0x38, 0xe2, 0xbb, 0xe0, 0x1e, 0xaf, 0xfe, 0xaf, 0x37, 0x17, 0xcb, + 0x30, 0x23, 0x86, 0x78, 0x0b, 0xb1, 0x03, 0x7a, 0xc0, 0x27, 0x64, 0xdc, 0x2a, 0x38, 0xca, 0xf5, + 0xe7, 0xf4, 0x40, 0x2f, 0xc3, 0x6c, 0x1a, 0xd7, 0xf4, 0xbd, 0xa6, 0x2c, 0x1e, 0xc5, 0x0e, 0x70, + 0xc3, 0xf7, 0x9a, 0xe6, 0x59, 0x58, 0x1a, 0xa0, 0x4e, 0xa9, 0xff, 0x55, 0x03, 0x90, 0x93, 0xb6, + 0x5d, 0xaf, 0x67, 0x88, 0x3e, 0x0b, 0x10, 0x31, 0x96, 0xec, 0x32, 0xb1, 0x33, 0x27, 0x23, 0xc6, + 0xe4, 0xbe, 0xba, 0x0a, 0xfa, 0x2e, 0x9f, 0x17, 0x3b, 0x5e, 0x2e, 0x5b, 0xd6, 0x33, 0xa1, 0x7d, + 0x56, 0x8c, 0x3c, 0xc2, 0x11, 0xda, 0x10, 0x95, 0xed, 0x16, 0x9c, 0x60, 0x11, 0x8a, 0xda, 0x4c, + 0x96, 0xf2, 0xab, 0x47, 0x55, 0x36, 0xd9, 0xc7, 0x2c, 0xec, 0x60, 0x7f, 0x1f, 0xd7, 0x39, 0xc7, + 0x92, 0x5c, 0xf3, 0xfb, 0x4e, 0x89, 0xde, 0xae, 0xd7, 0xff, 0x9f, 0x7a, 0x16, 0xc3, 0x64, 0x62, + 0xac, 0xed, 0x38, 0x49, 0xa3, 0x9a, 0xb0, 0x0a, 0xc2, 0x5a, 0x17, 0x46, 0xf3, 0x00, 0x0a, 0x5b, + 0xcc, 0xbb, 0x4d, 0x50, 0x23, 0xc0, 0x6b, 0x6b, 0xdb, 0x5f, 0x66, 0xcc, 0xe4, 0x79, 0x28, 0x60, + 0x8e, 0xdb, 0x24, 0x0d, 0xda, 0x26, 0x2a, 0x6e, 0x97, 0x51, 0x5f, 0x86, 0xa2, 0x30, 0xdc, 0x6f, + 0x47, 0x02, 0x26, 0xe2, 0xf6, 0x58, 0xcd, 0x33, 0x70, 0xba, 0x2b, 0xb0, 0x5a, 0xd9, 0xa7, 0xbc, + 0xf8, 0xdc, 0xf2, 0x59, 0x0e, 0x49, 0xcb, 0x50, 0x74, 0x05, 0xb0, 0x5b, 0x53, 0x8f, 0x55, 0x2f, + 0xc3, 0x8c, 0xb4, 0xf4, 0xa8, 0xea, 0x35, 0xcb, 0xa2, 0x91, 0x8a, 0xae, 0x74, 0xfd, 0xa2, 0x41, + 0x49, 0x9d, 0xa5, 0x75, 0xc4, 0x1e, 0x84, 0xbe, 0x83, 0x37, 0x49, 0x2c, 0x85, 0xe1, 0x3b, 0xf1, + 0x05, 0x2e, 0x43, 0x28, 0x81, 0xd3, 0xde, 0x20, 0x8a, 0xec, 0x1a, 0xb5, 0xcc, 0xb2, 0x31, 0x30, + 0x98, 0x2c, 0x23, 0x83, 0xdd, 0x9a, 0x65, 0x58, 0xce, 0xd6, 0xaa, 0xd2, 0xfa, 0x51, 0x4b, 0xdf, + 0x26, 0x3a, 0xf7, 0xb5, 0x61, 0x19, 0x7d, 0x05, 0xa7, 0x52, 0xb7, 0x3b, 0x71, 0x83, 0x95, 0xd9, + 0x5c, 0xcb, 0xbe, 0xe0, 0xf4, 0xc4, 0x90, 0x89, 0xcc, 0xd2, 0x1e, 0xbb, 0xf9, 0x2e, 0xbc, 0x73, + 0xa4, 0xb0, 0x44, 0x7e, 0xed, 0x8f, 0x69, 0x18, 0xdf, 0x62, 0x9e, 0x4e, 0x61, 0x2a, 0xdd, 0xaf, + 0xae, 0x64, 0x4a, 0xe8, 0x6e, 0x0b, 0xc6, 0x8d, 0x11, 0xc0, 0xea, 0xb4, 0x3e, 0x86, 0x62, 0xcf, + 0x65, 0xb3, 0x32, 0xcc, 0x4d, 0x37, 0xde, 0x78, 0x7f, 0x34, 0xbc, 0x8a, 0xfc, 0x9d, 0x06, 0xa7, + 0xfa, 0xef, 0x29, 0x2b, 0xf9, 0xbc, 0xa5, 0x28, 0xc6, 0x47, 0x23, 0x53, 0xba, 0x34, 0xf4, 0xdf, + 0x46, 0x86, 0x6a, 0xe8, 0xa3, 0x0c, 0xd7, 0x70, 0xe4, 0x35, 0x45, 0xf7, 0x61, 0xb2, 0xd3, 0xc1, + 0x2f, 0x0d, 0xf3, 0xa3, 0xa0, 0xc6, 0x4a, 0x6e, 0xa8, 0x0a, 0x15, 0xc2, 0x74, 0x57, 0x1b, 0xbe, + 0x9a, 0x6f, 0xe6, 0x04, 0xda, 0x78, 0x6f, 0x14, 0xb4, 0x8a, 0xf9, 0x0d, 0xcc, 0xf4, 0x5e, 0xe2, + 0xab, 0xf9, 0x94, 0x2b, 0x82, 0xf1, 0xc1, 0x88, 0x04, 0x15, 0xfc, 0x5b, 0x98, 0xed, 0xbb, 0x18, + 0x5c, 0x1f, 0xbe, 0x54, 0xdd, 0x0c, 0xe3, 0xc3, 0x51, 0x19, 0x2a, 0xbe, 0x03, 0x27, 0x93, 0xd6, + 0x7e, 0x31, 0x4f, 0x0e, 0xdb, 0xf5, 0xba, 0x51, 0xcd, 0x09, 0x54, 0x41, 0x02, 0x80, 0x54, 0xe3, + 0xbb, 0x3c, 0x8c, 0xde, 0xc1, 0x1a, 0xb5, 0xfc, 0x58, 0x15, 0x8d, 0xc2, 0x54, 0xba, 0xa9, 0x0d, + 0xad, 0x50, 0x29, 0xf0, 0xf0, 0x0a, 0x35, 0xa0, 0x61, 0xe9, 0x3f, 0x6b, 0xb0, 0x94, 0xd5, 0xad, + 0x3e, 0xc9, 0xb7, 0x2d, 0x07, 0x92, 0x8d, 0xb5, 0xff, 0x40, 0x56, 0x0a, 0x9f, 0x69, 0x30, 0x7f, + 0x44, 0xe3, 0xc9, 0x5b, 0x1c, 0x7b, 0x78, 0xc6, 0xa7, 0xaf, 0xc7, 0x4b, 0x24, 0xdd, 0xbc, 0xfd, + 0xfc, 0x65, 0x49, 0x7b, 0xf1, 0xb2, 0xa4, 0xfd, 0xfd, 0xb2, 0xa4, 0x3d, 0x7b, 0x55, 0x1a, 0x7b, + 0xf1, 0xaa, 0x34, 0xf6, 0xe7, 0xab, 0xd2, 0xd8, 0xa3, 0x2b, 0x9e, 0x1f, 0x35, 0xdb, 0x8d, 0x8a, + 0x43, 0xf7, 0xf8, 0xe7, 0x88, 0x6b, 0xe2, 0xcb, 0x44, 0xfc, 0x8f, 0xa4, 0xfa, 0x38, 0xf5, 0x65, + 0xe4, 0xb0, 0x85, 0x59, 0xe3, 0x04, 0xff, 0x2a, 0x71, 0xe3, 0xdf, 0x00, 0x00, 0x00, 0xff, 0xff, + 0x05, 0x3a, 0x52, 0xd9, 0x83, 0x12, 0x00, 0x00, } // Reference imports to suppress errors if they are not otherwise used. diff --git a/zetaclient/metrics/metrics.go b/zetaclient/metrics/metrics.go index 36dc5ad813..6e46037209 100644 --- a/zetaclient/metrics/metrics.go +++ b/zetaclient/metrics/metrics.go @@ -101,6 +101,13 @@ var ( Help: "Difference between system time and block time from zetacore", }) + // CoreBlockLatencySleep is a gauge of the duration we sleep before signing + CoreBlockLatencySleep = promauto.NewGauge(prometheus.GaugeOpts{ + Namespace: ZetaClientNamespace, + Name: "core_block_latency_sleep", + Help: "The duration we sleep before signing", + }) + // Info is a gauge that contains information about the zetaclient environment Info = promauto.NewGaugeVec(prometheus.GaugeOpts{ Namespace: ZetaClientNamespace, diff --git a/zetaclient/orchestrator/orchestrator.go b/zetaclient/orchestrator/orchestrator.go index 6e818329b1..3e475fe442 100644 --- a/zetaclient/orchestrator/orchestrator.go +++ b/zetaclient/orchestrator/orchestrator.go @@ -68,6 +68,9 @@ type Orchestrator struct { dbDirectory string baseLogger base.Logger + // signerBlockTimeOffset + signerBlockTimeOffset time.Duration + // misc logger multiLogger ts *metrics.TelemetryServer @@ -135,6 +138,12 @@ func (oc *Orchestrator) Start(ctx context.Context) error { bg.Work(ctx, oc.runScheduler, bg.WithName("runScheduler"), bg.WithLogger(oc.logger.Logger)) bg.Work(ctx, oc.runObserverSignerSync, bg.WithName("runObserverSignerSync"), bg.WithLogger(oc.logger.Logger)) bg.Work(ctx, oc.runAppContextUpdater, bg.WithName("runAppContextUpdater"), bg.WithLogger(oc.logger.Logger)) + bg.Work( + ctx, + oc.runSyncObserverOperationalFlags, + bg.WithName("runSyncObserverOperationalFlags"), + bg.WithLogger(oc.logger.Logger), + ) return nil } @@ -318,6 +327,13 @@ func (oc *Orchestrator) runScheduler(ctx context.Context) error { continue } + sleepDuration := time.Until(newBlock.Block.Time.Add(oc.signerBlockTimeOffset)) + if sleepDuration < 0 { + sleepDuration = 0 + } + metrics.CoreBlockLatencySleep.Set(sleepDuration.Seconds()) + time.Sleep(sleepDuration) + balance, err := oc.zetacoreClient.GetZetaHotKeyBalance(ctx) if err != nil { oc.logger.Error().Err(err).Msgf("couldn't get operator balance") @@ -795,3 +811,48 @@ func (oc *Orchestrator) syncObserverSigner(ctx context.Context) error { return nil } + +func (oc *Orchestrator) runSyncObserverOperationalFlags(ctx context.Context) error { + // every other block + const cadence = 2 * constant.ZetaBlockTime + + task := func(ctx context.Context, _ *ticker.Ticker) error { + if err := oc.syncObserverOperationalFlags(ctx); err != nil { + oc.logger.Error().Err(err).Msg("syncObserverOperationalFlags failed") + } + + return nil + } + + return ticker.Run( + ctx, + cadence, + task, + ticker.WithLogger(oc.logger.Logger, "SyncObserverOperationalFlags"), + ticker.WithStopChan(oc.stop), + ) +} + +func (oc *Orchestrator) syncObserverOperationalFlags(ctx context.Context) error { + client := oc.zetacoreClient + flags, err := client.GetOperationalFlags(ctx) + if err != nil { + return fmt.Errorf("get operational flags: %w", err) + } + + oc.mu.Lock() + defer oc.mu.Unlock() + newSignerBlockTimeOffsetPtr := flags.SignerBlockTimeOffset + if newSignerBlockTimeOffsetPtr == nil { + return nil + } + newSignerBlockTimeOffset := *newSignerBlockTimeOffsetPtr + if oc.signerBlockTimeOffset != newSignerBlockTimeOffset { + oc.logger.Info(). + Dur("offset", newSignerBlockTimeOffset). + Msg("block time offset updated") + oc.signerBlockTimeOffset = newSignerBlockTimeOffset + } + + return nil +} diff --git a/zetaclient/orchestrator/orchestrator_test.go b/zetaclient/orchestrator/orchestrator_test.go index 8637a47e17..2a961a43fa 100644 --- a/zetaclient/orchestrator/orchestrator_test.go +++ b/zetaclient/orchestrator/orchestrator_test.go @@ -3,6 +3,7 @@ package orchestrator import ( "context" "testing" + "time" sdk "github.com/cosmos/cosmos-sdk/types" ethcommon "github.com/ethereum/go-ethereum/common" @@ -14,6 +15,7 @@ import ( "github.com/zeta-chain/node/pkg/chains" "github.com/zeta-chain/node/pkg/coin" + "github.com/zeta-chain/node/pkg/ptr" "github.com/zeta-chain/node/testutil/sample" crosschainkeeper "github.com/zeta-chain/node/x/crosschain/keeper" crosschaintypes "github.com/zeta-chain/node/x/crosschain/types" @@ -540,6 +542,58 @@ func Test_GetPendingCctxsWithinRateLimit(t *testing.T) { } } +func TestSyncObserverOperationalFlags(t *testing.T) { + ctx := context.Background() + t.Run("no flags set", func(t *testing.T) { + client := mocks.NewZetacoreClient(t) + + client.Mock.On("GetOperationalFlags", ctx).Return(observertypes.OperationalFlags{}, nil) + orchestrator := mockOrchestrator(t, client) + err := orchestrator.syncObserverOperationalFlags(ctx) + require.NoError(t, err) + require.Zero( + t, + orchestrator.signerBlockTimeOffset, + "block time offset should be zero if operational flags not set", + ) + }) + + t.Run("flags set", func(t *testing.T) { + client := mocks.NewZetacoreClient(t) + + expectedDuration := time.Second + client.Mock.On("GetOperationalFlags", ctx).Return(observertypes.OperationalFlags{ + SignerBlockTimeOffset: ptr.Ptr(expectedDuration), + }, nil) + orchestrator := mockOrchestrator(t, client) + err := orchestrator.syncObserverOperationalFlags(ctx) + require.NoError(t, err) + require.Equal(t, expectedDuration, orchestrator.signerBlockTimeOffset) + }) + + t.Run("flags updated", func(t *testing.T) { + client := mocks.NewZetacoreClient(t) + + expectedDuration := time.Second + mock := client.Mock.On("GetOperationalFlags", ctx).Return(observertypes.OperationalFlags{ + SignerBlockTimeOffset: ptr.Ptr(expectedDuration), + }, nil) + orchestrator := mockOrchestrator(t, client) + err := orchestrator.syncObserverOperationalFlags(ctx) + require.NoError(t, err) + require.Equal(t, expectedDuration, orchestrator.signerBlockTimeOffset) + + mock.Unset() + expectedDuration = time.Second * 2 + client.Mock.On("GetOperationalFlags", ctx).Return(observertypes.OperationalFlags{ + SignerBlockTimeOffset: ptr.Ptr(expectedDuration), + }, nil) + err = orchestrator.syncObserverOperationalFlags(ctx) + require.NoError(t, err) + require.Equal(t, expectedDuration, orchestrator.signerBlockTimeOffset) + }) +} + func mockOrchestrator(t *testing.T, zetaClient interfaces.ZetacoreClient, chainsOrParams ...any) *Orchestrator { supportedChains, obsParams := parseChainsWithParams(t, chainsOrParams...) From 80ca9214bf531668b0b18199f0743fa81957f3ca Mon Sep 17 00:00:00 2001 From: cuiweiyuan Date: Fri, 3 Jan 2025 01:36:38 +0800 Subject: [PATCH 2/7] chore: fix function name in comment (#3327) Signed-off-by: cuiweiyuan Co-authored-by: Alex Gartner --- zetaclient/chains/bitcoin/rpc/rpc.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/zetaclient/chains/bitcoin/rpc/rpc.go b/zetaclient/chains/bitcoin/rpc/rpc.go index 48182c8726..d29291c582 100644 --- a/zetaclient/chains/bitcoin/rpc/rpc.go +++ b/zetaclient/chains/bitcoin/rpc/rpc.go @@ -61,7 +61,7 @@ func GetTxResultByHash( return hash, txResult, nil } -// GetTXRawResultByHash gets the raw transaction by hash +// GetRawTxByHash gets the raw transaction by hash func GetRawTxByHash(rpcClient interfaces.BTCRPCClient, txID string) (*btcutil.Tx, error) { hash, err := chainhash.NewHashFromStr(txID) if err != nil { From ef4c1d4eb87ff6fa6acba379f083e6b90393d187 Mon Sep 17 00:00:00 2001 From: Lucas Bertrand Date: Mon, 6 Jan 2025 10:14:29 +0100 Subject: [PATCH 3/7] fix: make crosschain-call with invalid withdraw revert (#3321) * withdrawer contract * deposit and withdraw dust test * add changelog * add test * don't commit state and revert when process withdraw fails * fix tests * add more assertions * update contract * update contract 2 * add back bitcoin tests * Update e2e/e2etests/test_bitcoin_deposit_and_withdraw_with_dust.go Co-authored-by: skosito * Update x/crosschain/keeper/evm_deposit.go Co-authored-by: skosito * add comment --------- Co-authored-by: skosito --- changelog.md | 1 + cmd/zetae2e/local/local.go | 1 + e2e/contracts/withdrawer/Withdrawer.abi | 116 +++++++ e2e/contracts/withdrawer/Withdrawer.bin | 1 + e2e/contracts/withdrawer/Withdrawer.go | 283 ++++++++++++++++++ e2e/contracts/withdrawer/Withdrawer.json | 119 ++++++++ e2e/contracts/withdrawer/Withdrawer.sol | 37 +++ e2e/contracts/withdrawer/bindings.go | 6 + e2e/e2etests/e2etests.go | 13 +- ..._bitcoin_deposit_and_withdraw_with_dust.go | 58 ++++ x/crosschain/keeper/evm_deposit.go | 27 +- x/crosschain/keeper/evm_deposit_test.go | 22 +- x/crosschain/keeper/evm_hooks.go | 6 +- x/crosschain/keeper/evm_hooks_test.go | 16 +- 14 files changed, 676 insertions(+), 30 deletions(-) create mode 100644 e2e/contracts/withdrawer/Withdrawer.abi create mode 100644 e2e/contracts/withdrawer/Withdrawer.bin create mode 100644 e2e/contracts/withdrawer/Withdrawer.go create mode 100644 e2e/contracts/withdrawer/Withdrawer.json create mode 100644 e2e/contracts/withdrawer/Withdrawer.sol create mode 100644 e2e/contracts/withdrawer/bindings.go create mode 100644 e2e/e2etests/test_bitcoin_deposit_and_withdraw_with_dust.go diff --git a/changelog.md b/changelog.md index 72d1ea2c59..5bb11d40cf 100644 --- a/changelog.md +++ b/changelog.md @@ -32,6 +32,7 @@ * [3278](https://github.com/zeta-chain/node/pull/3278) - enforce checksum format for asset address in ZRC20 * [3289](https://github.com/zeta-chain/node/pull/3289) - remove all dynamic peer discovery (zetaclient) * [3314](https://github.com/zeta-chain/node/pull/3314) - update `last_scanned_block_number` metrics more frequently for Solana chain +* [3321](https://github.com/zeta-chain/node/pull/3321) - make crosschain-call with invalid withdraw revert ## v24.0.0 diff --git a/cmd/zetae2e/local/local.go b/cmd/zetae2e/local/local.go index 65b5fdcf44..8368e0d0bc 100644 --- a/cmd/zetae2e/local/local.go +++ b/cmd/zetae2e/local/local.go @@ -303,6 +303,7 @@ func localE2ETest(cmd *cobra.Command, _ []string) { bitcoinDepositTestsAdvanced := []string{ e2etests.TestBitcoinDepositAndCallRevertWithDustName, e2etests.TestBitcoinStdMemoDepositAndCallRevertOtherAddressName, + e2etests.TestBitcoinDepositAndWithdrawWithDustName, } bitcoinWithdrawTests := []string{ e2etests.TestBitcoinWithdrawSegWitName, diff --git a/e2e/contracts/withdrawer/Withdrawer.abi b/e2e/contracts/withdrawer/Withdrawer.abi new file mode 100644 index 0000000000..9d462137f0 --- /dev/null +++ b/e2e/contracts/withdrawer/Withdrawer.abi @@ -0,0 +1,116 @@ +[ + { + "inputs": [ + { + "internalType": "uint256", + "name": "_withdrawAmount", + "type": "uint256" + } + ], + "stateMutability": "nonpayable", + "type": "constructor" + }, + { + "inputs": [ + { + "components": [ + { + "internalType": "bytes", + "name": "origin", + "type": "bytes" + }, + { + "internalType": "address", + "name": "sender", + "type": "address" + }, + { + "internalType": "uint256", + "name": "chainID", + "type": "uint256" + } + ], + "internalType": "struct Context", + "name": "context", + "type": "tuple" + }, + { + "internalType": "address", + "name": "zrc20", + "type": "address" + }, + { + "internalType": "uint256", + "name": "", + "type": "uint256" + }, + { + "internalType": "bytes", + "name": "", + "type": "bytes" + } + ], + "name": "onCall", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "components": [ + { + "internalType": "bytes", + "name": "origin", + "type": "bytes" + }, + { + "internalType": "address", + "name": "sender", + "type": "address" + }, + { + "internalType": "uint256", + "name": "chainID", + "type": "uint256" + } + ], + "internalType": "struct Context", + "name": "context", + "type": "tuple" + }, + { + "internalType": "address", + "name": "zrc20", + "type": "address" + }, + { + "internalType": "uint256", + "name": "", + "type": "uint256" + }, + { + "internalType": "bytes", + "name": "", + "type": "bytes" + } + ], + "name": "onCrossChainCall", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "withdrawAmount", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + } +] diff --git a/e2e/contracts/withdrawer/Withdrawer.bin b/e2e/contracts/withdrawer/Withdrawer.bin new file mode 100644 index 0000000000..7f0200971d --- /dev/null +++ b/e2e/contracts/withdrawer/Withdrawer.bin @@ -0,0 +1 @@ +60a0604052348015600f57600080fd5b506040516107f63803806107f68339818101604052810190602f91906072565b806080818152505050609a565b600080fd5b6000819050919050565b6052816041565b8114605c57600080fd5b50565b600081519050606c81604b565b92915050565b6000602082840312156085576084603c565b5b6000609184828501605f565b91505092915050565b6080516107346100c260003960008181609e0152818161018d01526102e201526107346000f3fe608060405234801561001057600080fd5b50600436106100415760003560e01c8063534844a2146100465780635bcfd61614610064578063de43156e14610080575b600080fd5b61004e61009c565b60405161005b9190610383565b60405180910390f35b61007e600480360381019061007991906104bb565b6100c0565b005b61009a600480360381019061009591906104bb565b610215565b005b7f000000000000000000000000000000000000000000000000000000000000000081565b8373ffffffffffffffffffffffffffffffffffffffff1663095ea7b3857fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff6040518363ffffffff1660e01b815260040161011b92919061056e565b6020604051808303816000875af115801561013a573d6000803e3d6000fd5b505050506040513d601f19601f8201168201806040525081019061015e91906105cf565b508373ffffffffffffffffffffffffffffffffffffffff1663c701262686806000019061018b919061060b565b7f00000000000000000000000000000000000000000000000000000000000000006040518463ffffffff1660e01b81526004016101ca939291906106cc565b6020604051808303816000875af11580156101e9573d6000803e3d6000fd5b505050506040513d601f19601f8201168201806040525081019061020d91906105cf565b505050505050565b8373ffffffffffffffffffffffffffffffffffffffff1663095ea7b3857fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff6040518363ffffffff1660e01b815260040161027092919061056e565b6020604051808303816000875af115801561028f573d6000803e3d6000fd5b505050506040513d601f19601f820116820180604052508101906102b391906105cf565b508373ffffffffffffffffffffffffffffffffffffffff1663c70126268680600001906102e0919061060b565b7f00000000000000000000000000000000000000000000000000000000000000006040518463ffffffff1660e01b815260040161031f939291906106cc565b6020604051808303816000875af115801561033e573d6000803e3d6000fd5b505050506040513d601f19601f8201168201806040525081019061036291906105cf565b505050505050565b6000819050919050565b61037d8161036a565b82525050565b60006020820190506103986000830184610374565b92915050565b600080fd5b600080fd5b600080fd5b6000606082840312156103c3576103c26103a8565b5b81905092915050565b600073ffffffffffffffffffffffffffffffffffffffff82169050919050565b60006103f7826103cc565b9050919050565b610407816103ec565b811461041257600080fd5b50565b600081359050610424816103fe565b92915050565b6104338161036a565b811461043e57600080fd5b50565b6000813590506104508161042a565b92915050565b600080fd5b600080fd5b600080fd5b60008083601f84011261047b5761047a610456565b5b8235905067ffffffffffffffff8111156104985761049761045b565b5b6020830191508360018202830111156104b4576104b3610460565b5b9250929050565b6000806000806000608086880312156104d7576104d661039e565b5b600086013567ffffffffffffffff8111156104f5576104f46103a3565b5b610501888289016103ad565b955050602061051288828901610415565b945050604061052388828901610441565b935050606086013567ffffffffffffffff811115610544576105436103a3565b5b61055088828901610465565b92509250509295509295909350565b610568816103ec565b82525050565b6000604082019050610583600083018561055f565b6105906020830184610374565b9392505050565b60008115159050919050565b6105ac81610597565b81146105b757600080fd5b50565b6000815190506105c9816105a3565b92915050565b6000602082840312156105e5576105e461039e565b5b60006105f3848285016105ba565b91505092915050565b600080fd5b600080fd5b600080fd5b60008083356001602003843603038112610628576106276105fc565b5b80840192508235915067ffffffffffffffff82111561064a57610649610601565b5b60208301925060018202360383131561066657610665610606565b5b509250929050565b600082825260208201905092915050565b82818337600083830152505050565b6000601f19601f8301169050919050565b60006106ab838561066e565b93506106b883858461067f565b6106c18361068e565b840190509392505050565b600060408201905081810360008301526106e781858761069f565b90506106f66020830184610374565b94935050505056fea2646970667358221220eb0d0178243bc765ecffd41945dfc69d032eaf9e1347d0b6ee2ec8408676acd564736f6c634300081a0033 diff --git a/e2e/contracts/withdrawer/Withdrawer.go b/e2e/contracts/withdrawer/Withdrawer.go new file mode 100644 index 0000000000..99bd5fdfe3 --- /dev/null +++ b/e2e/contracts/withdrawer/Withdrawer.go @@ -0,0 +1,283 @@ +// Code generated - DO NOT EDIT. +// This file is a generated binding and any manual changes will be lost. + +package withdrawer + +import ( + "errors" + "math/big" + "strings" + + ethereum "github.com/ethereum/go-ethereum" + "github.com/ethereum/go-ethereum/accounts/abi" + "github.com/ethereum/go-ethereum/accounts/abi/bind" + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/core/types" + "github.com/ethereum/go-ethereum/event" +) + +// Reference imports to suppress errors if they are not otherwise used. +var ( + _ = errors.New + _ = big.NewInt + _ = strings.NewReader + _ = ethereum.NotFound + _ = bind.Bind + _ = common.Big1 + _ = types.BloomLookup + _ = event.NewSubscription + _ = abi.ConvertType +) + +// Context is an auto generated low-level Go binding around an user-defined struct. +type Context struct { + Origin []byte + Sender common.Address + ChainID *big.Int +} + +// WithdrawerMetaData contains all meta data concerning the Withdrawer contract. +var WithdrawerMetaData = &bind.MetaData{ + ABI: "[{\"inputs\":[{\"internalType\":\"uint256\",\"name\":\"_withdrawAmount\",\"type\":\"uint256\"}],\"stateMutability\":\"nonpayable\",\"type\":\"constructor\"},{\"inputs\":[{\"components\":[{\"internalType\":\"bytes\",\"name\":\"origin\",\"type\":\"bytes\"},{\"internalType\":\"address\",\"name\":\"sender\",\"type\":\"address\"},{\"internalType\":\"uint256\",\"name\":\"chainID\",\"type\":\"uint256\"}],\"internalType\":\"structContext\",\"name\":\"context\",\"type\":\"tuple\"},{\"internalType\":\"address\",\"name\":\"zrc20\",\"type\":\"address\"},{\"internalType\":\"uint256\",\"name\":\"\",\"type\":\"uint256\"},{\"internalType\":\"bytes\",\"name\":\"\",\"type\":\"bytes\"}],\"name\":\"onCall\",\"outputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[{\"components\":[{\"internalType\":\"bytes\",\"name\":\"origin\",\"type\":\"bytes\"},{\"internalType\":\"address\",\"name\":\"sender\",\"type\":\"address\"},{\"internalType\":\"uint256\",\"name\":\"chainID\",\"type\":\"uint256\"}],\"internalType\":\"structContext\",\"name\":\"context\",\"type\":\"tuple\"},{\"internalType\":\"address\",\"name\":\"zrc20\",\"type\":\"address\"},{\"internalType\":\"uint256\",\"name\":\"\",\"type\":\"uint256\"},{\"internalType\":\"bytes\",\"name\":\"\",\"type\":\"bytes\"}],\"name\":\"onCrossChainCall\",\"outputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[],\"name\":\"withdrawAmount\",\"outputs\":[{\"internalType\":\"uint256\",\"name\":\"\",\"type\":\"uint256\"}],\"stateMutability\":\"view\",\"type\":\"function\"}]", + Bin: "0x60a0604052348015600f57600080fd5b506040516107f63803806107f68339818101604052810190602f91906072565b806080818152505050609a565b600080fd5b6000819050919050565b6052816041565b8114605c57600080fd5b50565b600081519050606c81604b565b92915050565b6000602082840312156085576084603c565b5b6000609184828501605f565b91505092915050565b6080516107346100c260003960008181609e0152818161018d01526102e201526107346000f3fe608060405234801561001057600080fd5b50600436106100415760003560e01c8063534844a2146100465780635bcfd61614610064578063de43156e14610080575b600080fd5b61004e61009c565b60405161005b9190610383565b60405180910390f35b61007e600480360381019061007991906104bb565b6100c0565b005b61009a600480360381019061009591906104bb565b610215565b005b7f000000000000000000000000000000000000000000000000000000000000000081565b8373ffffffffffffffffffffffffffffffffffffffff1663095ea7b3857fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff6040518363ffffffff1660e01b815260040161011b92919061056e565b6020604051808303816000875af115801561013a573d6000803e3d6000fd5b505050506040513d601f19601f8201168201806040525081019061015e91906105cf565b508373ffffffffffffffffffffffffffffffffffffffff1663c701262686806000019061018b919061060b565b7f00000000000000000000000000000000000000000000000000000000000000006040518463ffffffff1660e01b81526004016101ca939291906106cc565b6020604051808303816000875af11580156101e9573d6000803e3d6000fd5b505050506040513d601f19601f8201168201806040525081019061020d91906105cf565b505050505050565b8373ffffffffffffffffffffffffffffffffffffffff1663095ea7b3857fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff6040518363ffffffff1660e01b815260040161027092919061056e565b6020604051808303816000875af115801561028f573d6000803e3d6000fd5b505050506040513d601f19601f820116820180604052508101906102b391906105cf565b508373ffffffffffffffffffffffffffffffffffffffff1663c70126268680600001906102e0919061060b565b7f00000000000000000000000000000000000000000000000000000000000000006040518463ffffffff1660e01b815260040161031f939291906106cc565b6020604051808303816000875af115801561033e573d6000803e3d6000fd5b505050506040513d601f19601f8201168201806040525081019061036291906105cf565b505050505050565b6000819050919050565b61037d8161036a565b82525050565b60006020820190506103986000830184610374565b92915050565b600080fd5b600080fd5b600080fd5b6000606082840312156103c3576103c26103a8565b5b81905092915050565b600073ffffffffffffffffffffffffffffffffffffffff82169050919050565b60006103f7826103cc565b9050919050565b610407816103ec565b811461041257600080fd5b50565b600081359050610424816103fe565b92915050565b6104338161036a565b811461043e57600080fd5b50565b6000813590506104508161042a565b92915050565b600080fd5b600080fd5b600080fd5b60008083601f84011261047b5761047a610456565b5b8235905067ffffffffffffffff8111156104985761049761045b565b5b6020830191508360018202830111156104b4576104b3610460565b5b9250929050565b6000806000806000608086880312156104d7576104d661039e565b5b600086013567ffffffffffffffff8111156104f5576104f46103a3565b5b610501888289016103ad565b955050602061051288828901610415565b945050604061052388828901610441565b935050606086013567ffffffffffffffff811115610544576105436103a3565b5b61055088828901610465565b92509250509295509295909350565b610568816103ec565b82525050565b6000604082019050610583600083018561055f565b6105906020830184610374565b9392505050565b60008115159050919050565b6105ac81610597565b81146105b757600080fd5b50565b6000815190506105c9816105a3565b92915050565b6000602082840312156105e5576105e461039e565b5b60006105f3848285016105ba565b91505092915050565b600080fd5b600080fd5b600080fd5b60008083356001602003843603038112610628576106276105fc565b5b80840192508235915067ffffffffffffffff82111561064a57610649610601565b5b60208301925060018202360383131561066657610665610606565b5b509250929050565b600082825260208201905092915050565b82818337600083830152505050565b6000601f19601f8301169050919050565b60006106ab838561066e565b93506106b883858461067f565b6106c18361068e565b840190509392505050565b600060408201905081810360008301526106e781858761069f565b90506106f66020830184610374565b94935050505056fea2646970667358221220eb0d0178243bc765ecffd41945dfc69d032eaf9e1347d0b6ee2ec8408676acd564736f6c634300081a0033", +} + +// WithdrawerABI is the input ABI used to generate the binding from. +// Deprecated: Use WithdrawerMetaData.ABI instead. +var WithdrawerABI = WithdrawerMetaData.ABI + +// WithdrawerBin is the compiled bytecode used for deploying new contracts. +// Deprecated: Use WithdrawerMetaData.Bin instead. +var WithdrawerBin = WithdrawerMetaData.Bin + +// DeployWithdrawer deploys a new Ethereum contract, binding an instance of Withdrawer to it. +func DeployWithdrawer(auth *bind.TransactOpts, backend bind.ContractBackend, _withdrawAmount *big.Int) (common.Address, *types.Transaction, *Withdrawer, error) { + parsed, err := WithdrawerMetaData.GetAbi() + if err != nil { + return common.Address{}, nil, nil, err + } + if parsed == nil { + return common.Address{}, nil, nil, errors.New("GetABI returned nil") + } + + address, tx, contract, err := bind.DeployContract(auth, *parsed, common.FromHex(WithdrawerBin), backend, _withdrawAmount) + if err != nil { + return common.Address{}, nil, nil, err + } + return address, tx, &Withdrawer{WithdrawerCaller: WithdrawerCaller{contract: contract}, WithdrawerTransactor: WithdrawerTransactor{contract: contract}, WithdrawerFilterer: WithdrawerFilterer{contract: contract}}, nil +} + +// Withdrawer is an auto generated Go binding around an Ethereum contract. +type Withdrawer struct { + WithdrawerCaller // Read-only binding to the contract + WithdrawerTransactor // Write-only binding to the contract + WithdrawerFilterer // Log filterer for contract events +} + +// WithdrawerCaller is an auto generated read-only Go binding around an Ethereum contract. +type WithdrawerCaller struct { + contract *bind.BoundContract // Generic contract wrapper for the low level calls +} + +// WithdrawerTransactor is an auto generated write-only Go binding around an Ethereum contract. +type WithdrawerTransactor struct { + contract *bind.BoundContract // Generic contract wrapper for the low level calls +} + +// WithdrawerFilterer is an auto generated log filtering Go binding around an Ethereum contract events. +type WithdrawerFilterer struct { + contract *bind.BoundContract // Generic contract wrapper for the low level calls +} + +// WithdrawerSession is an auto generated Go binding around an Ethereum contract, +// with pre-set call and transact options. +type WithdrawerSession struct { + Contract *Withdrawer // Generic contract binding to set the session for + CallOpts bind.CallOpts // Call options to use throughout this session + TransactOpts bind.TransactOpts // Transaction auth options to use throughout this session +} + +// WithdrawerCallerSession is an auto generated read-only Go binding around an Ethereum contract, +// with pre-set call options. +type WithdrawerCallerSession struct { + Contract *WithdrawerCaller // Generic contract caller binding to set the session for + CallOpts bind.CallOpts // Call options to use throughout this session +} + +// WithdrawerTransactorSession is an auto generated write-only Go binding around an Ethereum contract, +// with pre-set transact options. +type WithdrawerTransactorSession struct { + Contract *WithdrawerTransactor // Generic contract transactor binding to set the session for + TransactOpts bind.TransactOpts // Transaction auth options to use throughout this session +} + +// WithdrawerRaw is an auto generated low-level Go binding around an Ethereum contract. +type WithdrawerRaw struct { + Contract *Withdrawer // Generic contract binding to access the raw methods on +} + +// WithdrawerCallerRaw is an auto generated low-level read-only Go binding around an Ethereum contract. +type WithdrawerCallerRaw struct { + Contract *WithdrawerCaller // Generic read-only contract binding to access the raw methods on +} + +// WithdrawerTransactorRaw is an auto generated low-level write-only Go binding around an Ethereum contract. +type WithdrawerTransactorRaw struct { + Contract *WithdrawerTransactor // Generic write-only contract binding to access the raw methods on +} + +// NewWithdrawer creates a new instance of Withdrawer, bound to a specific deployed contract. +func NewWithdrawer(address common.Address, backend bind.ContractBackend) (*Withdrawer, error) { + contract, err := bindWithdrawer(address, backend, backend, backend) + if err != nil { + return nil, err + } + return &Withdrawer{WithdrawerCaller: WithdrawerCaller{contract: contract}, WithdrawerTransactor: WithdrawerTransactor{contract: contract}, WithdrawerFilterer: WithdrawerFilterer{contract: contract}}, nil +} + +// NewWithdrawerCaller creates a new read-only instance of Withdrawer, bound to a specific deployed contract. +func NewWithdrawerCaller(address common.Address, caller bind.ContractCaller) (*WithdrawerCaller, error) { + contract, err := bindWithdrawer(address, caller, nil, nil) + if err != nil { + return nil, err + } + return &WithdrawerCaller{contract: contract}, nil +} + +// NewWithdrawerTransactor creates a new write-only instance of Withdrawer, bound to a specific deployed contract. +func NewWithdrawerTransactor(address common.Address, transactor bind.ContractTransactor) (*WithdrawerTransactor, error) { + contract, err := bindWithdrawer(address, nil, transactor, nil) + if err != nil { + return nil, err + } + return &WithdrawerTransactor{contract: contract}, nil +} + +// NewWithdrawerFilterer creates a new log filterer instance of Withdrawer, bound to a specific deployed contract. +func NewWithdrawerFilterer(address common.Address, filterer bind.ContractFilterer) (*WithdrawerFilterer, error) { + contract, err := bindWithdrawer(address, nil, nil, filterer) + if err != nil { + return nil, err + } + return &WithdrawerFilterer{contract: contract}, nil +} + +// bindWithdrawer binds a generic wrapper to an already deployed contract. +func bindWithdrawer(address common.Address, caller bind.ContractCaller, transactor bind.ContractTransactor, filterer bind.ContractFilterer) (*bind.BoundContract, error) { + parsed, err := WithdrawerMetaData.GetAbi() + if err != nil { + return nil, err + } + return bind.NewBoundContract(address, *parsed, caller, transactor, filterer), nil +} + +// Call invokes the (constant) contract method with params as input values and +// sets the output to result. The result type might be a single field for simple +// returns, a slice of interfaces for anonymous returns and a struct for named +// returns. +func (_Withdrawer *WithdrawerRaw) Call(opts *bind.CallOpts, result *[]interface{}, method string, params ...interface{}) error { + return _Withdrawer.Contract.WithdrawerCaller.contract.Call(opts, result, method, params...) +} + +// Transfer initiates a plain transaction to move funds to the contract, calling +// its default method if one is available. +func (_Withdrawer *WithdrawerRaw) Transfer(opts *bind.TransactOpts) (*types.Transaction, error) { + return _Withdrawer.Contract.WithdrawerTransactor.contract.Transfer(opts) +} + +// Transact invokes the (paid) contract method with params as input values. +func (_Withdrawer *WithdrawerRaw) Transact(opts *bind.TransactOpts, method string, params ...interface{}) (*types.Transaction, error) { + return _Withdrawer.Contract.WithdrawerTransactor.contract.Transact(opts, method, params...) +} + +// Call invokes the (constant) contract method with params as input values and +// sets the output to result. The result type might be a single field for simple +// returns, a slice of interfaces for anonymous returns and a struct for named +// returns. +func (_Withdrawer *WithdrawerCallerRaw) Call(opts *bind.CallOpts, result *[]interface{}, method string, params ...interface{}) error { + return _Withdrawer.Contract.contract.Call(opts, result, method, params...) +} + +// Transfer initiates a plain transaction to move funds to the contract, calling +// its default method if one is available. +func (_Withdrawer *WithdrawerTransactorRaw) Transfer(opts *bind.TransactOpts) (*types.Transaction, error) { + return _Withdrawer.Contract.contract.Transfer(opts) +} + +// Transact invokes the (paid) contract method with params as input values. +func (_Withdrawer *WithdrawerTransactorRaw) Transact(opts *bind.TransactOpts, method string, params ...interface{}) (*types.Transaction, error) { + return _Withdrawer.Contract.contract.Transact(opts, method, params...) +} + +// WithdrawAmount is a free data retrieval call binding the contract method 0x534844a2. +// +// Solidity: function withdrawAmount() view returns(uint256) +func (_Withdrawer *WithdrawerCaller) WithdrawAmount(opts *bind.CallOpts) (*big.Int, error) { + var out []interface{} + err := _Withdrawer.contract.Call(opts, &out, "withdrawAmount") + + if err != nil { + return *new(*big.Int), err + } + + out0 := *abi.ConvertType(out[0], new(*big.Int)).(**big.Int) + + return out0, err + +} + +// WithdrawAmount is a free data retrieval call binding the contract method 0x534844a2. +// +// Solidity: function withdrawAmount() view returns(uint256) +func (_Withdrawer *WithdrawerSession) WithdrawAmount() (*big.Int, error) { + return _Withdrawer.Contract.WithdrawAmount(&_Withdrawer.CallOpts) +} + +// WithdrawAmount is a free data retrieval call binding the contract method 0x534844a2. +// +// Solidity: function withdrawAmount() view returns(uint256) +func (_Withdrawer *WithdrawerCallerSession) WithdrawAmount() (*big.Int, error) { + return _Withdrawer.Contract.WithdrawAmount(&_Withdrawer.CallOpts) +} + +// OnCall is a paid mutator transaction binding the contract method 0x5bcfd616. +// +// Solidity: function onCall((bytes,address,uint256) context, address zrc20, uint256 , bytes ) returns() +func (_Withdrawer *WithdrawerTransactor) OnCall(opts *bind.TransactOpts, context Context, zrc20 common.Address, arg2 *big.Int, arg3 []byte) (*types.Transaction, error) { + return _Withdrawer.contract.Transact(opts, "onCall", context, zrc20, arg2, arg3) +} + +// OnCall is a paid mutator transaction binding the contract method 0x5bcfd616. +// +// Solidity: function onCall((bytes,address,uint256) context, address zrc20, uint256 , bytes ) returns() +func (_Withdrawer *WithdrawerSession) OnCall(context Context, zrc20 common.Address, arg2 *big.Int, arg3 []byte) (*types.Transaction, error) { + return _Withdrawer.Contract.OnCall(&_Withdrawer.TransactOpts, context, zrc20, arg2, arg3) +} + +// OnCall is a paid mutator transaction binding the contract method 0x5bcfd616. +// +// Solidity: function onCall((bytes,address,uint256) context, address zrc20, uint256 , bytes ) returns() +func (_Withdrawer *WithdrawerTransactorSession) OnCall(context Context, zrc20 common.Address, arg2 *big.Int, arg3 []byte) (*types.Transaction, error) { + return _Withdrawer.Contract.OnCall(&_Withdrawer.TransactOpts, context, zrc20, arg2, arg3) +} + +// OnCrossChainCall is a paid mutator transaction binding the contract method 0xde43156e. +// +// Solidity: function onCrossChainCall((bytes,address,uint256) context, address zrc20, uint256 , bytes ) returns() +func (_Withdrawer *WithdrawerTransactor) OnCrossChainCall(opts *bind.TransactOpts, context Context, zrc20 common.Address, arg2 *big.Int, arg3 []byte) (*types.Transaction, error) { + return _Withdrawer.contract.Transact(opts, "onCrossChainCall", context, zrc20, arg2, arg3) +} + +// OnCrossChainCall is a paid mutator transaction binding the contract method 0xde43156e. +// +// Solidity: function onCrossChainCall((bytes,address,uint256) context, address zrc20, uint256 , bytes ) returns() +func (_Withdrawer *WithdrawerSession) OnCrossChainCall(context Context, zrc20 common.Address, arg2 *big.Int, arg3 []byte) (*types.Transaction, error) { + return _Withdrawer.Contract.OnCrossChainCall(&_Withdrawer.TransactOpts, context, zrc20, arg2, arg3) +} + +// OnCrossChainCall is a paid mutator transaction binding the contract method 0xde43156e. +// +// Solidity: function onCrossChainCall((bytes,address,uint256) context, address zrc20, uint256 , bytes ) returns() +func (_Withdrawer *WithdrawerTransactorSession) OnCrossChainCall(context Context, zrc20 common.Address, arg2 *big.Int, arg3 []byte) (*types.Transaction, error) { + return _Withdrawer.Contract.OnCrossChainCall(&_Withdrawer.TransactOpts, context, zrc20, arg2, arg3) +} diff --git a/e2e/contracts/withdrawer/Withdrawer.json b/e2e/contracts/withdrawer/Withdrawer.json new file mode 100644 index 0000000000..26bfe6df7b --- /dev/null +++ b/e2e/contracts/withdrawer/Withdrawer.json @@ -0,0 +1,119 @@ +{ + "abi": [ + { + "inputs": [ + { + "internalType": "uint256", + "name": "_withdrawAmount", + "type": "uint256" + } + ], + "stateMutability": "nonpayable", + "type": "constructor" + }, + { + "inputs": [ + { + "components": [ + { + "internalType": "bytes", + "name": "origin", + "type": "bytes" + }, + { + "internalType": "address", + "name": "sender", + "type": "address" + }, + { + "internalType": "uint256", + "name": "chainID", + "type": "uint256" + } + ], + "internalType": "struct Context", + "name": "context", + "type": "tuple" + }, + { + "internalType": "address", + "name": "zrc20", + "type": "address" + }, + { + "internalType": "uint256", + "name": "", + "type": "uint256" + }, + { + "internalType": "bytes", + "name": "", + "type": "bytes" + } + ], + "name": "onCall", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "components": [ + { + "internalType": "bytes", + "name": "origin", + "type": "bytes" + }, + { + "internalType": "address", + "name": "sender", + "type": "address" + }, + { + "internalType": "uint256", + "name": "chainID", + "type": "uint256" + } + ], + "internalType": "struct Context", + "name": "context", + "type": "tuple" + }, + { + "internalType": "address", + "name": "zrc20", + "type": "address" + }, + { + "internalType": "uint256", + "name": "", + "type": "uint256" + }, + { + "internalType": "bytes", + "name": "", + "type": "bytes" + } + ], + "name": "onCrossChainCall", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "withdrawAmount", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + } + ], + "bin": "60a0604052348015600f57600080fd5b506040516107f63803806107f68339818101604052810190602f91906072565b806080818152505050609a565b600080fd5b6000819050919050565b6052816041565b8114605c57600080fd5b50565b600081519050606c81604b565b92915050565b6000602082840312156085576084603c565b5b6000609184828501605f565b91505092915050565b6080516107346100c260003960008181609e0152818161018d01526102e201526107346000f3fe608060405234801561001057600080fd5b50600436106100415760003560e01c8063534844a2146100465780635bcfd61614610064578063de43156e14610080575b600080fd5b61004e61009c565b60405161005b9190610383565b60405180910390f35b61007e600480360381019061007991906104bb565b6100c0565b005b61009a600480360381019061009591906104bb565b610215565b005b7f000000000000000000000000000000000000000000000000000000000000000081565b8373ffffffffffffffffffffffffffffffffffffffff1663095ea7b3857fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff6040518363ffffffff1660e01b815260040161011b92919061056e565b6020604051808303816000875af115801561013a573d6000803e3d6000fd5b505050506040513d601f19601f8201168201806040525081019061015e91906105cf565b508373ffffffffffffffffffffffffffffffffffffffff1663c701262686806000019061018b919061060b565b7f00000000000000000000000000000000000000000000000000000000000000006040518463ffffffff1660e01b81526004016101ca939291906106cc565b6020604051808303816000875af11580156101e9573d6000803e3d6000fd5b505050506040513d601f19601f8201168201806040525081019061020d91906105cf565b505050505050565b8373ffffffffffffffffffffffffffffffffffffffff1663095ea7b3857fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff6040518363ffffffff1660e01b815260040161027092919061056e565b6020604051808303816000875af115801561028f573d6000803e3d6000fd5b505050506040513d601f19601f820116820180604052508101906102b391906105cf565b508373ffffffffffffffffffffffffffffffffffffffff1663c70126268680600001906102e0919061060b565b7f00000000000000000000000000000000000000000000000000000000000000006040518463ffffffff1660e01b815260040161031f939291906106cc565b6020604051808303816000875af115801561033e573d6000803e3d6000fd5b505050506040513d601f19601f8201168201806040525081019061036291906105cf565b505050505050565b6000819050919050565b61037d8161036a565b82525050565b60006020820190506103986000830184610374565b92915050565b600080fd5b600080fd5b600080fd5b6000606082840312156103c3576103c26103a8565b5b81905092915050565b600073ffffffffffffffffffffffffffffffffffffffff82169050919050565b60006103f7826103cc565b9050919050565b610407816103ec565b811461041257600080fd5b50565b600081359050610424816103fe565b92915050565b6104338161036a565b811461043e57600080fd5b50565b6000813590506104508161042a565b92915050565b600080fd5b600080fd5b600080fd5b60008083601f84011261047b5761047a610456565b5b8235905067ffffffffffffffff8111156104985761049761045b565b5b6020830191508360018202830111156104b4576104b3610460565b5b9250929050565b6000806000806000608086880312156104d7576104d661039e565b5b600086013567ffffffffffffffff8111156104f5576104f46103a3565b5b610501888289016103ad565b955050602061051288828901610415565b945050604061052388828901610441565b935050606086013567ffffffffffffffff811115610544576105436103a3565b5b61055088828901610465565b92509250509295509295909350565b610568816103ec565b82525050565b6000604082019050610583600083018561055f565b6105906020830184610374565b9392505050565b60008115159050919050565b6105ac81610597565b81146105b757600080fd5b50565b6000815190506105c9816105a3565b92915050565b6000602082840312156105e5576105e461039e565b5b60006105f3848285016105ba565b91505092915050565b600080fd5b600080fd5b600080fd5b60008083356001602003843603038112610628576106276105fc565b5b80840192508235915067ffffffffffffffff82111561064a57610649610601565b5b60208301925060018202360383131561066657610665610606565b5b509250929050565b600082825260208201905092915050565b82818337600083830152505050565b6000601f19601f8301169050919050565b60006106ab838561066e565b93506106b883858461067f565b6106c18361068e565b840190509392505050565b600060408201905081810360008301526106e781858761069f565b90506106f66020830184610374565b94935050505056fea2646970667358221220eb0d0178243bc765ecffd41945dfc69d032eaf9e1347d0b6ee2ec8408676acd564736f6c634300081a0033" +} diff --git a/e2e/contracts/withdrawer/Withdrawer.sol b/e2e/contracts/withdrawer/Withdrawer.sol new file mode 100644 index 0000000000..13ac900ace --- /dev/null +++ b/e2e/contracts/withdrawer/Withdrawer.sol @@ -0,0 +1,37 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.7; + +struct Context { + bytes origin; + address sender; + uint256 chainID; +} + +interface IZRC20 { + function approve(address spender, uint256 amount) external returns (bool); + function withdraw(bytes memory to, uint256 amount) external returns (bool); +} + +// Withdrawer is a simple contract performing a withdraw of deposited ZRC20 +// The amount to withdraw can be set during the contract deployment, it also to tests some edge cases like withdrawing BTC dust amount +contract Withdrawer { + uint256 immutable public withdrawAmount; + + constructor(uint256 _withdrawAmount) { + withdrawAmount = _withdrawAmount; + } + + // perform a withdraw on cross chain call + function onCrossChainCall(Context calldata context, address zrc20, uint256, bytes calldata) external { + // perform withdrawal with the target token + IZRC20(zrc20).approve(address(zrc20), type(uint256).max); + IZRC20(zrc20).withdraw(context.origin, withdrawAmount); + } + + // perform a withdraw on cross chain call, v2 + function onCall(Context calldata context, address zrc20, uint256, bytes calldata) external { + // perform withdrawal with the target token + IZRC20(zrc20).approve(address(zrc20), type(uint256).max); + IZRC20(zrc20).withdraw(context.origin, withdrawAmount); + } +} \ No newline at end of file diff --git a/e2e/contracts/withdrawer/bindings.go b/e2e/contracts/withdrawer/bindings.go new file mode 100644 index 0000000000..0ec483e439 --- /dev/null +++ b/e2e/contracts/withdrawer/bindings.go @@ -0,0 +1,6 @@ +//go:generate sh -c "solc --evm-version paris Withdrawer.sol --combined-json abi,bin --allow-paths .. | jq '.contracts.\"Withdrawer.sol:Withdrawer\"' > Withdrawer.json" +//go:generate sh -c "cat Withdrawer.json | jq .abi > Withdrawer.abi" +//go:generate sh -c "cat Withdrawer.json | jq .bin | tr -d '\"' > Withdrawer.bin" +//go:generate sh -c "abigen --abi Withdrawer.abi --bin Withdrawer.bin --pkg withdrawer --type Withdrawer --out Withdrawer.go" + +package withdrawer diff --git a/e2e/e2etests/e2etests.go b/e2e/e2etests/e2etests.go index 1b9f3ef419..884573d2fa 100644 --- a/e2e/e2etests/e2etests.go +++ b/e2e/e2etests/e2etests.go @@ -75,6 +75,7 @@ const ( TestBitcoinDepositAndCallName = "bitcoin_deposit_and_call" TestBitcoinDepositAndCallRevertName = "bitcoin_deposit_and_call_revert" TestBitcoinDepositAndCallRevertWithDustName = "bitcoin_deposit_and_call_revert_with_dust" + TestBitcoinDepositAndWithdrawWithDustName = "bitcoin_deposit_and_withdraw_with_dust" TestBitcoinDonationName = "bitcoin_donation" TestBitcoinStdMemoDepositName = "bitcoin_std_memo_deposit" TestBitcoinStdMemoDepositAndCallName = "bitcoin_std_memo_deposit_and_call" @@ -587,16 +588,24 @@ var AllE2ETests = []runner.E2ETest{ ), runner.NewE2ETest( TestBitcoinDepositAndCallRevertName, - "deposit Bitcoin into ZEVM; expect refund", []runner.ArgDefinition{ + "deposit Bitcoin into ZEVM; expect refund", + []runner.ArgDefinition{ {Description: "amount in btc", DefaultValue: "0.1"}, }, TestBitcoinDepositAndCallRevert, ), runner.NewE2ETest( TestBitcoinDepositAndCallRevertWithDustName, - "deposit Bitcoin into ZEVM; revert with dust amount that aborts the CCTX", []runner.ArgDefinition{}, + "deposit Bitcoin into ZEVM; revert with dust amount that aborts the CCTX", + []runner.ArgDefinition{}, TestBitcoinDepositAndCallRevertWithDust, ), + runner.NewE2ETest( + TestBitcoinDepositAndWithdrawWithDustName, + "deposit Bitcoin into ZEVM and withdraw with dust amount that fails the CCTX", + []runner.ArgDefinition{}, + TestBitcoinDepositAndWithdrawWithDust, + ), runner.NewE2ETest( TestBitcoinStdMemoDepositName, "deposit Bitcoin into ZEVM with standard memo", diff --git a/e2e/e2etests/test_bitcoin_deposit_and_withdraw_with_dust.go b/e2e/e2etests/test_bitcoin_deposit_and_withdraw_with_dust.go new file mode 100644 index 0000000000..8b108f2103 --- /dev/null +++ b/e2e/e2etests/test_bitcoin_deposit_and_withdraw_with_dust.go @@ -0,0 +1,58 @@ +package e2etests + +import ( + "math/big" + + "github.com/ethereum/go-ethereum/accounts/abi/bind" + "github.com/stretchr/testify/require" + + "github.com/zeta-chain/node/e2e/contracts/withdrawer" + "github.com/zeta-chain/node/e2e/runner" + "github.com/zeta-chain/node/e2e/utils" + crosschaintypes "github.com/zeta-chain/node/x/crosschain/types" +) + +// TestBitcoinDepositAndWithdrawWithDust deposits Bitcoin and call a smart contract that withdraw dust amount +// It tests the edge case where during a cross-chain call, a invaild withdraw is initiated (processLogs fails) +func TestBitcoinDepositAndWithdrawWithDust(r *runner.E2ERunner, args []string) { + // Given "Live" BTC network + stop := r.MineBlocksIfLocalBitcoin() + defer stop() + + require.Len(r, args, 0) + + // ARRANGE + + // Deploy the withdrawer contract on ZetaChain with a withdraw amount of 100 satoshis (dust amount is 1000 satoshis) + withdrawerAddr, tx, _, err := withdrawer.DeployWithdrawer(r.ZEVMAuth, r.ZEVMClient, big.NewInt(100)) + require.NoError(r, err) + + // Wait for the transaction to be mined + receipt := utils.MustWaitForTxReceipt(r.Ctx, r.ZEVMClient, tx, r.Logger, r.ReceiptTimeout) + require.Equal(r, receipt.Status, uint64(1)) + + // Given a list of UTXOs + utxos, err := r.ListDeployerUTXOs() + require.NoError(r, err) + require.NotEmpty(r, utxos) + + // ACT + // Deposit 0.01 BTC to withdrawer, this is an arbitrary amount, must be greater than dust amount + txHash, err := r.SendToTSSFromDeployerWithMemo(0.01, utxos, withdrawerAddr.Bytes()) + require.NoError(r, err) + require.NotEmpty(r, txHash) + + // ASSERT + // Now we want to make sure the cctx is reverted with expected error message + + // cctx status would be pending revert if using v21 or before + cctx := utils.WaitCctxRevertedByInboundHash(r.Ctx, r, txHash.String(), r.CctxClient) + require.Equal(r, crosschaintypes.CctxStatus_Reverted, cctx.CctxStatus.Status) + require.Contains(r, cctx.CctxStatus.ErrorMessage, crosschaintypes.ErrCannotProcessWithdrawal.Error()) + + // check the contract has no BTC balance, this would mean the contract call state transition is not reverted + // get BTC ZRC20 balance of the withdrawer contract + bal, err := r.BTCZRC20.BalanceOf(&bind.CallOpts{}, withdrawerAddr) + require.NoError(r, err) + require.Zero(r, bal.Uint64()) +} diff --git a/x/crosschain/keeper/evm_deposit.go b/x/crosschain/keeper/evm_deposit.go index e57dc16d1b..d6c8a77a66 100644 --- a/x/crosschain/keeper/evm_deposit.go +++ b/x/crosschain/keeper/evm_deposit.go @@ -100,8 +100,13 @@ func (k Keeper) HandleEVMDeposit(ctx sdk.Context, cctx *types.CrossChainTx) (boo return false, fmt.Errorf("HandleEVMDeposit: unable to decode address: %w", err) } + // use a temporary context to not commit any state change in case of error + // note: ZRC20DepositAndCallContract is solely responsible for calling the contract and depositing tokens if needed + // and does not include any other side effects or any logic that modifies the state directly + tmpCtx, commit := ctx.CacheContext() + evmTxResponse, contractCall, err := k.fungibleKeeper.ZRC20DepositAndCallContract( - ctx, + tmpCtx, from, to, inboundAmount, @@ -113,8 +118,11 @@ func (k Keeper) HandleEVMDeposit(ctx sdk.Context, cctx *types.CrossChainTx) (boo cctx.InboundParams.IsCrossChainCall, ) if fungibletypes.IsContractReverted(evmTxResponse, err) || errShouldRevertCctx(err) { + // this is a contract revert, we commit the state to save the emitted logs related to revert + commit() return true, err } else if err != nil { + // this should not happen and we don't commit the state to avoid inconsistent state return false, err } @@ -123,18 +131,21 @@ func (k Keeper) HandleEVMDeposit(ctx sdk.Context, cctx *types.CrossChainTx) (boo if !evmTxResponse.Failed() && contractCall { logs := evmtypes.LogsToEthereum(evmTxResponse.Logs) if len(logs) > 0 { - ctx = ctx.WithValue(InCCTXIndexKey, cctx.Index) + tmpCtx = tmpCtx.WithValue(InCCTXIndexKey, cctx.Index) txOrigin := cctx.InboundParams.TxOrigin if txOrigin == "" { txOrigin = inboundSender } - err = k.ProcessLogs(ctx, logs, to, txOrigin) + // process logs to process cctx events initiated during the contract call + err = k.ProcessLogs(tmpCtx, logs, to, txOrigin) if err != nil { - // ProcessLogs should not error; error indicates exception, should abort - return false, errors.Wrap(types.ErrCannotProcessWithdrawal, err.Error()) + // this happens if the cctx events are not processed correctly with invalid withdrawals + // in this situation we want the CCTX to be reverted, we don't commit the state so the contract call is not persisted + // the contract call is considered as reverted + return true, errors.Wrap(types.ErrCannotProcessWithdrawal, err.Error()) } - ctx.EventManager().EmitEvent( + tmpCtx.EventManager().EmitEvent( sdk.NewEvent(sdk.EventTypeMessage, sdk.NewAttribute(sdk.AttributeKeyModule, types.ModuleName), sdk.NewAttribute("action", "DepositZRC20AndCallContract"), @@ -145,7 +156,11 @@ func (k Keeper) HandleEVMDeposit(ctx sdk.Context, cctx *types.CrossChainTx) (boo ) } } + + // commit state change from the deposit and eventual cctx events + commit() } + return false, nil } diff --git a/x/crosschain/keeper/evm_deposit_test.go b/x/crosschain/keeper/evm_deposit_test.go index 45c0a0aecc..b65071a13f 100644 --- a/x/crosschain/keeper/evm_deposit_test.go +++ b/x/crosschain/keeper/evm_deposit_test.go @@ -98,7 +98,7 @@ func TestMsgServer_HandleEVMDeposit(t *testing.T) { // ZRC20DepositAndCallContract(ctx, from, to, msg.Amount.BigInt(), senderChain, msg.Message, contract, data, msg.CoinType, msg.Asset) fungibleMock.On( "ZRC20DepositAndCallContract", - ctx, + mock.Anything, mock.Anything, receiver, amount, @@ -145,7 +145,7 @@ func TestMsgServer_HandleEVMDeposit(t *testing.T) { // ZRC20DepositAndCallContract(ctx, from, to, msg.Amount.BigInt(), senderChain, msg.Message, contract, data, msg.CoinType, msg.Asset) fungibleMock.On( "ZRC20DepositAndCallContract", - ctx, + mock.Anything, mock.Anything, receiver, amount, @@ -187,7 +187,7 @@ func TestMsgServer_HandleEVMDeposit(t *testing.T) { cctx, ) require.Error(t, err) - require.False(t, reverted) + require.True(t, reverted) fungibleMock.AssertExpectations(t) }, ) @@ -209,7 +209,7 @@ func TestMsgServer_HandleEVMDeposit(t *testing.T) { // ZRC20DepositAndCallContract(ctx, from, to, msg.Amount.BigInt(), senderChain, msg.Message, contract, data, msg.CoinType, msg.Asset) fungibleMock.On( "ZRC20DepositAndCallContract", - ctx, + mock.Anything, mock.Anything, receiver, amount, @@ -300,7 +300,7 @@ func TestMsgServer_HandleEVMDeposit(t *testing.T) { errDeposit := errors.New("deposit failed") fungibleMock.On( "ZRC20DepositAndCallContract", - ctx, + mock.Anything, mock.Anything, receiver, amount, @@ -346,7 +346,7 @@ func TestMsgServer_HandleEVMDeposit(t *testing.T) { errDeposit := errors.New("deposit failed") fungibleMock.On( "ZRC20DepositAndCallContract", - ctx, + mock.Anything, mock.Anything, receiver, amount, @@ -391,7 +391,7 @@ func TestMsgServer_HandleEVMDeposit(t *testing.T) { // ZRC20DepositAndCallContract(ctx, from, to, msg.Amount.BigInt(), senderChain, msg.Message, contract, data, msg.CoinType, msg.Asset) fungibleMock.On( "ZRC20DepositAndCallContract", - ctx, + mock.Anything, mock.Anything, receiver, amount, @@ -436,7 +436,7 @@ func TestMsgServer_HandleEVMDeposit(t *testing.T) { // ZRC20DepositAndCallContract(ctx, from, to, msg.Amount.BigInt(), senderChain, msg.Message, contract, data, msg.CoinType, msg.Asset) fungibleMock.On( "ZRC20DepositAndCallContract", - ctx, + mock.Anything, mock.Anything, receiver, amount, @@ -481,7 +481,7 @@ func TestMsgServer_HandleEVMDeposit(t *testing.T) { fungibleMock.On( "ZRC20DepositAndCallContract", - ctx, + mock.Anything, mock.Anything, receiver, amount, @@ -552,7 +552,7 @@ func TestMsgServer_HandleEVMDeposit(t *testing.T) { ctx = ctx.WithTxBytes(b) fungibleMock.On( "ZRC20DepositAndCallContract", - ctx, + mock.Anything, mock.Anything, receiver, amount, @@ -596,7 +596,7 @@ func TestMsgServer_HandleEVMDeposit(t *testing.T) { require.NoError(t, err) fungibleMock.On( "ZRC20DepositAndCallContract", - ctx, + mock.Anything, mock.Anything, receiver, amount, diff --git a/x/crosschain/keeper/evm_hooks.go b/x/crosschain/keeper/evm_hooks.go index 38726c287a..d09f771fbb 100644 --- a/x/crosschain/keeper/evm_hooks.go +++ b/x/crosschain/keeper/evm_hooks.go @@ -133,7 +133,7 @@ func (k Keeper) ProcessZEVMInboundV1( // If Validation fails, we will not process the event and return and error. This condition means that the event was correct, and emitted from a registered ZRC20 contract // But the information entered by the user is incorrect. In this case we can return an error and roll back the transaction - if err := k.ValidateZrc20WithdrawEvent(ctx, eventZRC20Withdrawal, coin.ForeignChainId); err != nil { + if err := k.ValidateZRC20WithdrawEvent(ctx, eventZRC20Withdrawal, coin.ForeignChainId); err != nil { return err } // If the event is valid, we will process it and create a new CCTX @@ -303,9 +303,9 @@ func (k Keeper) ProcessZetaSentEvent( return nil } -// ValidateZrc20WithdrawEvent checks if the ZRC20Withdrawal event is valid +// ValidateZRC20WithdrawEvent checks if the ZRC20Withdrawal event is valid // It verifies event information for BTC chains and returns an error if the event is invalid -func (k Keeper) ValidateZrc20WithdrawEvent(ctx sdk.Context, event *zrc20.ZRC20Withdrawal, chainID int64) error { +func (k Keeper) ValidateZRC20WithdrawEvent(ctx sdk.Context, event *zrc20.ZRC20Withdrawal, chainID int64) error { // The event was parsed; that means the user has deposited tokens to the contract. return k.validateZRC20Withdrawal(ctx, chainID, event.Value, event.To) } diff --git a/x/crosschain/keeper/evm_hooks_test.go b/x/crosschain/keeper/evm_hooks_test.go index fff7e31f02..e9b4b49b0c 100644 --- a/x/crosschain/keeper/evm_hooks_test.go +++ b/x/crosschain/keeper/evm_hooks_test.go @@ -161,7 +161,7 @@ func TestValidateZrc20WithdrawEvent(t *testing.T) { *sample.ValidZRC20WithdrawToBTCReceipt(t).Logs[3], ) require.NoError(t, err) - err = k.ValidateZrc20WithdrawEvent(ctx, btcMainNetWithdrawalEvent, chains.BitcoinMainnet.ChainId) + err = k.ValidateZRC20WithdrawEvent(ctx, btcMainNetWithdrawalEvent, chains.BitcoinMainnet.ChainId) require.NoError(t, err) }) @@ -174,12 +174,12 @@ func TestValidateZrc20WithdrawEvent(t *testing.T) { // 1000 satoshis is the minimum amount that can be withdrawn btcMainNetWithdrawalEvent.Value = big.NewInt(constant.BTCWithdrawalDustAmount) - err = k.ValidateZrc20WithdrawEvent(ctx, btcMainNetWithdrawalEvent, chains.BitcoinMainnet.ChainId) + err = k.ValidateZRC20WithdrawEvent(ctx, btcMainNetWithdrawalEvent, chains.BitcoinMainnet.ChainId) require.NoError(t, err) // 999 satoshis cannot be withdrawn btcMainNetWithdrawalEvent.Value = big.NewInt(constant.BTCWithdrawalDustAmount - 1) - err = k.ValidateZrc20WithdrawEvent(ctx, btcMainNetWithdrawalEvent, chains.BitcoinMainnet.ChainId) + err = k.ValidateZRC20WithdrawEvent(ctx, btcMainNetWithdrawalEvent, chains.BitcoinMainnet.ChainId) require.ErrorContains(t, err, "less than dust amount") }) @@ -189,7 +189,7 @@ func TestValidateZrc20WithdrawEvent(t *testing.T) { *sample.ValidZRC20WithdrawToBTCReceipt(t).Logs[3], ) require.NoError(t, err) - err = k.ValidateZrc20WithdrawEvent(ctx, btcMainNetWithdrawalEvent, chains.BitcoinTestnet.ChainId) + err = k.ValidateZRC20WithdrawEvent(ctx, btcMainNetWithdrawalEvent, chains.BitcoinTestnet.ChainId) require.ErrorContains(t, err, "invalid address") }) @@ -201,7 +201,7 @@ func TestValidateZrc20WithdrawEvent(t *testing.T) { require.NoError(t, err) btcMainNetWithdrawalEvent.To = []byte("04b2891ba8cb491828db3ebc8a780d43b169e7b3974114e6e50f9bab6ec" + "63c2f20f6d31b2025377d05c2a704d3bd799d0d56f3a8543d79a01ab6084a1cb204f260") - err = k.ValidateZrc20WithdrawEvent(ctx, btcMainNetWithdrawalEvent, chains.BitcoinMainnet.ChainId) + err = k.ValidateZRC20WithdrawEvent(ctx, btcMainNetWithdrawalEvent, chains.BitcoinMainnet.ChainId) require.ErrorContains(t, err, "unsupported address") }) @@ -213,7 +213,7 @@ func TestValidateZrc20WithdrawEvent(t *testing.T) { value := big.NewInt(constant.SolanaWalletRentExempt) solWithdrawalEvent := sample.ZRC20Withdrawal(to, value) - err := k.ValidateZrc20WithdrawEvent(ctx, solWithdrawalEvent, chains.SolanaMainnet.ChainId) + err := k.ValidateZRC20WithdrawEvent(ctx, solWithdrawalEvent, chains.SolanaMainnet.ChainId) require.ErrorContains(t, err, "invalid address") }) @@ -227,12 +227,12 @@ func TestValidateZrc20WithdrawEvent(t *testing.T) { solWithdrawalEvent := sample.ZRC20Withdrawal(to, value) // 1000000 lamports can be withdrawn - err := k.ValidateZrc20WithdrawEvent(ctx, solWithdrawalEvent, chainID) + err := k.ValidateZRC20WithdrawEvent(ctx, solWithdrawalEvent, chainID) require.NoError(t, err) // 999999 lamports cannot be withdrawn solWithdrawalEvent.Value = big.NewInt(constant.SolanaWalletRentExempt - 1) - err = k.ValidateZrc20WithdrawEvent(ctx, solWithdrawalEvent, chainID) + err = k.ValidateZRC20WithdrawEvent(ctx, solWithdrawalEvent, chainID) require.ErrorContains(t, err, "less than rent exempt") }) } From 51e3f9751d02bad584b5aec939cbc293c5e12361 Mon Sep 17 00:00:00 2001 From: Alex Gartner Date: Wed, 8 Jan 2025 06:46:24 -0800 Subject: [PATCH 4/7] chore: remove main Dockerfile (#3329) --- .../build-docker-images-generic/action.yml | 71 ---- .../actions/build-docker-images/action.yml | 54 --- .github/workflows/docker-build-and-push.yml | 86 ----- Dockerfile | 66 ---- Makefile | 43 --- contrib/docker-scripts/download_binaries.py | 58 --- contrib/docker-scripts/install_cosmovisor.py | 69 ---- contrib/docker-scripts/start.sh | 335 ------------------ contrib/rpc/zetacored/docker-compose.yml | 36 -- contrib/rpc/zetacored/init_docker_compose.sh | 65 ---- contrib/rpc/zetacored/kill_docker_compose.sh | 14 - contrib/rpc/zetacored/networks/.athens3 | 14 - .../zetacored/networks/.athens3-localbuild | 14 - contrib/rpc/zetacored/networks/.mainnet | 14 - .../zetacored/networks/.mainnet-localbuild | 14 - 15 files changed, 953 deletions(-) delete mode 100644 .github/actions/build-docker-images-generic/action.yml delete mode 100644 .github/actions/build-docker-images/action.yml delete mode 100644 .github/workflows/docker-build-and-push.yml delete mode 100644 Dockerfile delete mode 100644 contrib/docker-scripts/download_binaries.py delete mode 100644 contrib/docker-scripts/install_cosmovisor.py delete mode 100644 contrib/docker-scripts/start.sh delete mode 100644 contrib/rpc/zetacored/docker-compose.yml delete mode 100644 contrib/rpc/zetacored/init_docker_compose.sh delete mode 100644 contrib/rpc/zetacored/kill_docker_compose.sh delete mode 100644 contrib/rpc/zetacored/networks/.athens3 delete mode 100644 contrib/rpc/zetacored/networks/.athens3-localbuild delete mode 100644 contrib/rpc/zetacored/networks/.mainnet delete mode 100644 contrib/rpc/zetacored/networks/.mainnet-localbuild diff --git a/.github/actions/build-docker-images-generic/action.yml b/.github/actions/build-docker-images-generic/action.yml deleted file mode 100644 index 245ac8a631..0000000000 --- a/.github/actions/build-docker-images-generic/action.yml +++ /dev/null @@ -1,71 +0,0 @@ -name: 'Build Docker Images' -description: 'Builds Docker images and pushes them to any repository.' -inputs: - DOCKER_FILENAME: - description: 'Name of the docker file to use for the build' - required: true - REPOSITORY_NAME: - description: 'Name of the Repository' - required: true - IMAGE_TAG: - description: 'Image Tag' - required: true - REGISTRY: - description: 'Docker or ORG you want to push to.' - required: true - DOCKER_ORG: - description: 'Docker ORG you want to push to.' - required: false - USERNAME: - description: 'Username for GitHub Container Registry' - required: true - TOKEN: - description: 'Token for GitHub Container Registry' - required: true - DOCKER_FILE_DIRECTORY: - description: 'Directory for your Dockerfile' - required: true - DOCKER_BUILD_KIT: - description: "whether or not to use docker build kit." - required: true - TAG_LATEST: - description: "should the pipeline tag latest" - required: true -runs: - using: "composite" - - steps: - - name: Set Environment Variables" - run: | - echo "DOCKER_BUILDKIT=${{ inputs.DOCKER_BUILD_KIT }}" >> $GITHUB_ENV - shell: bash - - - name: Log in to the Docker Registry - uses: docker/login-action@v2 - with: - registry: ${{ inputs.REGISTRY }} - username: ${{ inputs.USERNAME }} - password: ${{ inputs.TOKEN }} - - - name: Build, tag, and push images - shell: bash - working-directory: ${{ inputs.DOCKER_FILE_DIRECTORY }} - run: | - if [ ! -z "${{ inputs.DOCKER_ORG }}" ]; then - echo "DOCKER ORG SPECIFIED SO USE DOCKER HUB" - docker build -f ${{ inputs.DOCKER_FILENAME }} -t ${{ inputs.DOCKER_ORG }}/${{ inputs.REPOSITORY_NAME }}:${{ inputs.IMAGE_TAG }} . - docker push ${{ inputs.DOCKER_ORG }}/${{ inputs.REPOSITORY_NAME }}:${{ inputs.IMAGE_TAG }} - - if [ "${{ inputs.TAG_LATEST }}" == "true" ]; then - docker tag ${{ inputs.DOCKER_ORG }}/${{ inputs.REPOSITORY_NAME }}:${{ inputs.IMAGE_TAG }} ${{ inputs.DOCKER_ORG }}/${{ inputs.REPOSITORY_NAME }}:latest - docker push ${{ inputs.DOCKER_ORG }}/${{ inputs.REPOSITORY_NAME }}:latest - fi - else - echo "DOCKER REGISTRY SPECIFIED WITH NO DOCKER_ORG USE NON ORG REGISTRIES" - docker build -f ${{ inputs.DOCKER_FILENAME }} -t ${{ inputs.REGISTRY }}/${{ inputs.REPOSITORY_NAME }}:${{ inputs.IMAGE_TAG }} . - docker push ${{ inputs.REGISTRY }}/${{ inputs.REPOSITORY_NAME }}:${{ inputs.IMAGE_TAG }} - if [ "${{ inputs.TAG_LATEST }}" == "true" ]; then - docker tag ${{ inputs.REGISTRY }}/${{ inputs.REPOSITORY_NAME }}:${{ inputs.IMAGE_TAG }} ${{ inputs.REGISTRY }}/${{ inputs.REPOSITORY_NAME }}:latest - docker push ${{ inputs.REGISTRY }}/${{ inputs.REPOSITORY_NAME }}:latest - fi - fi diff --git a/.github/actions/build-docker-images/action.yml b/.github/actions/build-docker-images/action.yml deleted file mode 100644 index b68efe7cbb..0000000000 --- a/.github/actions/build-docker-images/action.yml +++ /dev/null @@ -1,54 +0,0 @@ -name: 'Build Docker Images' -description: 'Builds Docker images and pushes them to ECR and GHCR' -inputs: - DOCKER_FILENAME: - description: 'Name of the docker file to use for the build' - required: true - REPOSITORY_NAME: - description: 'Name of the Repository' - required: true - IMAGE_TAG: - description: 'Image Tag' - required: true - GHCR_USERNAME: - description: 'Username for GitHub Container Registry' - required: true - GHCR_TOKEN: - description: 'Token for GitHub Container Registry' - required: true - -runs: - using: "composite" - - steps: - - name: Set Environment Variables" - run: | - echo "DOCKER_FILENAME=${{ inputs.DOCKER_FILENAME }}" >> $GITHUB_ENV - echo "REPOSITORY_NAME=${{ inputs.REPOSITORY_NAME }}" >> $GITHUB_ENV - echo "IMAGE_TAG=${{ inputs.IMAGE_TAG }}" >> $GITHUB_ENV - echo "GHCR_USERNAME=${{ inputs.GHCR_USERNAME }}" >> $GITHUB_ENV - echo "GHCR_TOKEN=${{ inputs.GHCR_TOKEN }}" >> $GITHUB_ENV - echo "DOCKER_BUILDKIT=1" >> $GITHUB_ENV - shell: bash - - - name: Log in to the GitHub Container Registry - id: login-ghcr - uses: docker/login-action@v2 - with: - registry: ghcr.io - username: ${{ env.GHCR_USERNAME }} - password: ${{ env.GHCR_TOKEN }} - - - name: Login to Amazon ECR - id: login-ecr - uses: aws-actions/amazon-ecr-login@v1 - - - name: Build, tag, and push images - shell: bash - env: - ECR_REGISTRY: ${{ steps.login-ecr.outputs.registry }} - GHCR_REGISTRY: ghcr.io/zeta-chain - run: | - docker build -f $DOCKER_FILENAME -t $ECR_REGISTRY/$REPOSITORY_NAME:$IMAGE_TAG -t $GHCR_REGISTRY/$REPOSITORY_NAME:$IMAGE_TAG . - # docker push $ECR_REGISTRY/$REPOSITORY_NAME:$IMAGE_TAG - docker push $GHCR_REGISTRY/$REPOSITORY_NAME:$IMAGE_TAG \ No newline at end of file diff --git a/.github/workflows/docker-build-and-push.yml b/.github/workflows/docker-build-and-push.yml deleted file mode 100644 index c2def3200d..0000000000 --- a/.github/workflows/docker-build-and-push.yml +++ /dev/null @@ -1,86 +0,0 @@ -name: Zetacored-Docker-Build - -on: - release: - types: - - created - workflow_dispatch: - inputs: - version: - description: 'Docker Tag Version For Manual Execution' - required: false - default: '' - -concurrency: - group: Zetacored-Docker-Build - cancel-in-progress: false - -env: - DOCKER_REPO: "zetacored" - DOCKER_ORG: "zetachain" - DOCKER_REGISTRY: "https://index.docker.io/v1/" - -jobs: - docker_build_ubuntu: - runs-on: ubuntu-22.04 - timeout-minutes: 30 - steps: - - uses: actions/checkout@v4 - with: - fetch-depth: 0 - - - name: Set Version from the release title. - if: github.event_name == 'workflow_dispatch' - run: | - echo "GITHUB_TAG_MAJOR_VERSION=${{ github.event.release.name }}" >> $GITHUB_ENV - - - name: Set Version for Hotfix Release from Input. - if: github.event_name == 'workflow_dispatch' - run: | - echo "GITHUB_TAG_MAJOR_VERSION=${{ github.event.inputs.version }}" >> ${GITHUB_ENV} - - - name: "BUILD:PUSH:MONITORING:DOCKER:IMAGE" - uses: ./.github/actions/build-docker-images-generic - with: - DOCKER_FILENAME: "Dockerfile" - REPOSITORY_NAME: "${{ env.DOCKER_REPO }}" - IMAGE_TAG: "ubuntu-${{ env.GITHUB_TAG_MAJOR_VERSION }}" - REGISTRY: "${{ env.DOCKER_REGISTRY }}" - DOCKER_ORG: "${{ env.DOCKER_ORG }}" - USERNAME: "${{ secrets.DOCKER_HUB_USERNAME }}" - TOKEN: "${{ secrets.DOCKERHUB_TOKEN }}" - DOCKER_FILE_DIRECTORY: "./" - DOCKER_BUILD_KIT: "0" - TAG_LATEST: "true" - - docker_build_arm: - runs-on: buildjet-4vcpu-ubuntu-2204-arm - timeout-minutes: 30 - steps: - - uses: actions/checkout@v4 - with: - fetch-depth: 0 - - - name: Set Version from the release title. - if: github.event_name == 'workflow_dispatch' - run: | - echo "GITHUB_TAG_MAJOR_VERSION=${{ github.event.release.name }}" >> $GITHUB_ENV - - - name: Set Version for Hotfix Release from Input. - if: github.event_name == 'workflow_dispatch' - run: | - echo "GITHUB_TAG_MAJOR_VERSION=${{ github.event.inputs.version }}" >> ${GITHUB_ENV} - - - name: "BUILD:PUSH:MONITORING:DOCKER:IMAGE" - uses: ./.github/actions/build-docker-images-generic - with: - DOCKER_FILENAME: "Dockerfile" - REPOSITORY_NAME: "${{ env.DOCKER_REPO }}" - IMAGE_TAG: "arm-${{ env.GITHUB_TAG_MAJOR_VERSION }}" - REGISTRY: "${{ env.DOCKER_REGISTRY }}" - DOCKER_ORG: "${{ env.DOCKER_ORG }}" - USERNAME: "${{ secrets.DOCKER_HUB_USERNAME }}" - TOKEN: "${{ secrets.DOCKERHUB_TOKEN }}" - DOCKER_FILE_DIRECTORY: "./" - DOCKER_BUILD_KIT: "0" - TAG_LATEST: "false" \ No newline at end of file diff --git a/Dockerfile b/Dockerfile deleted file mode 100644 index 0f9804d087..0000000000 --- a/Dockerfile +++ /dev/null @@ -1,66 +0,0 @@ -# Build Stage -FROM golang:1.22-alpine3.18 AS builder - -ENV GOPATH /go -ENV GOOS=linux -ENV CGO_ENABLED=1 - -# Install build dependencies -RUN apk --no-cache add git make build-base jq openssh libusb-dev linux-headers bash curl python3 py3-pip - -# Set the working directory -WORKDIR /go/delivery/zeta-node - -# Copy module files and download dependencies -COPY go.mod . -COPY go.sum . - -RUN go mod download - -# Copy the rest of the source code and build the application -COPY . . - -RUN expected_major_version=$(grep 'const releaseVersion = ' app/setup_handlers.go | awk -F'"' '{print $2}') && \ - make install VERSION="${expected_major_version}" && \ - git_hash=$(git rev-parse --short HEAD) && \ - echo -n "${expected_major_version}-${git_hash}" > /go/delivery/zeta-node/expected_major_version - -# Run Stage -FROM alpine:3.18 - -ENV COSMOVISOR_CHECKSUM="626dfc58c266b85f84a7ed8e2fe0e2346c15be98cfb9f9b88576ba899ed78cdc" -ENV COSMOVISOR_VERSION="v1.5.0" -# Copy Start Script Helpers -COPY contrib/docker-scripts/* /scripts/ - -# Install runtime dependencies -RUN apk --no-cache add git jq bash curl nano vim tmux python3 libusb-dev linux-headers make build-base bind-tools psmisc coreutils wget py3-pip qemu-img qemu-system-x86_64 && \ - pip install requests && \ - chmod a+x -R /scripts && \ - wget https://github.com/sgerrand/alpine-pkg-glibc/releases/download/2.31-r0/glibc-2.31-r0.apk && \ - apk add --force-overwrite --allow-untrusted glibc-2.31-r0.apk && \ - curl https://dl.google.com/dl/cloudsdk/release/google-cloud-sdk.tar.gz > /tmp/google-cloud-sdk.tar.gz && \ - mkdir -p /usr/local/gcloud && \ - tar -C /usr/local/gcloud -xvf /tmp/google-cloud-sdk.tar.gz && \ - /usr/local/gcloud/google-cloud-sdk/install.sh --quiet && \ - ln -s /usr/local/gcloud/google-cloud-sdk/bin/gcloud /usr/bin/gcloud && \ - python /scripts/install_cosmovisor.py - -# Copy the binaries from the build stage -COPY --from=builder /go/bin/zetaclientd /usr/local/bin/zetaclientd -COPY --from=builder /go/bin/zetacored /usr/local/bin/zetacored -COPY --from=builder /go/delivery/zeta-node/expected_major_version /scripts/expected_major_version - -# Set the working directory -WORKDIR /usr/local/bin - -# Set the default shell -ENV SHELL /bin/bash - -EXPOSE 26656 -EXPOSE 1317 -EXPOSE 8545 -EXPOSE 8546 -EXPOSE 9090 -EXPOSE 26657 -EXPOSE 9091 \ No newline at end of file diff --git a/Makefile b/Makefile index 1ecd3bc599..344ae150e4 100644 --- a/Makefile +++ b/Makefile @@ -482,49 +482,6 @@ stop-eth-node-mainnet: clean-eth-node-mainnet: cd contrib/rpc/ethereum && DOCKER_TAG=$(DOCKER_TAG) docker-compose down -v -#ZETA - -#FULL-NODE-RPC-FROM-BUILT-IMAGE -start-zetacored-rpc-mainnet: - cd contrib/rpc/zetacored && bash init_docker_compose.sh mainnet image $(DOCKER_TAG) - -stop-zetacored-rpc-mainnet: - cd contrib/rpc/zetacored && bash kill_docker_compose.sh mainnet false - -clean-zetacored-rpc-mainnet: - cd contrib/rpc/zetacored && bash kill_docker_compose.sh mainnet true - -#FULL-NODE-RPC-FROM-BUILT-IMAGE -start-zetacored-rpc-testnet: - cd contrib/rpc/zetacored && bash init_docker_compose.sh athens3 image $(DOCKER_TAG) - -stop-zetacored-rpc-testnet: - cd contrib/rpc/zetacored && bash kill_docker_compose.sh athens3 false - -clean-zetacored-rpc-testnet: - cd contrib/rpc/zetacored && bash kill_docker_compose.sh athens3 true - -#FULL-NODE-RPC-FROM-LOCAL-BUILD -start-zetacored-rpc-mainnet-localbuild: - cd contrib/rpc/zetacored && bash init_docker_compose.sh mainnet localbuild $(DOCKER_TAG) - -stop-zetacored-rpc-mainnet-localbuild: - cd contrib/rpc/zetacored && bash kill_docker_compose.sh mainnet false - -clean-zetacored-rpc-mainnet-localbuild: - cd contrib/rpc/zetacored && bash kill_docker_compose.sh mainnet true - -#FULL-NODE-RPC-FROM-LOCAL-BUILD -start-zetacored-rpc-testnet-localbuild: - cd contrib/rpc/zetacored && bash init_docker_compose.sh athens3 localbuild $(DOCKER_TAG) - -stop-zetacored-rpc-testnet-localbuild: - cd contrib/rpc/zetacored && bash kill_docker_compose.sh athens3 false - -clean-zetacored-rpc-testnet-localbuild: - cd contrib/rpc/zetacored && bash kill_docker_compose.sh athens3 true - - ############################################################################### ### Debug Tools ### ############################################################################### diff --git a/contrib/docker-scripts/download_binaries.py b/contrib/docker-scripts/download_binaries.py deleted file mode 100644 index 99063a40f0..0000000000 --- a/contrib/docker-scripts/download_binaries.py +++ /dev/null @@ -1,58 +0,0 @@ -import requests -import os -import json -import logging -import sys - -# Logger class for easier logging setup -class Logger: - def __init__(self): - self.log = logging.getLogger() - self.log.setLevel(logging.INFO) - self.handler = logging.StreamHandler(sys.stdout) - self.handler.setLevel(logging.DEBUG) - self.formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s') - self.handler.setFormatter(self.formatter) - self.log.addHandler(self.handler) - - -# Initialize logger instance -logger = Logger() - -# Parse JSON from an environment variable to get binary download information -info = json.loads(os.environ["DOWNLOAD_BINARIES"]) - -try: - # Iterate over binaries to download - for binary in info["binaries"]: - download_link = binary["download_url"] - binary_location = f'{os.environ["DAEMON_HOME"]}/{binary["binary_location"]}' - binary_directory = os.path.dirname(binary_location) - # Log download link - logger.log.info(f"DOWNLOAD LINK: {download_link}") - split_download_link = download_link.split("/") - # Log split download link parts - logger.log.info(f"SPLIT DOWNLOAD LINK: {split_download_link}") - # Extract binary name and version from the download link - binary_name = download_link.split("/")[8] - # Check if binary already exists - logger.log.info(f"CHECKING / DOWNLOADING {binary_location}") - - if os.path.exists(binary_location): - # If binary exists, log and do nothing - logger.log.info(f"BINARY EXISTS ALREADY: {binary_location}") - else: - # If binary doesn't exist, download and save it - logger.log.info("BINARY DOES NOT EXIST.") - os.makedirs(binary_directory, exist_ok=True) - response = requests.get(download_link) - if response.status_code == 200: - with open(binary_location, "wb") as f: - f.write(response.content) - os.chmod(binary_location, 0o755) - logger.log.info("BINARY DOWNLOADED SUCCESSFULLY.") - else: - logger.log.info("FAILED TO DOWNLOAD BINARY. Status code:", response.status_code) - logger.log.info("BINARIES DOWNLOAD FINISHED...") -except Exception as e: - logger.log.error(str(e)) diff --git a/contrib/docker-scripts/install_cosmovisor.py b/contrib/docker-scripts/install_cosmovisor.py deleted file mode 100644 index 568c9bbe8e..0000000000 --- a/contrib/docker-scripts/install_cosmovisor.py +++ /dev/null @@ -1,69 +0,0 @@ -#!/usr/bin/env python3 -import hashlib -import logging -import os -import requests -import sys - -# Constants defining the binary name, version, expected checksum, download URL, and installation path -BINARY_NAME = "cosmovisor" -BINARY_VERSION = os.getenv("COSMOVISOR_VERSION") # Get the cosmovisor version from environment variable -EXPECTED_CHECKSUM = os.getenv("COSMOVISOR_CHECKSUM") # Get the expected checksum from environment variable -BINARY_URL = f"https://binary-pickup.zetachain.com/cosmovisor-{BINARY_VERSION}-linux-amd64" # Construct the binary download URL -INSTALL_PATH = f"/usr/local/bin/{BINARY_NAME}" # Define the installation path for the binary - -# Check if necessary environment variables are set; exit if not -if not BINARY_VERSION or not EXPECTED_CHECKSUM: - logging.error("Environment variables COSMOVISOR_VERSION and COSMOVISOR_CHECKSUM must be set.") - sys.exit(1) - -# Configure logging to both stdout and a file -logging.basicConfig( - level=logging.INFO, # Set logging level to INFO - format="%(levelname)s: %(message)s", # Define log message format - handlers=[ - logging.StreamHandler(sys.stdout), # Log to stdout - logging.FileHandler("/var/log/update_cosmovisor.log") # Log to a file - ] -) - - -# Function to calculate the SHA-256 checksum of the downloaded binary -def calculate_checksum(file_path): - sha256 = hashlib.sha256() # Create a new SHA-256 hash object - with open(file_path, "rb") as f: # Open the binary file in binary read mode - for byte_block in iter(lambda: f.read(4096), - b""): # Read the file in chunks to avoid loading it all into memory - sha256.update(byte_block) # Update the hash object with the chunk - return sha256.hexdigest() # Return the hexadecimal digest of the hash object - - -# Function to download the binary and update it if the checksum matches -def download_and_update_binary(): - try: - response = requests.get(BINARY_URL) # Attempt to download the binary - response.raise_for_status() # Check if the download was successful, raises exception on failure - logging.info("Binary downloaded successfully.") - except requests.exceptions.RequestException as e: - logging.error(f"Failed to download the binary: {e}") # Log any error during download - sys.exit(1) # Exit the script on download failure - - with open(INSTALL_PATH, "wb") as f: # Open the installation path file in binary write mode - f.write(response.content) # Write the downloaded binary content to the file - - actual_checksum = calculate_checksum(INSTALL_PATH) # Calculate the checksum of the downloaded binary - if actual_checksum == EXPECTED_CHECKSUM: # Compare the actual checksum with the expected checksum - logging.info("Cosmovisor binary checksum verified.") # Log success if checksums match - os.chmod(INSTALL_PATH, 0o755) # Make the binary executable - logging.info("Cosmovisor binary updated successfully.") - else: - logging.error( - "Checksums do not match. Possible corrupted download. Deleting the downloaded binary.") # Log failure if checksums do not match - os.remove(INSTALL_PATH) # Remove the potentially corrupted binary - sys.exit(1) # Exit the script due to checksum mismatch - - -# Main script execution starts here -logging.info( - f"Downloading the {BINARY_NAME} binary (version {BINARY_VERSION})...") # Log the start of the download process -download_and_update_binary() # Call the function to download and update the binary diff --git a/contrib/docker-scripts/start.sh b/contrib/docker-scripts/start.sh deleted file mode 100644 index bb151a49fe..0000000000 --- a/contrib/docker-scripts/start.sh +++ /dev/null @@ -1,335 +0,0 @@ -#!/bin/bash - -logt() { - echo "$(date '+%Y-%m-%d %H:%M:%S') $1" -} - - -function load_defaults { - #DEFAULT: Mainnet Statesync. - export DAEMON_HOME=${DAEMON_HOME:=/root/.zetacored} - export NETWORK=${NETWORK:=mainnet} - export RESTORE_TYPE=${RESTORE_TYPE:=statesync} - export SNAPSHOT_API=${SNAPSHOT_API:=https://snapshots.rpc.zetachain.com} - export TRUST_HEIGHT_DIFFERENCE_STATE_SYNC=${TRUST_HEIGHT_DIFFERENCE_STATE_SYNC:=40000} - export COSMOVISOR_VERSION=${COSMOVISOR_VERSION:=v1.5.0} - export CHAIN_ID=${CHAIN_ID:=zetachain_7000-1} - export COSMOVISOR_CHECKSUM=${COSMOVISOR_CHECKSUM:=626dfc58c266b85f84a7ed8e2fe0e2346c15be98cfb9f9b88576ba899ed78cdc} - export VISOR_NAME=${VISOR_NAME:=cosmovisor} - export DAEMON_NAME=${DAEMON_NAME:=zetacored} - export DAEMON_ALLOW_DOWNLOAD_BINARIES=${DAEMON_ALLOW_DOWNLOAD_BINARIES:=false} - export DAEMON_RESTART_AFTER_UPGRADE=${DAEMON_RESTART_AFTER_UPGRADE:=true} - export UNSAFE_SKIP_BACKUP=${UNSAFE_SKIP_BACKUP:=true} - export CLIENT_DAEMON_NAME=${CLIENT_DAEMON_NAME:=zetaclientd} - export CLIENT_DAEMON_ARGS=${CLIENT_DAEMON_ARGS:""} - export CLIENT_SKIP_UPGRADE=${CLIENT_SKIP_UPGRADE:=true} - export CLIENT_START_PROCESS=${CLIENT_START_PROCESS:=false} - export MONIKER=${MONIKER:=local-test} - export RE_DO_START_SEQUENCE=${RE_DO_START_SEQUENCE:=false} - - #ATHENS3 - export BINARY_LIST_ATHENS3=${BINARY_LIST_ATHENS3:=https://raw.githubusercontent.com/zeta-chain/network-config/main/athens3/binary_list.json} - export STATE_SYNC_RPC_NODE_FILE_ATHENS3=${STATE_SYNC_RPC_NODE_FILE_ATHENS3:=https://raw.githubusercontent.com/zeta-chain/network-config/main/athens3/state_sync_node} - export RPC_STATE_SYNC_RPC_LIST_FILE_ATHENS3=${RPC_STATE_SYNC_RPC_LIST_FILE_ATHENS3:=https://raw.githubusercontent.com/zeta-chain/network-config/main/athens3/rpc_state_sync_nodes} - export APP_TOML_FILE_ATHENS3=${APP_TOML_FILE_ATHENS3:=https://raw.githubusercontent.com/zeta-chain/network-config/main/athens3/app.toml} - export CONFIG_TOML_FILE_ATHENS3=${CONFIG_TOML_FILE_ATHENS3:=https://raw.githubusercontent.com/zeta-chain/network-config/main/athens3/config.toml} - export CLIENT_TOML_FILE_ATHENS3=${CLIENT_TOML_FILE_ATHENS3:=https://raw.githubusercontent.com/zeta-chain/network-config/main/athens3/client.toml} - export GENESIS_FILE_ATHENS3=${GENESIS_FILE_ATHENS3:=https://raw.githubusercontent.com/zeta-chain/network-config/main/athens3/genesis.json} - - #MAINNET - export BINARY_LIST_MAINNET=${BINARY_LIST_MAINNET:=https://raw.githubusercontent.com/zeta-chain/network-config/main/mainnet/binary_list.json} - export STATE_SYNC_RPC_NODE_FILE_MAINNET=${STATE_SYNC_RPC_NODE_FILE_MAINNET:=https://raw.githubusercontent.com/zeta-chain/network-config/main/mainnet/state_sync_node} - export RPC_STATE_SYNC_RPC_LIST_FILE_MAINNET=${RPC_STATE_SYNC_RPC_LIST_FILE_MAINNET:=https://raw.githubusercontent.com/zeta-chain/network-config/main/mainnet/rpc_state_sync_nodes} - export APP_TOML_FILE_MAINNET=${APP_TOML_FILE_MAINNET:=https://raw.githubusercontent.com/zeta-chain/network-config/main/mainnet/app.toml} - export CONFIG_TOML_FILE_MAINNET=${CONFIG_TOML_FILE_MAINNET:=https://raw.githubusercontent.com/zeta-chain/network-config/main/mainnet/config.toml} - export CLIENT_TOML_FILE_MAINNET=${CLIENT_TOML_FILE_MAINNET:=https://raw.githubusercontent.com/zeta-chain/network-config/main/mainnet/client.toml} - export GENESIS_FILE_MAINNET=${GENESIS_FILE_MAINNET:=https://raw.githubusercontent.com/zeta-chain/network-config/main/mainnet/genesis.json} - -} - -function init_chain { - if [ -d "${DAEMON_HOME}/config" ]; then - logt "${DAEMON_NAME} home directory already initialized." - else - logt "${DAEMON_NAME} home directory not initialized." - logt "MONIKER: ${MONIKER}" - logt "DAEMON_HOME: ${DAEMON_HOME}" - ${DAEMON_NAME} init ${MONIKER} --home ${DAEMON_HOME} --chain-id ${CHAIN_ID} - fi -} - -function download_configs { - if [ "${NETWORK}" == "mainnet" ]; then - wget -q ${APP_TOML_FILE_MAINNET} -O ${DAEMON_HOME}/config/app.toml - wget -q ${CONFIG_TOML_FILE_MAINNET} -O ${DAEMON_HOME}/config/config.toml - wget -q ${CLIENT_TOML_FILE_MAINNET} -O ${DAEMON_HOME}/config/client.toml - wget -q ${GENESIS_FILE_MAINNET} -O ${DAEMON_HOME}/config/genesis.json - wget -q ${BINARY_LIST_MAINNET} - export DOWNLOAD_BINARIES=$(cat binary_list.json | tr -d '\n') - rm -rf binary_list.json - logt "BINARY_LIST: ${DOWNLOAD_BINARIES}" - elif [ "${NETWORK}" == "athens3" ]; then - wget -q ${APP_TOML_FILE_ATHENS3} -O ${DAEMON_HOME}/config/app.toml - wget -q ${CONFIG_TOML_FILE_ATHENS3} -O ${DAEMON_HOME}/config/config.toml - wget -q ${CLIENT_TOML_FILE_ATHENS3} -O ${DAEMON_HOME}/config/client.toml - wget -q ${GENESIS_FILE_ATHENS3} -O ${DAEMON_HOME}/config/genesis.json - wget -q ${BINARY_LIST_ATHENS3} - export DOWNLOAD_BINARIES=$(cat binary_list.json | tr -d '\n') - rm -rf binary_list.json - logt "BINARY_LIST: ${DOWNLOAD_BINARIES}" - else - logt "Initialize for Localnet." - fi -} - -function setup_restore_type { - if [ "${RESTORE_TYPE}" == "statesync" ]; then - logt "Statesync restore. Download state sync rpc address from network-config" - if [ "${NETWORK}" == "mainnet" ]; then - logt "MAINNET STATE SYNC" - logt "STATE_SYNC_RPC_NODE_FILE_MAINNET: ${STATE_SYNC_RPC_NODE_FILE_MAINNET}" - logt "RPC_STATE_SYNC_RPC_LIST_FILE_MAINNET: ${RPC_STATE_SYNC_RPC_LIST_FILE_MAINNET}" - wget -q ${STATE_SYNC_RPC_NODE_FILE_MAINNET} - wget -q ${RPC_STATE_SYNC_RPC_LIST_FILE_MAINNET} - export STATE_SYNC_SERVER=$(cat state_sync_node) - export RPC_STATE_SYNC_SERVERS=$(cat rpc_state_sync_nodes) - rm -rf state_sync_node - rm -rf rpc_state_sync_nodes - elif [ "${NETWORK}" == "athens3" ]; then - logt "ATHENS STATE SYNC" - logt "STATE_SYNC_RPC_NODE_FILE_MAINNET: ${STATE_SYNC_RPC_NODE_FILE_ATHENS3}" - logt "RPC_STATE_SYNC_RPC_LIST_FILE_MAINNET: ${RPC_STATE_SYNC_RPC_LIST_FILE_ATHENS3}" - wget -q ${STATE_SYNC_RPC_NODE_FILE_ATHENS3} - wget -q ${RPC_STATE_SYNC_RPC_LIST_FILE_ATHENS3} - export STATE_SYNC_SERVER=$(cat state_sync_node) - export RPC_STATE_SYNC_SERVERS=$(cat rpc_state_sync_nodes) - rm -rf state_sync_node - rm -rf rpc_state_sync_nodes - fi - elif [ "${RESTORE_TYPE}" == "snapshot" ]; then - if [ "${NETWORK}" == "mainnet" ]; then - logt "Get Latest Snapshot URL" - SNAPSHOT_URL=$(curl -s ${SNAPSHOT_API}/mainnet/fullnode/latest.json | jq -r '.snapshots[0].link') - SNAPSHOT_FILENAME=$(curl -s ${SNAPSHOT_API}/mainnet/fullnode/latest.json | jq -r '.snapshots[0].filename') - SNAPSHOT_DIR=$(pwd) - logt "Download Snapshot from url: ${SNAPSHOT_URL}" - curl -o "${SNAPSHOT_FILENAME}" "${SNAPSHOT_URL}" - logt "Change to: ${DAEMON_HOME} and extract snapshot." - cd ${DAEMON_HOME} - tar xvf ${SNAPSHOT_DIR}/${SNAPSHOT_FILENAME} - logt " Cleanup Snapshot" - rm -rf ${SNAPSHOT_DIR}/${SNAPSHOT_FILENAME} - elif [ "${NETWORK}" == "athens3" ]; then - SNAPSHOT_URL=$(curl -s ${SNAPSHOT_API}/testnet/fullnode/latest.json | jq -r '.snapshots[0].link') - SNAPSHOT_FILENAME=$(curl -s ${SNAPSHOT_API}/testnet/fullnode/latest.json | jq -r '.snapshots[0].filename') - SNAPSHOT_DIR=$(pwd) - logt "Download Snapshot from url: ${SNAPSHOT_URL}" - curl -o "${SNAPSHOT_FILENAME}" "${SNAPSHOT_URL}" - logt "Change to: ${DAEMON_HOME} and extract snapshot." - cd ${DAEMON_HOME} - tar xvf ${SNAPSHOT_DIR}/${SNAPSHOT_FILENAME} - logt " Cleanup Snapshot" - rm -rf ${SNAPSHOT_DIR}/${SNAPSHOT_FILENAME} - fi - elif [ "${RESTORE_TYPE}" == "snapshot-archive" ]; then - if [ "${NETWORK}" == "mainnet" ]; then - logt "Get Latest Snapshot URL" - SNAPSHOT_URL=$(curl -s ${SNAPSHOT_API}/mainnet/archive/latest.json | jq -r '.snapshots[0].link') - SNAPSHOT_FILENAME=$(curl -s ${SNAPSHOT_API}/mainnet/archive/latest.json | jq -r '.snapshots[0].filename') - SNAPSHOT_DIR=$(pwd) - logt "Download Snapshot from url: ${SNAPSHOT_URL}" - curl -o "${SNAPSHOT_FILENAME}" "${SNAPSHOT_URL}" - logt "Change to: ${DAEMON_HOME} and extract snapshot." - cd ${DAEMON_HOME} - tar xvf ${SNAPSHOT_DIR}/${SNAPSHOT_FILENAME} - logt " Cleanup Snapshot" - rm -rf ${SNAPSHOT_DIR}/${SNAPSHOT_FILENAME} - elif [ "${NETWORK}" == "athens3" ]; then - SNAPSHOT_URL=$(curl -s ${SNAPSHOT_API}/testnet/archive/latest.json | jq -r '.snapshots[0].link') - SNAPSHOT_FILENAME=$(curl -s ${SNAPSHOT_API}/testnet/archive/latest.json | jq -r '.snapshots[0].filename') - SNAPSHOT_DIR=$(pwd) - logt "Download Snapshot from url: ${SNAPSHOT_URL}" - curl -o "${SNAPSHOT_FILENAME}" "${SNAPSHOT_URL}" - logt "Change to: ${DAEMON_HOME} and extract snapshot." - cd ${DAEMON_HOME} - tar xvf ${SNAPSHOT_DIR}/${SNAPSHOT_FILENAME} - logt " Cleanup Snapshot" - rm -rf ${SNAPSHOT_DIR}/${SNAPSHOT_FILENAME} - fi - else - logt "Initialize for Localnet." - fi -} - -function change_config_values { - if [ "${RESTORE_TYPE}" == "statesync" ]; then - export STATE_SYNC_SERVER="${STATE_SYNC_SERVER}" - export TRUST_HEIGHT=$(curl -s ${STATE_SYNC_SERVER}/block | jq -r '.result.block.header.height') - export HEIGHT=$((TRUST_HEIGHT-${TRUST_HEIGHT_DIFFERENCE_STATE_SYNC})) - export TRUST_HASH=$(curl -s "${STATE_SYNC_SERVER}/block?height=${HEIGHT}" | jq -r '.result.block_id.hash') - export RPC_STATE_SYNC_SERVERS="${RPC_STATE_SYNC_SERVERS}" - export EXTERNAL_IP=$(curl -4 icanhazip.com) - - logt "******* DEBUG STATE SYNC VALUES *******" - logt "STATE_SYNC_SERVER: ${STATE_SYNC_SERVER}" - logt "RPC_STATE_SYNC_SERVERS: ${RPC_STATE_SYNC_SERVERS}" - logt "TRUST_HEIGHT: ${TRUST_HEIGHT}" - logt "TRUST_HASH: ${TRUST_HASH}" - logt "HEIGHT: ${HEIGHT}" - logt "EXTERNAL_IP: ${EXTERNAL_IP}" - - logt "SED Change Config Files." - sed -i -e "s/^enable = .*/enable = \"true\"/" ${DAEMON_HOME}/config/config.toml - sed -i -e "s/^rpc_servers = .*/rpc_servers = \"${RPC_STATE_SYNC_SERVERS}\"/" ${DAEMON_HOME}/config/config.toml - sed -i -e "s/^trust_height = .*/trust_height = \"${HEIGHT}\"/" ${DAEMON_HOME}/config/config.toml - sed -i -e "s/^trust_hash = .*/trust_hash = \"${TRUST_HASH}\"/" ${DAEMON_HOME}/config/config.toml - sed -i -e "s/^moniker = .*/moniker = \"${MONIKER}\"/" ${DAEMON_HOME}/config/config.toml - sed -i -e "s/^external_address = .*/external_address = \"${EXTERNAL_IP}:26656\"/" ${DAEMON_HOME}/config/config.toml - else - export EXTERNAL_IP=$(curl -4 icanhazip.com) - logt "******* DEBUG STATE SYNC VALUES *******" - logt "EXTERNAL_IP: ${EXTERNAL_IP}" - logt "SED Change Config Files." - sed -i -e "s/^enable = .*/enable = \"true\"/" ${DAEMON_HOME}/config/config.toml - sed '/^\[statesync\]/,/^\[/ s/enable = "true"/enable = "false"/' ${DAEMON_HOME}/config/config.toml - sed -i -e "s/^moniker = .*/moniker = \"${MONIKER}\"/" ${DAEMON_HOME}/config/config.toml - sed -i -e "s/^external_address = .*/external_address = \"${EXTERNAL_IP}:26656\"/" ${DAEMON_HOME}/config/config.toml - fi -} - -function setup_basic_keyring { - if ${DAEMON_NAME} keys show "$MONIKER" --keyring-backend test > /dev/null 2>&1; then - echo "Key $MONIKER already exists." - else - ${DAEMON_NAME} keys add "$MONIKER" --keyring-backend test - echo "Key $MONIKER created." - fi -} - -function download_binary_version { - if [ "${NETWORK}" == "mainnet" ]; then - wget -q ${BINARY_LIST_MAINNET} - export DOWNLOAD_BINARIES=$(cat binary_list.json | tr -d '\n') - rm -rf binary_list.json - logt "BINARY_LIST: ${DOWNLOAD_BINARIES}" - elif [ "${NETWORK}" == "athens3" ]; then - wget -q ${BINARY_LIST_ATHENS3} - export DOWNLOAD_BINARIES=$(cat binary_list.json | tr -d '\n') - rm -rf binary_list.json - logt "BINARY_LIST: ${DOWNLOAD_BINARIES}" - fi - python3 /scripts/download_binaries.py -} - -function move_zetacored_binaries { - mkdir -p ${DAEMON_HOME}/cosmovisor || logt "Directory already exists ${DAEMON_HOME}/cosmovisor" - mkdir -p ${DAEMON_HOME}/cosmovisor/genesis || logt "Directory already exists ${DAEMON_HOME}/cosmovisor/genesis" - mkdir -p ${DAEMON_HOME}/cosmovisor/genesis/bin || logt "Directory already exists ${DAEMON_HOME}/cosmovisor/genesis/bin" - cp /usr/local/bin/zetacored ${DAEMON_HOME}/cosmovisor/genesis/bin/zetacored - - if [ "${RESTORE_TYPE}" == "statesync" ]; then - logt "Its statesync so cosmosvisor won't know which binary to start from so make sure it starts from the latest version reported in ABCI_INFO from statesync server rpc." - export VERSION_CHECK=$(curl -s ${STATE_SYNC_SERVER}/abci_info | jq -r '.result.response.version') - logt "CURRENT VERSION_CHECK: ${VERSION_CHECK}" - cp ${DAEMON_HOME}/cosmovisor/upgrades/v${VERSION_CHECK}/bin/zetacored ${DAEMON_HOME}/cosmovisor/genesis/bin/zetacored - fi -} - -function start_network { - if [ "${IS_LOCAL_DEVELOPMENT}" == "true" ]; then - cp /usr/local/bin/zetacored ${DAEMON_HOME}/cosmovisor/genesis/bin/zetacored - find /root/.zetacored/cosmovisor/upgrades/ -type f -path "*/bin/zetacored" -exec cp /usr/local/bin/zetacored {} \; - fi - expected_major_version=$(cat /scripts/expected_major_version | cut -d '-' -f 1) - VISOR_VERSION=v$(${VISOR_NAME} version | tail -n 1 | tr -d '(devel)' | tr -d '\n') - DAEMON_VERSION=$(${DAEMON_NAME} version) - VISOR_MAJOR_VERSION=$(echo $VISOR_VERSION | grep -o '^v[0-9]*') - DAEMON_MAJOR_VERSION=$(echo $DAEMON_VERSION | grep -o '^v[0-9]*') - - logt "EXPECTED_VERSION_WITH_HASH: $(cat /scripts/expected_major_version | cut -d '-' -f 1)" - logt "EXPECTED_MAJOR_VERSION: ${expected_major_version}" - logt "VISOR_VERSION: ${VISOR_VERSION}" - logt "DAEMON_VERSION: ${DAEMON_VERSION}" - logt "VISOR_MAJOR_VERSION: ${VISOR_MAJOR_VERSION}" - logt "DAEMON_MAJOR_VERSION: ${DAEMON_MAJOR_VERSION}" - - if [ "$VISOR_MAJOR_VERSION" != "$expected_major_version" ] || [ "$DAEMON_MAJOR_VERSION" != "$expected_major_version" ]; then - logt "One or both versions don't match the expected major release version: $expected_major_version" - else - logt "Both versions match the expected major release version: $expected_major_version" - fi - - if [ "$VISOR_VERSION" != "$DAEMON_VERSION" ]; then - logt "cosmovisor version doesn't appear to match your daemon version. Start ${DAEMON_NAME}" - else - logt "cosmovisor version match your daemon version. Start ${VISOR_NAME}" - fi - - ${VISOR_NAME} run start --home ${DAEMON_HOME} \ - --log_level info \ - --moniker ${MONIKER} \ - --rpc.laddr tcp://0.0.0.0:26657 \ - --minimum-gas-prices 1.0azeta "--grpc.enable=true" -} - -logt "Load Default Values for ENV Vars if not set." -load_defaults - -if [[ -f "${DAEMON_HOME}/start_sequence_status" ]] && grep -q "START_SEQUENCE_COMPLETE" "${DAEMON_HOME}/start_sequence_status" && [[ "$RE_DO_START_SEQUENCE" != "true" ]]; then - logt "The start sequence is complete and no redo is required." - - logt "Download Configs" - download_configs - - logt "Download Historical Binaries" - download_binary_version - - if [ "${RESTORE_TYPE}" == "statesync" ]; then - logt "Setup Restore Type: ${RESTORE_TYPE}" - logt "During restarts, we re-do this to ensure to update the configs with valid values. When you call change config the stuff that gets set in this function for statesync needs to be set. Doesn't effect to re-set this." - setup_restore_type - fi - - logt "Modify Chain Configs" - change_config_values - - logt "Move Zetacored Binaries." - move_zetacored_binaries - - logt "Start sequence has completed, echo into file so on restart it doesn't download snapshots again." - echo "START_SEQUENCE_COMPLETE" >> ${DAEMON_HOME}/start_sequence_status - - logt "Start Network" - start_network -else - logt "START_SEQUENCE_COMPLETE is not true, or RE_DO_START_SEQUENCE is set to true." - - if [[ "$RE_DO_START_SEQUENCE" == "true" ]]; then - logt "Clean any files that may exist in: ${DAEMON_HOME}" - rm -rf ${DAEMON_HOME}/* || logt "directory doesn't exist." - fi - - logt "Init Chain" - init_chain - - logt "Download Configs" - download_configs - - logt "Download Historical Binaries" - download_binary_version - - logt "Setup Restore Type: ${RESTORE_TYPE}" - setup_restore_type - - logt "Modify Chain Configs" - change_config_values - - logt "Move root binaries to current" - move_zetacored_binaries - - logt "Start sequence has completed, echo into file so on restart it doesn't download snapshots again." - echo "START_SEQUENCE_COMPLETE" >> ${DAEMON_HOME}/start_sequence_status - - logt "Start Network" - start_network -fi \ No newline at end of file diff --git a/contrib/rpc/zetacored/docker-compose.yml b/contrib/rpc/zetacored/docker-compose.yml deleted file mode 100644 index 50dbd0a731..0000000000 --- a/contrib/rpc/zetacored/docker-compose.yml +++ /dev/null @@ -1,36 +0,0 @@ -version: '3.8' -services: - -=name=-: - platform: linux/amd64 - -=image_block=- - container_name: "zetachain_${NETWORK:-mainnet}_rpc" - environment: - DAEMON_HOME: "${DAEMON_HOME:-/root/.zetacored}" - NETWORK: ${NETWORK:-mainnet} - RESTORE_TYPE: "${RESTORE_TYPE:-snapshot}" - SNAPSHOT_API: ${SNAPSHOT_API:-https://snapshots.rpc.zetachain.com} - TRUST_HEIGHT_DIFFERENCE_STATE_SYNC: ${TRUST_HEIGHT_DIFFERENCE_STATE_SYNC:-40000} - CHAIN_ID: "${CHAIN_ID:-zetachain_7000-1}" - VISOR_NAME: "${VISOR_NAME:-cosmovisor}" - DAEMON_NAME: "${DAEMON_NAME:-zetacored}" - DAEMON_ALLOW_DOWNLOAD_BINARIES: "${DAEMON_ALLOW_DOWNLOAD_BINARIES:-false}" - DAEMON_RESTART_AFTER_UPGRADE: "${DAEMON_RESTART_AFTER_UPGRADE:-true}" - UNSAFE_SKIP_BACKUP: "${UNSAFE_SKIP_BACKUP:-true}" - MONIKER: ${MONIKER:-mainnet-docker-rpc} - #If this is true it will erase everything and start over from scratch. - RE_DO_START_SEQUENCE: "${RE_DO_START_SEQUENCE:-false}" - #If this is true it will build the dockerfile and use binary from built docker file instead of remote docker image for local development testing on non-governance upgrades. - IS_LOCAL_DEVELOPMENT: "${IS_LOCAL_DEVELOPMENT:-false}" - ports: - - "26656:26656" - - "1317:1317" - - "8545:8545" - - "8546:8546" - - "26657:26657" - - "9090:9090" - - "9091:9091" - volumes: - - -=name=-:/root/.zetacored/ - entrypoint: bash /scripts/start.sh -volumes: - -=name=-: diff --git a/contrib/rpc/zetacored/init_docker_compose.sh b/contrib/rpc/zetacored/init_docker_compose.sh deleted file mode 100644 index 04e9f6857c..0000000000 --- a/contrib/rpc/zetacored/init_docker_compose.sh +++ /dev/null @@ -1,65 +0,0 @@ -#!/bin/bash - -NETWORK=${1} -TYPE=${2} -DOCKER_TAG=${3} - -if [ "$TYPE" == "image" ]; then - echo "Source Environment File." - SOURCE_FILE_NAME="networks/.${NETWORK}" - if [ ! -f "$SOURCE_FILE_NAME" ]; then - echo "Environment file $SOURCE_FILE_NAME does not exist." - exit 1 - fi - source ${SOURCE_FILE_NAME} -elif [ "$TYPE" == "localbuild" ]; then - echo "Source Environment File." - SOURCE_FILE_NAME="networks/.${NETWORK}-localbuild" - if [ ! -f "$SOURCE_FILE_NAME" ]; then - echo "Environment file $SOURCE_FILE_NAME does not exist." - exit 1 - fi - source ${SOURCE_FILE_NAME} -fi - -# Define the path to the Docker Compose file -FILE_PATH="${NETWORK}-docker-compose.yml" -cp docker-compose.yml ${FILE_PATH} - -# Determine the appropriate Docker Compose configuration based on TYPE -if [ "$TYPE" == "image" ]; then - IMAGE_BLOCK="image: zetachain/zetacored:\${DOCKER_TAG:-ubuntu-v14.0.1.0}" - NAME="zetacored-rpc-${NETWORK}" -elif [ "$TYPE" == "localbuild" ]; then - IMAGE_BLOCK=$(cat << 'EOF' -build: - context: ../../.. - dockerfile: Dockerfile -EOF -) - NAME="zetacored-rpc-${NETWORK}-localbuild" -else - echo "Invalid TYPE. Please specify 'image' or 'localbuild'." - exit 1 -fi - -IMAGE_BLOCK_ESCAPED=$(echo "$IMAGE_BLOCK" | sed 's/[&/]/\\&/g; s/$/\\/') -IMAGE_BLOCK_ESCAPED=${IMAGE_BLOCK_ESCAPED%?} - -# Replace placeholders in the Docker Compose file -sed -i '' "s|-=name=-|$NAME|g" $FILE_PATH -sed -i '' "s|-=image_block=-|$IMAGE_BLOCK_ESCAPED|g" $FILE_PATH - -echo "DEBUG ENV VARS" -printenv -echo "================" - -echo "Placeholders have been replaced in $FILE_PATH." -cat $FILE_PATH -echo "================" - -if [ "$TYPE" == "image" ]; then - docker-compose -f ${FILE_PATH} up -elif [ "$TYPE" == "localbuild" ]; then - docker-compose -f ${FILE_PATH} up --build -fi diff --git a/contrib/rpc/zetacored/kill_docker_compose.sh b/contrib/rpc/zetacored/kill_docker_compose.sh deleted file mode 100644 index 5d6a2c192d..0000000000 --- a/contrib/rpc/zetacored/kill_docker_compose.sh +++ /dev/null @@ -1,14 +0,0 @@ -#!/bin/bash - -NETWORK=${1} -CLEAN=${2} -FILE_PATH="${NETWORK}-docker-compose.yml" - -if [ "${CLEAN}" == "true" ]; then - docker-compose -f ${FILE_PATH} down -v - rm -rf ${FILE_PATH} -else - docker-compose -f ${FILE_PATH} down - rm -rf ${FILE_PATH} -fi - diff --git a/contrib/rpc/zetacored/networks/.athens3 b/contrib/rpc/zetacored/networks/.athens3 deleted file mode 100644 index 7de4cede3b..0000000000 --- a/contrib/rpc/zetacored/networks/.athens3 +++ /dev/null @@ -1,14 +0,0 @@ -export DAEMON_HOME="/root/.zetacored" -export NETWORK=athens3 -export RESTORE_TYPE="snapshot" -export SNAPSHOT_API=https://snapshots.rpc.zetachain.com -export TRUST_HEIGHT_DIFFERENCE_STATE_SYNC=40000 -export CHAIN_ID="athens_7001-1" -export VISOR_NAME="cosmovisor" -export DAEMON_NAME="zetacored" -export DAEMON_ALLOW_DOWNLOAD_BINARIES="false" -export DAEMON_RESTART_AFTER_UPGRADE="true" -export UNSAFE_SKIP_BACKUP="true" -export MONIKER=testnet-docker-rpc -export RE_DO_START_SEQUENCE="false" -export IS_LOCAL_DEVELOPMENT="false" diff --git a/contrib/rpc/zetacored/networks/.athens3-localbuild b/contrib/rpc/zetacored/networks/.athens3-localbuild deleted file mode 100644 index b79c14c220..0000000000 --- a/contrib/rpc/zetacored/networks/.athens3-localbuild +++ /dev/null @@ -1,14 +0,0 @@ -export DAEMON_HOME="/root/.zetacored" -export NETWORK=athens3 -export RESTORE_TYPE="snapshot" -export SNAPSHOT_API=https://snapshots.rpc.zetachain.com -export TRUST_HEIGHT_DIFFERENCE_STATE_SYNC=40000 -export CHAIN_ID="athens_7001-1" -export VISOR_NAME="cosmovisor" -export DAEMON_NAME="zetacored" -export DAEMON_ALLOW_DOWNLOAD_BINARIES="false" -export DAEMON_RESTART_AFTER_UPGRADE="false" -export UNSAFE_SKIP_BACKUP="true" -export MONIKER=testnet-docker-rpc -export RE_DO_START_SEQUENCE="false" -export IS_LOCAL_DEVELOPMENT="true" diff --git a/contrib/rpc/zetacored/networks/.mainnet b/contrib/rpc/zetacored/networks/.mainnet deleted file mode 100644 index ff260bb5ca..0000000000 --- a/contrib/rpc/zetacored/networks/.mainnet +++ /dev/null @@ -1,14 +0,0 @@ -export DAEMON_HOME="/root/.zetacored" -export NETWORK=mainnet -export RESTORE_TYPE="snapshot" -export SNAPSHOT_API=https://snapshots.rpc.zetachain.com -export TRUST_HEIGHT_DIFFERENCE_STATE_SYNC=40000 -export CHAIN_ID="zetachain_7000-1" -export VISOR_NAME="cosmovisor" -export DAEMON_NAME="zetacored" -export DAEMON_ALLOW_DOWNLOAD_BINARIES="false" -export DAEMON_RESTART_AFTER_UPGRADE="true" -export UNSAFE_SKIP_BACKUP="true" -export MONIKER=mainnet-docker-rpc -export RE_DO_START_SEQUENCE="false" -export IS_LOCAL_DEVELOPMENT="false" diff --git a/contrib/rpc/zetacored/networks/.mainnet-localbuild b/contrib/rpc/zetacored/networks/.mainnet-localbuild deleted file mode 100644 index 381c34bd6d..0000000000 --- a/contrib/rpc/zetacored/networks/.mainnet-localbuild +++ /dev/null @@ -1,14 +0,0 @@ -export DAEMON_HOME="/root/.zetacored" -export NETWORK=mainnet -export RESTORE_TYPE="snapshot" -export SNAPSHOT_API=https://snapshots.rpc.zetachain.com -export TRUST_HEIGHT_DIFFERENCE_STATE_SYNC=40000 -export CHAIN_ID="zetachain_7000-1" -export VISOR_NAME="cosmovisor" -export DAEMON_NAME="zetacored" -export DAEMON_ALLOW_DOWNLOAD_BINARIES="false" -export DAEMON_RESTART_AFTER_UPGRADE="false" -export UNSAFE_SKIP_BACKUP="true" -export MONIKER=mainnet-docker-rpc -export RE_DO_START_SEQUENCE="false" -export IS_LOCAL_DEVELOPMENT="true" From 148935c27a5d70a9402b2f5b103d1853b2c7a31f Mon Sep 17 00:00:00 2001 From: Lucas Bertrand Date: Wed, 8 Jan 2025 18:15:58 +0100 Subject: [PATCH 5/7] chore: prepare v25 (#3338) * disable precompiles * complete changelog * set v24 as base for upgrade tests * lint error --- Makefile | 4 ++-- changelog.md | 5 ++--- cmd/zetae2e/local/local.go | 28 +++++++++++++++------------- precompiles/precompiles.go | 6 +++--- 4 files changed, 22 insertions(+), 21 deletions(-) diff --git a/Makefile b/Makefile index 344ae150e4..02ac397d51 100644 --- a/Makefile +++ b/Makefile @@ -327,7 +327,7 @@ ifdef UPGRADE_TEST_FROM_SOURCE zetanode-upgrade: e2e-images @echo "Building zetanode-upgrade from source" $(DOCKER) build -t zetanode:old -f Dockerfile-localnet --target old-runtime-source \ - --build-arg OLD_VERSION='release/v23' \ + --build-arg OLD_VERSION='release/v24' \ --build-arg NODE_VERSION=$(NODE_VERSION) \ --build-arg NODE_COMMIT=$(NODE_COMMIT) . @@ -336,7 +336,7 @@ else zetanode-upgrade: e2e-images @echo "Building zetanode-upgrade from binaries" $(DOCKER) build -t zetanode:old -f Dockerfile-localnet --target old-runtime \ - --build-arg OLD_VERSION='https://github.com/zeta-chain/node/releases/download/v23.1.5' \ + --build-arg OLD_VERSION='https://github.com/zeta-chain/node/releases/download/v24.0.0' \ --build-arg NODE_VERSION=$(NODE_VERSION) \ --build-arg NODE_COMMIT=$(NODE_COMMIT) \ . diff --git a/changelog.md b/changelog.md index 5bb11d40cf..f878b67b3c 100644 --- a/changelog.md +++ b/changelog.md @@ -1,13 +1,12 @@ # CHANGELOG -## Unreleased +## v25.0.0 ### Features * [3235](https://github.com/zeta-chain/node/pull/3235) - add /systemtime telemetry endpoint (zetaclient) * [3317](https://github.com/zeta-chain/node/pull/3317) - add configurable signer latency correction (zetaclient) - ### Tests * [3205](https://github.com/zeta-chain/node/issues/3205) - move Bitcoin revert address test to advanced group to avoid upgrade test failure @@ -15,7 +14,7 @@ * [3095](https://github.com/zeta-chain/node/pull/3095) - initialize simulation tests for custom zetachain modules * [3276](https://github.com/zeta-chain/node/pull/3276) - add Solana E2E performance tests and improve Solana outbounds performance -## Refactor +### Refactor * [3170](https://github.com/zeta-chain/node/pull/3170) - revamp TSS package in zetaclient * [3291](https://github.com/zeta-chain/node/pull/3291) - revamp zetaclient initialization (+ graceful shutdown) diff --git a/cmd/zetae2e/local/local.go b/cmd/zetae2e/local/local.go index 8368e0d0bc..e32bfc0d77 100644 --- a/cmd/zetae2e/local/local.go +++ b/cmd/zetae2e/local/local.go @@ -338,23 +338,25 @@ func localE2ETest(cmd *cobra.Command, _ []string) { if !skipPrecompiles { precompiledContractTests := []string{ - e2etests.TestPrecompilesPrototypeName, - e2etests.TestPrecompilesPrototypeThroughContractName, - // Disabled until further notice, check https://github.com/zeta-chain/node/issues/3005. - // e2etests.TestPrecompilesStakingThroughContractName, - e2etests.TestPrecompilesBankName, - e2etests.TestPrecompilesBankFailName, - e2etests.TestPrecompilesBankThroughContractName, + //e2etests.TestPrecompilesPrototypeName, + //e2etests.TestPrecompilesPrototypeThroughContractName, + //// Disabled until further notice, check https://github.com/zeta-chain/node/issues/3005. + //// e2etests.TestPrecompilesStakingThroughContractName, + //e2etests.TestPrecompilesBankName, + //e2etests.TestPrecompilesBankFailName, + //e2etests.TestPrecompilesBankThroughContractName, } if e2eStartHeight < 100 { // these tests require a clean system // since unstaking has an unbonding period - precompiledContractTests = append(precompiledContractTests, - e2etests.TestPrecompilesStakingName, - e2etests.TestPrecompilesDistributeName, - e2etests.TestPrecompilesDistributeNonZRC20Name, - e2etests.TestPrecompilesDistributeThroughContractName, - ) + //precompiledContractTests = append(precompiledContractTests, + // e2etests.TestPrecompilesStakingName, + // e2etests.TestPrecompilesDistributeName, + // e2etests.TestPrecompilesDistributeNonZRC20Name, + // e2etests.TestPrecompilesDistributeThroughContractName, + //) + // prevent lint error + _ = precompiledContractTests } else { logger.Print("⚠️ partial precompiled run (unclean state)") } diff --git a/precompiles/precompiles.go b/precompiles/precompiles.go index 3c9a066812..557d880ab4 100644 --- a/precompiles/precompiles.go +++ b/precompiles/precompiles.go @@ -22,9 +22,9 @@ import ( // This is useful for listing and reading from other packages, such as BlockedAddrs() function. // Setting to false a contract here will disable it, not being included in the blockchain. var EnabledStatefulContracts = map[common.Address]bool{ - prototype.ContractAddress: true, - staking.ContractAddress: true, - bank.ContractAddress: true, + prototype.ContractAddress: false, + staking.ContractAddress: false, + bank.ContractAddress: false, } // StatefulContracts returns all the registered precompiled contracts. From 47c9444bbd115176a2736f31be8cb7d23b0059e7 Mon Sep 17 00:00:00 2001 From: Alex Gartner Date: Wed, 8 Jan 2025 09:45:22 -0800 Subject: [PATCH 6/7] feat: add zetaclient minimum version check (#3320) * feat: add minimum version check * review feedback * changelog --- changelog.md | 1 + cmd/zetaclientd/start.go | 27 ++++-- docs/openapi/openapi.swagger.yaml | 6 ++ .../zetacore/observer/operational.proto | 5 + .../zetacore/observer/operational_pb.d.ts | 9 ++ x/observer/types/errors.go | 5 + x/observer/types/operational.go | 4 + x/observer/types/operational.pb.go | 94 +++++++++++++++---- x/observer/types/operational_test.go | 18 ++++ zetaclient/maintenance/shutdown_listener.go | 65 +++++++++++-- .../maintenance/shutdown_listener_test.go | 59 +++++++++++- 11 files changed, 256 insertions(+), 37 deletions(-) diff --git a/changelog.md b/changelog.md index f878b67b3c..4442a0cbba 100644 --- a/changelog.md +++ b/changelog.md @@ -6,6 +6,7 @@ * [3235](https://github.com/zeta-chain/node/pull/3235) - add /systemtime telemetry endpoint (zetaclient) * [3317](https://github.com/zeta-chain/node/pull/3317) - add configurable signer latency correction (zetaclient) +* [3320](https://github.com/zeta-chain/node/pull/3320) - add zetaclient minimum version check ### Tests diff --git a/cmd/zetaclientd/start.go b/cmd/zetaclientd/start.go index 2a46dabdbd..ae7da7e6ac 100644 --- a/cmd/zetaclientd/start.go +++ b/cmd/zetaclientd/start.go @@ -83,6 +83,20 @@ func Start(_ *cobra.Command, _ []string) error { return errors.Wrap(err, "unable to resolve observer pub key bech32") } + isObserver, err := isObserverNode(ctx, zetacoreClient) + switch { + case err != nil: + return errors.Wrap(err, "unable to check if observer node") + case !isObserver: + logger.Std.Warn().Msg("This node is not an observer node. Exit 0") + return nil + } + + shutdownListener := maintenance.NewShutdownListener(zetacoreClient, logger.Std) + if err := shutdownListener.RunPreStartCheck(ctx); err != nil { + return errors.Wrap(err, "pre start check failed") + } + tssSetupProps := zetatss.SetupProps{ Config: cfg, Zetacore: zetacoreClient, @@ -94,20 +108,13 @@ func Start(_ *cobra.Command, _ []string) error { Telemetry: telemetry, } + // This will start p2p communication so it should only happen after + // preflight checks have completed tss, err := zetatss.Setup(ctx, tssSetupProps, logger.Std) if err != nil { return errors.Wrap(err, "unable to setup TSS service") } - isObserver, err := isObserverNode(ctx, zetacoreClient) - switch { - case err != nil: - return errors.Wrap(err, "unable to check if observer node") - case !isObserver: - logger.Std.Warn().Msg("This node is not an observer node. Exit 0") - return nil - } - // Starts various background TSS listeners. // Shuts down zetaclientd if any is triggered. maintenance.NewTSSListener(zetacoreClient, logger.Std).Listen(ctx, func() { @@ -115,7 +122,7 @@ func Start(_ *cobra.Command, _ []string) error { graceful.ShutdownNow() }) - maintenance.NewShutdownListener(zetacoreClient, logger.Std).Listen(ctx, func() { + shutdownListener.Listen(ctx, func() { logger.Std.Info().Msg("Shutdown listener received an action to shutdown zetaclientd.") graceful.ShutdownNow() }) diff --git a/docs/openapi/openapi.swagger.yaml b/docs/openapi/openapi.swagger.yaml index 694f839db6..11756a1c7b 100644 --- a/docs/openapi/openapi.swagger.yaml +++ b/docs/openapi/openapi.swagger.yaml @@ -58115,6 +58115,12 @@ definitions: description: |- Offset from the zetacore block time to initiate signing. Should be calculated and set based on max(zetaclient_core_block_latency). + minimum_version: + type: string + description: |- + Minimum version of zetaclient that is allowed to run. This must be either + a valid semver string (v23.0.1) or empty. If empty, all versions are + allowed. description: Flags for the top-level operation of zetaclient. observerPendingNonces: type: object diff --git a/proto/zetachain/zetacore/observer/operational.proto b/proto/zetachain/zetacore/observer/operational.proto index 62ff737074..05096a57c3 100644 --- a/proto/zetachain/zetacore/observer/operational.proto +++ b/proto/zetachain/zetacore/observer/operational.proto @@ -16,4 +16,9 @@ message OperationalFlags { // Should be calculated and set based on max(zetaclient_core_block_latency). google.protobuf.Duration signer_block_time_offset = 2 [ (gogoproto.stdduration) = true ]; + + // Minimum version of zetaclient that is allowed to run. This must be either + // a valid semver string (v23.0.1) or empty. If empty, all versions are + // allowed. + string minimum_version = 3; } \ No newline at end of file diff --git a/typescript/zetachain/zetacore/observer/operational_pb.d.ts b/typescript/zetachain/zetacore/observer/operational_pb.d.ts index c9fc213927..4f18ce91a0 100644 --- a/typescript/zetachain/zetacore/observer/operational_pb.d.ts +++ b/typescript/zetachain/zetacore/observer/operational_pb.d.ts @@ -28,6 +28,15 @@ export declare class OperationalFlags extends Message { */ signerBlockTimeOffset?: Duration; + /** + * Minimum version of zetaclient that is allowed to run. This must be either + * a valid semver string (v23.0.1) or empty. If empty, all versions are + * allowed. + * + * @generated from field: string minimum_version = 3; + */ + minimumVersion: string; + constructor(data?: PartialMessage); static readonly runtime: typeof proto3; diff --git a/x/observer/types/errors.go b/x/observer/types/errors.go index fbc6261d76..aaebb297b4 100644 --- a/x/observer/types/errors.go +++ b/x/observer/types/errors.go @@ -71,4 +71,9 @@ var ( 1140, "signer block time offset exceeds limit", ) + ErrOperationalFlagsInvalidMinimumVersion = errorsmod.Register( + ModuleName, + 1141, + "minimum version is not a valid semver string", + ) ) diff --git a/x/observer/types/operational.go b/x/observer/types/operational.go index 4da766c53a..e90415acdf 100644 --- a/x/observer/types/operational.go +++ b/x/observer/types/operational.go @@ -4,6 +4,7 @@ import ( "time" cosmoserrors "cosmossdk.io/errors" + "golang.org/x/mod/semver" ) const ( @@ -23,5 +24,8 @@ func (f *OperationalFlags) Validate() error { return cosmoserrors.Wrapf(ErrOperationalFlagsSignerBlockTimeOffsetLimit, "(%s)", signerBlockTimeOffset) } } + if f.MinimumVersion != "" && !semver.IsValid(f.MinimumVersion) { + return ErrOperationalFlagsInvalidMinimumVersion + } return nil } diff --git a/x/observer/types/operational.pb.go b/x/observer/types/operational.pb.go index e5fb9c3281..a23aed758b 100644 --- a/x/observer/types/operational.pb.go +++ b/x/observer/types/operational.pb.go @@ -35,6 +35,10 @@ type OperationalFlags struct { // Offset from the zetacore block time to initiate signing. // Should be calculated and set based on max(zetaclient_core_block_latency). SignerBlockTimeOffset *time.Duration `protobuf:"bytes,2,opt,name=signer_block_time_offset,json=signerBlockTimeOffset,proto3,stdduration" json:"signer_block_time_offset,omitempty"` + // Minimum version of zetaclient that is allowed to run. This must be either + // a valid semver string (v23.0.1) or empty. If empty, all versions are + // allowed. + MinimumVersion string `protobuf:"bytes,3,opt,name=minimum_version,json=minimumVersion,proto3" json:"minimum_version,omitempty"` } func (m *OperationalFlags) Reset() { *m = OperationalFlags{} } @@ -84,6 +88,13 @@ func (m *OperationalFlags) GetSignerBlockTimeOffset() *time.Duration { return nil } +func (m *OperationalFlags) GetMinimumVersion() string { + if m != nil { + return m.MinimumVersion + } + return "" +} + func init() { proto.RegisterType((*OperationalFlags)(nil), "zetachain.zetacore.observer.OperationalFlags") } @@ -93,25 +104,27 @@ func init() { } var fileDescriptor_ea3eed2ec55093b5 = []byte{ - // 282 bytes of a gzipped FileDescriptorProto - 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0x44, 0x90, 0xc1, 0x4a, 0xec, 0x30, - 0x14, 0x86, 0x27, 0xf7, 0x8a, 0x8b, 0x8a, 0x22, 0x83, 0x42, 0x1d, 0x21, 0x0e, 0x82, 0x30, 0x20, - 0x93, 0x80, 0xbe, 0x41, 0x51, 0x71, 0x37, 0x30, 0xb8, 0x10, 0x37, 0x25, 0xed, 0x9c, 0xa6, 0xc1, - 0xb6, 0xa7, 0x24, 0xa9, 0xa8, 0x4f, 0x21, 0xb8, 0xf1, 0x91, 0x5c, 0xce, 0xd2, 0x9d, 0xd2, 0xbe, - 0x88, 0x4c, 0x32, 0x75, 0x76, 0x27, 0x27, 0xdf, 0x7f, 0x3e, 0xf8, 0x83, 0xe9, 0x2b, 0x58, 0x91, - 0xe6, 0x42, 0x55, 0xdc, 0x4d, 0xa8, 0x81, 0x63, 0x62, 0x40, 0x3f, 0x81, 0xe6, 0x58, 0x83, 0x16, - 0x56, 0x61, 0x25, 0x0a, 0x56, 0x6b, 0xb4, 0x38, 0x3c, 0xfe, 0xc3, 0x59, 0x8f, 0xb3, 0x1e, 0x1f, - 0x1d, 0x48, 0x94, 0xe8, 0x38, 0xbe, 0x9a, 0x7c, 0x64, 0x44, 0x25, 0xa2, 0x2c, 0x80, 0xbb, 0x57, - 0xd2, 0x64, 0x7c, 0xd1, 0xf8, 0xa3, 0xfe, 0xff, 0xf4, 0x9d, 0x04, 0xfb, 0xb3, 0x8d, 0xe8, 0xa6, - 0x10, 0xd2, 0x0c, 0xcf, 0x82, 0x3d, 0x0d, 0xc6, 0x0a, 0x6d, 0xe3, 0x1c, 0x94, 0xcc, 0x6d, 0x48, - 0xc6, 0x64, 0xf2, 0x7f, 0xbe, 0xbb, 0xde, 0xde, 0xba, 0xe5, 0xf0, 0x3e, 0x08, 0x8d, 0x92, 0x15, - 0xe8, 0x38, 0x29, 0x30, 0x7d, 0x8c, 0xad, 0x2a, 0x21, 0xc6, 0x2c, 0x33, 0x60, 0xc3, 0x7f, 0x63, - 0x32, 0xd9, 0xb9, 0x38, 0x62, 0x5e, 0xcf, 0x7a, 0x3d, 0xbb, 0x5a, 0xeb, 0xa3, 0xad, 0x8f, 0xef, - 0x13, 0x32, 0x3f, 0xf4, 0x07, 0xa2, 0x55, 0xfe, 0x4e, 0x95, 0x30, 0x73, 0xe9, 0xe8, 0xfa, 0xb3, - 0xa5, 0x64, 0xd9, 0x52, 0xf2, 0xd3, 0x52, 0xf2, 0xd6, 0xd1, 0xc1, 0xb2, 0xa3, 0x83, 0xaf, 0x8e, - 0x0e, 0x1e, 0xce, 0xa5, 0xb2, 0x79, 0x93, 0xb0, 0x14, 0x4b, 0x57, 0xd9, 0xd4, 0xb7, 0x57, 0xe1, - 0x02, 0xf8, 0xf3, 0xa6, 0x3b, 0xfb, 0x52, 0x83, 0x49, 0xb6, 0x9d, 0xf6, 0xf2, 0x37, 0x00, 0x00, - 0xff, 0xff, 0xf0, 0xe0, 0x3c, 0x60, 0x67, 0x01, 0x00, 0x00, + // 308 bytes of a gzipped FileDescriptorProto + 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0x44, 0x90, 0x41, 0x4a, 0x03, 0x31, + 0x14, 0x86, 0x1b, 0x2b, 0x82, 0x23, 0x56, 0x19, 0x14, 0xc6, 0x0a, 0xb1, 0x08, 0x62, 0x41, 0x9a, + 0x80, 0xde, 0xa0, 0xa8, 0xb8, 0x2b, 0x14, 0x11, 0x71, 0x33, 0xcc, 0xb4, 0xaf, 0x99, 0xe0, 0x24, + 0xaf, 0x24, 0x99, 0xa2, 0x9e, 0xc2, 0xa5, 0x87, 0xf1, 0x00, 0x2e, 0xbb, 0x74, 0xa7, 0xb4, 0x17, + 0x91, 0x26, 0xad, 0xdd, 0xbd, 0xfc, 0xf9, 0xff, 0xf7, 0x3d, 0xfe, 0xa8, 0xf3, 0x06, 0x2e, 0x1b, + 0x14, 0x99, 0xd4, 0xdc, 0x4f, 0x68, 0x80, 0x63, 0x6e, 0xc1, 0x4c, 0xc0, 0x70, 0x1c, 0x83, 0xc9, + 0x9c, 0x44, 0x9d, 0x95, 0x6c, 0x6c, 0xd0, 0x61, 0x7c, 0xfc, 0x6f, 0x67, 0x2b, 0x3b, 0x5b, 0xd9, + 0x9b, 0x07, 0x02, 0x05, 0x7a, 0x1f, 0x5f, 0x4c, 0x21, 0xd2, 0xa4, 0x02, 0x51, 0x94, 0xc0, 0xfd, + 0x2b, 0xaf, 0x46, 0x7c, 0x58, 0x85, 0xa5, 0xe1, 0xff, 0xf4, 0x93, 0x44, 0xfb, 0xbd, 0x35, 0xe8, + 0xb6, 0xcc, 0x84, 0x8d, 0xcf, 0xa2, 0x86, 0x01, 0xeb, 0x32, 0xe3, 0xd2, 0x02, 0xa4, 0x28, 0x5c, + 0x42, 0x5a, 0xa4, 0x5d, 0xef, 0xef, 0x2e, 0xd5, 0x3b, 0x2f, 0xc6, 0x8f, 0x51, 0x62, 0xa5, 0xd0, + 0x60, 0xd2, 0xbc, 0xc4, 0xc1, 0x73, 0xea, 0xa4, 0x82, 0x14, 0x47, 0x23, 0x0b, 0x2e, 0xd9, 0x68, + 0x91, 0xf6, 0xce, 0xe5, 0x11, 0x0b, 0x78, 0xb6, 0xc2, 0xb3, 0xeb, 0x25, 0xbe, 0xbb, 0xf9, 0xf1, + 0x73, 0x42, 0xfa, 0x87, 0x61, 0x41, 0x77, 0x91, 0xbf, 0x97, 0x0a, 0x7a, 0x3e, 0x1d, 0x9f, 0x47, + 0x7b, 0x4a, 0x6a, 0xa9, 0x2a, 0x95, 0x4e, 0xc0, 0x58, 0x89, 0x3a, 0xa9, 0xb7, 0x48, 0x7b, 0xbb, + 0xdf, 0x58, 0xca, 0x0f, 0x41, 0xed, 0xde, 0x7c, 0xcd, 0x28, 0x99, 0xce, 0x28, 0xf9, 0x9d, 0x51, + 0xf2, 0x3e, 0xa7, 0xb5, 0xe9, 0x9c, 0xd6, 0xbe, 0xe7, 0xb4, 0xf6, 0x74, 0x21, 0xa4, 0x2b, 0xaa, + 0x9c, 0x0d, 0x50, 0xf9, 0x6e, 0x3b, 0xa1, 0x66, 0x8d, 0x43, 0xe0, 0x2f, 0xeb, 0x92, 0xdd, 0xeb, + 0x18, 0x6c, 0xbe, 0xe5, 0xef, 0xbb, 0xfa, 0x0b, 0x00, 0x00, 0xff, 0xff, 0xe9, 0x88, 0xe2, 0xfd, + 0x90, 0x01, 0x00, 0x00, } func (m *OperationalFlags) Marshal() (dAtA []byte, err error) { @@ -134,6 +147,13 @@ func (m *OperationalFlags) MarshalToSizedBuffer(dAtA []byte) (int, error) { _ = i var l int _ = l + if len(m.MinimumVersion) > 0 { + i -= len(m.MinimumVersion) + copy(dAtA[i:], m.MinimumVersion) + i = encodeVarintOperational(dAtA, i, uint64(len(m.MinimumVersion))) + i-- + dAtA[i] = 0x1a + } if m.SignerBlockTimeOffset != nil { n1, err1 := github_com_cosmos_gogoproto_types.StdDurationMarshalTo(*m.SignerBlockTimeOffset, dAtA[i-github_com_cosmos_gogoproto_types.SizeOfStdDuration(*m.SignerBlockTimeOffset):]) if err1 != nil { @@ -176,6 +196,10 @@ func (m *OperationalFlags) Size() (n int) { l = github_com_cosmos_gogoproto_types.SizeOfStdDuration(*m.SignerBlockTimeOffset) n += 1 + l + sovOperational(uint64(l)) } + l = len(m.MinimumVersion) + if l > 0 { + n += 1 + l + sovOperational(uint64(l)) + } return n } @@ -269,6 +293,38 @@ func (m *OperationalFlags) Unmarshal(dAtA []byte) error { return err } iNdEx = postIndex + case 3: + if wireType != 2 { + return fmt.Errorf("proto: wrong wireType = %d for field MinimumVersion", wireType) + } + var stringLen uint64 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowOperational + } + 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 ErrInvalidLengthOperational + } + postIndex := iNdEx + intStringLen + if postIndex < 0 { + return ErrInvalidLengthOperational + } + if postIndex > l { + return io.ErrUnexpectedEOF + } + m.MinimumVersion = string(dAtA[iNdEx:postIndex]) + iNdEx = postIndex default: iNdEx = preIndex skippy, err := skipOperational(dAtA[iNdEx:]) diff --git a/x/observer/types/operational_test.go b/x/observer/types/operational_test.go index 79c8caf0dc..ca0e6056b8 100644 --- a/x/observer/types/operational_test.go +++ b/x/observer/types/operational_test.go @@ -15,6 +15,10 @@ func TestOperationalFlags_Validate(t *testing.T) { of types.OperationalFlags errContains string }{ + { + name: "empty is valid", + of: types.OperationalFlags{}, + }, { name: "invalid restart height", of: types.OperationalFlags{ @@ -48,11 +52,25 @@ func TestOperationalFlags_Validate(t *testing.T) { }, errContains: types.ErrOperationalFlagsSignerBlockTimeOffsetLimit.Error(), }, + { + name: "minimum version valid", + of: types.OperationalFlags{ + MinimumVersion: "v1.1.1", + }, + }, + { + name: "minimum version invalid", + of: types.OperationalFlags{ + MinimumVersion: "asdf", + }, + errContains: types.ErrOperationalFlagsInvalidMinimumVersion.Error(), + }, { name: "all flags valid", of: types.OperationalFlags{ RestartHeight: 1, SignerBlockTimeOffset: ptr.Ptr(time.Second), + MinimumVersion: "v1.1.1", }, }, } diff --git a/zetaclient/maintenance/shutdown_listener.go b/zetaclient/maintenance/shutdown_listener.go index 0ac3b18d75..edbd93bedd 100644 --- a/zetaclient/maintenance/shutdown_listener.go +++ b/zetaclient/maintenance/shutdown_listener.go @@ -2,12 +2,16 @@ package maintenance import ( "context" + "fmt" + "strings" "time" "cosmossdk.io/errors" "github.com/rs/zerolog" + "golang.org/x/mod/semver" "github.com/zeta-chain/node/pkg/bg" + "github.com/zeta-chain/node/pkg/constant" "github.com/zeta-chain/node/pkg/retry" observertypes "github.com/zeta-chain/node/x/observer/types" "github.com/zeta-chain/node/zetaclient/chains/interfaces" @@ -22,17 +26,30 @@ type ShutdownListener struct { logger zerolog.Logger lastRestartHeightMissed int64 + // get the current version of zetaclient + getVersion func() string } // NewShutdownListener creates a new ShutdownListener. func NewShutdownListener(client interfaces.ZetacoreClient, logger zerolog.Logger) *ShutdownListener { log := logger.With().Str("module", "shutdown_listener").Logger() return &ShutdownListener{ - client: client, - logger: log, + client: client, + logger: log, + getVersion: getVersionDefault, } } +// RunPreStartCheck runs any checks that must run before fully starting zetaclient. +// Specifically this should be run before any TSS P2P is started. +func (o *ShutdownListener) RunPreStartCheck(ctx context.Context) error { + operationalFlags, err := o.getOperationalFlagsWithRetry(ctx) + if err != nil { + return errors.Wrap(err, "unable to get initial operational flags") + } + return o.checkMinimumVersion(operationalFlags) +} + func (o *ShutdownListener) Listen(ctx context.Context, action func()) { var ( withLogger = bg.WithLogger(o.logger) @@ -43,12 +60,9 @@ func (o *ShutdownListener) Listen(ctx context.Context, action func()) { } func (o *ShutdownListener) waitForUpdate(ctx context.Context) error { - operationalFlags, err := retry.DoTypedWithBackoffAndRetry( - func() (observertypes.OperationalFlags, error) { return o.client.GetOperationalFlags(ctx) }, - retry.DefaultConstantBackoff(), - ) + operationalFlags, err := o.getOperationalFlagsWithRetry(ctx) if err != nil { - return errors.Wrap(err, "unable to get initial operational flags") + return errors.Wrap(err, "get initial operational flags") } if o.handleNewFlags(ctx, operationalFlags) { return nil @@ -74,8 +88,19 @@ func (o *ShutdownListener) waitForUpdate(ctx context.Context) error { } } +func (o *ShutdownListener) getOperationalFlagsWithRetry(ctx context.Context) (observertypes.OperationalFlags, error) { + return retry.DoTypedWithBackoffAndRetry( + func() (observertypes.OperationalFlags, error) { return o.client.GetOperationalFlags(ctx) }, + retry.DefaultConstantBackoff(), + ) +} + // handleNewFlags processes the flags and returns true if a shutdown should be signaled func (o *ShutdownListener) handleNewFlags(ctx context.Context, f observertypes.OperationalFlags) bool { + if err := o.checkMinimumVersion(f); err != nil { + o.logger.Error().Err(err).Any("operational_flags", f).Msg("minimum version check") + return true + } if f.RestartHeight < 1 { return false } @@ -123,3 +148,29 @@ func (o *ShutdownListener) handleNewFlags(ctx context.Context, f observertypes.O } } } + +func (o *ShutdownListener) checkMinimumVersion(f observertypes.OperationalFlags) error { + if f.MinimumVersion != "" { + // we typically store the version without the required v prefix + currentVersion := ensurePrefix(o.getVersion(), "v") + if semver.Compare(currentVersion, f.MinimumVersion) == -1 { + return fmt.Errorf( + "current version (%s) is less than minimum version (%s)", + currentVersion, + f.MinimumVersion, + ) + } + } + return nil +} + +func getVersionDefault() string { + return constant.Version +} + +func ensurePrefix(s, prefix string) string { + if !strings.HasPrefix(s, prefix) { + return prefix + s + } + return s +} diff --git a/zetaclient/maintenance/shutdown_listener_test.go b/zetaclient/maintenance/shutdown_listener_test.go index 2b2c9128d1..c85c70cde0 100644 --- a/zetaclient/maintenance/shutdown_listener_test.go +++ b/zetaclient/maintenance/shutdown_listener_test.go @@ -100,8 +100,65 @@ func TestShutdownListener(t *testing.T) { return len(client.Calls) == 2 }, time.Second, time.Millisecond) assertChannelNotClosed(t, complete) - cancel() }) + + t.Run("minimum version ok", func(t *testing.T) { + client := mocks.NewZetacoreClient(t) + + listener := NewShutdownListener(client, logger) + listener.getVersion = func() string { + return "1.1.2" + } + + client.Mock.On("GetOperationalFlags", ctx).Return(observertypes.OperationalFlags{ + MinimumVersion: "v1.1.1", + }, nil) + + // pre start checks passed + err := listener.RunPreStartCheck(ctx) + require.NoError(t, err) + + // listener also does not shutdown + complete := make(chan interface{}) + listener.Listen(ctx, func() { + close(complete) + }) + + require.Eventually(t, func() bool { + return len(client.Calls) == 2 + }, time.Second, time.Millisecond) + assertChannelNotClosed(t, complete) + }) + + t.Run("minimum version failed", func(t *testing.T) { + client := mocks.NewZetacoreClient(t) + + listener := NewShutdownListener(client, logger) + listener.getVersion = func() string { + return "1.1.1" + } + + client.Mock.On("GetOperationalFlags", ctx).Return(observertypes.OperationalFlags{ + MinimumVersion: "v1.1.2", + }, nil) + + // pre start checks would return error + err := listener.RunPreStartCheck(ctx) + require.Error(t, err) + + // listener would also shutdown + complete := make(chan interface{}) + listener.Listen(ctx, func() { + close(complete) + }) + + require.Eventually(t, func() bool { + return len(client.Calls) == 2 + }, time.Second, time.Millisecond) + <-complete + }) + // avoid Log in goroutine after TestShutdownListener has completed + cancel() time.Sleep(time.Millisecond * 100) } From 2edfa9c42f4d9e2fa4d86daaf12b316acdbc85d2 Mon Sep 17 00:00:00 2001 From: Tanmay Date: Fri, 10 Jan 2025 11:25:13 -0500 Subject: [PATCH 7/7] test: add crosschain and observer operations (#3207) * add crosschain decoders * decoders test * add oeprations * add fungible deploy contracts * add fungible deploy contracts * add simulation for observer module * add comments for crosschain operations * add comments for observer and fungible operations * generate files * debug import export test * fix import export tests * fix import export tests * fix app determinism test * update codecov.yml * reduce weight for DeployedSystemContracts operation * add new function to generate eth address from provided randomness * Update x/fungible/simulation/decoders.go Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> * Update testutil/sample/sample.go Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> * add validations * add validations * add randmoness to tss * make requested changes 1 * update state.go file * update state.go file * change chains.IsEVMChain to chains.IsEthereumChain in deposit tests * add basic structure for outbound vote message * add randomised outbound message * add cointype randomisation to inbound message * add cointype gas * add cointype erc20 * add outbound tracker * improve outbound tracker nonce selection * remove block limit for outbound tracker * add operation remove outbound tracker * add operation whitelist erc20 * fix unit tests * fix unit tests * fix Abort CCTX test * add operation refund aborted cctx * add operation update rate limiter flags * add operation UpdateErc20PauseStatus * add additional checks to whitelist erc20 * add default gas prices to the state * add default gas prices to the state * update zeta accounting genesis * update operation whitelist erc20 to not try whitelisting duplicate assets * add check for aborted status when finalizing an outbound * add check for aborted status when finalizing an outbound * reduce errors for RefundAbortedCCTX * reduce errors for RefundAbortedCCTX * reafactor update TSS to use existing cctx * remove setting nonce to cctx twice * set observe count for even is observer set is empty * add updte keygen message * add update chain params * add SimulateMsgResetChainNonces * add more operations from observer module * add helper functions * format code * add additional check to voting messages to for existing ballots * add msg vote tss * add msg vote tss * remove isPending check for aborted cctx * fix unit tests * add comments to simulation_test.go * add comments and unit tests * add comments to operations * refactor based on comments * fix formating * improve formating for decoders.go * remove overflow check when not needed * resolve comments 2 * add protocol v2 * generate files 2 * reduce weight of message enable CCTX * add solana address from rand * make changes based on feedback * refacctor updateCrossChainState to updateCrosschainState * rename Maxed to MaxReached * change test names for tests under TestCoinType_SupportsRefund * rename functions updateState to extractState * rebase develop * revert format for makefile * generate files --------- Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> --- Makefile | 8 +- changelog.md | 1 + pkg/chains/chain_filters.go | 5 + pkg/chains/chain_filters_test.go | 33 +- pkg/coin/coin.go | 4 + pkg/coin/coin_test.go | 42 +- pkg/memo/memo_test.go | 25 + simulation/simulation_test.go | 40 +- simulation/state.go | 217 +++++--- testutil/keeper/mocks/crosschain/authority.go | 32 +- testutil/keeper/mocks/observer/staking.go | 20 + testutil/sample/crosschain.go | 169 ++++++- testutil/sample/crypto.go | 56 +++ testutil/sample/observer.go | 54 +- testutil/sample/sample.go | 10 + x/crosschain/genesis.go | 22 +- x/crosschain/keeper/cctx_utils.go | 1 - .../keeper/msg_server_add_outbound_tracker.go | 4 +- .../msg_server_add_outbound_tracker_test.go | 4 +- .../keeper/msg_server_refund_aborted_tx.go | 1 - .../keeper/msg_server_vote_inbound_tx.go | 1 + .../keeper/msg_server_vote_inbound_tx_test.go | 8 +- .../keeper/msg_server_whitelist_erc20.go | 1 + .../keeper/msg_server_whitelist_erc20_test.go | 11 + x/crosschain/keeper/refund.go | 2 +- x/crosschain/simulation/decoders.go | 54 +- x/crosschain/simulation/decoders_test.go | 39 +- .../simulation/operation_abort_stuck_cctx.go | 128 +++++ .../operation_add_inbound_tracker.go | 72 +++ .../operation_add_outbound_tracker.go | 152 ++++++ .../simulation/operation_gas_price_voter.go | 78 +++ .../operation_refund_aborted_cctx.go | 84 ++++ .../operation_remove_outbound_tracker.go | 72 +++ .../operation_update_erc20_pause_status.go | 120 +++++ .../operation_update_rate_limiter_flags.go | 60 +++ .../operation_update_tss_address.go | 129 +++++ .../simulation/operation_vote_inbound.go | 190 +++++++ .../simulation/operation_vote_outbound.go | 226 +++++++++ .../simulation/operation_whitelist_erc20.go | 147 ++++++ x/crosschain/simulation/operations.go | 464 +++++++----------- x/crosschain/types/expected_keepers.go | 2 + x/crosschain/types/keys.go | 3 +- x/crosschain/types/outbound_tracker.go | 6 + x/crosschain/types/outbound_tracker_test.go | 48 ++ x/fungible/keeper/deposits.go | 1 + x/fungible/simulation/operations.go | 6 +- x/observer/genesis.go | 18 +- .../keeper/msg_server_update_observer.go | 8 +- x/observer/keeper/msg_server_vote_tss.go | 5 +- x/observer/simulation/decoders.go | 63 ++- x/observer/simulation/decoders_test.go | 53 +- .../simulation/operation_add_observer.go | 95 ++++ .../operation_add_observer_node_account.go | 110 +++++ .../simulation/operation_disable_cctx.go | 55 +++ .../simulation/operation_enable_cctx.go | 55 +++ .../operation_remove_chain_params.go | 83 ++++ .../operation_reset_chain_nonces.go | 87 ++++ .../operation_update_chain_params.go | 67 +++ ...eration_update_gas_price_increase_flags.go | 58 +++ .../simulation/operation_update_keygen.go | 66 +++ .../simulation/operation_update_observer.go | 118 +++++ x/observer/simulation/operation_vote_tss.go | 177 +++++++ x/observer/simulation/operations.go | 410 ++++++++++++++-- x/observer/types/expected_keepers.go | 1 + x/observer/types/keys.go | 4 + zetaclient/chains/evm/observer/outbound.go | 3 +- 66 files changed, 3841 insertions(+), 547 deletions(-) create mode 100644 x/crosschain/simulation/operation_abort_stuck_cctx.go create mode 100644 x/crosschain/simulation/operation_add_inbound_tracker.go create mode 100644 x/crosschain/simulation/operation_add_outbound_tracker.go create mode 100644 x/crosschain/simulation/operation_gas_price_voter.go create mode 100644 x/crosschain/simulation/operation_refund_aborted_cctx.go create mode 100644 x/crosschain/simulation/operation_remove_outbound_tracker.go create mode 100644 x/crosschain/simulation/operation_update_erc20_pause_status.go create mode 100644 x/crosschain/simulation/operation_update_rate_limiter_flags.go create mode 100644 x/crosschain/simulation/operation_update_tss_address.go create mode 100644 x/crosschain/simulation/operation_vote_inbound.go create mode 100644 x/crosschain/simulation/operation_vote_outbound.go create mode 100644 x/crosschain/simulation/operation_whitelist_erc20.go create mode 100644 x/crosschain/types/outbound_tracker.go create mode 100644 x/crosschain/types/outbound_tracker_test.go create mode 100644 x/observer/simulation/operation_add_observer.go create mode 100644 x/observer/simulation/operation_add_observer_node_account.go create mode 100644 x/observer/simulation/operation_disable_cctx.go create mode 100644 x/observer/simulation/operation_enable_cctx.go create mode 100644 x/observer/simulation/operation_remove_chain_params.go create mode 100644 x/observer/simulation/operation_reset_chain_nonces.go create mode 100644 x/observer/simulation/operation_update_chain_params.go create mode 100644 x/observer/simulation/operation_update_gas_price_increase_flags.go create mode 100644 x/observer/simulation/operation_update_keygen.go create mode 100644 x/observer/simulation/operation_update_observer.go create mode 100644 x/observer/simulation/operation_vote_tss.go diff --git a/Makefile b/Makefile index 02ac397d51..89a42ab4e9 100644 --- a/Makefile +++ b/Makefile @@ -409,7 +409,7 @@ test-sim-fullappsimulation: $(call run-sim-test,"TestFullAppSimulation",TestFullAppSimulation,100,200,30m) test-sim-import-export: - $(call run-sim-test,"test-import-export",TestAppImportExport,50,100,30m) + $(call run-sim-test,"test-import-export",TestAppImportExport,100,200,30m) test-sim-after-import: $(call run-sim-test,"test-sim-after-import",TestAppSimulationAfterImport,100,200,30m) @@ -430,6 +430,12 @@ test-sim-after-import-long: runsim @echo "Running application simulation-after-import. This may take several minute" @$(BINDIR)/runsim -Jobs=4 -SimAppPkg=$(SIMAPP) -ExitOnFail 500 50 TestAppSimulationAfterImport +# Use to run all simulation tests quickly (for example, before a creating a PR) +test-sim-quick: + $(call run-sim-test,"test-full-app-sim",TestFullAppSimulation,10,20,30m) + $(call run-sim-test,"test-import-export",TestAppImportExport,10,20,30m) + $(call run-sim-test,"test-sim-after-import",TestAppSimulationAfterImport,10,20,30m) + .PHONY: \ test-sim-nondeterminism \ test-sim-fullappsimulation \ diff --git a/changelog.md b/changelog.md index 4442a0cbba..44c0a2765c 100644 --- a/changelog.md +++ b/changelog.md @@ -14,6 +14,7 @@ * [3254](https://github.com/zeta-chain/node/pull/3254) - rename v2 E2E tests as evm tests and rename old evm tests as legacy * [3095](https://github.com/zeta-chain/node/pull/3095) - initialize simulation tests for custom zetachain modules * [3276](https://github.com/zeta-chain/node/pull/3276) - add Solana E2E performance tests and improve Solana outbounds performance +* [3207](https://github.com/zeta-chain/node/pull/3207) - add simulation test operations for all messages in crosschain and observer module ### Refactor diff --git a/pkg/chains/chain_filters.go b/pkg/chains/chain_filters.go index 3235fb98b1..c7961860e1 100644 --- a/pkg/chains/chain_filters.go +++ b/pkg/chains/chain_filters.go @@ -18,6 +18,11 @@ func FilterByConsensus(cs Consensus) ChainFilter { return func(chain Chain) bool { return chain.Consensus == cs } } +// FilterByVM filters chains by VM type +func FilterByVM(vm Vm) ChainFilter { + return func(chain Chain) bool { return chain.Vm == vm } +} + // FilterChains applies a list of filters to a list of chains func FilterChains(chainList []Chain, filters ...ChainFilter) []Chain { // Apply each filter to the list of supported chains diff --git a/pkg/chains/chain_filters_test.go b/pkg/chains/chain_filters_test.go index 6254600452..d1fc057572 100644 --- a/pkg/chains/chain_filters_test.go +++ b/pkg/chains/chain_filters_test.go @@ -14,42 +14,57 @@ func TestFilterChains(t *testing.T) { expected func() []chains.Chain }{ { - name: "Filter external chains", + name: "filter external chains", filters: []chains.ChainFilter{chains.FilterExternalChains}, expected: func() []chains.Chain { return chains.ExternalChainList([]chains.Chain{}) }, }, { - name: "Filter gateway observer chains", + name: "filter gateway observer chains", filters: []chains.ChainFilter{chains.FilterByGateway(chains.CCTXGateway_observers)}, expected: func() []chains.Chain { return chains.ChainListByGateway(chains.CCTXGateway_observers, []chains.Chain{}) }, }, { - name: "Filter consensus ethereum chains", + name: "filter consensus ethereum chains", filters: []chains.ChainFilter{chains.FilterByConsensus(chains.Consensus_ethereum)}, expected: func() []chains.Chain { return chains.ChainListByConsensus(chains.Consensus_ethereum, []chains.Chain{}) }, }, { - name: "Filter consensus bitcoin chains", + name: "filter consensus bitcoin chains", filters: []chains.ChainFilter{chains.FilterByConsensus(chains.Consensus_bitcoin)}, expected: func() []chains.Chain { return chains.ChainListByConsensus(chains.Consensus_bitcoin, []chains.Chain{}) }, }, { - name: "Filter consensus solana chains", + name: "filter consensus solana chains", filters: []chains.ChainFilter{chains.FilterByConsensus(chains.Consensus_solana_consensus)}, expected: func() []chains.Chain { return chains.ChainListByConsensus(chains.Consensus_solana_consensus, []chains.Chain{}) }, }, { - name: "Apply multiple filters external chains and gateway observer", + name: "filter evm chains", + filters: []chains.ChainFilter{ + chains.FilterByVM(chains.Vm_evm), + }, + expected: func() []chains.Chain { + var chainList []chains.Chain + for _, chain := range chains.ExternalChainList([]chains.Chain{}) { + if chain.Vm == chains.Vm_evm { + chainList = append(chainList, chain) + } + } + return chainList + }, + }, + { + name: "apply multiple filters external chains and gateway observer", filters: []chains.ChainFilter{ chains.FilterExternalChains, chains.FilterByGateway(chains.CCTXGateway_observers), @@ -66,7 +81,7 @@ func TestFilterChains(t *testing.T) { }, }, { - name: "Apply multiple filters external chains with gateway observer and consensus ethereum", + name: "apply multiple filters external chains with gateway observer and consensus ethereum", filters: []chains.ChainFilter{ chains.FilterExternalChains, chains.FilterByGateway(chains.CCTXGateway_observers), @@ -85,7 +100,7 @@ func TestFilterChains(t *testing.T) { }, }, { - name: "Apply multiple filters external chains with gateway observer and consensus bitcoin", + name: "apply multiple filters external chains with gateway observer and consensus bitcoin", filters: []chains.ChainFilter{ chains.FilterExternalChains, chains.FilterByGateway(chains.CCTXGateway_observers), @@ -116,7 +131,7 @@ func TestFilterChains(t *testing.T) { }, }, { - name: "Test multiple filters in random order", + name: "test multiple filters in random order", filters: []chains.ChainFilter{ chains.FilterByGateway(chains.CCTXGateway_observers), chains.FilterByConsensus(chains.Consensus_ethereum), diff --git a/pkg/coin/coin.go b/pkg/coin/coin.go index 223c2cbed9..e66095ebad 100644 --- a/pkg/coin/coin.go +++ b/pkg/coin/coin.go @@ -34,3 +34,7 @@ func GetAzetaDecFromAmountInZeta(zetaAmount string) (sdk.Dec, error) { zetaToAzetaConvertionFactor := sdk.NewDecFromInt(sdk.NewInt(1000000000000000000)) return zetaDec.Mul(zetaToAzetaConvertionFactor), nil } + +func (c CoinType) SupportsRefund() bool { + return c == CoinType_ERC20 || c == CoinType_Gas || c == CoinType_Zeta +} diff --git a/pkg/coin/coin_test.go b/pkg/coin/coin_test.go index 9f930039f7..504a743b2c 100644 --- a/pkg/coin/coin_test.go +++ b/pkg/coin/coin_test.go @@ -1,14 +1,15 @@ -package coin +package coin_test import ( "testing" sdk "github.com/cosmos/cosmos-sdk/types" "github.com/stretchr/testify/require" + "github.com/zeta-chain/node/pkg/coin" ) func Test_AzetaPerZeta(t *testing.T) { - require.Equal(t, sdk.NewDec(1e18), AzetaPerZeta()) + require.Equal(t, sdk.NewDec(1e18), coin.AzetaPerZeta()) } func Test_GetAzetaDecFromAmountInZeta(t *testing.T) { @@ -57,7 +58,7 @@ func Test_GetAzetaDecFromAmountInZeta(t *testing.T) { } for _, tc := range tt { t.Run(tc.name, func(t *testing.T) { - azeta, err := GetAzetaDecFromAmountInZeta(tc.zetaAmount) + azeta, err := coin.GetAzetaDecFromAmountInZeta(tc.zetaAmount) tc.err(t, err) if err == nil { require.Equal(t, tc.azetaAmount, azeta) @@ -71,31 +72,31 @@ func TestGetCoinType(t *testing.T) { tests := []struct { name string coin string - want CoinType + want coin.CoinType wantErr bool }{ { name: "valid coin type 0", coin: "0", - want: CoinType(0), + want: coin.CoinType(0), wantErr: false, }, { name: "valid coin type 1", coin: "1", - want: CoinType(1), + want: coin.CoinType(1), wantErr: false, }, { name: "valid coin type 2", coin: "2", - want: CoinType(2), + want: coin.CoinType(2), wantErr: false, }, { name: "valid coin type 3", coin: "3", - want: CoinType(3), + want: coin.CoinType(3), wantErr: false, }, { @@ -106,7 +107,7 @@ func TestGetCoinType(t *testing.T) { { name: "invalid coin type large number", coin: "4", - want: CoinType(4), + want: coin.CoinType(4), }, { name: "invalid coin type non-integer", @@ -117,7 +118,7 @@ func TestGetCoinType(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - got, err := GetCoinType(tt.coin) + got, err := coin.GetCoinType(tt.coin) if tt.wantErr { require.Error(t, err) } else { @@ -127,3 +128,24 @@ func TestGetCoinType(t *testing.T) { }) } } + +func TestCoinType_SupportsRefund(t *testing.T) { + tests := []struct { + name string + c coin.CoinType + want bool + }{ + {"should support refund for ERC20", coin.CoinType_ERC20, true}, + {"should support refund forGas", coin.CoinType_Gas, true}, + {"should support refund forZeta", coin.CoinType_Zeta, true}, + {"should not support refund forCmd", coin.CoinType_Cmd, false}, + {"should not support refund forUnknown", coin.CoinType(100), false}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := tt.c.SupportsRefund(); got != tt.want { + t.Errorf("CoinType.SupportsRefund() = %v, want %v", got, tt.want) + } + }) + } +} diff --git a/pkg/memo/memo_test.go b/pkg/memo/memo_test.go index 4eabd6a18f..4979b29d41 100644 --- a/pkg/memo/memo_test.go +++ b/pkg/memo/memo_test.go @@ -2,11 +2,13 @@ package memo_test import ( "encoding/hex" + mathrand "math/rand" "testing" "github.com/ethereum/go-ethereum/common" "github.com/stretchr/testify/require" "github.com/zeta-chain/node/pkg/memo" + "github.com/zeta-chain/node/testutil/sample" crosschaintypes "github.com/zeta-chain/node/x/crosschain/types" ) @@ -306,6 +308,9 @@ func Test_Memo_DecodeFromBytes(t *testing.T) { func Test_DecodeLegacyMemoHex(t *testing.T) { expectedShortMsgResult, err := hex.DecodeString("1a2b3c4d5e6f708192a3b4c5d6e7f808") + r := mathrand.New(mathrand.NewSource(42)) + address, data, memoHex := sample.MemoFromRand(r) + require.NoError(t, err) tests := []struct { name string @@ -324,6 +329,8 @@ func Test_DecodeLegacyMemoHex(t *testing.T) { {"empty msg", "", common.Address{}, nil, false}, {"invalid hex", "invalidHex", common.Address{}, nil, true}, {"short msg", "1a2b3c4d5e6f708192a3b4c5d6e7f808", common.Address{}, expectedShortMsgResult, false}, + {"random message", sample.EthAddress().String(), common.Address{}, nil, true}, + {"random message with hex encoding", memoHex, address, data, false}, } for _, tt := range tests { @@ -339,3 +346,21 @@ func Test_DecodeLegacyMemoHex(t *testing.T) { }) } } + +func Test_DecodeLegacyMemoHex_Random(t *testing.T) { + r := mathrand.New(mathrand.NewSource(42)) + + // Generate a random memo hex + randomMemo := common.BytesToAddress([]byte{0x12, 0x34, 0x56, 0x78, 0x9a, 0xbc, 0xde, 0xf0, 0x12, 0x34, 0x56, 0x78, 0x9a, 0xbc, 0xde, 0xf0, 0x12, 0x34, 0x56, 0x78}). + Hex() + randomData := []byte(sample.StringRandom(r, 10)) + randomMemoHex := hex.EncodeToString(append(common.FromHex(randomMemo), randomData...)) + + // Decode the random memo hex + addr, data, err := memo.DecodeLegacyMemoHex(randomMemoHex) + + // Validate the results + require.NoError(t, err) + require.Equal(t, common.HexToAddress(randomMemo), addr) + require.Equal(t, randomData, data) +} diff --git a/simulation/simulation_test.go b/simulation/simulation_test.go index 818250cbaf..6309753011 100644 --- a/simulation/simulation_test.go +++ b/simulation/simulation_test.go @@ -42,8 +42,6 @@ func init() { zetasimulation.GetSimulatorFlags() } -// StoreKeysPrefixes defines a struct used in comparing two keys for two different stores -// SkipPrefixes is used to skip certain prefixes when comparing the stores type StoreKeysPrefixes struct { A storetypes.StoreKey B storetypes.StoreKey @@ -66,6 +64,12 @@ func interBlockCacheOpt() func(*baseapp.BaseApp) { } // TestAppStateDeterminism runs a full application simulation , and produces multiple blocks as per the config +// It does the following +// 1. It runs the simulation multiple times with the same seed value +// 2. It checks the apphash at the end of each run +// 3. It compares the apphash at the end of each run to check for determinism +// 4. Repeat steps 1-3 for multiple seeds + // It checks the determinism of the application by comparing the apphash at the end of each run to other runs // The following test certifies that , for the same set of operations ( irrespective of what the operations are ) , // we would reach the same final state if the initial state is the same @@ -179,7 +183,10 @@ func TestAppStateDeterminism(t *testing.T) { } // TestFullAppSimulation runs a full simApp simulation with the provided configuration. -// At the end of the run it tries to export the genesis state to make sure the export works. +// This test does the following +// 1. It runs a full simulation with the provided configuration +// 2. It exports the state and validators +// 3. Verifies that the run and export were successful func TestFullAppSimulation(t *testing.T) { config := zetasimulation.NewConfigFromFlags() @@ -252,6 +259,15 @@ func TestFullAppSimulation(t *testing.T) { zetasimulation.PrintStats(db) } +// TestAppImportExport tests the application simulation after importing the state exported from a previous.At a high level,it does the following +// 1. It runs a full simulation and exports the state +// 2. It creates a new app, and db +// 3. It imports the exported state into the new app +// 4. It compares the key value pairs for the two apps.The comparison function takes a list of keys to skip as an input as well +// a. First app which ran the simulation +// b. Second app which imported the state + +// This can verify the export and import process do not modify the state in anyway irrespective of the operations performed func TestAppImportExport(t *testing.T) { config := zetasimulation.NewConfigFromFlags() @@ -270,7 +286,6 @@ func TestAppImportExport(t *testing.T) { t.Skip("skipping application simulation") } require.NoError(t, err, "simulation setup failed") - t.Cleanup(func() { if err := db.Close(); err != nil { require.NoError(t, err, "Error closing new database") @@ -375,7 +390,6 @@ func TestAppImportExport(t *testing.T) { ChainID: SimAppChainID, }) - t.Log("initializing genesis for the new app using exported genesis state") // Use genesis state from the first app to initialize the second app newSimApp.ModuleManager().InitGenesis(ctxNewSimApp, newSimApp.AppCodec(), genesisState) newSimApp.StoreConsensusParams(ctxNewSimApp, exported.ConsensusParams) @@ -390,7 +404,7 @@ func TestAppImportExport(t *testing.T) { // We will need to explore this further to find a definitive answer // TODO:https://github.com/zeta-chain/node/issues/3263 - // {simApp.GetKey(authtypes.StoreKey), newSimApp.GetKey(authtypes.StoreKey), [][]byte{}}, + //{simApp.GetKey(authtypes.StoreKey), newSimApp.GetKey(authtypes.StoreKey), [][]byte{}}, { simApp.GetKey(stakingtypes.StoreKey), newSimApp.GetKey(stakingtypes.StoreKey), [][]byte{ @@ -439,6 +453,12 @@ func TestAppImportExport(t *testing.T) { } } +// TestAppSimulationAfterImport tests the application simulation after importing the state exported from a previous simulation run. +// It does the following steps +// 1. It runs a full simulation and exports the state +// 2. It creates a new app, and db +// 3. It imports the exported state into the new app +// 4. It runs a simulation on the new app and verifies that there is no error in the second simulation func TestAppSimulationAfterImport(t *testing.T) { config := zetasimulation.NewConfigFromFlags() @@ -516,6 +536,7 @@ func TestAppSimulationAfterImport(t *testing.T) { exported, err := simApp.ExportAppStateAndValidators(true, []string{}, []string{}) require.NoError(t, err) + // Setup a new app with new database and directory newDB, newDir, _, _, err := cosmossimutils.SetupSimulation( config, SimDBBackend+"_new", @@ -523,9 +544,7 @@ func TestAppSimulationAfterImport(t *testing.T) { zetasimulation.FlagVerboseValue, zetasimulation.FlagEnabledValue, ) - require.NoError(t, err, "simulation setup failed") - t.Cleanup(func() { if err := newDB.Close(); err != nil { require.NoError(t, err, "Error closing new database") @@ -534,7 +553,6 @@ func TestAppSimulationAfterImport(t *testing.T) { require.NoError(t, err, "Error removing directory") } }) - newSimApp, err := zetasimulation.NewSimApp( logger, newDB, @@ -544,12 +562,14 @@ func TestAppSimulationAfterImport(t *testing.T) { ) require.NoError(t, err) + // Initialize the new app with the exported genesis state of the first run t.Log("Importing genesis into the new app") newSimApp.InitChain(abci.RequestInitChain{ ChainId: SimAppChainID, AppStateBytes: exported.AppState, }) + // Run simulation on the new app stopEarly, simParams, simErr = simulation.SimulateFromSeed( t, os.Stdout, @@ -567,5 +587,5 @@ func TestAppSimulationAfterImport(t *testing.T) { config, simApp.AppCodec(), ) - require.NoError(t, err) + require.NoError(t, simErr) } diff --git a/simulation/state.go b/simulation/state.go index 9bf2c24949..4e76f7d623 100644 --- a/simulation/state.go +++ b/simulation/state.go @@ -3,17 +3,12 @@ package simulation import ( "encoding/json" "fmt" - "io" "math/rand" - "os" "testing" "time" "cosmossdk.io/math" - cmtjson "github.com/cometbft/cometbft/libs/json" - tmtypes "github.com/cometbft/cometbft/types" "github.com/cosmos/cosmos-sdk/codec" - "github.com/cosmos/cosmos-sdk/crypto/keys/secp256k1" sdk "github.com/cosmos/cosmos-sdk/types" "github.com/cosmos/cosmos-sdk/types/module" simtypes "github.com/cosmos/cosmos-sdk/types/simulation" @@ -23,9 +18,12 @@ import ( "github.com/stretchr/testify/require" evmtypes "github.com/zeta-chain/ethermint/x/evm/types" - zetaapp "github.com/zeta-chain/node/app" + zetachains "github.com/zeta-chain/node/pkg/chains" + "github.com/zeta-chain/node/pkg/coin" + "github.com/zeta-chain/node/pkg/crypto" "github.com/zeta-chain/node/testutil/sample" authoritytypes "github.com/zeta-chain/node/x/authority/types" + crosschaintypes "github.com/zeta-chain/node/x/crosschain/types" fungibletypes "github.com/zeta-chain/node/x/fungible/types" observertypes "github.com/zeta-chain/node/x/observer/types" ) @@ -36,7 +34,10 @@ const ( InitiallyBondedValidators = "initially_bonded_validators" ) -func updateBankState( +// extractBankGenesisState extracts and updates the bank genesis state. +// It adds the following +// - The not bonded balance for the not bonded pool +func extractBankGenesisState( t *testing.T, rawState map[string]json.RawMessage, cdc codec.Codec, @@ -67,7 +68,9 @@ func updateBankState( return bankState } -func updateEVMState( +// extractEVMGenesisState extracts and updates the evm genesis state. +// It replaces the EvmDenom with BondDenom +func extractEVMGenesisState( t *testing.T, rawState map[string]json.RawMessage, cdc codec.Codec, @@ -85,7 +88,11 @@ func updateEVMState( return evmState } -func updateStakingState( +// extractStakingGenesisState extracts and updates the staking genesis state. +// It adds the following +// - The not bonded balance for the not bonded pool +// It additionally returns the non-bonded coins as well +func extractStakingGenesisState( t *testing.T, rawState map[string]json.RawMessage, cdc codec.Codec, @@ -112,7 +119,16 @@ func updateStakingState( return stakingState, notBondedCoins } -func updateObserverState( +// extractObserverGenesisState extracts and updates the observer genesis state. +// It adds the following +// - A random observer set which is a subset of the current validator set +// - A randomised node account for each observer +// - A random TSS +// - A TSS history for the TSS created +// - Chain nonces for each chain +// - Pending nonces for each chain +// - Crosschain flags, inbound and outbound enabled +func extractObserverGenesisState( t *testing.T, rawState map[string]json.RawMessage, cdc codec.Codec, @@ -125,6 +141,7 @@ func updateObserverState( observerState := new(observertypes.GenesisState) cdc.MustUnmarshalJSON(observerStateBz, observerState) + // Create an observer set as a subset of the current validator set observers := make([]string, 0) for _, validator := range validators { accAddress, err := observertypes.GetAccAddressFromOperatorAddress(validator.OperatorAddress) @@ -138,24 +155,67 @@ func updateObserverState( observers[i], observers[j] = observers[j], observers[i] }) - numObservers := r.Intn(11) + 5 + numObservers := r.Intn(21) + 5 if numObservers > len(observers) { numObservers = len(observers) } observers = observers[:numObservers] + // Create node account list for the observers set + nodeAccounts := make([]*observertypes.NodeAccount, len(observers)) + for i, observer := range observers { + nodeAccounts[i] = &observertypes.NodeAccount{ + Operator: observer, + GranteeAddress: observer, + GranteePubkey: &crypto.PubKeySet{}, + NodeStatus: observertypes.NodeStatus_Active, + } + } + // Create a random tss + tss, err := sample.TSSFromRand(r) + require.NoError(t, err) + tss.OperatorAddressList = observers + + // Create a tss history + tssHistory := make([]observertypes.TSS, 0) + tssHistory = append(tssHistory, tss) + + // Create chainnonces and pendingnonces + chains := zetachains.DefaultChainsList() + chainsNonces := make([]observertypes.ChainNonces, 0) + pendingNonces := make([]observertypes.PendingNonces, 0) + for _, chain := range chains { + chainNonce := observertypes.ChainNonces{ + ChainId: chain.ChainId, + Nonce: 0, + } + chainsNonces = append(chainsNonces, chainNonce) + pendingNonce := observertypes.PendingNonces{ + NonceLow: 0, + NonceHigh: 0, + ChainId: chain.ChainId, + Tss: tss.TssPubkey, + } + pendingNonces = append(pendingNonces, pendingNonce) + } + + observerState.Tss = &tss observerState.Observers.ObserverList = observers + observerState.NodeAccountList = nodeAccounts observerState.CrosschainFlags.IsInboundEnabled = true observerState.CrosschainFlags.IsOutboundEnabled = true - - tss := sample.TSSFromRand(t, r) - tss.OperatorAddressList = observers - observerState.Tss = &tss + observerState.ChainNonces = chainsNonces + observerState.PendingNonces = pendingNonces + observerState.TssHistory = tssHistory return observerState } -func updateAuthorityState( +// extractAuthorityGenesisState extracts and updates the authority genesis state. +// It adds the following +// - A policy for each policy type; +// the address is a random account address selected from the simulation accounts list +func extractAuthorityGenesisState( t *testing.T, rawState map[string]json.RawMessage, cdc codec.Codec, @@ -190,7 +250,40 @@ func updateAuthorityState( return authorityState } -func updateFungibleState( +// extractCrosschainGenesisState extracts and updates the crosschain genesis state. +// It adds the following +// - A gas price list for each chain +func extractCrosschainGenesisState( + t *testing.T, + rawState map[string]json.RawMessage, + cdc codec.Codec, + r *rand.Rand, +) *crosschaintypes.GenesisState { + crossChainStateBz, ok := rawState[crosschaintypes.ModuleName] + require.True(t, ok, "crosschain genesis state is missing") + + crossChainState := new(crosschaintypes.GenesisState) + cdc.MustUnmarshalJSON(crossChainStateBz, crossChainState) + + // Add a gasprice for each chain + chains := zetachains.DefaultChainsList() + gasPriceList := make([]*crosschaintypes.GasPrice, len(chains)) + for i, chain := range chains { + gasPriceList[i] = sample.GasPriceFromRand(r, chain.ChainId) + } + + crossChainState.GasPriceList = gasPriceList + + return crossChainState +} + +// extractFungibleGenesisState extracts and updates the fungible genesis state. +// It adds the following +// - A random system contract address +// - A random connector zevm address +// - A random gateway address +// - A foreign coin for each chain under the default chain list. +func extractFungibleGenesisState( t *testing.T, rawState map[string]json.RawMessage, cdc codec.Codec, @@ -207,9 +300,28 @@ func updateFungibleState( Gateway: sample.EthAddressFromRand(r).String(), } + foreignCoins := make([]fungibletypes.ForeignCoins, 0) + chains := zetachains.DefaultChainsList() + + for _, chain := range chains { + foreignCoin := fungibletypes.ForeignCoins{ + ForeignChainId: chain.ChainId, + Asset: sample.EthAddressFromRand(r).String(), + Zrc20ContractAddress: sample.EthAddressFromRand(r).String(), + Decimals: 18, + Paused: false, + CoinType: coin.CoinType_Gas, + LiquidityCap: math.ZeroUint(), + } + foreignCoins = append(foreignCoins, foreignCoin) + } + fungibleState.ForeignCoinsList = foreignCoins + return fungibleState } +// updateRawState updates the raw genesis state for the application. +// This is used to inject values needed to run the simulation tests. func updateRawState( t *testing.T, rawState map[string]json.RawMessage, @@ -217,12 +329,12 @@ func updateRawState( r *rand.Rand, accs []simtypes.Account, ) { - stakingState, notBondedCoins := updateStakingState(t, rawState, cdc) - bankState := updateBankState(t, rawState, cdc, notBondedCoins) - evmState := updateEVMState(t, rawState, cdc, stakingState.Params.BondDenom) - observerState := updateObserverState(t, rawState, cdc, r, stakingState.Validators) - authorityState := updateAuthorityState(t, rawState, cdc, r, accs) - fungibleState := updateFungibleState(t, rawState, cdc, r) + stakingState, notBondedCoins := extractStakingGenesisState(t, rawState, cdc) + bankState := extractBankGenesisState(t, rawState, cdc, notBondedCoins) + evmState := extractEVMGenesisState(t, rawState, cdc, stakingState.Params.BondDenom) + observerState := extractObserverGenesisState(t, rawState, cdc, r, stakingState.Validators) + authorityState := extractAuthorityGenesisState(t, rawState, cdc, r, accs) + fungibleState := extractFungibleGenesisState(t, rawState, cdc, r) rawState[stakingtypes.ModuleName] = cdc.MustMarshalJSON(stakingState) rawState[banktypes.ModuleName] = cdc.MustMarshalJSON(bankState) @@ -230,6 +342,7 @@ func updateRawState( rawState[observertypes.ModuleName] = cdc.MustMarshalJSON(observerState) rawState[authoritytypes.ModuleName] = cdc.MustMarshalJSON(authorityState) rawState[fungibletypes.ModuleName] = cdc.MustMarshalJSON(fungibleState) + rawState[crosschaintypes.ModuleName] = cdc.MustMarshalJSON(extractCrosschainGenesisState(t, rawState, cdc, r)) } // AppStateFn returns the initial application state using a genesis or the simulation parameters. @@ -253,7 +366,7 @@ func AppStateFn( chainID = config.ChainID - // if exported state is provided then use it + // if exported state is provided, then use it if exportedState != nil { return exportedState, accs, chainID, genesisTimestamp } @@ -342,61 +455,5 @@ func AppStateRandomizedFn( if err != nil { panic(err) } - return appState, accs } - -// AppStateFromGenesisFileFn util function to generate the genesis AppState -// from a genesis.json file. -func AppStateFromGenesisFileFn( - r io.Reader, - cdc codec.JSONCodec, - genesisFile string, -) (tmtypes.GenesisDoc, []simtypes.Account, error) { - bytes, err := os.ReadFile(genesisFile) // #nosec G304 -- genesisFile value is controlled - if err != nil { - panic(err) - } - - var genesis tmtypes.GenesisDoc - // NOTE: Comet uses a custom JSON decoder for GenesisDoc - err = cmtjson.Unmarshal(bytes, &genesis) - if err != nil { - panic(err) - } - - var appState zetaapp.GenesisState - err = json.Unmarshal(genesis.AppState, &appState) - if err != nil { - panic(err) - } - - var authGenesis authtypes.GenesisState - if appState[authtypes.ModuleName] != nil { - cdc.MustUnmarshalJSON(appState[authtypes.ModuleName], &authGenesis) - } - - newAccs := make([]simtypes.Account, len(authGenesis.Accounts)) - for i, acc := range authGenesis.Accounts { - // Pick a random private key, since we don't know the actual key - // This should be fine as it's only used for mock Tendermint validators - // and these keys are never actually used to sign by mock Tendermint. - privkeySeed := make([]byte, 15) - if _, err := r.Read(privkeySeed); err != nil { - panic(err) - } - - privKey := secp256k1.GenPrivKeyFromSecret(privkeySeed) - - a, ok := acc.GetCachedValue().(authtypes.AccountI) - if !ok { - return genesis, nil, fmt.Errorf("expected account") - } - - // create simulator accounts - simAcc := simtypes.Account{PrivKey: privKey, PubKey: privKey.PubKey(), Address: a.GetAddress()} - newAccs[i] = simAcc - } - - return genesis, newAccs, nil -} diff --git a/testutil/keeper/mocks/crosschain/authority.go b/testutil/keeper/mocks/crosschain/authority.go index 7dafb1331b..9f2a9a8837 100644 --- a/testutil/keeper/mocks/crosschain/authority.go +++ b/testutil/keeper/mocks/crosschain/authority.go @@ -3,8 +3,10 @@ package mocks import ( - mock "github.com/stretchr/testify/mock" chains "github.com/zeta-chain/node/pkg/chains" + authoritytypes "github.com/zeta-chain/node/x/authority/types" + + mock "github.com/stretchr/testify/mock" types "github.com/cosmos/cosmos-sdk/types" ) @@ -52,6 +54,34 @@ func (_m *CrosschainAuthorityKeeper) GetAdditionalChainList(ctx types.Context) [ return r0 } +// GetPolicies provides a mock function with given fields: ctx +func (_m *CrosschainAuthorityKeeper) GetPolicies(ctx types.Context) (authoritytypes.Policies, bool) { + ret := _m.Called(ctx) + + if len(ret) == 0 { + panic("no return value specified for GetPolicies") + } + + var r0 authoritytypes.Policies + var r1 bool + if rf, ok := ret.Get(0).(func(types.Context) (authoritytypes.Policies, bool)); ok { + return rf(ctx) + } + if rf, ok := ret.Get(0).(func(types.Context) authoritytypes.Policies); ok { + r0 = rf(ctx) + } else { + r0 = ret.Get(0).(authoritytypes.Policies) + } + + if rf, ok := ret.Get(1).(func(types.Context) bool); ok { + r1 = rf(ctx) + } else { + r1 = ret.Get(1).(bool) + } + + return r0, r1 +} + // NewCrosschainAuthorityKeeper creates a new instance of CrosschainAuthorityKeeper. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. // The first argument is typically a *testing.T value. func NewCrosschainAuthorityKeeper(t interface { diff --git a/testutil/keeper/mocks/observer/staking.go b/testutil/keeper/mocks/observer/staking.go index 90007b6c35..af2537bac0 100644 --- a/testutil/keeper/mocks/observer/staking.go +++ b/testutil/keeper/mocks/observer/staking.go @@ -15,6 +15,26 @@ type ObserverStakingKeeper struct { mock.Mock } +// GetAllValidators provides a mock function with given fields: ctx +func (_m *ObserverStakingKeeper) GetAllValidators(ctx types.Context) []stakingtypes.Validator { + ret := _m.Called(ctx) + + if len(ret) == 0 { + panic("no return value specified for GetAllValidators") + } + + var r0 []stakingtypes.Validator + if rf, ok := ret.Get(0).(func(types.Context) []stakingtypes.Validator); ok { + r0 = rf(ctx) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).([]stakingtypes.Validator) + } + } + + return r0 +} + // GetDelegation provides a mock function with given fields: ctx, delAddr, valAddr func (_m *ObserverStakingKeeper) GetDelegation(ctx types.Context, delAddr types.AccAddress, valAddr types.ValAddress) (stakingtypes.Delegation, bool) { ret := _m.Called(ctx, delAddr, valAddr) diff --git a/testutil/sample/crosschain.go b/testutil/sample/crosschain.go index 82e683604a..b3ed7d5ee8 100644 --- a/testutil/sample/crosschain.go +++ b/testutil/sample/crosschain.go @@ -2,6 +2,7 @@ package sample import ( "encoding/base64" + "encoding/hex" "encoding/json" "fmt" "math/big" @@ -11,7 +12,7 @@ import ( "cosmossdk.io/math" sdk "github.com/cosmos/cosmos-sdk/types" - ethcommon "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/common" ethtypes "github.com/ethereum/go-ethereum/core/types" "github.com/ethereum/go-ethereum/crypto" "github.com/stretchr/testify/require" @@ -54,6 +55,36 @@ func RateLimiterFlags() types.RateLimiterFlags { } } +func RateLimiterFlagsFromRand(r *rand.Rand) types.RateLimiterFlags { + return types.RateLimiterFlags{ + Enabled: true, + Window: r.Int63(), + Rate: sdk.NewUint(r.Uint64()), + Conversions: []types.Conversion{ + { + Zrc20: EthAddressFromRand(r).Hex(), + Rate: sdk.NewDec(r.Int63()), + }, + { + Zrc20: EthAddressFromRand(r).Hex(), + Rate: sdk.NewDec(r.Int63()), + }, + { + Zrc20: EthAddressFromRand(r).Hex(), + Rate: sdk.NewDec(r.Int63()), + }, + { + Zrc20: EthAddressFromRand(r).Hex(), + Rate: sdk.NewDec(r.Int63()), + }, + { + Zrc20: EthAddressFromRand(r).Hex(), + Rate: sdk.NewDec(r.Int63()), + }, + }, + } +} + // CustomAssetRate creates a custom asset rate with the given parameters func CustomAssetRate( chainID int64, @@ -118,6 +149,25 @@ func GasPriceWithChainID(t *testing.T, chainID int64) types.GasPrice { } } +func GasPriceFromRand(r *rand.Rand, chainID int64) *types.GasPrice { + var price uint64 + for price == 0 { + maxGasPrice := uint64(1000 * 1e9) // 1000 Gwei + price = uint64(1e9) + r.Uint64()%maxGasPrice + } + // Select priority fee between 0 and price + priorityFee := r.Uint64() % price + return &types.GasPrice{ + Creator: "", + ChainId: chainID, + Signers: []string{AccAddressFromRand(r)}, + BlockNums: []uint64{r.Uint64()}, + Prices: []uint64{price}, + MedianIndex: 0, + PriorityFees: []uint64{priorityFee}, + } +} + func InboundParams(r *rand.Rand) *types.InboundParams { return &types.InboundParams{ Sender: EthAddress().String(), @@ -314,8 +364,10 @@ func InboundVote(coinType coin.CoinType, from, to int64) types.MsgVoteInbound { } // InboundVoteFromRand creates a simulated inbound vote message. This function uses the provided source of randomness to generate the vote -func InboundVoteFromRand(coinType coin.CoinType, from, to int64, r *rand.Rand) types.MsgVoteInbound { - EthAddress() +func InboundVoteFromRand(from, to int64, r *rand.Rand, asset string) types.MsgVoteInbound { + coinType := CoinTypeFromRand(r) + _, _, memo := MemoFromRand(r) + return types.MsgVoteInbound{ Creator: "", Sender: EthAddressFromRand(r).String(), @@ -323,19 +375,118 @@ func InboundVoteFromRand(coinType coin.CoinType, from, to int64, r *rand.Rand) t Receiver: EthAddressFromRand(r).String(), ReceiverChain: to, Amount: math.NewUint(r.Uint64()), - Message: base64.StdEncoding.EncodeToString(RandomBytes(r)), + Message: memo, InboundBlockHeight: r.Uint64(), CallOptions: &types.CallOptions{ GasLimit: 1000000000, }, - InboundHash: ethcommon.BytesToHash(RandomBytes(r)).String(), - CoinType: coinType, - TxOrigin: EthAddressFromRand(r).String(), - Asset: StringRandom(r, 32), - EventIndex: r.Uint64(), + InboundHash: common.BytesToHash(RandomBytes(r)).String(), + CoinType: coinType, + TxOrigin: EthAddressFromRand(r).String(), + Asset: asset, + EventIndex: r.Uint64(), + ProtocolContractVersion: ProtocolVersionFromRand(r), } } +func ProtocolVersionFromRand(r *rand.Rand) types.ProtocolContractVersion { + versions := []types.ProtocolContractVersion{types.ProtocolContractVersion_V1, types.ProtocolContractVersion_V2} + return versions[r.Intn(len(versions))] +} + +func CoinTypeFromRand(r *rand.Rand) coin.CoinType { + coinTypes := []coin.CoinType{coin.CoinType_Gas, coin.CoinType_ERC20, coin.CoinType_Zeta} + coinType := coinTypes[r.Intn(len(coinTypes))] + return coinType +} + +func MemoFromRand(r *rand.Rand) (common.Address, []byte, string) { + randomMemo := common.BytesToAddress([]byte{0x12, 0x34, 0x56, 0x78, 0x9a, 0xbc, 0xde, 0xf0, 0x12, 0x34, 0x56, 0x78, 0x9a, 0xbc, 0xde, 0xf0, 0x12, 0x34, 0x56, 0x78}). + Hex() + randomData := []byte(StringRandom(r, 10)) + memoHex := hex.EncodeToString(append(common.FromHex(randomMemo), randomData...)) + return common.HexToAddress(randomMemo), randomData, memoHex +} + +func CCTXfromRand(r *rand.Rand, + creator string, + index string, + to int64, + from int64, + tssPubkey string, + asset string, +) types.CrossChainTx { + coinType := CoinTypeFromRand(r) + + amount := math.NewUint(uint64(r.Int63())) + inbound := &types.InboundParams{ + Sender: EthAddressFromRand(r).String(), + SenderChainId: from, + TxOrigin: EthAddressFromRand(r).String(), + CoinType: coinType, + Asset: asset, + Amount: amount, + ObservedHash: StringRandom(r, 32), + ObservedExternalHeight: r.Uint64(), + BallotIndex: StringRandom(r, 32), + FinalizedZetaHeight: r.Uint64(), + } + + outbound := &types.OutboundParams{ + Receiver: EthAddressFromRand(r).String(), + ReceiverChainId: to, + CoinType: coinType, + Amount: math.NewUint(uint64(r.Int63())), + TssNonce: 0, + TssPubkey: tssPubkey, + CallOptions: &types.CallOptions{ + GasLimit: r.Uint64(), + }, + GasPrice: math.NewUint(uint64(r.Int63())).String(), + Hash: StringRandom(r, 32), + BallotIndex: StringRandom(r, 32), + ObservedExternalHeight: r.Uint64(), + GasUsed: 100, + EffectiveGasPrice: math.NewInt(r.Int63()), + EffectiveGasLimit: 100, + } + + cctx := types.CrossChainTx{ + Creator: creator, + Index: index, + ZetaFees: sdk.NewUint(1), + RelayedMessage: base64.StdEncoding.EncodeToString(RandomBytes(r)), + CctxStatus: &types.Status{ + IsAbortRefunded: false, + Status: types.CctxStatus_PendingOutbound, + }, + InboundParams: inbound, + OutboundParams: []*types.OutboundParams{outbound}, + ProtocolContractVersion: ProtocolVersionFromRand(r), + } + return cctx +} + +func OutboundVoteSim(r *rand.Rand, + cctx types.CrossChainTx, +) (types.CrossChainTx, types.MsgVoteOutbound) { + msg := types.MsgVoteOutbound{ + CctxHash: cctx.Index, + OutboundTssNonce: cctx.GetCurrentOutboundParam().TssNonce, + OutboundChain: cctx.GetCurrentOutboundParam().ReceiverChainId, + Status: chains.ReceiveStatus_success, + Creator: cctx.Creator, + ObservedOutboundHash: common.BytesToHash(EthAddressFromRand(r).Bytes()).String(), + ValueReceived: cctx.GetCurrentOutboundParam().Amount, + ObservedOutboundBlockHeight: cctx.GetCurrentOutboundParam().ObservedExternalHeight, + ObservedOutboundEffectiveGasPrice: cctx.GetCurrentOutboundParam().EffectiveGasPrice, + ObservedOutboundGasUsed: cctx.GetCurrentOutboundParam().GasUsed, + CoinType: cctx.InboundParams.CoinType, + } + + return cctx, msg +} + func ZRC20Withdrawal(to []byte, value *big.Int) *zrc20.ZRC20Withdrawal { return &zrc20.ZRC20Withdrawal{ From: EthAddress(), diff --git a/testutil/sample/crypto.go b/testutil/sample/crypto.go index 9e643fa123..783ffa4a8d 100644 --- a/testutil/sample/crypto.go +++ b/testutil/sample/crypto.go @@ -2,6 +2,7 @@ package sample import ( "crypto/ecdsa" + cryptoed25519 "crypto/ed25519" "math/big" "math/rand" "strconv" @@ -33,6 +34,15 @@ func PubKeySet() *crypto.PubKeySet { return &pubKeySet } +func Ed25519PrivateKeyFromRand(r *rand.Rand) (*ed25519.PrivKey, error) { + randomBytes := make([]byte, 32) + _, err := r.Read(randomBytes) + if err != nil { + return nil, err + } + return ed25519.GenPrivKeyFromSecret(randomBytes), nil +} + // PubKeyString returns a sample public key string func PubKeyString() string { priKey := ed25519.GenPrivKey() @@ -47,6 +57,22 @@ func PubKeyString() string { return pubkey.String() } +func PubkeyStringFromRand(r *rand.Rand) (string, error) { + priKey, err := Ed25519PrivateKeyFromRand(r) + if err != nil { + return "", err + } + s, err := cosmos.Bech32ifyPubKey(cosmos.Bech32PubKeyTypeAccPub, priKey.PubKey()) + if err != nil { + return "", err + } + pubkey, err := crypto.NewPubKey(s) + if err != nil { + return "", err + } + return pubkey.String(), nil +} + // PrivKeyAddressPair returns a private key, address pair func PrivKeyAddressPair() (*ed25519.PrivKey, sdk.AccAddress) { privKey := ed25519.GenPrivKey() @@ -83,6 +109,16 @@ func SolanaPrivateKey(t *testing.T) solana.PrivateKey { return privKey } +func SolanaPrivateKeyFromRand(r *rand.Rand) (solana.PrivateKey, error) { + pub, priv, err := cryptoed25519.GenerateKey(r) + if err != nil { + return nil, err + } + var publicKey cryptoed25519.PublicKey + copy(publicKey[:], pub) + return solana.PrivateKey(priv), nil +} + // SolanaAddress returns a sample solana address func SolanaAddress(t *testing.T) string { privKey, err := solana.NewRandomPrivateKey() @@ -90,6 +126,14 @@ func SolanaAddress(t *testing.T) string { return privKey.PublicKey().String() } +func SolanaAddressFromRand(r *rand.Rand) (string, error) { + privKey, err := SolanaPrivateKeyFromRand(r) + if err != nil { + return "", err + } + return privKey.PublicKey().String(), nil +} + // SolanaSignature returns a sample solana signature func SolanaSignature(t *testing.T) solana.Signature { // Generate a random keypair @@ -113,6 +157,11 @@ func Hash() ethcommon.Hash { return ethcommon.BytesToHash(EthAddress().Bytes()) } +// Hash returns a sample hash +func HashFromRand(r *rand.Rand) ethcommon.Hash { + return ethcommon.BytesToHash(EthAddressFromRand(r).Bytes()) +} + // BtcHash returns a sample btc hash func BtcHash() chainhash.Hash { return chainhash.Hash(Hash()) @@ -138,6 +187,13 @@ func AccAddress() string { return sdk.AccAddress(addr).String() } +// AccAddressFromRand returns a sample account address in string +func AccAddressFromRand(r *rand.Rand) string { + pk := PubKey(r) + addr := pk.Address() + return sdk.AccAddress(addr).String() +} + // ValAddress returns a sample validator operator address func ValAddress(r *rand.Rand) sdk.ValAddress { return sdk.ValAddress(PubKey(r).Address()) diff --git a/testutil/sample/observer.go b/testutil/sample/observer.go index 1a0ed8f2ba..5ee8c6f648 100644 --- a/testutil/sample/observer.go +++ b/testutil/sample/observer.go @@ -10,7 +10,6 @@ import ( "github.com/cosmos/cosmos-sdk/testutil/testdata" sdk "github.com/cosmos/cosmos-sdk/types" "github.com/ethereum/go-ethereum/crypto" - "github.com/stretchr/testify/require" "github.com/zeta-chain/node/pkg/chains" "github.com/zeta-chain/node/pkg/cosmos" @@ -72,6 +71,15 @@ func Keygen(t *testing.T) *types.Keygen { } } +func KeygenFromRand(r *rand.Rand) types.Keygen { + pubkey := PubKey(r) + return types.Keygen{ + Status: types.KeygenStatus_KeyGenSuccess, + GranteePubkeys: []string{pubkey.String()}, + BlockNumber: r.Int63(), + } +} + func LastObserverCount(lastChangeHeight int64) *types.LastObserverCount { r := newRandFromSeed(lastChangeHeight) @@ -108,6 +116,27 @@ func ChainParams(chainID int64) *types.ChainParams { } } +func ChainParamsFromRand(r *rand.Rand, chainID int64) *types.ChainParams { + fiftyPercent := sdk.MustNewDecFromStr("0.5") + return &types.ChainParams{ + ChainId: chainID, + ConfirmationCount: r.Uint64(), + + GasPriceTicker: Uint64InRangeFromRand(r, 1, 300), + InboundTicker: Uint64InRangeFromRand(r, 1, 300), + OutboundTicker: Uint64InRangeFromRand(r, 1, 300), + WatchUtxoTicker: Uint64InRangeFromRand(r, 1, 300), + ZetaTokenContractAddress: EthAddressFromRand(r).String(), + ConnectorContractAddress: EthAddressFromRand(r).String(), + Erc20CustodyContractAddress: EthAddressFromRand(r).String(), + OutboundScheduleInterval: Int64InRangeFromRand(r, 1, 100), + OutboundScheduleLookahead: Int64InRangeFromRand(r, 1, 500), + BallotThreshold: fiftyPercent, + MinObserverDelegation: sdk.NewDec(r.Int63()), + IsSupported: true, + } +} + func ChainParamsSupported(chainID int64) *types.ChainParams { cp := ChainParams(chainID) cp.IsSupported = true @@ -124,12 +153,16 @@ func ChainParamsList() (cpl types.ChainParamsList) { } // TSSFromRand returns a random TSS,it uses the randomness provided as a parameter -func TSSFromRand(t *testing.T, r *rand.Rand) types.TSS { +func TSSFromRand(r *rand.Rand) (types.TSS, error) { pubKey := PubKey(r) spk, err := cosmos.Bech32ifyPubKey(cosmos.Bech32PubKeyTypeAccPub, pubKey) - require.NoError(t, err) + if err != nil { + return types.TSS{}, err + } pk, err := zetacrypto.NewPubKey(spk) - require.NoError(t, err) + if err != nil { + return types.TSS{}, err + } pubkeyString := pk.String() return types.TSS{ TssPubkey: pubkeyString, @@ -137,7 +170,7 @@ func TSSFromRand(t *testing.T, r *rand.Rand) types.TSS { OperatorAddressList: []string{}, FinalizedZetaHeight: r.Int63(), KeyGenZetaHeight: r.Int63(), - } + }, nil } // TODO: rename to TSS @@ -287,6 +320,17 @@ func GasPriceIncreaseFlags() types.GasPriceIncreaseFlags { } } +func GasPriceIncreaseFlagsFromRand(r *rand.Rand) types.GasPriceIncreaseFlags { + minValue := 1 + maxValue := 100 + return types.GasPriceIncreaseFlags{ + EpochLength: int64(r.Intn(maxValue-minValue) + minValue), + RetryInterval: time.Duration(r.Intn(maxValue-minValue) + minValue), + GasPriceIncreasePercent: 1, + MaxPendingCctxs: 100, + } +} + func OperationalFlags() types.OperationalFlags { return types.OperationalFlags{ RestartHeight: 1, diff --git a/testutil/sample/sample.go b/testutil/sample/sample.go index 3f0390ddad..c9f416a088 100644 --- a/testutil/sample/sample.go +++ b/testutil/sample/sample.go @@ -93,12 +93,22 @@ func Uint64InRange(low, high uint64) uint64 { return r.Uint64()%(high-low) + low } +// Uint64InRange returns a sample uint64 in the given ranges +func Uint64InRangeFromRand(r *rand.Rand, low, high uint64) uint64 { + return r.Uint64()%(high-low) + low +} + // Int64InRange returns a sample int64 in the given ranges func Int64InRange(low, high int64) int64 { r := newRandFromSeed(low) return r.Int63()%(high-low) + low } +// Int64InRangeFromRand returns a sample int64 in the given ranges +func Int64InRangeFromRand(r *rand.Rand, low, high int64) int64 { + return r.Int63()%(high-low) + low +} + func UintInRange(low, high uint64) sdkmath.Uint { u := Uint64InRange(low, high) return sdkmath.NewUint(u) diff --git a/x/crosschain/genesis.go b/x/crosschain/genesis.go index 8ea365e059..0f6856d559 100644 --- a/x/crosschain/genesis.go +++ b/x/crosschain/genesis.go @@ -1,7 +1,6 @@ package crosschain import ( - sdkmath "cosmossdk.io/math" sdk "github.com/cosmos/cosmos-sdk/types" "github.com/zeta-chain/node/x/crosschain/keeper" @@ -11,9 +10,8 @@ import ( // InitGenesis initializes the crosschain module's state from a provided genesis // state. func InitGenesis(ctx sdk.Context, k keeper.Keeper, genState types.GenesisState) { - // Always set the zeta accounting to zero at genesis. - // ZetaAccounting value is build by iterating through all the cctxs and adding the amount to the zeta accounting. - k.SetZetaAccounting(ctx, types.ZetaAccounting{AbortedZetaAmount: sdkmath.ZeroUint()}) + k.SetZetaAccounting(ctx, genState.ZetaAccounting) + // Set all the outbound tracker for _, elem := range genState.OutboundTrackerList { k.SetOutboundTracker(ctx, elem) @@ -36,8 +34,6 @@ func InitGenesis(ctx sdk.Context, k keeper.Keeper, genState types.GenesisState) } } - // Set all the chain nonces - // Set all the last block heights for _, elem := range genState.LastBlockHeightList { if elem != nil { @@ -45,15 +41,15 @@ func InitGenesis(ctx sdk.Context, k keeper.Keeper, genState types.GenesisState) } } - // Set all the cross-chain txs - tss, found := k.GetObserverKeeper().GetTSS(ctx) - if found { - for _, elem := range genState.CrossChainTxs { - if elem != nil { - k.SaveCCTXUpdate(ctx, *elem, tss.TssPubkey) - } + // Set the cross-chain transactions only, + // We don't need to call SaveCCTXUpdate as the other fields are being set already + for _, elem := range genState.CrossChainTxs { + if elem != nil { + cctx := *elem + k.SetCrossChainTx(ctx, cctx) } } + for _, elem := range genState.FinalizedInbounds { k.SetFinalizedInbound(ctx, elem) } diff --git a/x/crosschain/keeper/cctx_utils.go b/x/crosschain/keeper/cctx_utils.go index 3f62202cb7..3fedf03cef 100644 --- a/x/crosschain/keeper/cctx_utils.go +++ b/x/crosschain/keeper/cctx_utils.go @@ -30,7 +30,6 @@ func (k Keeper) SetObserverOutboundInfo(ctx sdk.Context, receiveChainID int64, c "identifiers: %s (chain %q)", cctx.LogIdentifierForCCTX(), chain.Name, ) } - // SET nonce cctx.GetCurrentOutboundParam().TssNonce = nonce.Nonce tss, found := k.GetObserverKeeper().GetTSS(ctx) diff --git a/x/crosschain/keeper/msg_server_add_outbound_tracker.go b/x/crosschain/keeper/msg_server_add_outbound_tracker.go index 9beffffd1e..360a8f8301 100644 --- a/x/crosschain/keeper/msg_server_add_outbound_tracker.go +++ b/x/crosschain/keeper/msg_server_add_outbound_tracker.go @@ -13,7 +13,6 @@ import ( ) // MaxOutboundTrackerHashes is the maximum number of hashes that can be stored in the outbound transaction tracker -const MaxOutboundTrackerHashes = 5 // AddOutboundTracker adds a new record to the outbound transaction tracker. // only the admin policy account and the observer validators are authorized to broadcast this message without proof. @@ -45,7 +44,6 @@ func (k msgServer) AddOutboundTracker( msg.Nonce, ) } - // tracker submission is only allowed when the cctx is pending if !IsPending(cctx.CrossChainTx) { // garbage tracker (for any reason) is harmful to outTx observation and should be removed if it exists @@ -87,7 +85,7 @@ func (k msgServer) AddOutboundTracker( } // check if max hashes are reached - if len(tracker.HashList) >= MaxOutboundTrackerHashes { + if tracker.MaxReached() { return nil, types.ErrMaxTxOutTrackerHashesReached.Wrapf( "max hashes reached for chain %d, nonce %d, hash number: %d", msg.ChainId, diff --git a/x/crosschain/keeper/msg_server_add_outbound_tracker_test.go b/x/crosschain/keeper/msg_server_add_outbound_tracker_test.go index fb477c9920..3de3bacf0b 100644 --- a/x/crosschain/keeper/msg_server_add_outbound_tracker_test.go +++ b/x/crosschain/keeper/msg_server_add_outbound_tracker_test.go @@ -226,8 +226,8 @@ func TestMsgServer_AddToOutboundTracker(t *testing.T) { observerMock.On("IsNonTombstonedObserver", mock.Anything, mock.Anything).Return(false) keepertest.MockCctxByNonce(t, ctx, *k, observerMock, types.CctxStatus_PendingOutbound, false) - hashes := make([]*types.TxHash, keeper.MaxOutboundTrackerHashes) - for i := 0; i < keeper.MaxOutboundTrackerHashes; i++ { + hashes := make([]*types.TxHash, types.MaxOutboundTrackerHashes) + for i := 0; i < types.MaxOutboundTrackerHashes; i++ { hashes[i] = &types.TxHash{ TxHash: sample.Hash().Hex(), } diff --git a/x/crosschain/keeper/msg_server_refund_aborted_tx.go b/x/crosschain/keeper/msg_server_refund_aborted_tx.go index 22e2b5da81..a4f899b9e2 100644 --- a/x/crosschain/keeper/msg_server_refund_aborted_tx.go +++ b/x/crosschain/keeper/msg_server_refund_aborted_tx.go @@ -75,7 +75,6 @@ func (k msgServer) RefundAbortedCCTX( cctx.CctxStatus.AbortRefunded() k.SetCrossChainTx(ctx, cctx) - return &types.MsgRefundAbortedCCTXResponse{}, nil } diff --git a/x/crosschain/keeper/msg_server_vote_inbound_tx.go b/x/crosschain/keeper/msg_server_vote_inbound_tx.go index 263b7b23bc..0c1b875479 100644 --- a/x/crosschain/keeper/msg_server_vote_inbound_tx.go +++ b/x/crosschain/keeper/msg_server_vote_inbound_tx.go @@ -64,6 +64,7 @@ func (k msgServer) VoteInbound( // vote on inbound ballot // use a temporary context to not commit any ballot state change in case of error tmpCtx, commit := ctx.CacheContext() + finalized, isNew, err := k.zetaObserverKeeper.VoteOnInboundBallot( tmpCtx, msg.SenderChainId, diff --git a/x/crosschain/keeper/msg_server_vote_inbound_tx_test.go b/x/crosschain/keeper/msg_server_vote_inbound_tx_test.go index 7fc5c9be99..57e73a72ae 100644 --- a/x/crosschain/keeper/msg_server_vote_inbound_tx_test.go +++ b/x/crosschain/keeper/msg_server_vote_inbound_tx_test.go @@ -52,7 +52,7 @@ func TestKeeper_VoteInbound(t *testing.T) { msgServer := keeper.NewMsgServerImpl(*k) validatorList := setObservers(t, k, ctx, zk) - to, from := int64(1337), int64(101) + to, from := chains.GoerliLocalnet.ChainId, chains.ZetaChainPrivnet.ChainId supportedChains := zk.ObserverKeeper.GetSupportedChains(ctx) for _, chain := range supportedChains { if chains.IsEthereumChain(chain.ChainId, []chains.Chain{}) { @@ -185,7 +185,7 @@ func TestKeeper_VoteInbound(t *testing.T) { observerMock.On("VoteOnInboundBallot", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything). Return(true, false, errors.New("err")) msgServer := keeper.NewMsgServerImpl(*k) - to, from := int64(1337), int64(101) + to, from := chains.GoerliLocalnet.ChainId, chains.ZetaChainPrivnet.ChainId msg := sample.InboundVote(0, from, to) res, err := msgServer.VoteInbound( @@ -208,7 +208,7 @@ func TestKeeper_VoteInbound(t *testing.T) { zk.ObserverKeeper.SetObserverSet(ctx, observertypes.ObserverSet{ ObserverList: observerSet, }) - to, from := int64(1337), int64(101) + to, from := chains.GoerliLocalnet.ChainId, chains.ZetaChainPrivnet.ChainId supportedChains := zk.ObserverKeeper.GetSupportedChains(ctx) for _, chain := range supportedChains { if chains.IsEthereumChain(chain.ChainId, []chains.Chain{}) { @@ -255,7 +255,7 @@ func TestKeeper_VoteInbound(t *testing.T) { Return(true, false, nil) observerMock.On("GetTSS", mock.Anything).Return(observertypes.TSS{}, false) msgServer := keeper.NewMsgServerImpl(*k) - to, from := int64(1337), int64(101) + to, from := chains.GoerliLocalnet.ChainId, chains.ZetaChainPrivnet.ChainId msg := sample.InboundVote(0, from, to) res, err := msgServer.VoteInbound( diff --git a/x/crosschain/keeper/msg_server_whitelist_erc20.go b/x/crosschain/keeper/msg_server_whitelist_erc20.go index fff9feb92a..155bf06770 100644 --- a/x/crosschain/keeper/msg_server_whitelist_erc20.go +++ b/x/crosschain/keeper/msg_server_whitelist_erc20.go @@ -107,6 +107,7 @@ func (k msgServer) WhitelistERC20( msg.ChainId, ) } + if zrc20Addr == (ethcommon.Address{}) { return nil, errorsmod.Wrapf( types.ErrDeployContract, diff --git a/x/crosschain/keeper/msg_server_whitelist_erc20_test.go b/x/crosschain/keeper/msg_server_whitelist_erc20_test.go index c82261bd05..1b2c03710c 100644 --- a/x/crosschain/keeper/msg_server_whitelist_erc20_test.go +++ b/x/crosschain/keeper/msg_server_whitelist_erc20_test.go @@ -18,6 +18,11 @@ import ( ) func TestKeeper_WhitelistERC20(t *testing.T) { + r := sample.Rand() + firstTokenAddress, err := sample.SolanaAddressFromRand(r) + require.NoError(t, err) + secondTokenAddress, err := sample.SolanaAddressFromRand(r) + require.NoError(t, err) tests := []struct { name string tokenAddress string @@ -36,6 +41,12 @@ func TestKeeper_WhitelistERC20(t *testing.T) { secondTokenAddress: sample.SolanaAddress(t), chainID: getValidSolanaChainID(), }, + { + name: "can deploy and whitelist a spl", + tokenAddress: firstTokenAddress, + secondTokenAddress: secondTokenAddress, + chainID: getValidSolanaChainID(), + }, } for _, tt := range tests { diff --git a/x/crosschain/keeper/refund.go b/x/crosschain/keeper/refund.go index 6c9deef32f..8d1e2da230 100644 --- a/x/crosschain/keeper/refund.go +++ b/x/crosschain/keeper/refund.go @@ -27,7 +27,7 @@ func (k Keeper) RefundAbortedAmountOnZetaChain( case coin.CoinType_ERC20: return k.RefundAmountOnZetaChainERC20(ctx, cctx, refundAddress) default: - return errors.New("unsupported coin type for refund on ZetaChain") + return fmt.Errorf("unsupported coin type for refund on ZetaChain : %s", coinType) } } diff --git a/x/crosschain/simulation/decoders.go b/x/crosschain/simulation/decoders.go index dd2c6c4e07..9c7f743960 100644 --- a/x/crosschain/simulation/decoders.go +++ b/x/crosschain/simulation/decoders.go @@ -19,44 +19,80 @@ func NewDecodeStore(cdc codec.Codec) func(kvA, kvB kv.Pair) string { var cctxA, cctxB types.CrossChainTx cdc.MustUnmarshal(kvA.Value, &cctxA) cdc.MustUnmarshal(kvB.Value, &cctxB) - return fmt.Sprintf("%v\n%v", cctxA, cctxB) + return fmt.Sprintf("key %s value A %v value B %v", types.CCTXKey, cctxA, cctxB) case bytes.Equal(kvA.Key, types.KeyPrefix(types.LastBlockHeightKey)): var lastBlockHeightA, lastBlockHeightB types.LastBlockHeight cdc.MustUnmarshal(kvA.Value, &lastBlockHeightA) cdc.MustUnmarshal(kvB.Value, &lastBlockHeightB) - return fmt.Sprintf("%v\n%v", lastBlockHeightA, lastBlockHeightB) + return fmt.Sprintf( + "key %s value A %v value B %v", + types.LastBlockHeightKey, + lastBlockHeightA, + lastBlockHeightB, + ) case bytes.Equal(kvA.Key, types.KeyPrefix(types.FinalizedInboundsKey)): var finalizedInboundsA, finalizedInboundsB []byte finalizedInboundsA = kvA.Value finalizedInboundsB = kvB.Value - return fmt.Sprintf("%v\n%v", finalizedInboundsA, finalizedInboundsB) + return fmt.Sprintf( + "key %s value A %v value B %v", + types.FinalizedInboundsKey, + finalizedInboundsA, + finalizedInboundsB, + ) case bytes.Equal(kvA.Key, types.KeyPrefix(types.GasPriceKey)): var gasPriceA, gasPriceB types.GasPrice cdc.MustUnmarshal(kvA.Value, &gasPriceA) cdc.MustUnmarshal(kvB.Value, &gasPriceB) - return fmt.Sprintf("%v\n%v", gasPriceA, gasPriceB) + return fmt.Sprintf("key %s value A %v value B %v", types.GasPriceKey, gasPriceA, gasPriceB) case bytes.Equal(kvA.Key, types.KeyPrefix(types.OutboundTrackerKeyPrefix)): var outboundTrackerA, outboundTrackerB types.OutboundTracker cdc.MustUnmarshal(kvA.Value, &outboundTrackerA) cdc.MustUnmarshal(kvB.Value, &outboundTrackerB) - return fmt.Sprintf("%v\n%v", outboundTrackerA, outboundTrackerB) + return fmt.Sprintf( + "key %s value A %v value B %v", + types.OutboundTrackerKeyPrefix, + outboundTrackerA, + outboundTrackerB, + ) case bytes.Equal(kvA.Key, types.KeyPrefix(types.InboundTrackerKeyPrefix)): var inboundTrackerA, inboundTrackerB types.InboundTracker cdc.MustUnmarshal(kvA.Value, &inboundTrackerA) cdc.MustUnmarshal(kvB.Value, &inboundTrackerB) - return fmt.Sprintf("%v\n%v", inboundTrackerA, inboundTrackerB) + return fmt.Sprintf( + "key %s value A %v value B %v", + types.InboundTrackerKeyPrefix, + inboundTrackerA, + inboundTrackerB, + ) case bytes.Equal(kvA.Key, types.KeyPrefix(types.ZetaAccountingKey)): var zetaAccountingA, zetaAccountingB types.ZetaAccounting cdc.MustUnmarshal(kvA.Value, &zetaAccountingA) cdc.MustUnmarshal(kvB.Value, &zetaAccountingB) - return fmt.Sprintf("%v\n%v", zetaAccountingA, zetaAccountingB) + return fmt.Sprintf( + "key %s value A %v value B %v", + types.ZetaAccountingKey, + zetaAccountingA, + zetaAccountingB, + ) case bytes.Equal(kvA.Key, types.KeyPrefix(types.RateLimiterFlagsKey)): var rateLimiterFlagsA, rateLimiterFlagsB types.RateLimiterFlags cdc.MustUnmarshal(kvA.Value, &rateLimiterFlagsA) cdc.MustUnmarshal(kvB.Value, &rateLimiterFlagsB) - return fmt.Sprintf("%v\n%v", rateLimiterFlagsA, rateLimiterFlagsB) + return fmt.Sprintf( + "key %s value A %v value B %v", + types.RateLimiterFlagsKey, + rateLimiterFlagsA, + rateLimiterFlagsB, + ) default: - panic(fmt.Sprintf("invalid crosschain key prefix %X", kvA.Key[:1])) + panic( + fmt.Sprintf( + "invalid crosschain key prefix %X (first 8 bytes: %X)", + kvA.Key[:1], + kvA.Key[:min(8, len(kvA.Key))], + ), + ) } } } diff --git a/x/crosschain/simulation/decoders_test.go b/x/crosschain/simulation/decoders_test.go index 9765aca43d..8c22cf9707 100644 --- a/x/crosschain/simulation/decoders_test.go +++ b/x/crosschain/simulation/decoders_test.go @@ -41,14 +41,37 @@ func TestDecodeStore(t *testing.T) { name string expectedLog string }{ - {"CrossChainTx", fmt.Sprintf("%v\n%v", *cctx, *cctx)}, - {"LastBlockHeight", fmt.Sprintf("%v\n%v", *lastBlockHeight, *lastBlockHeight)}, - {"GasPrice", fmt.Sprintf("%v\n%v", *gasPrice, *gasPrice)}, - {"OutboundTracker", fmt.Sprintf("%v\n%v", outboundTracker, outboundTracker)}, - {"InboundTracker", fmt.Sprintf("%v\n%v", inboundTracker, inboundTracker)}, - {"ZetaAccounting", fmt.Sprintf("%v\n%v", zetaAccounting, zetaAccounting)}, - {"RateLimiterFlags", fmt.Sprintf("%v\n%v", rateLimiterFlags, rateLimiterFlags)}, - {"FinalizedInbounds", fmt.Sprintf("%v\n%v", []byte{1}, []byte{1})}, + {"CrossChainTx", fmt.Sprintf("key %s value A %v value B %v", types.CCTXKey, *cctx, *cctx)}, + { + "LastBlockHeight", + fmt.Sprintf("key %s value A %v value B %v", types.LastBlockHeightKey, *lastBlockHeight, *lastBlockHeight), + }, + {"GasPrice", fmt.Sprintf("key %s value A %v value B %v", types.GasPriceKey, *gasPrice, *gasPrice)}, + { + "OutboundTracker", + fmt.Sprintf( + "key %s value A %v value B %v", + types.OutboundTrackerKeyPrefix, + outboundTracker, + outboundTracker, + ), + }, + { + "InboundTracker", + fmt.Sprintf("key %s value A %v value B %v", types.InboundTrackerKeyPrefix, inboundTracker, inboundTracker), + }, + { + "ZetaAccounting", + fmt.Sprintf("key %s value A %v value B %v", types.ZetaAccountingKey, zetaAccounting, zetaAccounting), + }, + { + "RateLimiterFlags", + fmt.Sprintf("key %s value A %v value B %v", types.RateLimiterFlagsKey, rateLimiterFlags, rateLimiterFlags), + }, + { + "FinalizedInbounds", + fmt.Sprintf("key %s value A %v value B %v", types.FinalizedInboundsKey, []byte{1}, []byte{1}), + }, } for i, tt := range tests { diff --git a/x/crosschain/simulation/operation_abort_stuck_cctx.go b/x/crosschain/simulation/operation_abort_stuck_cctx.go new file mode 100644 index 0000000000..55d0a1cd60 --- /dev/null +++ b/x/crosschain/simulation/operation_abort_stuck_cctx.go @@ -0,0 +1,128 @@ +package simulation + +import ( + "math/rand" + + "github.com/cosmos/cosmos-sdk/baseapp" + sdk "github.com/cosmos/cosmos-sdk/types" + moduletestutil "github.com/cosmos/cosmos-sdk/types/module/testutil" + simtypes "github.com/cosmos/cosmos-sdk/types/simulation" + "github.com/cosmos/cosmos-sdk/x/simulation" + + "github.com/zeta-chain/node/pkg/chains" + "github.com/zeta-chain/node/x/crosschain/keeper" + "github.com/zeta-chain/node/x/crosschain/types" +) + +// SimulateMsgAbortStuckCCTX generates a MsgAbortStuckCCTX with random values +func SimulateMsgAbortStuckCCTX(k keeper.Keeper) simtypes.Operation { + return func(r *rand.Rand, app *baseapp.BaseApp, ctx sdk.Context, accounts []simtypes.Account, _ string, + ) (OperationMsg simtypes.OperationMsg, futureOps []simtypes.FutureOperation, err error) { + // Pick a ethereum chain to abort a stuck cctx + chainID := chains.GoerliLocalnet.ChainId + supportedChains := k.GetObserverKeeper().GetSupportedChains(ctx) + if len(supportedChains) == 0 { + return simtypes.NoOpMsg( + types.ModuleName, + types.TypeMsgAbortStuckCCTX, + "no supported chains found", + ), nil, nil + } + + for _, chain := range supportedChains { + if chains.IsEthereumChain(chain.ChainId, []chains.Chain{}) { + chainID = chain.ChainId + } + } + + policyAccount, err := GetPolicyAccount(ctx, k.GetAuthorityKeeper(), accounts) + if err != nil { + return simtypes.NoOpMsg(types.ModuleName, types.TypeMsgAbortStuckCCTX, err.Error()), nil, nil + } + + authAccount := k.GetAuthKeeper().GetAccount(ctx, policyAccount.Address) + spendable := k.GetBankKeeper().SpendableCoins(ctx, authAccount.GetAddress()) + + tss, found := k.GetObserverKeeper().GetTSS(ctx) + if !found { + return simtypes.NoOpMsg( + types.ModuleName, + types.TypeMsgAbortStuckCCTX, + "no TSS found", + ), nil, nil + } + + pendingNonces, found := k.GetObserverKeeper().GetPendingNonces(ctx, tss.TssPubkey, chainID) + if !found { + return simtypes.NoOpMsg( + types.ModuleName, + types.TypeMsgAbortStuckCCTX, + "no pending nonces found", + ), nil, nil + } + + // If nonce low is the same as nonce high, it means that there are no pending nonces to add trackers for + if pendingNonces.NonceLow == pendingNonces.NonceHigh { + return simtypes.NoOpMsg( + types.ModuleName, + types.TypeMsgAbortStuckCCTX, + "no pending nonces found", + ), nil, nil + } + // Pick a random pending nonce + nonce := 0 + switch { + case pendingNonces.NonceHigh <= 1: + nonce = int(pendingNonces.NonceLow) + case pendingNonces.NonceLow == 0: + nonce = r.Intn(int(pendingNonces.NonceHigh)) + default: + nonce = r.Intn(int(pendingNonces.NonceHigh)-int(pendingNonces.NonceLow)) + int(pendingNonces.NonceLow) + } + + nonceToCctx, found := k.GetObserverKeeper().GetNonceToCctx(ctx, tss.TssPubkey, chainID, int64(nonce)) + if !found { + return simtypes.NoOpMsg( + types.ModuleName, + types.TypeMsgAbortStuckCCTX, + "no cctx found", + ), nil, nil + } + + cctx, found := k.GetCrossChainTx(ctx, nonceToCctx.CctxIndex) + if !found { + return simtypes.NoOpMsg( + types.ModuleName, + types.TypeMsgAbortStuckCCTX, + "no cctx found", + ), nil, nil + } + + msg := types.MsgAbortStuckCCTX{ + Creator: policyAccount.Address.String(), + CctxIndex: cctx.Index, + } + + err = msg.ValidateBasic() + if err != nil { + return simtypes.NoOpMsg(types.ModuleName, msg.Type(), "unable to validate MsgAbortStuckCCTX msg"), nil, err + } + + txCtx := simulation.OperationInput{ + R: r, + App: app, + TxGen: moduletestutil.MakeTestEncodingConfig().TxConfig, + Cdc: nil, + Msg: &msg, + MsgType: msg.Type(), + Context: ctx, + SimAccount: policyAccount, + AccountKeeper: k.GetAuthKeeper(), + Bankkeeper: k.GetBankKeeper(), + ModuleName: types.ModuleName, + CoinsSpentInMsg: spendable, + } + + return simulation.GenAndDeliverTxWithRandFees(txCtx) + } +} diff --git a/x/crosschain/simulation/operation_add_inbound_tracker.go b/x/crosschain/simulation/operation_add_inbound_tracker.go new file mode 100644 index 0000000000..909c5cd9dc --- /dev/null +++ b/x/crosschain/simulation/operation_add_inbound_tracker.go @@ -0,0 +1,72 @@ +package simulation + +import ( + "math/rand" + + "github.com/cosmos/cosmos-sdk/baseapp" + sdk "github.com/cosmos/cosmos-sdk/types" + moduletestutil "github.com/cosmos/cosmos-sdk/types/module/testutil" + simtypes "github.com/cosmos/cosmos-sdk/types/simulation" + "github.com/cosmos/cosmos-sdk/x/simulation" + + "github.com/zeta-chain/node/testutil/sample" + "github.com/zeta-chain/node/x/crosschain/keeper" + "github.com/zeta-chain/node/x/crosschain/types" +) + +// SimulateMsgAddInboundTracker generates a MsgAddInboundTracker with random values +func SimulateMsgAddInboundTracker(k keeper.Keeper) simtypes.Operation { + return func(r *rand.Rand, app *baseapp.BaseApp, ctx sdk.Context, accounts []simtypes.Account, _ string, + ) (OperationMsg simtypes.OperationMsg, futureOps []simtypes.FutureOperation, err error) { + // Get a random account and observer + // If this returns an error, it is likely that the entire observer set has been removed + simAccount, randomObserver, err := GetRandomAccountAndObserver(r, ctx, k, accounts) + if err != nil { + return simtypes.OperationMsg{}, nil, nil + } + authAccount := k.GetAuthKeeper().GetAccount(ctx, simAccount.Address) + spendable := k.GetBankKeeper().SpendableCoins(ctx, authAccount.GetAddress()) + + supportedChains := k.GetObserverKeeper().GetSupportedChains(ctx) + if len(supportedChains) == 0 { + return simtypes.NoOpMsg( + types.ModuleName, + types.TypeMsgAddInboundTracker, + "no supported chains found", + ), nil, nil + } + randomChainID := GetRandomChainID(r, supportedChains) + txHash := sample.HashFromRand(r) + coinType := sample.CoinTypeFromRand(r) + + // Add a new inbound Tracker + msg := types.MsgAddInboundTracker{ + Creator: randomObserver, + ChainId: randomChainID, + TxHash: txHash.String(), + CoinType: coinType, + } + + err = msg.ValidateBasic() + if err != nil { + return simtypes.NoOpMsg(types.ModuleName, msg.Type(), "unable to validate MsgAddInboundTracker"), nil, err + } + + txCtx := simulation.OperationInput{ + R: r, + App: app, + TxGen: moduletestutil.MakeTestEncodingConfig().TxConfig, + Cdc: nil, + Msg: &msg, + MsgType: msg.Type(), + Context: ctx, + SimAccount: simAccount, + AccountKeeper: k.GetAuthKeeper(), + Bankkeeper: k.GetBankKeeper(), + ModuleName: types.ModuleName, + CoinsSpentInMsg: spendable, + } + + return simulation.GenAndDeliverTxWithRandFees(txCtx) + } +} diff --git a/x/crosschain/simulation/operation_add_outbound_tracker.go b/x/crosschain/simulation/operation_add_outbound_tracker.go new file mode 100644 index 0000000000..1640bfcf07 --- /dev/null +++ b/x/crosschain/simulation/operation_add_outbound_tracker.go @@ -0,0 +1,152 @@ +package simulation + +import ( + "math/rand" + + "github.com/cosmos/cosmos-sdk/baseapp" + sdk "github.com/cosmos/cosmos-sdk/types" + moduletestutil "github.com/cosmos/cosmos-sdk/types/module/testutil" + simtypes "github.com/cosmos/cosmos-sdk/types/simulation" + "github.com/cosmos/cosmos-sdk/x/simulation" + + "github.com/zeta-chain/node/pkg/chains" + "github.com/zeta-chain/node/testutil/sample" + "github.com/zeta-chain/node/x/crosschain/keeper" + "github.com/zeta-chain/node/x/crosschain/types" +) + +// SimulateMsgAddOutboundTracker generates a MsgAddOutboundTracker with random values +func SimulateMsgAddOutboundTracker(k keeper.Keeper) simtypes.Operation { + return func(r *rand.Rand, app *baseapp.BaseApp, ctx sdk.Context, accounts []simtypes.Account, _ string, + ) (OperationMsg simtypes.OperationMsg, futureOps []simtypes.FutureOperation, err error) { + chainID := chains.GoerliLocalnet.ChainId + supportedChains := k.GetObserverKeeper().GetSupportedChains(ctx) + if len(supportedChains) == 0 { + return simtypes.NoOpMsg( + types.ModuleName, + types.TypeMsgAddOutboundTracker, + "no supported chains found", + ), nil, nil + } + + for _, chain := range supportedChains { + if chains.IsEthereumChain(chain.ChainId, []chains.Chain{}) { + chainID = chain.ChainId + } + } + // Get a random account and observer + // If this returns an error, it is likely that the entire observer set has been removed + simAccount, randomObserver, err := GetRandomAccountAndObserver(r, ctx, k, accounts) + if err != nil { + return simtypes.OperationMsg{}, nil, nil + } + authAccount := k.GetAuthKeeper().GetAccount(ctx, simAccount.Address) + spendable := k.GetBankKeeper().SpendableCoins(ctx, authAccount.GetAddress()) + + txHash := sample.HashFromRand(r) + tss, found := k.GetObserverKeeper().GetTSS(ctx) + if !found { + return simtypes.NoOpMsg( + types.ModuleName, + types.TypeMsgAddOutboundTracker, + "no TSS found", + ), nil, nil + } + + pendingNonces, found := k.GetObserverKeeper().GetPendingNonces(ctx, tss.TssPubkey, chainID) + if !found { + return simtypes.NoOpMsg( + types.ModuleName, + types.TypeMsgAddOutboundTracker, + "pending nonces object not found", + ), nil, nil + } + + // pick a random nonce from the pending nonces between 0 and nonceLow + // If nonce low is the same as nonce high, it means that there are no pending nonces to add trackers for + if pendingNonces.NonceLow == pendingNonces.NonceHigh { + return simtypes.NoOpMsg( + types.ModuleName, + types.TypeMsgAddOutboundTracker, + "no pending nonces found", + ), nil, nil + } + // Pick a random pending nonce + nonce := 0 + switch { + case pendingNonces.NonceHigh <= 1: + nonce = int(pendingNonces.NonceLow) + case pendingNonces.NonceLow == 0: + nonce = r.Intn(int(pendingNonces.NonceHigh)) + default: + nonce = r.Intn(int(pendingNonces.NonceHigh)-int(pendingNonces.NonceLow)) + int(pendingNonces.NonceLow) + } + + // Verify if the tracker is maxed + tracker, found := k.GetOutboundTracker( + ctx, + chainID, + uint64(nonce), + ) // #nosec G115 - overflow is not an issue here + if found && tracker.MaxReached() { + return simtypes.NoOpMsg( + types.ModuleName, + types.TypeMsgAddOutboundTracker, + "tracker is maxed", + ), nil, nil + } + + // Verify the nonceToCCTX exists + nonceToCCTX, found := k.GetObserverKeeper().GetNonceToCctx(ctx, tss.TssPubkey, chainID, int64(nonce)) + if !found { + return simtypes.NoOpMsg( + types.ModuleName, + types.TypeMsgAddOutboundTracker, + "no nonce to cctx found", + ), nil, nil + } + + // Verify the cctx exists + _, found = k.GetCrossChainTx(ctx, nonceToCCTX.CctxIndex) + if !found { + return simtypes.NoOpMsg( + types.ModuleName, + types.TypeMsgAddOutboundTracker, + "no cctx found for nonce", + ), nil, nil + } + // Add a new inbound Tracker + msg := types.MsgAddOutboundTracker{ + Creator: randomObserver, + ChainId: chainID, + Nonce: uint64(nonce), // #nosec G115 - overflow is not an issue here + TxHash: txHash.String(), + } + + err = msg.ValidateBasic() + if err != nil { + return simtypes.NoOpMsg( + types.ModuleName, + msg.Type(), + "unable to validate MsgAddOutboundTracker msg", + ), nil, err + } + + txCtx := simulation.OperationInput{ + R: r, + App: app, + TxGen: moduletestutil.MakeTestEncodingConfig().TxConfig, + Cdc: nil, + Msg: &msg, + MsgType: msg.Type(), + Context: ctx, + SimAccount: simAccount, + AccountKeeper: k.GetAuthKeeper(), + Bankkeeper: k.GetBankKeeper(), + ModuleName: types.ModuleName, + CoinsSpentInMsg: spendable, + } + + return simulation.GenAndDeliverTxWithRandFees(txCtx) + } +} diff --git a/x/crosschain/simulation/operation_gas_price_voter.go b/x/crosschain/simulation/operation_gas_price_voter.go new file mode 100644 index 0000000000..65970dcc91 --- /dev/null +++ b/x/crosschain/simulation/operation_gas_price_voter.go @@ -0,0 +1,78 @@ +package simulation + +import ( + "math/rand" + + "github.com/cosmos/cosmos-sdk/baseapp" + sdk "github.com/cosmos/cosmos-sdk/types" + moduletestutil "github.com/cosmos/cosmos-sdk/types/module/testutil" + simtypes "github.com/cosmos/cosmos-sdk/types/simulation" + "github.com/cosmos/cosmos-sdk/x/simulation" + + "github.com/zeta-chain/node/pkg/authz" + "github.com/zeta-chain/node/testutil/sample" + "github.com/zeta-chain/node/x/crosschain/keeper" + "github.com/zeta-chain/node/x/crosschain/types" +) + +// SimulateMsgVoteGasPrice generates a MsgVoteGasPrice and delivers it +func SimulateMsgVoteGasPrice(k keeper.Keeper) simtypes.Operation { + return func(r *rand.Rand, app *baseapp.BaseApp, ctx sdk.Context, accounts []simtypes.Account, _ string, + ) (OperationMsg simtypes.OperationMsg, futureOps []simtypes.FutureOperation, err error) { + // Get a random account and observer + // If this returns an error, it is likely that the entire observer set has been removed + simAccount, randomObserver, err := GetRandomAccountAndObserver(r, ctx, k, accounts) + if err != nil { + return simtypes.OperationMsg{}, nil, nil + } + authAccount := k.GetAuthKeeper().GetAccount(ctx, simAccount.Address) + spendable := k.GetBankKeeper().SpendableCoins(ctx, authAccount.GetAddress()) + + supportedChains := k.GetObserverKeeper().GetSupportedChains(ctx) + if len(supportedChains) == 0 { + return simtypes.NoOpMsg( + types.ModuleName, + authz.GasPriceVoter.String(), + "no supported chains found", + ), nil, nil + } + randomChainID := GetRandomChainID(r, supportedChains) + // Vote for random gas price. Gas prices do not use a ballot system, so we can vote directly without having to schedule future operations. + gasPrice := sample.GasPriceFromRand(r, randomChainID) + msg := types.MsgVoteGasPrice{ + Creator: randomObserver, + ChainId: randomChainID, + Price: gasPrice.Prices[0], + PriorityFee: gasPrice.PriorityFees[0], + BlockNumber: uint64(ctx.BlockHeight()) + r.Uint64()%1000, // #nosec G115 - overflow is not a issue here + Supply: sdk.NewInt(r.Int63n(1e18)).String(), + } + + // System contracts are deployed on the first block, so we cannot vote on gas prices before that + if ctx.BlockHeight() <= 1 { + return simtypes.NewOperationMsg(&msg, true, "block height less than 1", nil), nil, nil + } + + err = msg.ValidateBasic() + if err != nil { + return simtypes.NoOpMsg(types.ModuleName, msg.Type(), "unable to validate vote gas price msg"), nil, err + } + + txCtx := simulation.OperationInput{ + R: r, + App: app, + TxGen: moduletestutil.MakeTestEncodingConfig().TxConfig, + Cdc: nil, + Msg: &msg, + MsgType: msg.Type(), + Context: ctx, + SimAccount: simAccount, + AccountKeeper: k.GetAuthKeeper(), + Bankkeeper: k.GetBankKeeper(), + ModuleName: types.ModuleName, + CoinsSpentInMsg: spendable, + } + + return simulation.GenAndDeliverTxWithRandFees(txCtx) + } +} diff --git a/x/crosschain/simulation/operation_refund_aborted_cctx.go b/x/crosschain/simulation/operation_refund_aborted_cctx.go new file mode 100644 index 0000000000..9f46da5933 --- /dev/null +++ b/x/crosschain/simulation/operation_refund_aborted_cctx.go @@ -0,0 +1,84 @@ +package simulation + +import ( + "math/rand" + + "github.com/cosmos/cosmos-sdk/baseapp" + sdk "github.com/cosmos/cosmos-sdk/types" + moduletestutil "github.com/cosmos/cosmos-sdk/types/module/testutil" + simtypes "github.com/cosmos/cosmos-sdk/types/simulation" + "github.com/cosmos/cosmos-sdk/x/simulation" + + "github.com/zeta-chain/node/testutil/sample" + "github.com/zeta-chain/node/x/crosschain/keeper" + "github.com/zeta-chain/node/x/crosschain/types" +) + +// SimulateMsgRefundAbortedCCTX generates a MsgRefundAbortedCCTX with random values +func SimulateMsgRefundAbortedCCTX(k keeper.Keeper, +) simtypes.Operation { + return func(r *rand.Rand, app *baseapp.BaseApp, ctx sdk.Context, accounts []simtypes.Account, _ string, + ) (OperationMsg simtypes.OperationMsg, futureOps []simtypes.FutureOperation, err error) { + // Fetch the account from the auth keeper which can then be used to fetch spendable coins} + policyAccount, err := GetPolicyAccount(ctx, k.GetAuthorityKeeper(), accounts) + if err != nil { + return simtypes.NoOpMsg(types.ModuleName, types.RefundAborted, err.Error()), nil, nil + } + + authAccount := k.GetAuthKeeper().GetAccount(ctx, policyAccount.Address) + spendable := k.GetBankKeeper().SpendableCoins(ctx, authAccount.GetAddress()) + + cctxList := k.GetAllCrossChainTx(ctx) + abortedCctx := types.CrossChainTx{} + abortedCctxFound := false + + for _, cctx := range cctxList { + if cctx.CctxStatus.Status == types.CctxStatus_Aborted { + if !cctx.InboundParams.CoinType.SupportsRefund() { + continue + } + if cctx.CctxStatus.IsAbortRefunded { + continue + } + abortedCctx = cctx + abortedCctxFound = true + break + } + } + if !abortedCctxFound { + return simtypes.NoOpMsg(types.ModuleName, types.RefundAborted, "no aborted cctx found"), nil, nil + } + + msg := types.MsgRefundAbortedCCTX{ + Creator: policyAccount.Address.String(), + CctxIndex: abortedCctx.Index, + RefundAddress: sample.EthAddressFromRand(r).String(), + } + + err = msg.ValidateBasic() + if err != nil { + return simtypes.NoOpMsg( + types.ModuleName, + msg.Type(), + "unable to validate MsgRefundAbortedCCTX msg", + ), nil, err + } + + txCtx := simulation.OperationInput{ + R: r, + App: app, + TxGen: moduletestutil.MakeTestEncodingConfig().TxConfig, + Cdc: nil, + Msg: &msg, + MsgType: msg.Type(), + Context: ctx, + SimAccount: policyAccount, + AccountKeeper: k.GetAuthKeeper(), + Bankkeeper: k.GetBankKeeper(), + ModuleName: types.ModuleName, + CoinsSpentInMsg: spendable, + } + + return simulation.GenAndDeliverTxWithRandFees(txCtx) + } +} diff --git a/x/crosschain/simulation/operation_remove_outbound_tracker.go b/x/crosschain/simulation/operation_remove_outbound_tracker.go new file mode 100644 index 0000000000..6eaa52be81 --- /dev/null +++ b/x/crosschain/simulation/operation_remove_outbound_tracker.go @@ -0,0 +1,72 @@ +package simulation + +import ( + "math/rand" + + "github.com/cosmos/cosmos-sdk/baseapp" + sdk "github.com/cosmos/cosmos-sdk/types" + moduletestutil "github.com/cosmos/cosmos-sdk/types/module/testutil" + simtypes "github.com/cosmos/cosmos-sdk/types/simulation" + "github.com/cosmos/cosmos-sdk/x/simulation" + + "github.com/zeta-chain/node/x/crosschain/keeper" + "github.com/zeta-chain/node/x/crosschain/types" +) + +// SimulateMsgRemoveOutboundTracker generates a MsgRemoveOutboundTracker with random values +func SimulateMsgRemoveOutboundTracker(k keeper.Keeper) simtypes.Operation { + return func(r *rand.Rand, app *baseapp.BaseApp, ctx sdk.Context, accounts []simtypes.Account, _ string, + ) (OperationMsg simtypes.OperationMsg, futureOps []simtypes.FutureOperation, err error) { + policyAccount, err := GetPolicyAccount(ctx, k.GetAuthorityKeeper(), accounts) + if err != nil { + return simtypes.NoOpMsg(types.ModuleName, types.TypeMsgRemoveOutboundTracker, err.Error()), nil, nil + } + + authAccount := k.GetAuthKeeper().GetAccount(ctx, policyAccount.Address) + spendable := k.GetBankKeeper().SpendableCoins(ctx, authAccount.GetAddress()) + + trackers := k.GetAllOutboundTracker(ctx) + + if len(trackers) == 0 { + return simtypes.NoOpMsg( + types.ModuleName, + types.TypeMsgRemoveOutboundTracker, + "no outbound trackers found", + ), nil, nil + } + + randomTracker := trackers[r.Intn(len(trackers))] + + msg := types.MsgRemoveOutboundTracker{ + ChainId: randomTracker.ChainId, + Nonce: randomTracker.Nonce, + Creator: policyAccount.Address.String(), + } + + err = msg.ValidateBasic() + if err != nil { + return simtypes.NoOpMsg( + types.ModuleName, + msg.Type(), + "unable to validate MsgRemoveOutboundTracker", + ), nil, err + } + + txCtx := simulation.OperationInput{ + R: r, + App: app, + TxGen: moduletestutil.MakeTestEncodingConfig().TxConfig, + Cdc: nil, + Msg: &msg, + MsgType: msg.Type(), + Context: ctx, + SimAccount: policyAccount, + AccountKeeper: k.GetAuthKeeper(), + Bankkeeper: k.GetBankKeeper(), + ModuleName: types.ModuleName, + CoinsSpentInMsg: spendable, + } + + return simulation.GenAndDeliverTxWithRandFees(txCtx) + } +} diff --git a/x/crosschain/simulation/operation_update_erc20_pause_status.go b/x/crosschain/simulation/operation_update_erc20_pause_status.go new file mode 100644 index 0000000000..a4fa5b30c0 --- /dev/null +++ b/x/crosschain/simulation/operation_update_erc20_pause_status.go @@ -0,0 +1,120 @@ +package simulation + +import ( + "math/rand" + + "github.com/cosmos/cosmos-sdk/baseapp" + sdk "github.com/cosmos/cosmos-sdk/types" + moduletestutil "github.com/cosmos/cosmos-sdk/types/module/testutil" + simtypes "github.com/cosmos/cosmos-sdk/types/simulation" + "github.com/cosmos/cosmos-sdk/x/simulation" + + "github.com/zeta-chain/node/pkg/chains" + "github.com/zeta-chain/node/x/crosschain/keeper" + "github.com/zeta-chain/node/x/crosschain/types" +) + +// SimulateUpdateERC20CustodyPauseStatus generates a MsgUpdateERC20CustodyPauseStatus with random values and delivers it +func SimulateUpdateERC20CustodyPauseStatus(k keeper.Keeper) simtypes.Operation { + return func(r *rand.Rand, app *baseapp.BaseApp, ctx sdk.Context, accounts []simtypes.Account, _ string, + ) (OperationMsg simtypes.OperationMsg, futureOps []simtypes.FutureOperation, err error) { + policyAccount, err := GetPolicyAccount(ctx, k.GetAuthorityKeeper(), accounts) + if err != nil { + return simtypes.NoOpMsg(types.ModuleName, types.TypeUpdateERC20CustodyPauseStatus, err.Error()), nil, nil + } + + authAccount := k.GetAuthKeeper().GetAccount(ctx, policyAccount.Address) + spendable := k.GetBankKeeper().SpendableCoins(ctx, authAccount.GetAddress()) + + supportedChains := k.GetObserverKeeper().GetSupportedChains(ctx) + if len(supportedChains) == 0 { + return simtypes.NoOpMsg( + types.ModuleName, + types.TypeUpdateERC20CustodyPauseStatus, + "no supported chains found", + ), nil, nil + } + + filteredChains := chains.FilterChains(supportedChains, chains.FilterExternalChains) + + //pick a random chain + randomChain := filteredChains[r.Intn(len(filteredChains))] + + _, found := k.GetObserverKeeper().GetChainNonces(ctx, randomChain.ChainId) + if !found { + return simtypes.NoOpMsg( + types.ModuleName, + types.TypeUpdateERC20CustodyPauseStatus, + "no chain nonces found", + ), nil, nil + } + + _, found = k.GetObserverKeeper().GetTSS(ctx) + if !found { + return simtypes.NoOpMsg( + types.ModuleName, + types.TypeUpdateERC20CustodyPauseStatus, + "no TSS found", + ), nil, nil + } + + _, found = k.GetObserverKeeper().GetChainParamsByChainID(ctx, randomChain.ChainId) + if !found { + return simtypes.NoOpMsg( + types.ModuleName, + types.TypeUpdateERC20CustodyPauseStatus, + "no chain params found", + ), nil, nil + } + medianGasPrice, priorityFee, found := k.GetMedianGasValues(ctx, randomChain.ChainId) + if !found { + return simtypes.NoOpMsg( + types.ModuleName, + types.TypeUpdateERC20CustodyPauseStatus, + "no median gas values found", + ), nil, nil + } + medianGasPrice = medianGasPrice.MulUint64(types.ERC20CustodyPausingGasMultiplierEVM) + priorityFee = priorityFee.MulUint64(types.ERC20CustodyPausingGasMultiplierEVM) + + if priorityFee.GT(medianGasPrice) { + return simtypes.NoOpMsg( + types.ModuleName, + types.TypeUpdateERC20CustodyPauseStatus, + "priorityFee is greater than median gasPrice", + ), nil, nil + } + + msg := types.MsgUpdateERC20CustodyPauseStatus{ + Creator: policyAccount.Address.String(), + ChainId: randomChain.ChainId, + Pause: r.Intn(2) == 0, + } + + err = msg.ValidateBasic() + if err != nil { + return simtypes.NoOpMsg( + types.ModuleName, + msg.Type(), + "unable to validate MsgUpdateERC20CustodyPauseStatus msg", + ), nil, err + } + + txCtx := simulation.OperationInput{ + R: r, + App: app, + TxGen: moduletestutil.MakeTestEncodingConfig().TxConfig, + Cdc: nil, + Msg: &msg, + MsgType: msg.Type(), + Context: ctx, + SimAccount: policyAccount, + AccountKeeper: k.GetAuthKeeper(), + Bankkeeper: k.GetBankKeeper(), + ModuleName: types.ModuleName, + CoinsSpentInMsg: spendable, + } + + return simulation.GenAndDeliverTxWithRandFees(txCtx) + } +} diff --git a/x/crosschain/simulation/operation_update_rate_limiter_flags.go b/x/crosschain/simulation/operation_update_rate_limiter_flags.go new file mode 100644 index 0000000000..f835e57e24 --- /dev/null +++ b/x/crosschain/simulation/operation_update_rate_limiter_flags.go @@ -0,0 +1,60 @@ +package simulation + +import ( + "math/rand" + + "github.com/cosmos/cosmos-sdk/baseapp" + sdk "github.com/cosmos/cosmos-sdk/types" + moduletestutil "github.com/cosmos/cosmos-sdk/types/module/testutil" + simtypes "github.com/cosmos/cosmos-sdk/types/simulation" + "github.com/cosmos/cosmos-sdk/x/simulation" + + "github.com/zeta-chain/node/testutil/sample" + "github.com/zeta-chain/node/x/crosschain/keeper" + "github.com/zeta-chain/node/x/crosschain/types" +) + +// SimulateMsgUpdateRateLimiterFlags generates a MsgUpdateRateLimiterFlags with random values +func SimulateMsgUpdateRateLimiterFlags(k keeper.Keeper) simtypes.Operation { + return func(r *rand.Rand, app *baseapp.BaseApp, ctx sdk.Context, accounts []simtypes.Account, _ string, + ) (OperationMsg simtypes.OperationMsg, futureOps []simtypes.FutureOperation, err error) { + // Fetch the account from the auth keeper which can then be used to fetch spendable coins} + policyAccount, err := GetPolicyAccount(ctx, k.GetAuthorityKeeper(), accounts) + if err != nil { + return simtypes.NoOpMsg(types.ModuleName, types.TypeMsgUpdateRateLimiterFlags, err.Error()), nil, nil + } + authAccount := k.GetAuthKeeper().GetAccount(ctx, policyAccount.Address) + spendable := k.GetBankKeeper().SpendableCoins(ctx, authAccount.GetAddress()) + + msg := types.MsgUpdateRateLimiterFlags{ + Creator: policyAccount.Address.String(), + RateLimiterFlags: sample.RateLimiterFlagsFromRand(r), + } + + err = msg.ValidateBasic() + if err != nil { + return simtypes.NoOpMsg( + types.ModuleName, + msg.Type(), + "unable to validate MsgUpdateRateLimiterFlags msg", + ), nil, err + } + + txCtx := simulation.OperationInput{ + R: r, + App: app, + TxGen: moduletestutil.MakeTestEncodingConfig().TxConfig, + Cdc: nil, + Msg: &msg, + MsgType: msg.Type(), + Context: ctx, + SimAccount: policyAccount, + AccountKeeper: k.GetAuthKeeper(), + Bankkeeper: k.GetBankKeeper(), + ModuleName: types.ModuleName, + CoinsSpentInMsg: spendable, + } + + return simulation.GenAndDeliverTxWithRandFees(txCtx) + } +} diff --git a/x/crosschain/simulation/operation_update_tss_address.go b/x/crosschain/simulation/operation_update_tss_address.go new file mode 100644 index 0000000000..aac70b512b --- /dev/null +++ b/x/crosschain/simulation/operation_update_tss_address.go @@ -0,0 +1,129 @@ +package simulation + +import ( + "math/rand" + + "github.com/cosmos/cosmos-sdk/baseapp" + sdk "github.com/cosmos/cosmos-sdk/types" + moduletestutil "github.com/cosmos/cosmos-sdk/types/module/testutil" + simtypes "github.com/cosmos/cosmos-sdk/types/simulation" + "github.com/cosmos/cosmos-sdk/x/simulation" + + "github.com/zeta-chain/node/testutil/sample" + "github.com/zeta-chain/node/x/crosschain/keeper" + "github.com/zeta-chain/node/x/crosschain/types" + observertypes "github.com/zeta-chain/node/x/observer/types" +) + +// SimulateMsgUpdateTssAddress generates a MsgUpdateTssAddress with random values and delivers it +func SimulateMsgUpdateTssAddress(k keeper.Keeper) simtypes.Operation { + return func(r *rand.Rand, app *baseapp.BaseApp, ctx sdk.Context, accounts []simtypes.Account, _ string, + ) (OperationMsg simtypes.OperationMsg, futureOps []simtypes.FutureOperation, err error) { + policyAccount, err := GetPolicyAccount(ctx, k.GetAuthorityKeeper(), accounts) + if err != nil { + return simtypes.NoOpMsg(types.ModuleName, types.TypeMsgUpdateTssAddress, err.Error()), nil, nil + } + + authAccount := k.GetAuthKeeper().GetAccount(ctx, policyAccount.Address) + spendable := k.GetBankKeeper().SpendableCoins(ctx, authAccount.GetAddress()) + + supportedChains := k.GetChainsSupportingTSSMigration(ctx) + if len(supportedChains) == 0 { + return simtypes.NoOpMsg( + types.ModuleName, + types.TypeMsgUpdateTssAddress, + "no chains found which support tss migration", + ), nil, nil + } + + cctxList := k.GetAllCrossChainTx(ctx) + if len(cctxList) == 0 { + return simtypes.NoOpMsg( + types.ModuleName, + types.TypeMsgUpdateTssAddress, + "no cross chain txs found", + ), nil, nil + } + + // Pick any cctx with status OutboundMined, and use its index for the migration + // We set the fund migrator directly as we are not simulating MsgMigrateTssFunds + minedCCTX := types.CrossChainTx{} + foundMined := false + for _, cctx := range cctxList { + if cctx.CctxStatus.Status == types.CctxStatus_OutboundMined { + minedCCTX = cctx + foundMined = true + break + } + } + if !foundMined { + return simtypes.NoOpMsg( + types.ModuleName, + types.TypeMsgUpdateTssAddress, + "no mined cross chain txs found in mined state", + ), nil, nil + } + + // Thee tss migrator is set for all chains supporting tss migration + for _, chain := range supportedChains { + tssMigrator := observertypes.TssFundMigratorInfo{ + ChainId: chain.ChainId, + MigrationCctxIndex: minedCCTX.Index, + } + k.GetObserverKeeper().SetFundMigrator(ctx, tssMigrator) + } + + oldTss, found := k.GetObserverKeeper().GetTSS(ctx) + if !found { + return simtypes.NoOpMsg( + types.ModuleName, + types.TypeMsgUpdateTssAddress, + "no TSS found", + ), nil, nil + } + + // Set the new TSS to state + newTss, err := sample.TSSFromRand(r) + newTss.FinalizedZetaHeight = oldTss.FinalizedZetaHeight + 10 + newTss.KeyGenZetaHeight = oldTss.KeyGenZetaHeight + 10 + if err != nil { + return simtypes.NoOpMsg( + types.ModuleName, + types.TypeMsgUpdateTssAddress, + err.Error()), + nil, nil + } + k.GetObserverKeeper().SetTSSHistory(ctx, newTss) + + msg := types.MsgUpdateTssAddress{ + Creator: policyAccount.Address.String(), + TssPubkey: newTss.TssPubkey, + } + + err = msg.ValidateBasic() + if err != nil { + return simtypes.NoOpMsg( + types.ModuleName, + msg.Type(), + "unable to validate MsgUpdateTssAddress msg", + ), nil, err + } + + txCtx := simulation.OperationInput{ + R: r, + App: app, + TxGen: moduletestutil.MakeTestEncodingConfig().TxConfig, + Cdc: nil, + Msg: &msg, + MsgType: msg.Type(), + Context: ctx, + SimAccount: policyAccount, + AccountKeeper: k.GetAuthKeeper(), + Bankkeeper: k.GetBankKeeper(), + ModuleName: types.ModuleName, + CoinsSpentInMsg: spendable, + } + + return simulation.GenAndDeliverTxWithRandFees(txCtx) + } +} diff --git a/x/crosschain/simulation/operation_vote_inbound.go b/x/crosschain/simulation/operation_vote_inbound.go new file mode 100644 index 0000000000..714d649aaa --- /dev/null +++ b/x/crosschain/simulation/operation_vote_inbound.go @@ -0,0 +1,190 @@ +package simulation + +import ( + "math" + "math/rand" + + "github.com/cosmos/cosmos-sdk/baseapp" + simtestutil "github.com/cosmos/cosmos-sdk/testutil/sims" + sdk "github.com/cosmos/cosmos-sdk/types" + moduletestutil "github.com/cosmos/cosmos-sdk/types/module/testutil" + simtypes "github.com/cosmos/cosmos-sdk/types/simulation" + "github.com/cosmos/cosmos-sdk/x/simulation" + + "github.com/zeta-chain/node/pkg/authz" + "github.com/zeta-chain/node/pkg/chains" + "github.com/zeta-chain/node/testutil/sample" + "github.com/zeta-chain/node/x/crosschain/keeper" + "github.com/zeta-chain/node/x/crosschain/types" +) + +// operationSimulateVoteInbound generates a MsgVoteInbound with a random vote and delivers it. +func operationSimulateVoteInbound( + k keeper.Keeper, + msg types.MsgVoteInbound, + simAccount simtypes.Account, +) simtypes.Operation { + return func(r *rand.Rand, app *baseapp.BaseApp, ctx sdk.Context, _ []simtypes.Account, _ string, + ) (OperationMsg simtypes.OperationMsg, futureOps []simtypes.FutureOperation, err error) { + // Fetch the account from the auth keeper which can then be used to fetch spendable coins + authAccount := k.GetAuthKeeper().GetAccount(ctx, simAccount.Address) + spendable := k.GetBankKeeper().SpendableCoins(ctx, authAccount.GetAddress()) + + // Generate a transaction with a random fee and deliver it + txCtx := simulation.OperationInput{ + R: r, + App: app, + TxGen: moduletestutil.MakeTestEncodingConfig().TxConfig, + Cdc: nil, + Msg: &msg, + MsgType: msg.Type(), + Context: ctx, + SimAccount: simAccount, + AccountKeeper: k.GetAuthKeeper(), + Bankkeeper: k.GetBankKeeper(), + ModuleName: types.ModuleName, + CoinsSpentInMsg: spendable, + } + + // Generate and deliver the transaction using the function defined by us instead of using the default function provided by the cosmos-sdk + // The main difference between the two functions is that the one defined by us does not error out if the vote fails. + // We need this behaviour as the votes are assigned to future operations, i.e., they are scheduled to be executed in a future block. We do not know at the time of scheduling if the vote will be successful or not. + // There might be multiple reasons for a vote to fail , like the observer not being present in the observer set, the observer not being an observer, etc. + return GenAndDeliverTxWithRandFees(txCtx) + } +} + +// SimulateVoteInbound generates a MsgVoteInbound with random values and delivers it. It also schedules future operations for subsequent votes. +func SimulateVoteInbound(k keeper.Keeper) simtypes.Operation { + observerVotesTransitionMatrix, statePercentageArray, curNumVotesState := ObserverVotesSimulationMatrix() + return func( + r *rand.Rand, + app *baseapp.BaseApp, + ctx sdk.Context, + accs []simtypes.Account, + chainID string, + ) (simtypes.OperationMsg, []simtypes.FutureOperation, error) { + to, from := chains.GoerliLocalnet.ChainId, chains.ZetaChainPrivnet.ChainId + supportedChains := k.GetObserverKeeper().GetSupportedChains(ctx) + for _, chain := range supportedChains { + if chains.IsEthereumChain(chain.ChainId, []chains.Chain{}) { + from = chain.ChainId + } + if chains.IsZetaChain(chain.ChainId, []chains.Chain{}) { + to = chain.ChainId + } + } + + asset, err := GetAsset(ctx, k.GetFungibleKeeper(), from) + if err != nil { + return simtypes.NoOpMsg(types.ModuleName, authz.InboundVoter.String(), "unable to get asset"), nil, err + } + + // Generate a random inbound vote , coin type is randomly selected + msg := sample.InboundVoteFromRand(from, to, r, asset) + + cf, found := k.GetObserverKeeper().GetCrosschainFlags(ctx) + if !found { + return simtypes.NoOpMsg(types.ModuleName, msg.Type(), "crosschain flags not found"), nil, nil + } + + // Return early if inbound is not enabled. + if !cf.IsInboundEnabled { + return simtypes.NoOpMsg(types.ModuleName, msg.Type(), "inbound is not enabled"), nil, nil + } + + // Return early if the inbound has already been finalized. + if k.IsFinalizedInbound(ctx, msg.InboundHash, msg.SenderChainId, msg.EventIndex) { + return simtypes.NoOpMsg(types.ModuleName, msg.Type(), "inbound already finalized"), nil, nil + } + // Pick a random observer to create the ballot + // If this returns an error, it is likely that the entire observer set has been removed + simAccount, firstVoter, err := GetRandomAccountAndObserver(r, ctx, k, accs) + if err != nil { + return simtypes.OperationMsg{}, nil, nil + } + + txGen := moduletestutil.MakeTestEncodingConfig().TxConfig + account := k.GetAuthKeeper().GetAccount(ctx, simAccount.Address) + firstMsg := msg + firstMsg.Creator = firstVoter + + // THe first vote should always create a new ballot + _, found = k.GetObserverKeeper().GetBallot(ctx, firstMsg.Digest()) + if found { + return simtypes.NoOpMsg(types.ModuleName, msg.Type(), "ballot already exists"), nil, nil + } + + err = firstMsg.ValidateBasic() + if err != nil { + return simtypes.NoOpMsg(types.ModuleName, msg.Type(), "unable to validate first inbound vote"), nil, err + } + tx, err := simtestutil.GenSignedMockTx( + r, + txGen, + []sdk.Msg{&firstMsg}, + sdk.Coins{sdk.NewInt64Coin(sdk.DefaultBondDenom, 0)}, + simtestutil.DefaultGenTxGas, + chainID, + []uint64{account.GetAccountNumber()}, + []uint64{account.GetSequence()}, + simAccount.PrivKey, + ) + if err != nil { + return simtypes.NoOpMsg(types.ModuleName, msg.Type(), "unable to generate mock tx"), nil, err + } + + // We can return error here as we can guarantee that the first vote will be successful. + // Since we query the observer set before adding votes + _, _, err = app.SimDeliver(txGen.TxEncoder(), tx) + if err != nil { + return simtypes.NoOpMsg(types.ModuleName, msg.Type(), "unable to deliver tx"), nil, err + } + + opMsg := simtypes.NewOperationMsg(&msg, true, "", nil) + + // Add subsequent votes + observerSet, found := k.GetObserverKeeper().GetObserverSet(ctx) + if !found { + return simtypes.NoOpMsg(types.ModuleName, authz.InboundVoter.String(), "observer set not found"), nil, nil + } + + // 1) Schedule operations for votes + // 1.1) first pick a number of people to vote. + curNumVotesState = observerVotesTransitionMatrix.NextState(r, curNumVotesState) + numVotes := int(math.Ceil(float64(len(observerSet.ObserverList)) * statePercentageArray[curNumVotesState])) + + // 1.2) select who votes + whoVotes := r.Perm(len(observerSet.ObserverList)) + whoVotes = whoVotes[:numVotes] + + var fops []simtypes.FutureOperation + + for _, observerIdx := range whoVotes { + observerAddress := observerSet.ObserverList[observerIdx] + // firstVoter has already voted. + if observerAddress == firstVoter { + continue + } + observerAccount, err := GetObserverAccount(observerAddress, accs) + if err != nil { + continue + } + // 1.3) schedule the vote + votingMsg := msg + votingMsg.Creator = observerAddress + + e := votingMsg.ValidateBasic() + if e != nil { + return simtypes.NoOpMsg(types.ModuleName, msg.Type(), "unable to validate voting msg"), nil, e + } + fops = append(fops, simtypes.FutureOperation{ + // Submit all subsequent votes in the next block. + // We can consider adding a random block height between 1 and ballot maturity blocks in the future. + BlockHeight: int(ctx.BlockHeight() + 1), + Op: operationSimulateVoteInbound(k, votingMsg, observerAccount), + }) + } + return opMsg, fops, nil + } +} diff --git a/x/crosschain/simulation/operation_vote_outbound.go b/x/crosschain/simulation/operation_vote_outbound.go new file mode 100644 index 0000000000..bf4dbaf5dd --- /dev/null +++ b/x/crosschain/simulation/operation_vote_outbound.go @@ -0,0 +1,226 @@ +package simulation + +import ( + "fmt" + "math" + "math/rand" + + "github.com/cosmos/cosmos-sdk/baseapp" + simtestutil "github.com/cosmos/cosmos-sdk/testutil/sims" + sdk "github.com/cosmos/cosmos-sdk/types" + moduletestutil "github.com/cosmos/cosmos-sdk/types/module/testutil" + simtypes "github.com/cosmos/cosmos-sdk/types/simulation" + "github.com/cosmos/cosmos-sdk/x/simulation" + ethcommon "github.com/ethereum/go-ethereum/common" + ethcrypto "github.com/ethereum/go-ethereum/crypto" + + "github.com/zeta-chain/node/pkg/authz" + "github.com/zeta-chain/node/pkg/chains" + "github.com/zeta-chain/node/testutil/sample" + "github.com/zeta-chain/node/x/crosschain/keeper" + "github.com/zeta-chain/node/x/crosschain/types" +) + +func operationSimulateVoteOutbound( + k keeper.Keeper, + msg types.MsgVoteOutbound, + simAccount simtypes.Account, +) simtypes.Operation { + return func(r *rand.Rand, app *baseapp.BaseApp, ctx sdk.Context, _ []simtypes.Account, _ string, + ) (OperationMsg simtypes.OperationMsg, futureOps []simtypes.FutureOperation, err error) { + // Fetch the account from the auth keeper which can then be used to fetch spendable coins + authAccount := k.GetAuthKeeper().GetAccount(ctx, simAccount.Address) + spendable := k.GetBankKeeper().SpendableCoins(ctx, authAccount.GetAddress()) + + // Generate a transaction with a random fee and deliver it + txCtx := simulation.OperationInput{ + R: r, + App: app, + TxGen: moduletestutil.MakeTestEncodingConfig().TxConfig, + Cdc: nil, + Msg: &msg, + MsgType: msg.Type(), + Context: ctx, + SimAccount: simAccount, + AccountKeeper: k.GetAuthKeeper(), + Bankkeeper: k.GetBankKeeper(), + ModuleName: types.ModuleName, + CoinsSpentInMsg: spendable, + } + + // Generate and deliver the transaction using the function defined by us instead of using the default function provided by the cosmos-sdk + // The main difference between the two functions is that the one defined by us does not error out if the vote fails. + // We need this behaviour as the votes are assigned to future operations, i.e., they are scheduled to be executed in a future block. We do not know at the time of scheduling if the vote will be successful or not. + // There might be multiple reasons for a vote to fail , like the observer not being present in the observer set, the observer not being an observer, etc. + return GenAndDeliverTxWithRandFees(txCtx) + } +} + +// SimulateVoteOutbound generates a MsgVoteOutbound with random values and delivers it. +//It also schedules future operations for subsequent votes. + +func SimulateVoteOutbound(k keeper.Keeper) simtypes.Operation { + defaultVote := chains.ReceiveStatus_success + alternativeVote := chains.ReceiveStatus_failed + observerVotesTransitionMatrix, statePercentageArray, curNumVotesState := ObserverVotesSimulationMatrix() + ballotVotesTransitionMatrix, yesVotePercentageArray, ballotVotesState := BallotVoteSimulationMatrix() + return func( + r *rand.Rand, + app *baseapp.BaseApp, + ctx sdk.Context, + accs []simtypes.Account, + chainID string, + ) (simtypes.OperationMsg, []simtypes.FutureOperation, error) { + to, from := chains.GoerliLocalnet.ChainId, chains.ZetaChainPrivnet.ChainId + supportedChains := k.GetObserverKeeper().GetSupportedChains(ctx) + for _, chain := range supportedChains { + if chains.IsEthereumChain(chain.ChainId, []chains.Chain{}) { + to = chain.ChainId + } + if chains.IsZetaChain(chain.ChainId, []chains.Chain{}) { + from = chain.ChainId + } + } + + _, creator, err := GetRandomAccountAndObserver(r, ctx, k, accs) + if err != nil { + return simtypes.OperationMsg{}, nil, nil + } + index := ethcrypto.Keccak256Hash([]byte(fmt.Sprintf("%d", r.Int63()))).Hex() + + tss, found := k.GetObserverKeeper().GetTSS(ctx) + if !found { + return simtypes.NoOpMsg(types.ModuleName, authz.OutboundVoter.String(), "tss not found"), nil, nil + } + + asset, err := GetAsset(ctx, k.GetFungibleKeeper(), from) + if err != nil { + return simtypes.NoOpMsg(types.ModuleName, authz.OutboundVoter.String(), "unable to get asset"), nil, err + } + + // Generate a new cctx and save it , which can be used to finalize the outbound + cctx := sample.CCTXfromRand(r, creator, index, to, from, tss.TssPubkey, asset) + msg := types.MsgVoteOutbound{ + CctxHash: cctx.Index, + OutboundTssNonce: cctx.GetCurrentOutboundParam().TssNonce, + OutboundChain: cctx.GetCurrentOutboundParam().ReceiverChainId, + Status: defaultVote, + Creator: cctx.Creator, + ObservedOutboundHash: ethcommon.BytesToHash(sample.EthAddressFromRand(r).Bytes()).String(), + ValueReceived: cctx.GetCurrentOutboundParam().Amount, + ObservedOutboundBlockHeight: cctx.GetCurrentOutboundParam().ObservedExternalHeight, + ObservedOutboundEffectiveGasPrice: cctx.GetCurrentOutboundParam().EffectiveGasPrice, + ObservedOutboundGasUsed: cctx.GetCurrentOutboundParam().GasUsed, + CoinType: cctx.InboundParams.CoinType, + } + + err = k.SetObserverOutboundInfo(ctx, to, &cctx) + if err != nil { + return simtypes.NoOpMsg(types.ModuleName, msg.Type(), "unable to set observer outbound info"), nil, err + } + + msg.OutboundTssNonce = cctx.GetCurrentOutboundParam().TssNonce + k.SaveCCTXUpdate(ctx, cctx, tss.TssPubkey) + + // Pick a random observer to create the ballot + // If this returns an error, it is likely that the entire observer set has been removed + simAccount, firstVoter, err := GetRandomAccountAndObserver(r, ctx, k, accs) + if err != nil { + return simtypes.OperationMsg{}, nil, nil + } + + txGen := moduletestutil.MakeTestEncodingConfig().TxConfig + account := k.GetAuthKeeper().GetAccount(ctx, simAccount.Address) + firstMsg := msg + firstMsg.Creator = firstVoter + + // THe first vote should always create a new ballot + _, found = k.GetObserverKeeper().GetBallot(ctx, firstMsg.Digest()) + if found { + return simtypes.NoOpMsg(types.ModuleName, msg.Type(), "ballot already exists"), nil, nil + } + + err = firstMsg.ValidateBasic() + if err != nil { + return simtypes.NoOpMsg(types.ModuleName, msg.Type(), "unable to validate first outbound vote"), nil, err + } + + tx, err := simtestutil.GenSignedMockTx( + r, + txGen, + []sdk.Msg{&firstMsg}, + sdk.Coins{sdk.NewInt64Coin(sdk.DefaultBondDenom, 0)}, + simtestutil.DefaultGenTxGas, + chainID, + []uint64{account.GetAccountNumber()}, + []uint64{account.GetSequence()}, + simAccount.PrivKey, + ) + if err != nil { + return simtypes.NoOpMsg(types.ModuleName, msg.Type(), "unable to generate mock tx"), nil, err + } + + // We can return error here as we can guarantee that the first vote will be successful. + // Since we query the observer set before adding votes + _, _, err = app.SimDeliver(txGen.TxEncoder(), tx) + if err != nil { + return simtypes.NoOpMsg(types.ModuleName, msg.Type(), "unable to deliver tx"), nil, err + } + + opMsg := simtypes.NewOperationMsg(&msg, true, "", nil) + + // Add subsequent votes + observerSet, found := k.GetObserverKeeper().GetObserverSet(ctx) + if !found { + return simtypes.NoOpMsg(types.ModuleName, authz.OutboundVoter.String(), "observer set not found"), nil, nil + } + + // 1) Schedule operations for votes + // 1.1) first pick a number of people to vote. + curNumVotesState = observerVotesTransitionMatrix.NextState(r, curNumVotesState) + numVotes := int(math.Ceil(float64(len(observerSet.ObserverList)) * statePercentageArray[curNumVotesState])) + + // 1.2) select who votes + whoVotes := r.Perm(len(observerSet.ObserverList)) + whoVotes = whoVotes[:numVotes] + + var fops []simtypes.FutureOperation + + ballotVotesState = ballotVotesTransitionMatrix.NextState(r, ballotVotesState) + yesVotePercentage := yesVotePercentageArray[ballotVotesState] + numberOfYesVotes := int(math.Ceil(float64(numVotes) * yesVotePercentage)) + vote := defaultVote + + for voteCount, observerIdx := range whoVotes { + if voteCount == numberOfYesVotes { + vote = alternativeVote + } + observerAddress := observerSet.ObserverList[observerIdx] + // firstVoter has already voted. + if observerAddress == firstVoter { + continue + } + observerAccount, err := GetObserverAccount(observerAddress, accs) + if err != nil { + continue + } + // 1.3) schedule the vote + votingMsg := msg + votingMsg.Creator = observerAddress + votingMsg.Status = vote + + e := votingMsg.ValidateBasic() + if e != nil { + return simtypes.NoOpMsg(types.ModuleName, msg.Type(), "unable to validate voting msg"), nil, e + } + + fops = append(fops, simtypes.FutureOperation{ + // Submit all subsequent votes in the next block. + // We can consider adding a random block height between 1 and ballot maturity blocks in the future. + BlockHeight: int(ctx.BlockHeight() + 1), + Op: operationSimulateVoteOutbound(k, votingMsg, observerAccount), + }) + } + return opMsg, fops, nil + } +} diff --git a/x/crosschain/simulation/operation_whitelist_erc20.go b/x/crosschain/simulation/operation_whitelist_erc20.go new file mode 100644 index 0000000000..b67cad6a44 --- /dev/null +++ b/x/crosschain/simulation/operation_whitelist_erc20.go @@ -0,0 +1,147 @@ +package simulation + +import ( + "math/rand" + + "github.com/cosmos/cosmos-sdk/baseapp" + sdk "github.com/cosmos/cosmos-sdk/types" + moduletestutil "github.com/cosmos/cosmos-sdk/types/module/testutil" + simtypes "github.com/cosmos/cosmos-sdk/types/simulation" + "github.com/cosmos/cosmos-sdk/x/simulation" + + "github.com/zeta-chain/node/pkg/chains" + "github.com/zeta-chain/node/testutil/sample" + "github.com/zeta-chain/node/x/crosschain/keeper" + "github.com/zeta-chain/node/x/crosschain/types" +) + +// SimulateMsgWhitelistERC20 generates a MsgWhitelistERC20 with random values and delivers it +func SimulateMsgWhitelistERC20(k keeper.Keeper) simtypes.Operation { + return func(r *rand.Rand, app *baseapp.BaseApp, ctx sdk.Context, accounts []simtypes.Account, _ string, + ) (OperationMsg simtypes.OperationMsg, futureOps []simtypes.FutureOperation, err error) { + policyAccount, err := GetPolicyAccount(ctx, k.GetAuthorityKeeper(), accounts) + if err != nil { + return simtypes.NoOpMsg(types.ModuleName, types.TypeMsgWhitelistERC20, err.Error()), nil, nil + } + + authAccount := k.GetAuthKeeper().GetAccount(ctx, policyAccount.Address) + spendable := k.GetBankKeeper().SpendableCoins(ctx, authAccount.GetAddress()) + + supportedChains := k.GetObserverKeeper().GetSupportedChains(ctx) + if len(supportedChains) == 0 { + return simtypes.NoOpMsg( + types.ModuleName, + types.TypeMsgWhitelistERC20, + "no supported chains found", + ), nil, nil + } + + filteredChains := chains.FilterChains(supportedChains, chains.FilterByVM(chains.Vm_evm)) + if len(filteredChains) == 0 { + return simtypes.NoOpMsg( + types.ModuleName, + types.TypeMsgWhitelistERC20, + "no EVM-compatible chains found", + ), nil, nil + } + + //pick a random chain + // Keep the switch case to add solana support in future + // TODO : https://github.com/zeta-chain/node/issues/3287 + randomChain := filteredChains[r.Intn(len(filteredChains))] + var tokenAddress string + switch { + case randomChain.IsEVMChain(): + tokenAddress = sample.EthAddressFromRand(r).String() + default: + return simtypes.NoOpMsg(types.ModuleName, types.TypeMsgWhitelistERC20, "unsupported chain"), nil, nil + } + + _, found := k.GetObserverKeeper().GetTSS(ctx) + if !found { + return simtypes.NoOpMsg( + types.ModuleName, + types.TypeMsgWhitelistERC20, + "no TSS found", + ), nil, nil + } + + _, found = k.GetObserverKeeper().GetChainParamsByChainID(ctx, randomChain.ChainId) + if !found { + return simtypes.NoOpMsg( + types.ModuleName, + types.TypeMsgWhitelistERC20, + "no chain params found", + ), nil, nil + } + + medianGasPrice, priorityFee, isFound := k.GetMedianGasValues(ctx, randomChain.ChainId) + if !isFound { + return simtypes.NoOpMsg( + types.ModuleName, + types.TypeMsgWhitelistERC20, + "median gas price not found", + ), nil, nil + } + + medianGasPrice = medianGasPrice.MulUint64(types.ERC20CustodyPausingGasMultiplierEVM) + priorityFee = priorityFee.MulUint64(types.ERC20CustodyPausingGasMultiplierEVM) + + if priorityFee.GT(medianGasPrice) { + return simtypes.NoOpMsg( + types.ModuleName, + types.TypeMsgWhitelistERC20, + "priorityFee is greater than median gasPrice", + ), nil, nil + } + + foreignCoins := k.GetFungibleKeeper().GetAllForeignCoins(ctx) + for _, fCoin := range foreignCoins { + if fCoin.Asset == tokenAddress && fCoin.ForeignChainId == randomChain.ChainId { + return simtypes.NoOpMsg( + types.ModuleName, + types.TypeMsgWhitelistERC20, + "ERC20 already whitelisted", + ), nil, nil + } + } + + gasLimit := r.Int63n(1000000000) + 1 + nameLength := r.Intn(97) + 3 + msg := types.MsgWhitelistERC20{ + Creator: policyAccount.Address.String(), + ChainId: randomChain.ChainId, + Erc20Address: tokenAddress, + GasLimit: gasLimit, + Decimals: 18, + Name: sample.StringRandom(r, nameLength), + Symbol: sample.StringRandom(r, 3), + } + + err = msg.ValidateBasic() + if err != nil { + return simtypes.NoOpMsg( + types.ModuleName, + msg.Type(), + "unable to validate MsgWhitelistERC20", + ), nil, err + } + + txCtx := simulation.OperationInput{ + R: r, + App: app, + TxGen: moduletestutil.MakeTestEncodingConfig().TxConfig, + Cdc: nil, + Msg: &msg, + MsgType: msg.Type(), + Context: ctx, + SimAccount: policyAccount, + AccountKeeper: k.GetAuthKeeper(), + Bankkeeper: k.GetBankKeeper(), + ModuleName: types.ModuleName, + CoinsSpentInMsg: spendable, + } + + return simulation.GenAndDeliverTxWithRandFees(txCtx) + } +} diff --git a/x/crosschain/simulation/operations.go b/x/crosschain/simulation/operations.go index 96b04970b9..5d2403a64f 100644 --- a/x/crosschain/simulation/operations.go +++ b/x/crosschain/simulation/operations.go @@ -2,26 +2,24 @@ package simulation import ( "fmt" - "math" "math/rand" - "github.com/cosmos/cosmos-sdk/baseapp" "github.com/cosmos/cosmos-sdk/codec" simtestutil "github.com/cosmos/cosmos-sdk/testutil/sims" sdk "github.com/cosmos/cosmos-sdk/types" - moduletestutil "github.com/cosmos/cosmos-sdk/types/module/testutil" simtypes "github.com/cosmos/cosmos-sdk/types/simulation" "github.com/cosmos/cosmos-sdk/x/simulation" - "github.com/zeta-chain/node/pkg/authz" "github.com/zeta-chain/node/pkg/chains" - "github.com/zeta-chain/node/testutil/sample" "github.com/zeta-chain/node/x/crosschain/keeper" "github.com/zeta-chain/node/x/crosschain/types" observerTypes "github.com/zeta-chain/node/x/observer/types" ) // Simulation operation weights constants +// Operation weights are used by the simulation program to simulate the weight of different operations. +// This decides what percentage of a certain type of operation is part of a block. +// Based on the weights assigned in the cosmos sdk modules , 100 seems to the max weight used , and therefore guarantees that at least one operation of that type is present in a block. // Operation weights are used by the `SimulateFromSeed` // function to pick a random operation based on the weights.The functions with higher weights are more likely to be picked. @@ -30,55 +28,57 @@ import ( // Based on the weights assigned in the cosmos sdk modules, // 100 seems to the max weight used,and we should use relative weights // to signify the number of each operation in a block. - -// TODO Add more details to comment based on what the number represents in terms of percentage of operations in a block -// https://github.com/zeta-chain/node/issues/3100 const ( - DefaultWeightMsgAddOutboundTracker = 50 - DefaultWeightAddInboundTracker = 50 - DefaultWeightRemoveOutboundTracker = 5 - DefaultWeightVoteGasPrice = 100 - DefaultWeightVoteOutbound = 50 - DefaultWeightVoteInbound = 100 - DefaultWeightWhitelistERC20 = 1 - DefaultWeightMigrateTssFunds = 1 - DefaultWeightUpdateTssAddress = 1 - DefaultWeightAbortStuckCCTX = 10 - DefaultWeightUpdateRateLimiterFlags = 1 - - OpWeightMsgAddOutboundTracker = "op_weight_msg_add_outbound_tracker" // #nosec G101 not a hardcoded credential - OpWeightAddInboundTracker = "op_weight_msg_add_inbound_tracker" // #nosec G101 not a hardcoded credential - OpWeightRemoveOutboundTracker = "op_weight_msg_remove_outbound_tracker" // #nosec G101 not a hardcoded credential - OpWeightVoteGasPrice = "op_weight_msg_vote_gas_price" // #nosec G101 not a hardcoded credential - OpWeightVoteOutbound = "op_weight_msg_vote_outbound" // #nosec G101 not a hardcoded credential - OpWeightVoteInbound = "op_weight_msg_vote_inbound" // #nosec G101 not a hardcoded credential - OpWeightWhitelistERC20 = "op_weight_msg_whitelist_erc20" // #nosec G101 not a hardcoded credential - OpWeightMigrateTssFunds = "op_weight_msg_migrate_tss_funds" // #nosec G101 not a hardcoded credential - OpWeightUpdateTssAddress = "op_weight_msg_update_tss_address" // #nosec G101 not a hardcoded credential - OpWeightAbortStuckCCTX = "op_weight_msg_abort_stuck_cctx" // #nosec G101 not a hardcoded credential - OpWeightUpdateRateLimiterFlags = "op_weight_msg_update_rate_limiter_flags" // #nosec G101 not a hardcoded credential - + DefaultWeightAddOutboundTracker = 10 + DefaultWeightAddInboundTracker = 10 + DefaultWeightRemoveOutboundTracker = 10 + DefaultWeightVoteGasPrice = 50 + DefaultWeightVoteOutbound = 10 + DefaultWeightVoteInbound = 10 + DefaultWeightWhitelistERC20 = 10 + DefaultWeightMigrateTssFunds = 1 + DefaultWeightUpdateTssAddress = 10 + DefaultWeightAbortStuckCCTX = 5 + DefaultWeightUpdateRateLimiterFlags = 10 + DefaultWeightRefundAbortedCCTX = 10 + DefaultWeightUpdateERC20CustodyPauseStatus = 10 + + OpWeightMsgAddOutboundTracker = "op_weight_msg_add_outbound_tracker" // #nosec G101 not a hardcoded credential + OpWeightAddInboundTracker = "op_weight_msg_add_inbound_tracker" // #nosec G101 not a hardcoded credential + OpWeightRemoveOutboundTracker = "op_weight_msg_remove_outbound_tracker" // #nosec G101 not a hardcoded credential + OpWeightVoteGasPrice = "op_weight_msg_vote_gas_price" // #nosec G101 not a hardcoded credential + OpWeightVoteOutbound = "op_weight_msg_vote_outbound" // #nosec G101 not a hardcoded credential + OpWeightVoteInbound = "op_weight_msg_vote_inbound" // #nosec G101 not a hardcoded credential + OpWeightWhitelistERC20 = "op_weight_msg_whitelist_erc20" // #nosec G101 not a hardcoded credential + OpWeightMigrateTssFunds = "op_weight_msg_migrate_tss_funds" // #nosec G101 not a hardcoded credential + OpWeightUpdateTssAddress = "op_weight_msg_update_tss_address" // #nosec G101 not a hardcoded credential + OpWeightAbortStuckCCTX = "op_weight_msg_abort_stuck_cctx" // #nosec G101 not a hardcoded credential + OpWeightUpdateRateLimiterFlags = "op_weight_msg_update_rate_limiter_flags" // #nosec G101 not a hardcoded credential + OpWeightRefundAbortedCCTX = "op_weight_msg_refund_aborted_cctx" // #nosec G101 not a hardcoded credential + OpWeightUpdateERC20CustodyPauseStatus = "op_weight_msg_update_erc20_custody_pause_status" // #nosec G101 not a hardcoded credential ) func WeightedOperations( appParams simtypes.AppParams, cdc codec.JSONCodec, k keeper.Keeper) simulation.WeightedOperations { var ( - weightMsgAddOutboundTracker int - weightAddInboundTracker int - weightRemoveOutboundTracker int - weightVoteGasPrice int - weightVoteOutbound int - weightVoteInbound int - weightWhitelistERC20 int - weightMigrateTssFunds int - weightUpdateTssAddress int - weightAbortStuckCCTX int - weightUpdateRateLimiterFlags int + weightAddOutboundTracker int + weightAddInboundTracker int + weightRemoveOutboundTracker int + weightVoteGasPrice int + weightVoteOutbound int + weightVoteInbound int + weightWhitelistERC20 int + weightMigrateTssFunds int + weightUpdateTssAddress int + weightAbortStuckCCTX int + weightUpdateRateLimiterFlags int + weightRefundAbortedCCTX int + weightUpdateERC20CustodyPauseStatus int ) - appParams.GetOrGenerate(cdc, OpWeightMsgAddOutboundTracker, &weightMsgAddOutboundTracker, nil, + appParams.GetOrGenerate(cdc, OpWeightMsgAddOutboundTracker, &weightAddOutboundTracker, nil, func(_ *rand.Rand) { - weightMsgAddOutboundTracker = DefaultWeightMsgAddOutboundTracker + weightAddOutboundTracker = DefaultWeightAddOutboundTracker }, ) @@ -142,6 +142,18 @@ func WeightedOperations( }, ) + appParams.GetOrGenerate(cdc, OpWeightRefundAbortedCCTX, &weightRefundAbortedCCTX, nil, + func(_ *rand.Rand) { + weightRefundAbortedCCTX = DefaultWeightRefundAbortedCCTX + }, + ) + + appParams.GetOrGenerate(cdc, OpWeightUpdateERC20CustodyPauseStatus, &weightUpdateERC20CustodyPauseStatus, nil, + func(_ *rand.Rand) { + weightUpdateERC20CustodyPauseStatus = DefaultWeightUpdateERC20CustodyPauseStatus + }, + ) + return simulation.WeightedOperations{ simulation.NewWeightedOperation( weightVoteGasPrice, @@ -151,239 +163,46 @@ func WeightedOperations( weightVoteInbound, SimulateVoteInbound(k), ), - } -} - -// operationSimulateVoteInbound generates a MsgVoteInbound with a random vote and delivers it. -func operationSimulateVoteInbound( - k keeper.Keeper, - msg types.MsgVoteInbound, - simAccount simtypes.Account, -) simtypes.Operation { - return func(r *rand.Rand, app *baseapp.BaseApp, ctx sdk.Context, _ []simtypes.Account, _ string, - ) (OperationMsg simtypes.OperationMsg, futureOps []simtypes.FutureOperation, err error) { - // Fetch the account from the auth keeper which can then be used to fetch spendable coins - authAccount := k.GetAuthKeeper().GetAccount(ctx, simAccount.Address) - spendable := k.GetBankKeeper().SpendableCoins(ctx, authAccount.GetAddress()) - - // Generate a transaction with a random fee and deliver it - txCtx := simulation.OperationInput{ - R: r, - App: app, - TxGen: moduletestutil.MakeTestEncodingConfig().TxConfig, - Cdc: nil, - Msg: &msg, - MsgType: msg.Type(), - Context: ctx, - SimAccount: simAccount, - AccountKeeper: k.GetAuthKeeper(), - Bankkeeper: k.GetBankKeeper(), - ModuleName: types.ModuleName, - CoinsSpentInMsg: spendable, - } - - // Generate and deliver the transaction using the function defined by us instead of using the default function provided by the cosmos-sdk - // The main difference between the two functions is that the one defined by us does not error out if the vote fails. - // We need this behaviour as the votes are assigned to future operations, i.e., they are scheduled to be executed in a future block. We do not know at the time of scheduling if the vote will be successful or not. - // There might be multiple reasons for a vote to fail , like the observer not being present in the observer set, the observer not being an observer, etc. - return GenAndDeliverTxWithRandFees(txCtx) - } -} - -func SimulateVoteInbound(k keeper.Keeper) simtypes.Operation { - // The states are: - // column 1: All observers vote - // column 2: 90% vote - // column 3: 75% vote - // column 4: 40% vote - // column 5: 15% vote - // column 6: noone votes - // All columns sum to 100 for simplicity, but this is arbitrary and can be changed - numVotesTransitionMatrix, _ := simulation.CreateTransitionMatrix([][]int{ - {20, 10, 0, 0, 0, 0}, - {55, 50, 20, 10, 0, 0}, - {25, 25, 30, 25, 30, 15}, - {0, 15, 30, 25, 30, 30}, - {0, 0, 20, 30, 30, 30}, - {0, 0, 0, 10, 10, 25}, - }) - - statePercentageArray := []float64{1, .9, .75, .4, .15, 0} - curNumVotesState := 1 - - return func( - r *rand.Rand, - app *baseapp.BaseApp, - ctx sdk.Context, - accs []simtypes.Account, - chainID string, - ) (simtypes.OperationMsg, []simtypes.FutureOperation, error) { - // TODO : randomize these values - // Right now we use a constant value for cctx creation , this is the same as the one used in unit tests for the successful condition. - // TestKeeper_VoteInbound/successfully vote on evm deposit - // But this can improved by adding more randomization - - to, from := int64(1337), int64(101) - supportedChains := k.GetObserverKeeper().GetSupportedChains(ctx) - for _, chain := range supportedChains { - if chains.IsEthereumChain(chain.ChainId, []chains.Chain{}) { - from = chain.ChainId - } - if chains.IsZetaChain(chain.ChainId, []chains.Chain{}) { - to = chain.ChainId - } - } - - msg := sample.InboundVoteFromRand(0, from, to, r) - - // Pick a random observer to create the ballot - // If this returns an error, it is likely that the entire observer set has been removed - simAccount, firstVoter, err := GetRandomAccountAndObserver(r, ctx, k, accs) - if err != nil { - return simtypes.OperationMsg{}, nil, nil - } - - txGen := moduletestutil.MakeTestEncodingConfig().TxConfig - account := k.GetAuthKeeper().GetAccount(ctx, simAccount.Address) - firstMsg := msg - firstMsg.Creator = firstVoter - - err = firstMsg.ValidateBasic() - if err != nil { - return simtypes.NoOpMsg(types.ModuleName, msg.Type(), "unable to validate first inbound vote"), nil, err - } - - tx, err := simtestutil.GenSignedMockTx( - r, - txGen, - []sdk.Msg{&firstMsg}, - sdk.Coins{sdk.NewInt64Coin(sdk.DefaultBondDenom, 0)}, - simtestutil.DefaultGenTxGas, - chainID, - []uint64{account.GetAccountNumber()}, - []uint64{account.GetSequence()}, - simAccount.PrivKey, - ) - if err != nil { - return simtypes.NoOpMsg(types.ModuleName, msg.Type(), "unable to generate mock tx"), nil, err - } - - // We can return error here as we can guarantee that the first vote will be successful. - // Since we query the observer set before adding votes - _, _, err = app.SimDeliver(txGen.TxEncoder(), tx) - if err != nil { - return simtypes.NoOpMsg(types.ModuleName, msg.Type(), "unable to deliver tx"), nil, err - } - - opMsg := simtypes.NewOperationMsg(&msg, true, "", nil) - - // Add subsequent votes - observerSet, found := k.GetObserverKeeper().GetObserverSet(ctx) - if !found { - return simtypes.NoOpMsg(types.ModuleName, authz.InboundVoter.String(), "observer set not found"), nil, nil - } - - // 1) Schedule operations for votes - // 1.1) first pick a number of people to vote. - curNumVotesState = numVotesTransitionMatrix.NextState(r, curNumVotesState) - numVotes := int(math.Ceil(float64(len(observerSet.ObserverList)) * statePercentageArray[curNumVotesState])) - - // 1.2) select who votes - whoVotes := r.Perm(len(observerSet.ObserverList)) - whoVotes = whoVotes[:numVotes] - - var fops []simtypes.FutureOperation - - for _, observerIdx := range whoVotes { - observerAddress := observerSet.ObserverList[observerIdx] - // firstVoter has already voted. - if observerAddress == firstVoter { - continue - } - observerAccount, err := GetObserverAccount(observerAddress, accs) - if err != nil { - continue - } - // 1.3) schedule the vote - votingMsg := msg - votingMsg.Creator = observerAddress - - e := votingMsg.ValidateBasic() - if e != nil { - return simtypes.NoOpMsg(types.ModuleName, msg.Type(), "unable to validate voting msg"), nil, e - } - - fops = append(fops, simtypes.FutureOperation{ - // Submit all subsequent votes in the next block. - // We can consider adding a random block height between 1 and ballot maturity blocks in the future. - BlockHeight: int(ctx.BlockHeight() + 1), - Op: operationSimulateVoteInbound(k, votingMsg, observerAccount), - }) - } - return opMsg, fops, nil - } -} - -// SimulateMsgVoteGasPrice generates a MsgVoteGasPrice and delivers it -func SimulateMsgVoteGasPrice(k keeper.Keeper) simtypes.Operation { - return func(r *rand.Rand, app *baseapp.BaseApp, ctx sdk.Context, accounts []simtypes.Account, _ string, - ) (OperationMsg simtypes.OperationMsg, futureOps []simtypes.FutureOperation, err error) { - // Get a random account and observer - // If this returns an error, it is likely that the entire observer set has been removed - simAccount, randomObserver, err := GetRandomAccountAndObserver(r, ctx, k, accounts) - if err != nil { - return simtypes.OperationMsg{}, nil, nil - } - authAccount := k.GetAuthKeeper().GetAccount(ctx, simAccount.Address) - spendable := k.GetBankKeeper().SpendableCoins(ctx, authAccount.GetAddress()) - - supportedChains := k.GetObserverKeeper().GetSupportedChains(ctx) - if len(supportedChains) == 0 { - return simtypes.NoOpMsg( - types.ModuleName, - authz.GasPriceVoter.String(), - "no supported chains found", - ), nil, nil - } - randomChainID := GetRandomChainID(r, supportedChains) - - // Vote for random gas price. Gas prices do not use a ballot system, so we can vote directly without having to schedule future operations. - // The random nature of the price might create weird gas prices for the chain, but it is fine for now. We can remove the randomness if needed - msg := types.MsgVoteGasPrice{ - Creator: randomObserver, - ChainId: randomChainID, - Price: r.Uint64(), - PriorityFee: r.Uint64(), - BlockNumber: r.Uint64(), - Supply: fmt.Sprintf("%d", r.Int63()), - } - - // System contracts are deployed on the first block, so we cannot vote on gas prices before that - if ctx.BlockHeight() <= 1 { - return simtypes.NewOperationMsg(&msg, true, "block height less than 1", nil), nil, nil - } - - err = msg.ValidateBasic() - if err != nil { - return simtypes.NoOpMsg(types.ModuleName, msg.Type(), "unable to validate vote gas price msg"), nil, err - } - - txCtx := simulation.OperationInput{ - R: r, - App: app, - TxGen: moduletestutil.MakeTestEncodingConfig().TxConfig, - Cdc: nil, - Msg: &msg, - MsgType: msg.Type(), - Context: ctx, - SimAccount: simAccount, - AccountKeeper: k.GetAuthKeeper(), - Bankkeeper: k.GetBankKeeper(), - ModuleName: types.ModuleName, - CoinsSpentInMsg: spendable, - } - - return simulation.GenAndDeliverTxWithRandFees(txCtx) + simulation.NewWeightedOperation( + weightVoteOutbound, + SimulateVoteOutbound(k), + ), + simulation.NewWeightedOperation( + weightAddInboundTracker, + SimulateMsgAddInboundTracker(k), + ), + simulation.NewWeightedOperation( + weightAddOutboundTracker, + SimulateMsgAddOutboundTracker(k), + ), + simulation.NewWeightedOperation( + weightRemoveOutboundTracker, + SimulateMsgRemoveOutboundTracker(k), + ), + simulation.NewWeightedOperation( + weightWhitelistERC20, + SimulateMsgWhitelistERC20(k), + ), + simulation.NewWeightedOperation( + weightAbortStuckCCTX, + SimulateMsgAbortStuckCCTX(k), + ), + simulation.NewWeightedOperation( + weightRefundAbortedCCTX, + SimulateMsgRefundAbortedCCTX(k), + ), + simulation.NewWeightedOperation( + weightUpdateRateLimiterFlags, + SimulateMsgUpdateRateLimiterFlags(k), + ), + simulation.NewWeightedOperation( + weightUpdateERC20CustodyPauseStatus, + SimulateUpdateERC20CustodyPauseStatus(k), + ), + simulation.NewWeightedOperation( + weightUpdateTssAddress, + SimulateMsgUpdateTssAddress(k), + ), } } @@ -413,7 +232,21 @@ func GetRandomAccountAndObserver( return simtypes.Account{}, "", fmt.Errorf("no observers present in observer set found") } - randomObserver := GetRandomObserver(r, observers.ObserverList) + randomObserver := "" + foundObserver := false + for i := 0; i < 10; i++ { + randomObserver = GetRandomObserver(r, observers.ObserverList) + ok := k.GetObserverKeeper().IsNonTombstonedObserver(ctx, randomObserver) + if ok { + foundObserver = true + break + } + } + + if !foundObserver { + return simtypes.Account{}, "", fmt.Errorf("no observer found") + } + simAccount, err := GetObserverAccount(randomObserver, accounts) if err != nil { return simtypes.Account{}, "", err @@ -490,3 +323,76 @@ func GenAndDeliverTx( return simtypes.NewOperationMsg(txCtx.Msg, true, "", txCtx.Cdc), nil, nil } + +func ObserverVotesSimulationMatrix() (simtypes.TransitionMatrix, []float64, int) { + observerVotesTransitionMatrix, _ := simulation.CreateTransitionMatrix([][]int{ + {20, 10, 0, 0, 0, 0}, + {55, 50, 20, 10, 0, 0}, + {25, 25, 30, 25, 30, 15}, + {0, 15, 30, 25, 30, 30}, + {0, 0, 20, 30, 30, 30}, + {0, 0, 0, 10, 10, 25}, + }) + // The states are: + // column 1: All observers vote + // column 2: 90% vote + // column 3: 75% vote + // column 4: 40% vote + // column 5: 15% vote + // column 6: noone votes + // All columns sum to 100 for simplicity, but this is arbitrary and can be changed + statePercentageArray := []float64{1, .9, .75, .4, .15, 0} + curNumVotesState := 1 + return observerVotesTransitionMatrix, statePercentageArray, curNumVotesState +} + +func BallotVoteSimulationMatrix() (simtypes.TransitionMatrix, []float64, int) { + ballotTransitionMatrix, _ := simulation.CreateTransitionMatrix([][]int{ + {70, 10, 20}, + {20, 30, 30}, + {10, 60, 50}, + }) + // The states are: + // column 1: 100% vote yes + // column 2: 50% vote yes + // column 3: 0% vote yes + // For all conditions we assume if the the vote is not a yes. + // then it is a no .Not voting condtion is handled by the ObserverVotesSimulationMatrix matrix + yesVoteArray := []float64{1, .5, 0} + ballotVotesState := 1 + return ballotTransitionMatrix, yesVoteArray, ballotVotesState +} + +func GetPolicyAccount(ctx sdk.Context, k types.AuthorityKeeper, accounts []simtypes.Account) (simtypes.Account, error) { + policies, found := k.GetPolicies(ctx) + if !found { + return simtypes.Account{}, fmt.Errorf("policies object not found") + } + if len(policies.Items) == 0 { + return simtypes.Account{}, fmt.Errorf("no policies found") + } + + admin := policies.Items[0].Address + address, err := observerTypes.GetOperatorAddressFromAccAddress(admin) + if err != nil { + return simtypes.Account{}, err + } + simAccount, found := simtypes.FindAccount(accounts, address) + if !found { + return simtypes.Account{}, fmt.Errorf("admin account not found in list of simulation accounts") + } + return simAccount, nil +} + +func GetAsset(ctx sdk.Context, k types.FungibleKeeper, chainID int64) (string, error) { + foreignCoins := k.GetAllForeignCoins(ctx) + asset := "" + + for _, coin := range foreignCoins { + if coin.ForeignChainId == chainID { + return coin.Asset, nil + } + } + + return asset, fmt.Errorf("asset not found for chain %d", chainID) +} diff --git a/x/crosschain/types/expected_keepers.go b/x/crosschain/types/expected_keepers.go index 2fee9cb31a..a738242a34 100644 --- a/x/crosschain/types/expected_keepers.go +++ b/x/crosschain/types/expected_keepers.go @@ -13,6 +13,7 @@ import ( "github.com/zeta-chain/node/pkg/chains" "github.com/zeta-chain/node/pkg/coin" "github.com/zeta-chain/node/pkg/proofs" + authoritytypes "github.com/zeta-chain/node/x/authority/types" fungibletypes "github.com/zeta-chain/node/x/fungible/types" observertypes "github.com/zeta-chain/node/x/observer/types" ) @@ -224,6 +225,7 @@ type FungibleKeeper interface { type AuthorityKeeper interface { CheckAuthorization(ctx sdk.Context, msg sdk.Msg) error GetAdditionalChainList(ctx sdk.Context) (list []chains.Chain) + GetPolicies(ctx sdk.Context) (val authoritytypes.Policies, found bool) } type LightclientKeeper interface { diff --git a/x/crosschain/types/keys.go b/x/crosschain/types/keys.go index 76d343dd3e..26422c6101 100644 --- a/x/crosschain/types/keys.go +++ b/x/crosschain/types/keys.go @@ -26,7 +26,8 @@ const ( ProtocolFee = 2000000000000000000 // CCTXIndexLength is the length of a crosschain transaction index - CCTXIndexLength = 66 + CCTXIndexLength = 66 + MaxOutboundTrackerHashes = 5 ) func GetProtocolFee() math.Uint { diff --git a/x/crosschain/types/outbound_tracker.go b/x/crosschain/types/outbound_tracker.go new file mode 100644 index 0000000000..af986b388c --- /dev/null +++ b/x/crosschain/types/outbound_tracker.go @@ -0,0 +1,6 @@ +package types + +// MaxReached returns true if the OutboundTracker has reached the maximum number of hashes it can store. +func (o *OutboundTracker) MaxReached() bool { + return len(o.HashList) >= MaxOutboundTrackerHashes +} diff --git a/x/crosschain/types/outbound_tracker_test.go b/x/crosschain/types/outbound_tracker_test.go new file mode 100644 index 0000000000..1357a76ee2 --- /dev/null +++ b/x/crosschain/types/outbound_tracker_test.go @@ -0,0 +1,48 @@ +package types_test + +import ( + "testing" + + "github.com/zeta-chain/node/x/crosschain/types" +) + +func TestOutboundTracker_IsMaxed(t *testing.T) { + tests := []struct { + name string + tracker types.OutboundTracker + want bool + }{ + {"Not maxed", types.OutboundTracker{HashList: []*types.TxHash{ + {TxHash: "hash1", TxSigner: "signer1"}, + {TxHash: "hash2", TxSigner: "signer2"}, + {TxHash: "hash3", TxSigner: "signer3"}, + }}, + false}, + + {"Maxed", types.OutboundTracker{HashList: []*types.TxHash{ + {TxHash: "hash1", TxSigner: "signer1"}, + {TxHash: "hash2", TxSigner: "signer2"}, + {TxHash: "hash3", TxSigner: "signer3"}, + {TxHash: "hash4", TxSigner: "signer4"}, + {TxHash: "hash5", TxSigner: "signer5"}, + }}, + true}, + {"More than Maxed", types.OutboundTracker{HashList: []*types.TxHash{ + {TxHash: "hash1", TxSigner: "signer1"}, + {TxHash: "hash2", TxSigner: "signer2"}, + {TxHash: "hash3", TxSigner: "signer3"}, + {TxHash: "hash4", TxSigner: "signer4"}, + {TxHash: "hash5", TxSigner: "signer5"}, + {TxHash: "hash6", TxSigner: "signer6"}, + {TxHash: "hash7", TxSigner: "signer7"}, + }}, + true}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := tt.tracker.MaxReached(); got != tt.want { + t.Errorf("OutboundTracker.MaxReached() = %v, want %v", got, tt.want) + } + }) + } +} diff --git a/x/fungible/keeper/deposits.go b/x/fungible/keeper/deposits.go index b6a0c72c4e..3a5c24d1bb 100644 --- a/x/fungible/keeper/deposits.go +++ b/x/fungible/keeper/deposits.go @@ -99,6 +99,7 @@ func (k Keeper) getAndCheckZRC20( // this simplify the current workflow and allow to pause calls by pausing the gas token // TODO: refactor this logic and create specific workflow for no asset call // https://github.com/zeta-chain/node/issues/2627 + if coinType == coin.CoinType_Gas || coinType == coin.CoinType_NoAssetCall { foreignCoin, found = k.GetGasCoinForForeignCoin(ctx, chainID) if !found { diff --git a/x/fungible/simulation/operations.go b/x/fungible/simulation/operations.go index 2a14713979..c66d1bb3c9 100644 --- a/x/fungible/simulation/operations.go +++ b/x/fungible/simulation/operations.go @@ -16,6 +16,9 @@ import ( ) // Simulation operation weights constants +// Operation weights are used by the simulation program to simulate the weight of different operations. +// This decides what percentage of a certain type of operation is part of a block. +// Based on the weights assigned in the cosmos sdk modules , 100 seems to the max weight used , and therefore guarantees that at least one operation of that type is present in a block. // Operation weights are used by the `SimulateFromSeed` // function to pick a random operation based on the weights.The functions with higher weights are more likely to be picked. @@ -24,9 +27,6 @@ import ( // Based on the weights assigned in the cosmos sdk modules, // 100 seems to the max weight used,and we should use relative weights // to signify the number of each operation in a block. - -// TODO Add more details to comment based on what the number represents in terms of percentage of operations in a block -// https://github.com/zeta-chain/node/issues/3100 const ( // #nosec G101 not a hardcoded credential OpWeightMsgDeploySystemContracts = "op_weight_msg_deploy_system_contracts" diff --git a/x/observer/genesis.go b/x/observer/genesis.go index e852e407bf..1910e8074d 100644 --- a/x/observer/genesis.go +++ b/x/observer/genesis.go @@ -11,13 +11,19 @@ import ( // InitGenesis initializes the observer module's state from a provided genesis // state. func InitGenesis(ctx sdk.Context, k keeper.Keeper, genState types.GenesisState) { - observerCount := uint64(0) if genState.Observers.Len() > 0 { k.SetObserverSet(ctx, genState.Observers) - observerCount = uint64(len(genState.Observers.ObserverList)) + } else { + k.SetObserverSet(ctx, types.ObserverSet{}) + } + + if genState.LastObserverCount != nil { + k.SetLastObserverCount(ctx, genState.LastObserverCount) + } else { + k.SetLastObserverCount(ctx, &types.LastObserverCount{LastChangeHeight: 0, Count: genState.Observers.LenUint()}) } - // if chain params are defined set them + // if chain params are defined, set them if len(genState.ChainParamsList.ChainParams) > 0 { k.SetChainParamsList(ctx, genState.ChainParamsList) } else { @@ -82,12 +88,6 @@ func InitGenesis(ctx sdk.Context, k keeper.Keeper, genState types.GenesisState) }) } - if genState.LastObserverCount != nil { - k.SetLastObserverCount(ctx, genState.LastObserverCount) - } else { - k.SetLastObserverCount(ctx, &types.LastObserverCount{LastChangeHeight: 0, Count: observerCount}) - } - tss := types.TSS{} if genState.Tss != nil { tss = *genState.Tss diff --git a/x/observer/keeper/msg_server_update_observer.go b/x/observer/keeper/msg_server_update_observer.go index 2e60b291fb..7bf02e1b1c 100644 --- a/x/observer/keeper/msg_server_update_observer.go +++ b/x/observer/keeper/msg_server_update_observer.go @@ -36,6 +36,7 @@ func (k msgServer) UpdateObserver( "Observer address is not authorized : %s", msg.OldObserverAddress) } + // The New address should be a validator, not jailed and bonded err = k.IsValidator(ctx, msg.NewObserverAddress) if err != nil { return nil, errorsmod.Wrap(types.ErrUpdateObserver, err.Error()) @@ -72,7 +73,12 @@ func (k msgServer) UpdateObserver( return nil, errorsmod.Wrap(types.ErrLastObserverCountNotFound, "Observer count not found") } if lastBlockCount.Count != totalObserverCountCurrentBlock { - return nil, errorsmod.Wrap(types.ErrUpdateObserver, "Observer count mismatch") + return nil, errorsmod.Wrapf( + types.ErrUpdateObserver, + "Observer count mismatch current block: %d , last block: %d", + totalObserverCountCurrentBlock, + lastBlockCount.Count, + ) } return &types.MsgUpdateObserverResponse{}, nil } diff --git a/x/observer/keeper/msg_server_vote_tss.go b/x/observer/keeper/msg_server_vote_tss.go index 9b39174fd4..fc0156ffdd 100644 --- a/x/observer/keeper/msg_server_vote_tss.go +++ b/x/observer/keeper/msg_server_vote_tss.go @@ -27,8 +27,7 @@ const voteTSSid = "Vote TSS" func (k msgServer) VoteTSS(goCtx context.Context, msg *types.MsgVoteTSS) (*types.MsgVoteTSSResponse, error) { ctx := sdk.UnwrapSDKContext(goCtx) - // Checks whether a signer is authorized to sign, by checking their address against the observer mapper - // which contains the observer list for the chain and type. + // Checks whether a signer is authorized to sign, by checking if the signer has a node account. _, found := k.GetNodeAccount(ctx, msg.Creator) if !found { return nil, errorsmod.Wrapf( @@ -104,7 +103,7 @@ func (k msgServer) VoteTSS(goCtx context.Context, msg *types.MsgVoteTSS) (*types }, nil } - // For cases when an observer tries to vote for an older pending ballot, associated with a keygen that was discarded , we would return at this check while still adding the vote to the ballot + // For cases when an observer tries to vote for an older pending ballot, associated with a keygen that was discarded, we would return at this check while still adding the vote to the ballot if msg.KeygenZetaHeight != keygen.BlockNumber { return &types.MsgVoteTSSResponse{ VoteFinalized: isFinalized, diff --git a/x/observer/simulation/decoders.go b/x/observer/simulation/decoders.go index fd8eae7535..8d5243745b 100644 --- a/x/observer/simulation/decoders.go +++ b/x/observer/simulation/decoders.go @@ -19,79 +19,110 @@ func NewDecodeStore(cdc codec.Codec) func(kvA, kvB kv.Pair) string { var crosschainFlagsA, crosschainFlagsB types.CrosschainFlags cdc.MustUnmarshal(kvA.Value, &crosschainFlagsA) cdc.MustUnmarshal(kvB.Value, &crosschainFlagsB) - return fmt.Sprintf("%v\n%v", crosschainFlagsA, crosschainFlagsB) + return fmt.Sprintf( + "key %s value A %v value B %v", + types.CrosschainFlagsKey, + crosschainFlagsA, + crosschainFlagsB, + ) case bytes.Equal(kvA.Key, types.KeyPrefix(types.LastBlockObserverCountKey)): var lastBlockObserverCountA, lastBlockObserverCountB types.LastObserverCount cdc.MustUnmarshal(kvA.Value, &lastBlockObserverCountA) cdc.MustUnmarshal(kvB.Value, &lastBlockObserverCountB) - return fmt.Sprintf("%v\n%v", lastBlockObserverCountA, lastBlockObserverCountB) + return fmt.Sprintf( + "key %s value A %v value B %v", + types.LastBlockObserverCountKey, + lastBlockObserverCountA, + lastBlockObserverCountB, + ) case bytes.Equal(kvA.Key, types.KeyPrefix(types.NodeAccountKey)): var nodeAccountA, nodeAccountB types.NodeAccount cdc.MustUnmarshal(kvA.Value, &nodeAccountA) cdc.MustUnmarshal(kvB.Value, &nodeAccountB) - return fmt.Sprintf("%v\n%v", nodeAccountA, nodeAccountB) + return fmt.Sprintf("key %s value A %v value B %v", types.NodeAccountKey, nodeAccountA, nodeAccountB) case bytes.Equal(kvA.Key, types.KeyPrefix(types.KeygenKey)): var keygenA, keygenB types.Keygen cdc.MustUnmarshal(kvA.Value, &keygenA) cdc.MustUnmarshal(kvB.Value, &keygenB) - return fmt.Sprintf("%v\n%v", keygenA, keygenB) + return fmt.Sprintf("key %s value A %v value B %v", types.KeygenKey, keygenA, keygenB) case bytes.Equal(kvA.Key, types.KeyPrefix(types.BallotListKey)): var ballotListA, ballotListB types.BallotListForHeight cdc.MustUnmarshal(kvA.Value, &ballotListA) cdc.MustUnmarshal(kvB.Value, &ballotListB) - return fmt.Sprintf("%v\n%v", ballotListA, ballotListB) + return fmt.Sprintf("key %s value A %v value B %v", types.BallotListKey, ballotListA, ballotListB) case bytes.Equal(kvA.Key, types.KeyPrefix(types.VoterKey)): var voterA, voterB types.Ballot cdc.MustUnmarshal(kvA.Value, &voterA) cdc.MustUnmarshal(kvB.Value, &voterB) - return fmt.Sprintf("%v\n%v", voterA, voterB) + return fmt.Sprintf("key %s value A %v value B %v", types.VoterKey, voterA, voterB) case bytes.Equal(kvA.Key, types.KeyPrefix(types.TSSKey)): var tssA, tssB types.TSS cdc.MustUnmarshal(kvA.Value, &tssA) cdc.MustUnmarshal(kvB.Value, &tssB) - return fmt.Sprintf("%v\n%v", tssA, tssB) + return fmt.Sprintf("key %s value A %v value B %v", types.TSSKey, tssA, tssB) case bytes.Equal(kvA.Key, types.KeyPrefix(types.ObserverSetKey)): var observerSetA, observerSetB types.ObserverSet cdc.MustUnmarshal(kvA.Value, &observerSetA) cdc.MustUnmarshal(kvB.Value, &observerSetB) - return fmt.Sprintf("%v\n%v", observerSetA, observerSetB) + return fmt.Sprintf("key %s value A %v value B %v", types.ObserverSetKey, observerSetA, observerSetB) case bytes.Equal(kvA.Key, types.KeyPrefix(types.AllChainParamsKey)): var allChainParamsA, allChainParamsB types.ChainParamsList cdc.MustUnmarshal(kvA.Value, &allChainParamsA) cdc.MustUnmarshal(kvB.Value, &allChainParamsB) - return fmt.Sprintf("%v\n%v", allChainParamsA, allChainParamsB) + return fmt.Sprintf( + "key %s value A %v value B %v", + types.AllChainParamsKey, + allChainParamsA, + allChainParamsB, + ) case bytes.Equal(kvA.Key, types.KeyPrefix(types.TSSHistoryKey)): var tssHistoryA, tssHistoryB types.TSS cdc.MustUnmarshal(kvA.Value, &tssHistoryA) cdc.MustUnmarshal(kvB.Value, &tssHistoryB) - return fmt.Sprintf("%v\n%v", tssHistoryA, tssHistoryB) + return fmt.Sprintf("key %s value A %v value B %v", types.TSSHistoryKey, tssHistoryA, tssHistoryB) case bytes.Equal(kvA.Key, types.KeyPrefix(types.TssFundMigratorKey)): var tssFundMigratorA, tssFundMigratorB types.TssFundMigratorInfo cdc.MustUnmarshal(kvA.Value, &tssFundMigratorA) cdc.MustUnmarshal(kvB.Value, &tssFundMigratorB) - return fmt.Sprintf("%v\n%v", tssFundMigratorA, tssFundMigratorB) + return fmt.Sprintf( + "key %s value A %v value B %v", + types.TssFundMigratorKey, + tssFundMigratorA, + tssFundMigratorB, + ) case bytes.Equal(kvA.Key, types.KeyPrefix(types.PendingNoncesKeyPrefix)): var pendingNoncesA, pendingNoncesB types.PendingNonces cdc.MustUnmarshal(kvA.Value, &pendingNoncesA) cdc.MustUnmarshal(kvB.Value, &pendingNoncesB) - return fmt.Sprintf("%v\n%v", pendingNoncesA, pendingNoncesB) + return fmt.Sprintf( + "key %s value A %v value B %v", + types.PendingNoncesKeyPrefix, + pendingNoncesA, + pendingNoncesB, + ) case bytes.Equal(kvA.Key, types.KeyPrefix(types.ChainNoncesKey)): var chainNoncesA, chainNoncesB types.ChainNonces cdc.MustUnmarshal(kvA.Value, &chainNoncesA) cdc.MustUnmarshal(kvB.Value, &chainNoncesB) - return fmt.Sprintf("%v\n%v", chainNoncesA, chainNoncesB) + return fmt.Sprintf("key %s value A %v value B %v", types.ChainNoncesKey, chainNoncesA, chainNoncesB) case bytes.Equal(kvA.Key, types.KeyPrefix(types.NonceToCctxKeyPrefix)): var nonceToCctxA, nonceToCctxB types.NonceToCctx cdc.MustUnmarshal(kvA.Value, &nonceToCctxA) cdc.MustUnmarshal(kvB.Value, &nonceToCctxB) - return fmt.Sprintf("%v\n%v", nonceToCctxA, nonceToCctxB) + return fmt.Sprintf("key %s value A %v value B %v", types.NonceToCctxKeyPrefix, nonceToCctxA, nonceToCctxB) case bytes.Equal(kvA.Key, types.KeyPrefix(types.ParamsKey)): var paramsA, paramsB types.Params cdc.MustUnmarshal(kvA.Value, ¶msA) cdc.MustUnmarshal(kvB.Value, ¶msB) - return fmt.Sprintf("%v\n%v", paramsA, paramsB) + return fmt.Sprintf("key %s value A %v value B %v", types.ParamsKey, paramsA, paramsB) default: - panic(fmt.Sprintf("invalid observer key prefix %X", kvA.Key)) + panic( + fmt.Sprintf( + "invalid observer key prefix %X (first 8 bytes: %X)", + kvA.Key[:1], + kvA.Key[:min(8, len(kvA.Key))], + ), + ) } } } diff --git a/x/observer/simulation/decoders_test.go b/x/observer/simulation/decoders_test.go index 616994a664..d5a397d3ad 100644 --- a/x/observer/simulation/decoders_test.go +++ b/x/observer/simulation/decoders_test.go @@ -62,21 +62,44 @@ func TestNewDecodeStore(t *testing.T) { name string expectedLog string }{ - {"CrosschainFlags", fmt.Sprintf("%v\n%v", *crosschainFlags, *crosschainFlags)}, - {"LastBlockObserverCount", fmt.Sprintf("%v\n%v", *lastBlockObserverCount, *lastBlockObserverCount)}, - {"NodeAccount", fmt.Sprintf("%v\n%v", *nodeAccount, *nodeAccount)}, - {"Keygen", fmt.Sprintf("%v\n%v", *keygen, *keygen)}, - {"BallotList", fmt.Sprintf("%v\n%v", ballotList, ballotList)}, - {"Ballot", fmt.Sprintf("%v\n%v", *ballot, *ballot)}, - {"TSS", fmt.Sprintf("%v\n%v", tss, tss)}, - {"TSSHistory", fmt.Sprintf("%v\n%v", tss, tss)}, - {"ObserverSet", fmt.Sprintf("%v\n%v", observerSet, observerSet)}, - {"ChainParamsList", fmt.Sprintf("%v\n%v", chainParamsList, chainParamsList)}, - {"TssFundMigrator", fmt.Sprintf("%v\n%v", tssFundMigrator, tssFundMigrator)}, - {"PendingNonces", fmt.Sprintf("%v\n%v", pendingNonce, pendingNonce)}, - {"ChainNonces", fmt.Sprintf("%v\n%v", chainNonces, chainNonces)}, - {"NonceToCctx", fmt.Sprintf("%v\n%v", nonceToCctx, nonceToCctx)}, - {"Params", fmt.Sprintf("%v\n%v", params, params)}, + { + "CrosschainFlags", + fmt.Sprintf("key %s value A %v value B %v", types.CrosschainFlagsKey, *crosschainFlags, *crosschainFlags), + }, + { + "LastBlockObserverCount", + fmt.Sprintf( + "key %s value A %v value B %v", + types.LastBlockObserverCountKey, + *lastBlockObserverCount, + *lastBlockObserverCount, + ), + }, + {"NodeAccount", fmt.Sprintf("key %s value A %v value B %v", types.NodeAccountKey, *nodeAccount, *nodeAccount)}, + {"Keygen", fmt.Sprintf("key %s value A %v value B %v", types.KeygenKey, *keygen, *keygen)}, + {"BallotList", fmt.Sprintf("key %s value A %v value B %v", types.BallotListKey, ballotList, ballotList)}, + {"Ballot", fmt.Sprintf("key %s value A %v value B %v", types.VoterKey, *ballot, *ballot)}, + {"TSS", fmt.Sprintf("key %s value A %v value B %v", types.TSSKey, tss, tss)}, + {"TSSHistory", fmt.Sprintf("key %s value A %v value B %v", types.TSSHistoryKey, tss, tss)}, + {"ObserverSet", fmt.Sprintf("key %s value A %v value B %v", types.ObserverSetKey, observerSet, observerSet)}, + { + "ChainParamsList", + fmt.Sprintf("key %s value A %v value B %v", types.AllChainParamsKey, chainParamsList, chainParamsList), + }, + { + "TssFundMigrator", + fmt.Sprintf("key %s value A %v value B %v", types.TssFundMigratorKey, tssFundMigrator, tssFundMigrator), + }, + { + "PendingNonces", + fmt.Sprintf("key %s value A %v value B %v", types.PendingNoncesKeyPrefix, pendingNonce, pendingNonce), + }, + {"ChainNonces", fmt.Sprintf("key %s value A %v value B %v", types.ChainNoncesKey, chainNonces, chainNonces)}, + { + "NonceToCctx", + fmt.Sprintf("key %s value A %v value B %v", types.NonceToCctxKeyPrefix, nonceToCctx, nonceToCctx), + }, + {"Params", fmt.Sprintf("key %s value A %v value B %v", types.ParamsKey, params, params)}, } for i, tt := range tests { diff --git a/x/observer/simulation/operation_add_observer.go b/x/observer/simulation/operation_add_observer.go new file mode 100644 index 0000000000..95eff5ddbb --- /dev/null +++ b/x/observer/simulation/operation_add_observer.go @@ -0,0 +1,95 @@ +package simulation + +import ( + "math/rand" + + "github.com/cosmos/cosmos-sdk/baseapp" + sdk "github.com/cosmos/cosmos-sdk/types" + moduletestutil "github.com/cosmos/cosmos-sdk/types/module/testutil" + simtypes "github.com/cosmos/cosmos-sdk/types/simulation" + "github.com/cosmos/cosmos-sdk/x/simulation" + + "github.com/zeta-chain/node/testutil/sample" + "github.com/zeta-chain/node/x/observer/keeper" + "github.com/zeta-chain/node/x/observer/types" +) + +// SimulateAddObserver generates a TypeMsgAddObserver and delivers it. This message sets AddNodeAccountOnly to false; +// Therefore, it adds the observer to the observer set +func SimulateAddObserver(k keeper.Keeper) simtypes.Operation { + return func(r *rand.Rand, app *baseapp.BaseApp, ctx sdk.Context, accounts []simtypes.Account, _ string, + ) (OperationMsg simtypes.OperationMsg, futureOps []simtypes.FutureOperation, err error) { + policyAccount, err := GetPolicyAccount(ctx, k.GetAuthorityKeeper(), accounts) + if err != nil { + return simtypes.NoOpMsg(types.ModuleName, types.TypeMsgAddObserver, err.Error()), nil, nil + } + + authAccount := k.GetAuthKeeper().GetAccount(ctx, policyAccount.Address) + spendable := k.GetBankKeeper().SpendableCoins(ctx, authAccount.GetAddress()) + + observerSet, found := k.GetObserverSet(ctx) + if !found { + return simtypes.NoOpMsg( + types.ModuleName, + types.TypeMsgAddObserver, + "no observer set found", + ), nil, nil + } + + observerMap := make(map[string]bool) + for _, observer := range observerSet.ObserverList { + observerMap[observer] = true + } + + nodeAccounts := k.GetAllNodeAccount(ctx) + + // Pick a random observer which part of the node account but not in the observer set + // New accounts are added to the node account list via SimulateAddObserverNodeAccount + var newObserver string + foundNA := RepeatCheck(func() bool { + newObserver = nodeAccounts[r.Intn(len(nodeAccounts))].Operator + if _, found := observerMap[newObserver]; !found { + return true + } + return false + }) + if !foundNA { + return simtypes.NoOpMsg( + types.ModuleName, + types.TypeMsgAddObserver, + "no node accounts available which can be added as observer", + ), nil, nil + } + pubkey, err := sample.PubkeyStringFromRand(r) + if err != nil { + return simtypes.NoOpMsg(types.ModuleName, types.TypeMsgAddObserver, err.Error()), nil, nil + } + msg := types.MsgAddObserver{ + Creator: policyAccount.Address.String(), + ObserverAddress: newObserver, + ZetaclientGranteePubkey: pubkey, + AddNodeAccountOnly: false, + } + + if err != nil { + return simtypes.NoOpMsg(types.ModuleName, msg.Type(), err.Error()), nil, err + } + + txCtx := simulation.OperationInput{ + R: r, + App: app, + TxGen: moduletestutil.MakeTestEncodingConfig().TxConfig, + Cdc: nil, + Msg: &msg, + MsgType: msg.Type(), + Context: ctx, + SimAccount: policyAccount, + AccountKeeper: k.GetAuthKeeper(), + Bankkeeper: k.GetBankKeeper(), + ModuleName: types.ModuleName, + CoinsSpentInMsg: spendable, + } + + return simulation.GenAndDeliverTxWithRandFees(txCtx) + } +} diff --git a/x/observer/simulation/operation_add_observer_node_account.go b/x/observer/simulation/operation_add_observer_node_account.go new file mode 100644 index 0000000000..e182123fe1 --- /dev/null +++ b/x/observer/simulation/operation_add_observer_node_account.go @@ -0,0 +1,110 @@ +package simulation + +import ( + "math/rand" + + "github.com/cosmos/cosmos-sdk/baseapp" + sdk "github.com/cosmos/cosmos-sdk/types" + moduletestutil "github.com/cosmos/cosmos-sdk/types/module/testutil" + simtypes "github.com/cosmos/cosmos-sdk/types/simulation" + "github.com/cosmos/cosmos-sdk/x/simulation" + + "github.com/zeta-chain/node/testutil/sample" + "github.com/zeta-chain/node/x/observer/keeper" + "github.com/zeta-chain/node/x/observer/types" +) + +// SimulateAddObserverNodeAccount generates a TypeMsgAddObserver and delivers it. +// This message sets AddNodeAccountOnly to true to it does not add the observer to the observer set +func SimulateAddObserverNodeAccount(k keeper.Keeper) simtypes.Operation { + return func(r *rand.Rand, app *baseapp.BaseApp, ctx sdk.Context, accounts []simtypes.Account, _ string, + ) (OperationMsg simtypes.OperationMsg, futureOps []simtypes.FutureOperation, err error) { + policyAccount, err := GetPolicyAccount(ctx, k.GetAuthorityKeeper(), accounts) + if err != nil { + return simtypes.NoOpMsg(types.ModuleName, types.TypeMsgAddObserver, err.Error()), nil, nil + } + + authAccount := k.GetAuthKeeper().GetAccount(ctx, policyAccount.Address) + spendable := k.GetBankKeeper().SpendableCoins(ctx, authAccount.GetAddress()) + + observerSet, found := k.GetObserverSet(ctx) + if !found { + return simtypes.NoOpMsg( + types.ModuleName, + types.TypeMsgAddObserver, + "no observer set found", + ), nil, nil + } + + observerMap := make(map[string]bool) + for _, observer := range observerSet.ObserverList { + observerMap[observer] = true + } + + validators := k.GetStakingKeeper().GetAllValidators(ctx) + if len(validators) == 0 { + return simtypes.NoOpMsg( + types.ModuleName, + types.TypeMsgUpdateObserver, + "no validators found", + ), nil, nil + } + newObserver := "" + foundNewObserver := RepeatCheck(func() bool { + randomValidator := validators[r.Intn(len(validators))] + randomValidatorAddress, err := types.GetAccAddressFromOperatorAddress(randomValidator.OperatorAddress) + if err != nil { + return false + } + newObserver = randomValidatorAddress.String() + err = k.IsValidator(ctx, newObserver) + if err != nil { + return false + } + if _, ok := observerMap[newObserver]; !ok { + return true + } + return false + }) + + if !foundNewObserver { + return simtypes.NoOpMsg( + types.ModuleName, + types.TypeMsgAddObserver, + "no new observer found", + ), nil, nil + } + + pubkey, err := sample.PubkeyStringFromRand(r) + if err != nil { + return simtypes.NoOpMsg(types.ModuleName, types.TypeMsgAddObserver, err.Error()), nil, nil + } + msg := types.MsgAddObserver{ + Creator: policyAccount.Address.String(), + ObserverAddress: newObserver, + ZetaclientGranteePubkey: pubkey, + AddNodeAccountOnly: true, + } + + if err != nil { + return simtypes.NoOpMsg(types.ModuleName, msg.Type(), err.Error()), nil, err + } + + txCtx := simulation.OperationInput{ + R: r, + App: app, + TxGen: moduletestutil.MakeTestEncodingConfig().TxConfig, + Cdc: nil, + Msg: &msg, + MsgType: msg.Type(), + Context: ctx, + SimAccount: policyAccount, + AccountKeeper: k.GetAuthKeeper(), + Bankkeeper: k.GetBankKeeper(), + ModuleName: types.ModuleName, + CoinsSpentInMsg: spendable, + } + + return simulation.GenAndDeliverTxWithRandFees(txCtx) + } +} diff --git a/x/observer/simulation/operation_disable_cctx.go b/x/observer/simulation/operation_disable_cctx.go new file mode 100644 index 0000000000..9364aa2d87 --- /dev/null +++ b/x/observer/simulation/operation_disable_cctx.go @@ -0,0 +1,55 @@ +package simulation + +import ( + "math/rand" + + "github.com/cosmos/cosmos-sdk/baseapp" + sdk "github.com/cosmos/cosmos-sdk/types" + moduletestutil "github.com/cosmos/cosmos-sdk/types/module/testutil" + simtypes "github.com/cosmos/cosmos-sdk/types/simulation" + "github.com/cosmos/cosmos-sdk/x/simulation" + + "github.com/zeta-chain/node/x/observer/keeper" + "github.com/zeta-chain/node/x/observer/types" +) + +// SimulateDisableCCTX generates a MsgDisableCCTX and delivers it. +func SimulateDisableCCTX(k keeper.Keeper) simtypes.Operation { + return func(r *rand.Rand, app *baseapp.BaseApp, ctx sdk.Context, accounts []simtypes.Account, _ string, + ) (OperationMsg simtypes.OperationMsg, futureOps []simtypes.FutureOperation, err error) { + policyAccount, err := GetPolicyAccount(ctx, k.GetAuthorityKeeper(), accounts) + if err != nil { + return simtypes.NoOpMsg(types.ModuleName, types.TypeMsgDisableCCTX, err.Error()), nil, nil + } + authAccount := k.GetAuthKeeper().GetAccount(ctx, policyAccount.Address) + spendable := k.GetBankKeeper().SpendableCoins(ctx, authAccount.GetAddress()) + + msg := types.MsgDisableCCTX{ + Creator: policyAccount.Address.String(), + DisableInbound: false, + DisableOutbound: true, + } + + err = msg.ValidateBasic() + if err != nil { + return simtypes.NoOpMsg(types.ModuleName, msg.Type(), err.Error()), nil, err + } + + txCtx := simulation.OperationInput{ + R: r, + App: app, + TxGen: moduletestutil.MakeTestEncodingConfig().TxConfig, + Cdc: nil, + Msg: &msg, + MsgType: msg.Type(), + Context: ctx, + SimAccount: policyAccount, + AccountKeeper: k.GetAuthKeeper(), + Bankkeeper: k.GetBankKeeper(), + ModuleName: types.ModuleName, + CoinsSpentInMsg: spendable, + } + + return simulation.GenAndDeliverTxWithRandFees(txCtx) + } +} diff --git a/x/observer/simulation/operation_enable_cctx.go b/x/observer/simulation/operation_enable_cctx.go new file mode 100644 index 0000000000..7ee1d06776 --- /dev/null +++ b/x/observer/simulation/operation_enable_cctx.go @@ -0,0 +1,55 @@ +package simulation + +import ( + "math/rand" + + "github.com/cosmos/cosmos-sdk/baseapp" + sdk "github.com/cosmos/cosmos-sdk/types" + moduletestutil "github.com/cosmos/cosmos-sdk/types/module/testutil" + simtypes "github.com/cosmos/cosmos-sdk/types/simulation" + "github.com/cosmos/cosmos-sdk/x/simulation" + + "github.com/zeta-chain/node/x/observer/keeper" + "github.com/zeta-chain/node/x/observer/types" +) + +// SimulateEnableCCTX generates a MsgEnableCCTX and delivers it. +func SimulateEnableCCTX(k keeper.Keeper) simtypes.Operation { + return func(r *rand.Rand, app *baseapp.BaseApp, ctx sdk.Context, accounts []simtypes.Account, _ string, + ) (OperationMsg simtypes.OperationMsg, futureOps []simtypes.FutureOperation, err error) { + policyAccount, err := GetPolicyAccount(ctx, k.GetAuthorityKeeper(), accounts) + if err != nil { + return simtypes.NoOpMsg(types.ModuleName, types.TypeMsgEnableCCTX, err.Error()), nil, nil + } + authAccount := k.GetAuthKeeper().GetAccount(ctx, policyAccount.Address) + spendable := k.GetBankKeeper().SpendableCoins(ctx, authAccount.GetAddress()) + + msg := types.MsgEnableCCTX{ + Creator: policyAccount.Address.String(), + EnableInbound: true, + EnableOutbound: true, + } + + err = msg.ValidateBasic() + if err != nil { + return simtypes.NoOpMsg(types.ModuleName, msg.Type(), err.Error()), nil, err + } + + txCtx := simulation.OperationInput{ + R: r, + App: app, + TxGen: moduletestutil.MakeTestEncodingConfig().TxConfig, + Cdc: nil, + Msg: &msg, + MsgType: msg.Type(), + Context: ctx, + SimAccount: policyAccount, + AccountKeeper: k.GetAuthKeeper(), + Bankkeeper: k.GetBankKeeper(), + ModuleName: types.ModuleName, + CoinsSpentInMsg: spendable, + } + + return simulation.GenAndDeliverTxWithRandFees(txCtx) + } +} diff --git a/x/observer/simulation/operation_remove_chain_params.go b/x/observer/simulation/operation_remove_chain_params.go new file mode 100644 index 0000000000..a0e9f71681 --- /dev/null +++ b/x/observer/simulation/operation_remove_chain_params.go @@ -0,0 +1,83 @@ +package simulation + +import ( + "math/rand" + + "github.com/cosmos/cosmos-sdk/baseapp" + sdk "github.com/cosmos/cosmos-sdk/types" + moduletestutil "github.com/cosmos/cosmos-sdk/types/module/testutil" + simtypes "github.com/cosmos/cosmos-sdk/types/simulation" + "github.com/cosmos/cosmos-sdk/x/simulation" + + "github.com/zeta-chain/node/x/observer/keeper" + "github.com/zeta-chain/node/x/observer/types" +) + +// SimulateMsgRemoveChainParams generates a MsgRemoveChainParams and delivers it. This message removes a chain from the list +// This is not being run right now as the removal causes a lot of errors for the other operations. +func SimulateMsgRemoveChainParams(k keeper.Keeper) simtypes.Operation { + return func(r *rand.Rand, app *baseapp.BaseApp, ctx sdk.Context, accounts []simtypes.Account, _ string, + ) (OperationMsg simtypes.OperationMsg, futureOps []simtypes.FutureOperation, err error) { + policyAccount, err := GetPolicyAccount(ctx, k.GetAuthorityKeeper(), accounts) + if err != nil { + return simtypes.NoOpMsg(types.ModuleName, types.TypeMsgRemoveChainParams, err.Error()), nil, nil + } + + authAccount := k.GetAuthKeeper().GetAccount(ctx, policyAccount.Address) + spendable := k.GetBankKeeper().SpendableCoins(ctx, authAccount.GetAddress()) + + supportedChains := k.GetSupportedChains(ctx) + if len(supportedChains) == 0 { + return simtypes.NoOpMsg( + types.ModuleName, + types.TypeMsgRemoveChainParams, + "no supported chains found", + ), nil, nil + } + + randomExternalChain := int64(0) + foundExternalChain := RepeatCheck(func() bool { + c := supportedChains[r.Intn(len(supportedChains))] + if !c.IsZetaChain() { + randomExternalChain = c.ChainId + return true + } + return false + }) + + if !foundExternalChain { + return simtypes.NoOpMsg( + types.ModuleName, + types.TypeMsgRemoveChainParams, + "no external chain found", + ), nil, nil + } + + msg := types.MsgRemoveChainParams{ + Creator: policyAccount.Address.String(), + ChainId: randomExternalChain, + } + + err = msg.ValidateBasic() + if err != nil { + return simtypes.NoOpMsg(types.ModuleName, msg.Type(), err.Error()), nil, err + } + + txCtx := simulation.OperationInput{ + R: r, + App: app, + TxGen: moduletestutil.MakeTestEncodingConfig().TxConfig, + Cdc: nil, + Msg: &msg, + MsgType: msg.Type(), + Context: ctx, + SimAccount: policyAccount, + AccountKeeper: k.GetAuthKeeper(), + Bankkeeper: k.GetBankKeeper(), + ModuleName: types.ModuleName, + CoinsSpentInMsg: spendable, + } + + return simulation.GenAndDeliverTxWithRandFees(txCtx) + } +} diff --git a/x/observer/simulation/operation_reset_chain_nonces.go b/x/observer/simulation/operation_reset_chain_nonces.go new file mode 100644 index 0000000000..38ccacac37 --- /dev/null +++ b/x/observer/simulation/operation_reset_chain_nonces.go @@ -0,0 +1,87 @@ +package simulation + +import ( + "fmt" + "math/rand" + + "github.com/cosmos/cosmos-sdk/baseapp" + sdk "github.com/cosmos/cosmos-sdk/types" + moduletestutil "github.com/cosmos/cosmos-sdk/types/module/testutil" + simtypes "github.com/cosmos/cosmos-sdk/types/simulation" + "github.com/cosmos/cosmos-sdk/x/simulation" + + "github.com/zeta-chain/node/x/observer/keeper" + "github.com/zeta-chain/node/x/observer/types" +) + +// SimulateResetChainNonces generates a MsgResetChainNonces and delivers it. +func SimulateResetChainNonces(k keeper.Keeper) simtypes.Operation { + return func(r *rand.Rand, app *baseapp.BaseApp, ctx sdk.Context, accounts []simtypes.Account, _ string, + ) (OperationMsg simtypes.OperationMsg, futureOps []simtypes.FutureOperation, err error) { + policyAccount, err := GetPolicyAccount(ctx, k.GetAuthorityKeeper(), accounts) + if err != nil { + return simtypes.NoOpMsg(types.ModuleName, types.TypeMsgResetChainNonces, err.Error()), nil, nil + } + + authAccount := k.GetAuthKeeper().GetAccount(ctx, policyAccount.Address) + spendable := k.GetBankKeeper().SpendableCoins(ctx, authAccount.GetAddress()) + + randomChain, err := GetExternalChain(ctx, k, r) + if err != nil { + return simtypes.NoOpMsg( + types.ModuleName, + types.TypeMsgResetChainNonces, + err.Error(), + ), nil, fmt.Errorf( + "error getting external chain", + ) + } + + tss, found := k.GetTSS(ctx) + if !found { + return simtypes.NoOpMsg( + types.ModuleName, + types.TypeMsgResetChainNonces, + "TSS not found", + ), nil, fmt.Errorf( + "TSS not found", + ) + } + pendingNonces, found := k.GetPendingNonces(ctx, tss.TssPubkey, randomChain.ChainId) + if !found { + return simtypes.NoOpMsg(types.ModuleName, types.TypeMsgResetChainNonces, "Pending nonces not found"), nil, + fmt.Errorf("pending nonces not found for chain %d %s", randomChain.ChainId, randomChain.ChainName) + } + + nonceIncrement := int64(r.Intn(99)) + 1 + + msg := types.MsgResetChainNonces{ + Creator: policyAccount.Address.String(), + ChainId: randomChain.ChainId, + ChainNonceHigh: pendingNonces.NonceHigh + nonceIncrement, + ChainNonceLow: pendingNonces.NonceLow + nonceIncrement, + } + + err = msg.ValidateBasic() + if err != nil { + return simtypes.NoOpMsg(types.ModuleName, msg.Type(), err.Error()), nil, err + } + + txCtx := simulation.OperationInput{ + R: r, + App: app, + TxGen: moduletestutil.MakeTestEncodingConfig().TxConfig, + Cdc: nil, + Msg: &msg, + MsgType: msg.Type(), + Context: ctx, + SimAccount: policyAccount, + AccountKeeper: k.GetAuthKeeper(), + Bankkeeper: k.GetBankKeeper(), + ModuleName: types.ModuleName, + CoinsSpentInMsg: spendable, + } + + return simulation.GenAndDeliverTxWithRandFees(txCtx) + } +} diff --git a/x/observer/simulation/operation_update_chain_params.go b/x/observer/simulation/operation_update_chain_params.go new file mode 100644 index 0000000000..21f45d9ee3 --- /dev/null +++ b/x/observer/simulation/operation_update_chain_params.go @@ -0,0 +1,67 @@ +package simulation + +import ( + "math/rand" + + "github.com/cosmos/cosmos-sdk/baseapp" + sdk "github.com/cosmos/cosmos-sdk/types" + moduletestutil "github.com/cosmos/cosmos-sdk/types/module/testutil" + simtypes "github.com/cosmos/cosmos-sdk/types/simulation" + "github.com/cosmos/cosmos-sdk/x/simulation" + + "github.com/zeta-chain/node/testutil/sample" + "github.com/zeta-chain/node/x/observer/keeper" + "github.com/zeta-chain/node/x/observer/types" +) + +// SimulateUpdateChainParams generates a MsgUpdateChainParams and delivers it. +func SimulateUpdateChainParams(k keeper.Keeper) simtypes.Operation { + return func(r *rand.Rand, app *baseapp.BaseApp, ctx sdk.Context, accounts []simtypes.Account, _ string, + ) (OperationMsg simtypes.OperationMsg, futureOps []simtypes.FutureOperation, err error) { + policyAccount, err := GetPolicyAccount(ctx, k.GetAuthorityKeeper(), accounts) + if err != nil { + return simtypes.NoOpMsg(types.ModuleName, types.TypeMsgUpdateChainParams, err.Error()), nil, nil + } + + authAccount := k.GetAuthKeeper().GetAccount(ctx, policyAccount.Address) + spendable := k.GetBankKeeper().SpendableCoins(ctx, authAccount.GetAddress()) + + randomChain, err := GetExternalChain(ctx, k, r) + if err != nil { + return simtypes.NoOpMsg(types.ModuleName, types.TypeMsgUpdateChainParams, err.Error()), nil, nil + } + + cp := sample.ChainParamsFromRand(r, randomChain.ChainId) + err = types.ValidateChainParams(cp) + if err != nil { + return simtypes.NoOpMsg(types.ModuleName, types.TypeMsgUpdateChainParams, err.Error()), nil, nil + } + + msg := types.MsgUpdateChainParams{ + Creator: policyAccount.Address.String(), + ChainParams: cp, + } + + err = msg.ValidateBasic() + if err != nil { + return simtypes.NoOpMsg(types.ModuleName, msg.Type(), err.Error()), nil, err + } + + txCtx := simulation.OperationInput{ + R: r, + App: app, + TxGen: moduletestutil.MakeTestEncodingConfig().TxConfig, + Cdc: nil, + Msg: &msg, + MsgType: msg.Type(), + Context: ctx, + SimAccount: policyAccount, + AccountKeeper: k.GetAuthKeeper(), + Bankkeeper: k.GetBankKeeper(), + ModuleName: types.ModuleName, + CoinsSpentInMsg: spendable, + } + + return simulation.GenAndDeliverTxWithRandFees(txCtx) + } +} diff --git a/x/observer/simulation/operation_update_gas_price_increase_flags.go b/x/observer/simulation/operation_update_gas_price_increase_flags.go new file mode 100644 index 0000000000..fdb59d789d --- /dev/null +++ b/x/observer/simulation/operation_update_gas_price_increase_flags.go @@ -0,0 +1,58 @@ +package simulation + +import ( + "math/rand" + + "github.com/cosmos/cosmos-sdk/baseapp" + sdk "github.com/cosmos/cosmos-sdk/types" + moduletestutil "github.com/cosmos/cosmos-sdk/types/module/testutil" + simtypes "github.com/cosmos/cosmos-sdk/types/simulation" + "github.com/cosmos/cosmos-sdk/x/simulation" + + "github.com/zeta-chain/node/testutil/sample" + "github.com/zeta-chain/node/x/observer/keeper" + "github.com/zeta-chain/node/x/observer/types" +) + +// SimulateUpdateGasPriceIncreaseFlags generates a MsgUpdateGasPriceIncreaseFlags with random values +func SimulateUpdateGasPriceIncreaseFlags(k keeper.Keeper) simtypes.Operation { + return func(r *rand.Rand, app *baseapp.BaseApp, ctx sdk.Context, accounts []simtypes.Account, _ string, + ) (OperationMsg simtypes.OperationMsg, futureOps []simtypes.FutureOperation, err error) { + policyAccount, err := GetPolicyAccount(ctx, k.GetAuthorityKeeper(), accounts) + if err != nil { + return simtypes.NoOpMsg(types.ModuleName, types.TypeMsgUpdateGasPriceIncreaseFlags, err.Error()), nil, nil + } + + authAccount := k.GetAuthKeeper().GetAccount(ctx, policyAccount.Address) + spendable := k.GetBankKeeper().SpendableCoins(ctx, authAccount.GetAddress()) + + gp := sample.GasPriceIncreaseFlagsFromRand(r) + + msg := types.MsgUpdateGasPriceIncreaseFlags{ + Creator: policyAccount.Address.String(), + GasPriceIncreaseFlags: gp, + } + + err = msg.ValidateBasic() + if err != nil { + return simtypes.NoOpMsg(types.ModuleName, msg.Type(), err.Error()), nil, err + } + + txCtx := simulation.OperationInput{ + R: r, + App: app, + TxGen: moduletestutil.MakeTestEncodingConfig().TxConfig, + Cdc: nil, + Msg: &msg, + MsgType: msg.Type(), + Context: ctx, + SimAccount: policyAccount, + AccountKeeper: k.GetAuthKeeper(), + Bankkeeper: k.GetBankKeeper(), + ModuleName: types.ModuleName, + CoinsSpentInMsg: spendable, + } + + return simulation.GenAndDeliverTxWithRandFees(txCtx) + } +} diff --git a/x/observer/simulation/operation_update_keygen.go b/x/observer/simulation/operation_update_keygen.go new file mode 100644 index 0000000000..c6718b0dcc --- /dev/null +++ b/x/observer/simulation/operation_update_keygen.go @@ -0,0 +1,66 @@ +package simulation + +import ( + "math/rand" + + "github.com/cosmos/cosmos-sdk/baseapp" + sdk "github.com/cosmos/cosmos-sdk/types" + moduletestutil "github.com/cosmos/cosmos-sdk/types/module/testutil" + simtypes "github.com/cosmos/cosmos-sdk/types/simulation" + "github.com/cosmos/cosmos-sdk/x/simulation" + + "github.com/zeta-chain/node/testutil/sample" + "github.com/zeta-chain/node/x/observer/keeper" + "github.com/zeta-chain/node/x/observer/types" +) + +// SimulateUpdateKeygen generates a MsgUpdateKeygen and delivers it. +func SimulateUpdateKeygen(k keeper.Keeper) simtypes.Operation { + return func(r *rand.Rand, app *baseapp.BaseApp, ctx sdk.Context, accounts []simtypes.Account, _ string, + ) (OperationMsg simtypes.OperationMsg, futureOps []simtypes.FutureOperation, err error) { + policyAccount, err := GetPolicyAccount(ctx, k.GetAuthorityKeeper(), accounts) + if err != nil { + return simtypes.NoOpMsg(types.ModuleName, types.TypeMsgUpdateKeygen, err.Error()), nil, nil + } + + authAccount := k.GetAuthKeeper().GetAccount(ctx, policyAccount.Address) + spendable := k.GetBankKeeper().SpendableCoins(ctx, authAccount.GetAddress()) + + _, found := k.GetKeygen(ctx) + if !found { + kg := sample.KeygenFromRand(r) + k.SetKeygen(ctx, kg) + } + + blockHeightMin := ctx.BlockHeight() + 11 + blockHeightMax := ctx.BlockHeight() + 1000 + keygenBlockHeight := int64(r.Intn(int(blockHeightMax-blockHeightMin))) + blockHeightMin + + msg := types.MsgUpdateKeygen{ + Creator: policyAccount.Address.String(), + Block: keygenBlockHeight, + } + + err = msg.ValidateBasic() + if err != nil { + return simtypes.NoOpMsg(types.ModuleName, msg.Type(), err.Error()), nil, err + } + + txCtx := simulation.OperationInput{ + R: r, + App: app, + TxGen: moduletestutil.MakeTestEncodingConfig().TxConfig, + Cdc: nil, + Msg: &msg, + MsgType: msg.Type(), + Context: ctx, + SimAccount: policyAccount, + AccountKeeper: k.GetAuthKeeper(), + Bankkeeper: k.GetBankKeeper(), + ModuleName: types.ModuleName, + CoinsSpentInMsg: spendable, + } + + return simulation.GenAndDeliverTxWithRandFees(txCtx) + } +} diff --git a/x/observer/simulation/operation_update_observer.go b/x/observer/simulation/operation_update_observer.go new file mode 100644 index 0000000000..bc2f729942 --- /dev/null +++ b/x/observer/simulation/operation_update_observer.go @@ -0,0 +1,118 @@ +package simulation + +import ( + "math/rand" + + "github.com/cosmos/cosmos-sdk/baseapp" + sdk "github.com/cosmos/cosmos-sdk/types" + moduletestutil "github.com/cosmos/cosmos-sdk/types/module/testutil" + simtypes "github.com/cosmos/cosmos-sdk/types/simulation" + "github.com/cosmos/cosmos-sdk/x/simulation" + + "github.com/zeta-chain/node/x/observer/keeper" + "github.com/zeta-chain/node/x/observer/types" +) + +// SimulateUpdateObserver generates a TypeMsgUpdateObserver and delivers it. +func SimulateUpdateObserver(k keeper.Keeper) simtypes.Operation { + return func(r *rand.Rand, app *baseapp.BaseApp, ctx sdk.Context, accounts []simtypes.Account, _ string, + ) (OperationMsg simtypes.OperationMsg, futureOps []simtypes.FutureOperation, err error) { + policyAccount, err := GetPolicyAccount(ctx, k.GetAuthorityKeeper(), accounts) + if err != nil { + return simtypes.NoOpMsg(types.ModuleName, types.TypeMsgUpdateObserver, err.Error()), nil, nil + } + + authAccount := k.GetAuthKeeper().GetAccount(ctx, policyAccount.Address) + spendable := k.GetBankKeeper().SpendableCoins(ctx, authAccount.GetAddress()) + + _, randomObserver, observerList, err := GetRandomAccountAndObserver(r, ctx, k, accounts) + if err != nil { + return simtypes.NoOpMsg(types.ModuleName, types.TypeMsgUpdateObserver, err.Error()), nil, nil + } + + observerMap := make(map[string]bool) + for _, observer := range observerList { + observerMap[observer] = true + } + + validators := k.GetStakingKeeper().GetAllValidators(ctx) + if len(validators) == 0 { + return simtypes.NoOpMsg( + types.ModuleName, + types.TypeMsgUpdateObserver, + "no validators found", + ), nil, nil + } + + newObserver := "" + foundNewObserver := RepeatCheck(func() bool { + randomValidator := validators[r.Intn(len(validators))] + randomValidatorAddress, err := types.GetAccAddressFromOperatorAddress(randomValidator.OperatorAddress) + if err != nil { + return false + } + newObserver = randomValidatorAddress.String() + err = k.IsValidator(ctx, newObserver) + if err != nil { + return false + } + if _, ok := observerMap[newObserver]; !ok { + return true + } + return false + }) + + if !foundNewObserver { + return simtypes.NoOpMsg( + types.ModuleName, + types.TypeMsgUpdateObserver, + "no new observer found", + ), nil, nil + } + + lastBlockCount, found := k.GetLastObserverCount(ctx) + if !found { + return simtypes.NoOpMsg( + types.ModuleName, + types.TypeMsgUpdateObserver, + "no last block count found", + ), nil, nil + } + // #nosec G115 - overflow is not a concern here + if int(lastBlockCount.Count) != len(observerList) { + return simtypes.NoOpMsg( + types.ModuleName, + types.TypeMsgUpdateObserver, + "observer count mismatch", + ), nil, nil + } + + msg := types.MsgUpdateObserver{ + Creator: policyAccount.Address.String(), + OldObserverAddress: randomObserver, + NewObserverAddress: newObserver, + UpdateReason: types.ObserverUpdateReason_AdminUpdate, + } + + if err != nil { + return simtypes.NoOpMsg(types.ModuleName, msg.Type(), err.Error()), nil, err + } + + txCtx := simulation.OperationInput{ + R: r, + App: app, + TxGen: moduletestutil.MakeTestEncodingConfig().TxConfig, + Cdc: nil, + Msg: &msg, + MsgType: msg.Type(), + Context: ctx, + SimAccount: policyAccount, + AccountKeeper: k.GetAuthKeeper(), + Bankkeeper: k.GetBankKeeper(), + ModuleName: types.ModuleName, + CoinsSpentInMsg: spendable, + } + + return simulation.GenAndDeliverTxWithRandFees(txCtx) + } +} diff --git a/x/observer/simulation/operation_vote_tss.go b/x/observer/simulation/operation_vote_tss.go new file mode 100644 index 0000000000..8dc3c5668d --- /dev/null +++ b/x/observer/simulation/operation_vote_tss.go @@ -0,0 +1,177 @@ +package simulation + +import ( + "math" + "math/rand" + + "github.com/cosmos/cosmos-sdk/baseapp" + simtestutil "github.com/cosmos/cosmos-sdk/testutil/sims" + sdk "github.com/cosmos/cosmos-sdk/types" + moduletestutil "github.com/cosmos/cosmos-sdk/types/module/testutil" + simtypes "github.com/cosmos/cosmos-sdk/types/simulation" + "github.com/cosmos/cosmos-sdk/x/simulation" + + "github.com/zeta-chain/node/pkg/chains" + "github.com/zeta-chain/node/testutil/sample" + "github.com/zeta-chain/node/x/observer/keeper" + "github.com/zeta-chain/node/x/observer/types" +) + +// operationSimulateVoteTss generates a MsgVoteTSS with random values +func operationSimulateVoteTss( + k keeper.Keeper, + msg types.MsgVoteTSS, + simAccount simtypes.Account, +) simtypes.Operation { + return func(r *rand.Rand, app *baseapp.BaseApp, ctx sdk.Context, _ []simtypes.Account, _ string, + ) (OperationMsg simtypes.OperationMsg, futureOps []simtypes.FutureOperation, err error) { + // Fetch the account from the auth keeper which can then be used to fetch spendable coins + authAccount := k.GetAuthKeeper().GetAccount(ctx, simAccount.Address) + spendable := k.GetBankKeeper().SpendableCoins(ctx, authAccount.GetAddress()) + + // Generate a transaction with a random fee and deliver it + txCtx := simulation.OperationInput{ + R: r, + App: app, + TxGen: moduletestutil.MakeTestEncodingConfig().TxConfig, + Cdc: nil, + Msg: &msg, + MsgType: msg.Type(), + Context: ctx, + SimAccount: simAccount, + AccountKeeper: k.GetAuthKeeper(), + Bankkeeper: k.GetBankKeeper(), + ModuleName: types.ModuleName, + CoinsSpentInMsg: spendable, + } + + // Generate and deliver the transaction using the function defined by us instead of using the default function provided by the cosmos-sdk + // The main difference between the two functions is that the one defined by us does not error out if the vote fails. + // We need this behaviour as the votes are assigned to future operations, i.e., they are scheduled to be executed in a future block. We do not know at the time of scheduling if the vote will be successful or not. + // There might be multiple reasons for a vote to fail , like the observer not being present in the observer set, the observer not being an observer, etc. + return GenAndDeliverTxWithRandFees(txCtx) + } +} + +// SimulateMsgVoteTSS generates a MsgVoteTSS with random values and delivers it, it also schedules future votes for the same ballot +func SimulateMsgVoteTSS(k keeper.Keeper) simtypes.Operation { + return func( + r *rand.Rand, + app *baseapp.BaseApp, + ctx sdk.Context, + accs []simtypes.Account, + chainID string, + ) (simtypes.OperationMsg, []simtypes.FutureOperation, error) { + yesVote := chains.ReceiveStatus_success + noVote := chains.ReceiveStatus_failed + ballotVotesTransitionMatrix, yesVotePercentageArray, ballotVotesState := BallotVoteSimulationMatrix() + nodeAccounts := k.GetAllNodeAccount(ctx) + numVotes := len(nodeAccounts) + ballotVotesState = ballotVotesTransitionMatrix.NextState(r, ballotVotesState) + yesVotePercentage := yesVotePercentageArray[ballotVotesState] + numberOfYesVotes := int(math.Ceil(float64(numVotes) * yesVotePercentage)) + + vote := yesVote + if numberOfYesVotes == 0 { + vote = noVote + } + + newTss, err := sample.TSSFromRand(r) + if err != nil { + return simtypes.OperationMsg{}, nil, err + } + + keygen, found := k.GetKeygen(ctx) + if !found { + return simtypes.NoOpMsg(types.ModuleName, types.TypeMsgVoteTSS, "keygen not found"), nil, nil + } + + msg := types.MsgVoteTSS{ + Creator: "", + TssPubkey: newTss.TssPubkey, + KeygenZetaHeight: keygen.BlockNumber, + Status: vote, + } + + // Pick a random observer to create the ballot + // If this returns an error, it is likely that the entire observer set has been removed + simAccount, firstVoter, err := GetRandomNodeAccount(r, ctx, k, accs) + if err != nil { + return simtypes.OperationMsg{}, nil, nil + } + + txGen := moduletestutil.MakeTestEncodingConfig().TxConfig + account := k.GetAuthKeeper().GetAccount(ctx, simAccount.Address) + + firstMsg := msg + firstMsg.Creator = firstVoter + + // THe first vote should always create a new ballot + _, found = k.GetBallot(ctx, firstMsg.Digest()) + if found { + return simtypes.NoOpMsg(types.ModuleName, msg.Type(), "ballot already exists"), nil, nil + } + + err = firstMsg.ValidateBasic() + if err != nil { + return simtypes.NoOpMsg(types.ModuleName, msg.Type(), "unable to validate first tss vote"), nil, err + } + + tx, err := simtestutil.GenSignedMockTx( + r, + txGen, + []sdk.Msg{&firstMsg}, + sdk.Coins{sdk.NewInt64Coin(sdk.DefaultBondDenom, 0)}, + simtestutil.DefaultGenTxGas, + chainID, + []uint64{account.GetAccountNumber()}, + []uint64{account.GetSequence()}, + simAccount.PrivKey, + ) + if err != nil { + return simtypes.NoOpMsg(types.ModuleName, msg.Type(), "unable to generate mock tx"), nil, err + } + + // We can return error here as we can guarantee that the first vote will be successful. + // Since we query the observer set before adding votes + _, _, err = app.SimDeliver(txGen.TxEncoder(), tx) + if err != nil { + return simtypes.NoOpMsg(types.ModuleName, msg.Type(), "unable to deliver tx"), nil, err + } + + opMsg := simtypes.NewOperationMsg(&msg, true, "", nil) + + var fops []simtypes.FutureOperation + + for voteCount, nodeAccount := range nodeAccounts { + if vote == yesVote && voteCount == numberOfYesVotes { + vote = noVote + } + // firstVoter has already voted. + if nodeAccount.Operator == firstVoter { + continue + } + observerAccount, err := GetSimAccount(nodeAccount.Operator, accs) + if err != nil { + continue + } + // 1.3) schedule the vote + votingMsg := msg + votingMsg.Creator = nodeAccount.Operator + votingMsg.Status = vote + + e := votingMsg.ValidateBasic() + if e != nil { + return simtypes.NoOpMsg(types.ModuleName, msg.Type(), "unable to validate voting msg"), nil, e + } + + fops = append(fops, simtypes.FutureOperation{ + // Submit all subsequent votes in the next block. + // We can consider adding a random block height between 1 and ballot maturity blocks in the future. + BlockHeight: int(ctx.BlockHeight() + 1), + Op: operationSimulateVoteTss(k, votingMsg, observerAccount), + }) + } + return opMsg, fops, nil + } +} diff --git a/x/observer/simulation/operations.go b/x/observer/simulation/operations.go index 7c12ced3c0..1132e8a87e 100644 --- a/x/observer/simulation/operations.go +++ b/x/observer/simulation/operations.go @@ -1,20 +1,24 @@ package simulation import ( + "fmt" "math/rand" - "github.com/cosmos/cosmos-sdk/baseapp" "github.com/cosmos/cosmos-sdk/codec" + simtestutil "github.com/cosmos/cosmos-sdk/testutil/sims" sdk "github.com/cosmos/cosmos-sdk/types" - moduletestutil "github.com/cosmos/cosmos-sdk/types/module/testutil" simtypes "github.com/cosmos/cosmos-sdk/types/simulation" "github.com/cosmos/cosmos-sdk/x/simulation" + "github.com/zeta-chain/node/pkg/chains" "github.com/zeta-chain/node/x/observer/keeper" "github.com/zeta-chain/node/x/observer/types" ) // Simulation operation weights constants +// Operation weights are used by the simulation program to simulate the weight of different operations. +// This decides what percentage of a certain type of operation is part of a block. +// Based on the weights assigned in the cosmos sdk modules , 100 seems to the max weight used , and therefore guarantees that at least one operation of that type is present in a block. // Operation weights are used by the `SimulateFromSeed` // function to pick a random operation based on the weights.The functions with higher weights are more likely to be picked. @@ -23,85 +27,389 @@ import ( // Based on the weights assigned in the cosmos sdk modules, // 100 seems to the max weight used,and we should use relative weights // to signify the number of each operation in a block. - -// TODO Add more details to comment based on what the number represents in terms of percentage of operations in a block -// https://github.com/zeta-chain/node/issues/3100 const ( - // #nosec G101 not a hardcoded credential - OpWeightMsgTypeMsgEnableCCTX = "op_weight_msg_enable_crosschain_flags" - // DefaultWeightMsgTypeMsgEnableCCTX We ues a high weight for this operation - // to ensure that it is present in the block more number of times than any operation that changes the validator set + OpWeightMsgTypeMsgEnableCCTX = "op_weight_msg_enable_crosschain_flags" // #nosec G101 not a hardcoded credential + OpWeightMsgTypeMsgDisableCCTX = "op_weight_msg_disable_crosschain_flags" // #nosec G101 not a hardcoded credential + OpWeightMsgTypeMsgVoteTSS = "op_weight_msg_vote_tss" // #nosec G101 not a hardcoded credential + OpWeightMsgTypeMsgUpdateKeygen = "op_weight_msg_update_keygen" // #nosec G101 not a hardcoded credential + OpWeightMsgTypeMsgUpdateObserver = "op_weight_msg_update_observer" // #nosec G101 not a hardcoded credential + OpWeightMsgTypeMsgUpdateChainParams = "op_weight_msg_update_chain_params" // #nosec G101 not a hardcoded credential + OpWeightMsgTypeMsgRemoveChainParams = "op_weight_msg_remove_chain_params" // #nosec G101 not a hardcoded credential + OpWeightMsgTypeMsgResetChainNonces = "op_weight_msg_reset_chain_nonces" // #nosec G101 not a hardcoded credential + OpWeightMsgTypeMsgUpdateGasPriceIncreaseFlags = "op_weight_msg_update_gas_price_increase_flags" // #nosec G101 not a hardcoded credential + OpWeightMsgTypeMsgAddObserver = "op_weight_msg_add_observer" // #nosec G101 not a hardcoded credential + // DefaultWeightMsgTypeMsgEnableCCTX We use a high weight for this operation + // to ensure that it is present in the block more number of times than any operation that changes the validator set // Arrived at this number based on the weights used in the cosmos sdk staking module and through some trial and error - DefaultWeightMsgTypeMsgEnableCCTX = 3650 + DefaultWeightMsgTypeMsgEnableCCTX = 100 + DefaultWeightMsgTypeMsgDisableCCTX = 10 + DefaultWeightMsgTypeMsgVoteTSS = 10 + DefaultWeightMsgTypeMsgUpdateKeygen = 10 + DefaultWeightMsgTypeMsgUpdateObserver = 10 + DefaultWeightMsgTypeMsgUpdateChainParams = 10 + DefaultWeightMsgTypeMsgRemoveChainParams = 10 + DefaultWeightMsgTypeMsgResetChainNonces = 5 + DefaultWeightMsgTypeMsgUpdateGasPriceIncreaseFlags = 10 + DefaultWeightMsgTypeMsgAddObserver = 5 + + DefaultRetryCount = 10 ) // WeightedOperations for observer module func WeightedOperations( appParams simtypes.AppParams, cdc codec.JSONCodec, k keeper.Keeper, ) simulation.WeightedOperations { - var weightMsgTypeMsgEnableCCTX int + var ( + weightMsgTypeMsgEnableCCTX int + weightMsgTypeMsgDisableCCTX int + weightMsgTypeMsgVoteTSS int + weightMsgTypeMsgUpdateKeygen int + weightMsgTypeMsgUpdateObserver int + weightMsgTypeMsgUpdateChainParams int + weightMsgTypeMsgRemoveChainParams int + weightMsgTypeMsgResetChainNonces int + weightMsgTypeMsgUpdateGasPriceIncreaseFlags int + weightMsgTypeMsgAddObserver int + ) appParams.GetOrGenerate(cdc, OpWeightMsgTypeMsgEnableCCTX, &weightMsgTypeMsgEnableCCTX, nil, func(_ *rand.Rand) { weightMsgTypeMsgEnableCCTX = DefaultWeightMsgTypeMsgEnableCCTX }) + appParams.GetOrGenerate(cdc, OpWeightMsgTypeMsgDisableCCTX, &weightMsgTypeMsgDisableCCTX, nil, + func(_ *rand.Rand) { + weightMsgTypeMsgDisableCCTX = DefaultWeightMsgTypeMsgDisableCCTX + }) + + appParams.GetOrGenerate(cdc, OpWeightMsgTypeMsgVoteTSS, &weightMsgTypeMsgVoteTSS, nil, + func(_ *rand.Rand) { + weightMsgTypeMsgVoteTSS = DefaultWeightMsgTypeMsgVoteTSS + }) + + appParams.GetOrGenerate(cdc, OpWeightMsgTypeMsgUpdateKeygen, &weightMsgTypeMsgUpdateKeygen, nil, + func(_ *rand.Rand) { + weightMsgTypeMsgUpdateKeygen = DefaultWeightMsgTypeMsgUpdateKeygen + }) + + appParams.GetOrGenerate(cdc, OpWeightMsgTypeMsgUpdateObserver, &weightMsgTypeMsgUpdateObserver, nil, + func(_ *rand.Rand) { + weightMsgTypeMsgUpdateObserver = DefaultWeightMsgTypeMsgUpdateObserver + }) + + appParams.GetOrGenerate(cdc, OpWeightMsgTypeMsgUpdateChainParams, &weightMsgTypeMsgUpdateChainParams, nil, + func(_ *rand.Rand) { + weightMsgTypeMsgUpdateChainParams = DefaultWeightMsgTypeMsgUpdateChainParams + }) + + appParams.GetOrGenerate(cdc, OpWeightMsgTypeMsgRemoveChainParams, &weightMsgTypeMsgRemoveChainParams, nil, + func(_ *rand.Rand) { + weightMsgTypeMsgRemoveChainParams = DefaultWeightMsgTypeMsgRemoveChainParams + }) + + appParams.GetOrGenerate(cdc, OpWeightMsgTypeMsgResetChainNonces, &weightMsgTypeMsgResetChainNonces, nil, + func(_ *rand.Rand) { + weightMsgTypeMsgResetChainNonces = DefaultWeightMsgTypeMsgResetChainNonces + }) + + appParams.GetOrGenerate( + cdc, + OpWeightMsgTypeMsgUpdateGasPriceIncreaseFlags, + &weightMsgTypeMsgUpdateGasPriceIncreaseFlags, + nil, + func(_ *rand.Rand) { + weightMsgTypeMsgUpdateGasPriceIncreaseFlags = DefaultWeightMsgTypeMsgUpdateGasPriceIncreaseFlags + }, + ) + + appParams.GetOrGenerate(cdc, OpWeightMsgTypeMsgAddObserver, &weightMsgTypeMsgAddObserver, nil, + func(_ *rand.Rand) { + weightMsgTypeMsgAddObserver = DefaultWeightMsgTypeMsgAddObserver + }) + return simulation.WeightedOperations{ simulation.NewWeightedOperation( weightMsgTypeMsgEnableCCTX, - SimulateMsgTypeMsgEnableCCTX(k), + SimulateEnableCCTX(k), + ), + + simulation.NewWeightedOperation( + weightMsgTypeMsgDisableCCTX, + SimulateDisableCCTX(k), + ), + + simulation.NewWeightedOperation( + weightMsgTypeMsgUpdateKeygen, + SimulateUpdateKeygen(k), + ), + // + simulation.NewWeightedOperation( + weightMsgTypeMsgUpdateChainParams, + SimulateUpdateChainParams(k), + ), + // + //simulation.NewWeightedOperation( + // weightMsgTypeMsgRemoveChainParams, + // SimulateMsgRemoveChainParams(k), + //), + + simulation.NewWeightedOperation( + weightMsgTypeMsgResetChainNonces, + SimulateResetChainNonces(k), + ), + + simulation.NewWeightedOperation( + weightMsgTypeMsgUpdateGasPriceIncreaseFlags, + SimulateUpdateGasPriceIncreaseFlags(k), + ), + + simulation.NewWeightedOperation( + weightMsgTypeMsgAddObserver, + SimulateUpdateObserver(k), + ), + + simulation.NewWeightedOperation( + weightMsgTypeMsgAddObserver, + SimulateAddObserverNodeAccount(k), + ), + + simulation.NewWeightedOperation( + weightMsgTypeMsgAddObserver, + SimulateAddObserver(k), + ), + + simulation.NewWeightedOperation( + weightMsgTypeMsgVoteTSS, + SimulateMsgVoteTSS(k), ), } } -// SimulateMsgTypeMsgEnableCCTX generates a MsgEnableCCTX and delivers it. -func SimulateMsgTypeMsgEnableCCTX(k keeper.Keeper) simtypes.Operation { - return func(r *rand.Rand, app *baseapp.BaseApp, ctx sdk.Context, accounts []simtypes.Account, _ string, - ) (OperationMsg simtypes.OperationMsg, futureOps []simtypes.FutureOperation, err error) { - policies, found := k.GetAuthorityKeeper().GetPolicies(ctx) - if !found { - return simtypes.NoOpMsg(types.ModuleName, types.TypeMsgEnableCCTX, "policies object not found"), nil, nil - } - if len(policies.Items) == 0 { - return simtypes.NoOpMsg(types.ModuleName, types.TypeMsgEnableCCTX, "no policies found"), nil, nil +func GetPolicyAccount(ctx sdk.Context, k types.AuthorityKeeper, accounts []simtypes.Account) (simtypes.Account, error) { + policies, found := k.GetPolicies(ctx) + if !found { + return simtypes.Account{}, fmt.Errorf("policies object not found") + } + if len(policies.Items) == 0 { + return simtypes.Account{}, fmt.Errorf("no policies found") + } + + admin := policies.Items[0].Address + address, err := types.GetOperatorAddressFromAccAddress(admin) + if err != nil { + return simtypes.Account{}, err + } + simAccount, found := simtypes.FindAccount(accounts, address) + if !found { + return simtypes.Account{}, fmt.Errorf("admin account not found in list of simulation accounts") + } + return simAccount, nil +} + +func GetExternalChain(ctx sdk.Context, k keeper.Keeper, r *rand.Rand) (chains.Chain, error) { + supportedChains := k.GetSupportedChains(ctx) + if len(supportedChains) == 0 { + return chains.Chain{}, fmt.Errorf("no supported chains found") + } + externalChain := chains.Chain{} + foundExternalChain := RepeatCheck(func() bool { + c := supportedChains[r.Intn(len(supportedChains))] + if !c.IsZetaChain() { + externalChain = c + return true } + return false + }) - admin := policies.Items[0].Address - address, err := types.GetOperatorAddressFromAccAddress(admin) - if err != nil { - return simtypes.NoOpMsg(types.ModuleName, types.TypeMsgEnableCCTX, err.Error()), nil, err + if !foundExternalChain { + return chains.Chain{}, fmt.Errorf("no external chain found") + } + return externalChain, nil +} + +// GetRandomAccountAndObserver returns a random account and the associated observer address +func GetRandomAccountAndObserver( + r *rand.Rand, + ctx sdk.Context, + k keeper.Keeper, + accounts []simtypes.Account, +) (simtypes.Account, string, []string, error) { + observerList := []string{} + observers, found := k.GetObserverSet(ctx) + if !found { + return simtypes.Account{}, "", observerList, fmt.Errorf("observer set not found") + } + + observerList = observers.ObserverList + + if len(observers.ObserverList) == 0 { + return simtypes.Account{}, "", observerList, fmt.Errorf("no observers present in observer set found") + } + + randomObserver := "" + foundObserver := RepeatCheck(func() bool { + randomObserver = GetRandomObserver(r, observerList) + _, foundNodeAccount := k.GetNodeAccount(ctx, randomObserver) + if !foundNodeAccount { + return false } - simAccount, found := simtypes.FindAccount(accounts, address) - if !found { - return simtypes.NoOpMsg(types.ModuleName, types.TypeMsgEnableCCTX, "admin account not found"), nil, nil + ok := k.IsNonTombstonedObserver(ctx, randomObserver) + if ok { + return true } + return false + }) - msg := types.MsgEnableCCTX{ - Creator: simAccount.Address.String(), - EnableInbound: true, - EnableOutbound: false, - } + if !foundObserver { + return simtypes.Account{}, "", nil, fmt.Errorf("no observer found") + } - err = msg.ValidateBasic() - if err != nil { - return simtypes.NoOpMsg(types.ModuleName, msg.Type(), err.Error()), nil, err - } + simAccount, err := GetSimAccount(randomObserver, accounts) + if err != nil { + return simtypes.Account{}, "", observerList, err + } + return simAccount, randomObserver, observerList, nil +} + +func GetRandomNodeAccount( + r *rand.Rand, + ctx sdk.Context, + k keeper.Keeper, + accounts []simtypes.Account, +) (simtypes.Account, string, error) { + nodeAccounts := k.GetAllNodeAccount(ctx) + + if len(nodeAccounts) == 0 { + return simtypes.Account{}, "", fmt.Errorf("no node accounts present") + } + + randomNodeAccount := nodeAccounts[r.Intn(len(nodeAccounts))].Operator - txCtx := simulation.OperationInput{ - R: r, - App: app, - TxGen: moduletestutil.MakeTestEncodingConfig().TxConfig, - Cdc: nil, - Msg: &msg, - MsgType: msg.Type(), - Context: ctx, - SimAccount: simAccount, - AccountKeeper: k.GetAuthKeeper(), - Bankkeeper: k.GetBankKeeper(), - ModuleName: types.ModuleName, + simAccount, err := GetSimAccount(randomNodeAccount, accounts) + if err != nil { + return simtypes.Account{}, "", err + } + return simAccount, randomNodeAccount, nil +} + +func GetRandomObserver(r *rand.Rand, observerList []string) string { + idx := r.Intn(len(observerList)) + return observerList[idx] +} + +// GetSimAccount returns the account associated with the observer address from the list of accounts provided +// GetSimAccount can fail if all the observers are removed from the observer set ,this can happen +//if the other modules create transactions which affect the validator +//and triggers any of the staking hooks defined in the observer modules + +func GetSimAccount(observerAddress string, accounts []simtypes.Account) (simtypes.Account, error) { + operatorAddress, err := types.GetOperatorAddressFromAccAddress(observerAddress) + if err != nil { + return simtypes.Account{}, fmt.Errorf("validator not found for observer ") + } + + simAccount, found := simtypes.FindAccount(accounts, operatorAddress) + if !found { + return simtypes.Account{}, fmt.Errorf("operator account not found") + } + return simAccount, nil +} + +func RepeatCheck(fn func() bool) bool { + for i := 0; i < DefaultRetryCount; i++ { + if fn() { + return true } + } + return false +} - return simulation.GenAndDeliverTxWithRandFees(txCtx) +func ObserverVotesSimulationMatrix() (simtypes.TransitionMatrix, []float64, int) { + observerVotesTransitionMatrix, _ := simulation.CreateTransitionMatrix([][]int{ + {20, 10, 0, 0, 0, 0}, + {55, 50, 20, 10, 0, 0}, + {25, 25, 30, 25, 30, 15}, + {0, 15, 30, 25, 30, 30}, + {0, 0, 20, 30, 30, 30}, + {0, 0, 0, 10, 10, 25}, + }) + // The states are: + // column 1: All observers vote + // column 2: 90% vote + // column 3: 75% vote + // column 4: 40% vote + // column 5: 15% vote + // column 6: noone votes + // All columns sum to 100 for simplicity, but this is arbitrary and can be changed + statePercentageArray := []float64{1, .9, .75, .4, .15, 0} + curNumVotesState := 1 + return observerVotesTransitionMatrix, statePercentageArray, curNumVotesState +} + +func BallotVoteSimulationMatrix() (simtypes.TransitionMatrix, []float64, int) { + ballotTransitionMatrix, _ := simulation.CreateTransitionMatrix([][]int{ + {70, 10}, + {30, 10}, + }) + // The states are: + // column 1: 100% vote yes + // column 2: 0% vote yes + // For all conditions we assume if the vote is not a yes + // then it is a no . + yesVoteArray := []float64{1, 0} + ballotVotesState := 1 + return ballotTransitionMatrix, yesVoteArray, ballotVotesState +} + +// GenAndDeliverTxWithRandFees generates a transaction with a random fee and delivers it. +func GenAndDeliverTxWithRandFees( + txCtx simulation.OperationInput, +) (simtypes.OperationMsg, []simtypes.FutureOperation, error) { + account := txCtx.AccountKeeper.GetAccount(txCtx.Context, txCtx.SimAccount.Address) + spendable := txCtx.Bankkeeper.SpendableCoins(txCtx.Context, account.GetAddress()) + + var fees sdk.Coins + var err error + + coins, hasNeg := spendable.SafeSub(txCtx.CoinsSpentInMsg...) + if hasNeg { + return simtypes.NoOpMsg(txCtx.ModuleName, txCtx.MsgType, "message doesn't leave room for fees"), nil, err + } + + fees, err = simtypes.RandomFees(txCtx.R, txCtx.Context, coins) + if err != nil { + return simtypes.NoOpMsg(txCtx.ModuleName, txCtx.MsgType, "unable to generate fees"), nil, err } + return GenAndDeliverTx(txCtx, fees) +} + +// GenAndDeliverTx generates a transactions and delivers it with the provided fees. +// This function does not return an error if the transaction fails to deliver. +func GenAndDeliverTx( + txCtx simulation.OperationInput, + fees sdk.Coins, +) (simtypes.OperationMsg, []simtypes.FutureOperation, error) { + account := txCtx.AccountKeeper.GetAccount(txCtx.Context, txCtx.SimAccount.Address) + tx, err := simtestutil.GenSignedMockTx( + txCtx.R, + txCtx.TxGen, + []sdk.Msg{txCtx.Msg}, + fees, + simtestutil.DefaultGenTxGas, + txCtx.Context.ChainID(), + []uint64{account.GetAccountNumber()}, + []uint64{account.GetSequence()}, + txCtx.SimAccount.PrivKey, + ) + if err != nil { + return simtypes.NoOpMsg(txCtx.ModuleName, txCtx.MsgType, "unable to generate mock tx"), nil, err + } + + _, _, err = txCtx.App.SimDeliver(txCtx.TxGen.TxEncoder(), tx) + if err != nil { + return simtypes.NoOpMsg(txCtx.ModuleName, txCtx.MsgType, "unable to deliver tx"), nil, nil + } + + return simtypes.NewOperationMsg(txCtx.Msg, true, "", txCtx.Cdc), nil, nil } diff --git a/x/observer/types/expected_keepers.go b/x/observer/types/expected_keepers.go index 2788187c94..15eb476af6 100644 --- a/x/observer/types/expected_keepers.go +++ b/x/observer/types/expected_keepers.go @@ -19,6 +19,7 @@ type StakingKeeper interface { valAddr sdk.ValAddress, ) (delegation stakingtypes.Delegation, found bool) SetValidator(ctx sdk.Context, validator stakingtypes.Validator) + GetAllValidators(ctx sdk.Context) (validators []stakingtypes.Validator) } type SlashingKeeper interface { diff --git a/x/observer/types/keys.go b/x/observer/types/keys.go index ed57b13d79..e2bed33dec 100644 --- a/x/observer/types/keys.go +++ b/x/observer/types/keys.go @@ -71,6 +71,10 @@ const ( NodeAccountKey = "NodeAccount-value-" KeygenKey = "Keygen-value-" + // TODO remove unused keys + BlockHeaderKey = "BlockHeader-value-" + BlockHeaderStateKey = "BlockHeaderState-value-" + // TODO rename to BallotListForHeightKey BallotListKey = "BallotList-value-" TSSKey = "TSS-value-" diff --git a/zetaclient/chains/evm/observer/outbound.go b/zetaclient/chains/evm/observer/outbound.go index 2998f094dc..f8808d495d 100644 --- a/zetaclient/chains/evm/observer/outbound.go +++ b/zetaclient/chains/evm/observer/outbound.go @@ -20,7 +20,6 @@ import ( "github.com/zeta-chain/node/pkg/chains" "github.com/zeta-chain/node/pkg/coin" - crosschainkeeper "github.com/zeta-chain/node/x/crosschain/keeper" crosschaintypes "github.com/zeta-chain/node/x/crosschain/types" "github.com/zeta-chain/node/zetaclient/chains/evm" "github.com/zeta-chain/node/zetaclient/chains/interfaces" @@ -126,7 +125,7 @@ func (ob *Observer) ProcessOutboundTrackers(ctx context.Context) error { // should not happen. We can't tell which txHash is true. It might happen (e.g. bug, glitchy/hacked endpoint) ob.Logger().Outbound.Error().Msgf("WatchOutbound: confirmed multiple (%d) outbound for chain %d nonce %d", txCount, chainID, nonce) } else { - if len(tracker.HashList) == crosschainkeeper.MaxOutboundTrackerHashes { + if tracker.MaxReached() { ob.Logger().Outbound.Error().Msgf("WatchOutbound: outbound tracker is full of hashes for chain %d nonce %d", chainID, nonce) } }