From 5c58cf39998e2784d1a39efc2622775a046182de Mon Sep 17 00:00:00 2001 From: Martin Tomazic Date: Wed, 10 Sep 2025 11:50:19 +0200 Subject: [PATCH 1/2] cmd/network: Add command calculating recommended trust --- cmd/network/network.go | 1 + cmd/network/trust.go | 161 ++++++++++++++++++++++++++++++ examples/network/trust.in.static | 1 + examples/network/trust.out.static | 5 + 4 files changed, 168 insertions(+) create mode 100644 cmd/network/trust.go create mode 100644 examples/network/trust.in.static create mode 100644 examples/network/trust.out.static diff --git a/cmd/network/network.go b/cmd/network/network.go index fd429420..d964a773 100644 --- a/cmd/network/network.go +++ b/cmd/network/network.go @@ -60,4 +60,5 @@ func init() { Cmd.AddCommand(setRPCCmd) Cmd.AddCommand(showCmd) Cmd.AddCommand(statusCmd) + Cmd.AddCommand(trustCmd) } diff --git a/cmd/network/trust.go b/cmd/network/trust.go new file mode 100644 index 00000000..23b68fcc --- /dev/null +++ b/cmd/network/trust.go @@ -0,0 +1,161 @@ +package network + +import ( + "context" + "fmt" + "time" + + "github.com/spf13/cobra" + + "github.com/oasisprotocol/oasis-core/go/consensus/api" + "github.com/oasisprotocol/oasis-core/go/consensus/cometbft/config" + "github.com/oasisprotocol/oasis-sdk/client-sdk/go/connection" + + "github.com/oasisprotocol/cli/cmd/common" + cliConfig "github.com/oasisprotocol/cli/config" +) + +const ( + // maxVerificationTime defines the upper bound within which the light client should + // be able to fetch and verify light headers and finally detect and penalize a possible + // byzantine behavior. This value intentionally overestimates the actual time required. + maxVerificationTime = 24 * time.Hour +) + +var trustCmd = &cobra.Command{ + Use: "trust", + Short: "Show the recommended light client trust for consensus state sync", + Long: `Show the recommended light client trust configuration for consensus state sync. + +WARNING: + The output is only reliable if the CLI is connected to the RPC endpoint you control. + If using (default) public gRPC endpoint you are encouraged to verify trust parameters + with external sources.`, + Args: cobra.NoArgs, + RunE: func(cmd *cobra.Command, _ []string) error { + ctx, cancel := context.WithTimeout(cmd.Context(), 30*time.Second) + defer cancel() + + // Establish connection with the target network. + cfg := cliConfig.Global() + npa := common.GetNPASelection(cfg) + conn, err := connection.Connect(ctx, npa.Network) + if err != nil { + return fmt.Errorf("failed to establish connection with the target network: %w", err) + } + + trust, err := calcTrust(ctx, conn) + if err != nil { + return fmt.Errorf("failed to calculate consensus state sync trust root: %w", err) + } + + switch common.OutputFormat() { + case common.FormatJSON: + str, err := common.PrettyJSONMarshal(trust) + if err != nil { + return fmt.Errorf("failed to pretty json marshal: %w", err) + } + fmt.Println(string(str)) + default: + fmt.Println("Trust period: ", trust.Period) + fmt.Println("Trust height: ", trust.Height) + fmt.Println("Trust hash: ", trust.Hash) + fmt.Println() + fmt.Println("WARNING: Cannot be trusted unless the CLI is connected to the RPC endpoint you control.") + } + + return nil + }, +} + +// calcTrust calculates and verifies the recommended trust config. +func calcTrust(ctx context.Context, conn connection.Connection) (config.TrustConfig, error) { + latest, err := conn.Consensus().Core().GetBlock(ctx, api.HeightLatest) + if err != nil { + return config.TrustConfig{}, fmt.Errorf("failed to get latest block: %w", err) + } + + cpInterval, err := fetchCheckpointInterval(ctx, conn, latest.Height) + if err != nil { + return config.TrustConfig{}, fmt.Errorf("failed to fetch checkpoint interval: %w", err) + } + if cpInterval > latest.Height { + return config.TrustConfig{}, fmt.Errorf("checkpoint interval exceeds latest height") + } + + blkTime, err := calcAvgBlkTime(ctx, conn, latest) + if err != nil { + return config.TrustConfig{}, fmt.Errorf("failed to calculate average block time: %w", err) + } + + debondingPeriod, err := calcDebondingPeriod(ctx, conn, latest.Height, blkTime) + if err != nil { + return config.TrustConfig{}, fmt.Errorf("failed to calculate debonding period (height: %d): %w", latest.Height, err) + } + trustPeriod := calcTrustPeriod(debondingPeriod) + + // Going back the whole checkpoint interval guarantees there should be at least + // one target checkpoint height inside this interval. + candidate, err := conn.Consensus().Core().GetBlock(ctx, latest.Height-cpInterval) + if err != nil { + return config.TrustConfig{}, fmt.Errorf("failed to get candidate trust (height: %d): %w", candidate.Height, err) + } + + // Sanity check: the trusted root must not be older than the sum of the trust + // period and the maximum verification time. If it is, the light client will + // not be able to safely use this block as a trusted root for the state sync. + if candidate.Time.Add(trustPeriod).Add(maxVerificationTime).Before(time.Now()) { + return config.TrustConfig{}, fmt.Errorf("impossible to calculate safe trust with current parameters") + } + + return config.TrustConfig{ + Period: trustPeriod.Round(time.Hour), + Height: uint64(candidate.Height), // #nosec G115 + Hash: candidate.Hash.Hex(), + }, nil +} + +func fetchCheckpointInterval(ctx context.Context, conn connection.Connection, height int64) (int64, error) { + params, err := conn.Consensus().Core().GetParameters(ctx, height) + if err != nil { + return 0, fmt.Errorf("failed to get consensus parameters (height: %d): %w", height, err) + } + return int64(params.Parameters.StateCheckpointInterval), nil // #nosec G115 +} + +func calcAvgBlkTime(ctx context.Context, conn connection.Connection, latest *api.Block) (time.Duration, error) { + const deltaBlocks int64 = 1000 + blk, err := conn.Consensus().Core().GetBlock(ctx, latest.Height-deltaBlocks) + if err != nil { + return 0, fmt.Errorf("failed to get block: %w", err) + } + + return latest.Time.Sub(blk.Time) / time.Duration(deltaBlocks), nil +} + +func calcDebondingPeriod(ctx context.Context, conn connection.Connection, height int64, blkTime time.Duration) (time.Duration, error) { + stakingParams, err := conn.Consensus().Staking().ConsensusParameters(ctx, height) + if err != nil { + return 0, fmt.Errorf("failed to get staking parameters: %w", err) + } + beaconParams, err := conn.Consensus().Beacon().ConsensusParameters(ctx, height) + if err != nil { + return 0, fmt.Errorf("failed to get beacon parameters: %w", err) + } + debondingBlks := int64(stakingParams.DebondingInterval) * beaconParams.Interval() // #nosec G115 + return time.Duration(debondingBlks) * blkTime, nil +} + +// calcTrustPeriod returns suggested trust period. +// +// According to the CometBFT documentation, the sum of the trust period, the time +// required to verify headers, and the time needed to detect and penalize misbehavior +// should be significantly smaller than the total debonding period. +func calcTrustPeriod(debondingPeriod time.Duration) time.Duration { + return debondingPeriod * 3 / 4 +} + +func init() { + trustCmd.Flags().AddFlagSet(common.FormatFlag) + trustCmd.Flags().AddFlagSet(common.SelectorNFlags) +} diff --git a/examples/network/trust.in.static b/examples/network/trust.in.static new file mode 100644 index 00000000..dae30882 --- /dev/null +++ b/examples/network/trust.in.static @@ -0,0 +1 @@ +./oasis network trust --network testnet diff --git a/examples/network/trust.out.static b/examples/network/trust.out.static new file mode 100644 index 00000000..1336f11a --- /dev/null +++ b/examples/network/trust.out.static @@ -0,0 +1,5 @@ +Trust period: 240h0m0s +Trust height: 29103886 +Trust hash: ecff618ed2e8991e3e81eb37b2b61cb6990104c170f0fe34b4b2268b70f98fb5 + +WARNING: Cannot be trusted unless the CLI is connected to the RPC endpoint you control. From 74d5b57e685ce15709452fe6f2893b0fd902bdf4 Mon Sep 17 00:00:00 2001 From: Martin Tomazic Date: Thu, 6 Nov 2025 09:49:52 +0100 Subject: [PATCH 2/2] cmd/network: Consider max checkpoint creation time for trust Prevents a problem where the most recent checkpoint target height is still being created on the remote nodes, but second oldest target height would be already older than the suggested trust. This is only relevant when configuring runtime where state is much bigger. In such case if you are connected to remote nodes that preserve many runtime state versions, checkpoint creation time is significant. --- cmd/network/trust.go | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/cmd/network/trust.go b/cmd/network/trust.go index 23b68fcc..73285708 100644 --- a/cmd/network/trust.go +++ b/cmd/network/trust.go @@ -20,6 +20,11 @@ const ( // be able to fetch and verify light headers and finally detect and penalize a possible // byzantine behavior. This value intentionally overestimates the actual time required. maxVerificationTime = 24 * time.Hour + + // maxCpCreationTime defines the upper bound within which remote node should produce + // checkpoint. This value intentionally overestimates the actual time required, + // assuming nodes with very bulky NodeDB, where checkpoint creation is slow. + maxCpCreationTime = 36 * time.Hour ) var trustCmd = &cobra.Command{ @@ -94,11 +99,12 @@ func calcTrust(ctx context.Context, conn connection.Connection) (config.TrustCon } trustPeriod := calcTrustPeriod(debondingPeriod) - // Going back the whole checkpoint interval guarantees there should be at least - // one target checkpoint height inside this interval. - candidate, err := conn.Consensus().Core().GetBlock(ctx, latest.Height-cpInterval) + // Going back the whole checkpoint interval plus max checkpoint creation time guarantees there wil be at least + // one target checkpoint height from the trust height onwards, for which remote nodes already created a checkpoint. + candidateHeight := latest.Height - cpInterval - int64(maxCpCreationTime/blkTime) + candidate, err := conn.Consensus().Core().GetBlock(ctx, candidateHeight) if err != nil { - return config.TrustConfig{}, fmt.Errorf("failed to get candidate trust (height: %d): %w", candidate.Height, err) + return config.TrustConfig{}, fmt.Errorf("failed to get candidate trust (height: %d): %w", candidateHeight, err) } // Sanity check: the trusted root must not be older than the sum of the trust