From 0f2f75e4dfd0ff82b2172606e61cd221641c9312 Mon Sep 17 00:00:00 2001 From: Owen Date: Mon, 15 Jun 2026 12:44:54 -0400 Subject: [PATCH] feat: support ACP-236 auto-renewed staking commands Add `validator add-auto-renewed` and `validator set-auto-config`, built on the interface-based issuer seams from the pkg/pchain refactor. Addressing review feedback on #28: - replace the func-typed injection seams for the auto-renewed issuers with small single-method interfaces (autoRenewedValidatorTxIssuer, setAutoRenewedValidatorConfigTxIssuer), matching the rest of pkg/pchain; the native pwallet IssueAddAutoRenewedValidatorTx / IssueSetAutoRenewed- ValidatorConfigTx methods satisfy them - drop the validator*Fn package globals in cmd/validator.go; the commands call the production pchain functions directly and coverage lives in the pchain layer + e2e Help/MissingArgs cases - narrow GetAutoRenewedValidatorAuthority via the getCurrentValidators nodeIDs filter (new optional --node-id) instead of fetching the full set - build the owner-authorized wallet once via wallet.NewWalletFromKeychain- WithOwner, removing the redundant P-Chain state fetch in set-auto-config - bound the auto-renewal period by network MaxStakeDuration - use reward.PercentDenominator instead of a literal 1_000_000 The avalanchego dependency still tracks the unreleased helicon-devnet pseudo-version because the ACP-236 tx types and wallet builders are not yet in a tagged release; this PR should wait for upstream release readiness. Co-authored-by: anishnar --- cmd/helpers_test.go | 69 ++++ cmd/root.go | 16 +- cmd/validator.go | 261 ++++++++++++++++ cmd/wallet.go | 45 +++ e2e/cli_test.go | 52 +++- go.mod | 58 ++-- go.sum | 138 ++++---- pkg/network/network.go | 16 +- pkg/pchain/acp236_devnetcompat_test.go | 347 +++++++++++++++++++++ pkg/pchain/acp236_test.go | 416 +++++++++++++++++++++++++ pkg/pchain/pchain.go | 194 ++++++++++++ pkg/wallet/wallet.go | 35 +++ 12 files changed, 1538 insertions(+), 109 deletions(-) create mode 100644 pkg/pchain/acp236_devnetcompat_test.go create mode 100644 pkg/pchain/acp236_test.go diff --git a/cmd/helpers_test.go b/cmd/helpers_test.go index 26b855e..8ea9c87 100644 --- a/cmd/helpers_test.go +++ b/cmd/helpers_test.go @@ -2,6 +2,7 @@ package cmd import ( "math" + "strings" "testing" "time" ) @@ -194,6 +195,74 @@ func TestParseTimeRange(t *testing.T) { } } +func TestFractionToShares(t *testing.T) { + got, err := fractionToShares("auto-compound", 0.3) + if err != nil { + t.Fatalf("fractionToShares() returned error: %v", err) + } + if got != 300_000 { + t.Fatalf("fractionToShares() = %d, want 300000", got) + } + + _, err = fractionToShares("auto-compound", 1.01) + if err == nil { + t.Fatal("fractionToShares() expected error for value above one") + } + if !strings.Contains(err.Error(), "auto-compound") { + t.Fatalf("fractionToShares() error = %v, want field name", err) + } +} + +func TestParseAutoRenewPeriod(t *testing.T) { + got, err := parseAutoRenewPeriod("336h") + if err != nil { + t.Fatalf("parseAutoRenewPeriod() returned error: %v", err) + } + if got != 14*24*time.Hour { + t.Fatalf("parseAutoRenewPeriod() = %s, want 336h", got) + } + + for _, bad := range []string{"0s", "1.5s", "bad-period"} { + if _, err := parseAutoRenewPeriod(bad); err == nil { + t.Fatalf("parseAutoRenewPeriod(%q) expected error", bad) + } + } +} + +func TestParseAutoRenewConfigPeriod(t *testing.T) { + tests := []struct { + name string + input string + want time.Duration + wantErr bool + }{ + {name: "zero duration", input: "0", want: 0}, + {name: "zero seconds", input: "0s", want: 0}, + {name: "non-zero duration", input: "336h", want: 14 * 24 * time.Hour}, + {name: "negative duration", input: "-1s", wantErr: true}, + {name: "sub-second duration", input: "1.5s", wantErr: true}, + {name: "invalid duration", input: "bad-period", wantErr: true}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := parseAutoRenewConfigPeriod(tt.input) + if tt.wantErr { + if err == nil { + t.Fatalf("parseAutoRenewConfigPeriod(%q) expected error", tt.input) + } + return + } + if err != nil { + t.Fatalf("parseAutoRenewConfigPeriod(%q) returned error: %v", tt.input, err) + } + if got != tt.want { + t.Fatalf("parseAutoRenewConfigPeriod(%q) = %s, want %s", tt.input, got, tt.want) + } + }) + } +} + func TestNormalizeValidatorNodeURI(t *testing.T) { tests := []struct { name string diff --git a/cmd/root.go b/cmd/root.go index 5e903d4..40e3ba3 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -9,6 +9,7 @@ import ( "syscall" "time" + "github.com/ava-labs/avalanchego/vms/platformvm/reward" "github.com/spf13/cobra" ) @@ -95,13 +96,20 @@ func avaxToNAVAX(avax float64) (uint64, error) { return uint64(math.Round(avax * 1e9)), nil } +// fractionToShares converts a decimal fraction (0.02 = 2%) to shares out of +// reward.PercentDenominator (0.02 -> 20,000). Uses rounding to avoid float +// precision issues. [name] is used in the error message. +func fractionToShares(name string, value float64) (uint32, error) { + if value < 0 || value > 1 { + return 0, fmt.Errorf("%s must be between 0 and 1 (got %.4f)", name, value) + } + return uint32(math.Round(value * reward.PercentDenominator)), nil +} + // feeToShares converts a decimal fee (0.02 = 2%) to shares (20,000 out of 1,000,000). // Uses rounding to avoid float precision issues. func feeToShares(fee float64) (uint32, error) { - if fee < 0 || fee > 1 { - return 0, fmt.Errorf("delegation fee must be between 0 and 1 (got %.4f)", fee) - } - return uint32(math.Round(fee * 1_000_000)), nil + return fractionToShares("delegation fee", fee) } // getOperationContext returns a context with timeout and signal handling. diff --git a/cmd/validator.go b/cmd/validator.go index 128de19..c4824cf 100644 --- a/cmd/validator.go +++ b/cmd/validator.go @@ -24,6 +24,14 @@ var ( valNodeEndpoint string valBLSPublicKey string valBLSPoP string + + valAutoPeriod string + valAutoCompound float64 + valOwnerAddr string + valSetAutoTxID string + valSetAutoNodeID string + valSetAutoPeriod string + valSetAutoCompound float64 ) var validatorCmd = &cobra.Command{ @@ -201,6 +209,204 @@ var validatorDelegateCmd = &cobra.Command{ }, } +var validatorAddAutoRenewedCmd = &cobra.Command{ + Use: "add-auto-renewed", + Short: "Add an auto-renewed primary network validator", + Long: `Add an auto-renewed validator to the Avalanche primary network.`, + RunE: func(cmd *cobra.Command, args []string) error { + ctx, cancel := getOperationContext() + defer cancel() + + if valStakeAmount <= 0 { + return fmt.Errorf("--stake is required and must be positive") + } + if valNodeID == "" { + return fmt.Errorf("--node-id is required") + } + nodeID, err := ids.NodeIDFromString(valNodeID) + if err != nil { + return fmt.Errorf("invalid node ID: %w", err) + } + + period, err := parseAutoRenewPeriod(valAutoPeriod) + if err != nil { + return err + } + + netConfig, err := getNetworkConfig(ctx) + if err != nil { + return fmt.Errorf("failed to get network config: %w", err) + } + if period < netConfig.MinStakeDuration { + return fmt.Errorf("period too short for %s: minimum is %s", netConfig.Name, netConfig.MinStakeDuration) + } + if period > netConfig.MaxStakeDuration { + return fmt.Errorf("period too long for %s: maximum is %s", netConfig.Name, netConfig.MaxStakeDuration) + } + + nodePoP, nodeURI, err := getValidatorPoP(ctx, nodeID) + if err != nil { + return err + } + + w, cleanup, err := loadPChainWallet(ctx, netConfig) + if err != nil { + return fmt.Errorf("failed to create wallet: %w", err) + } + defer cleanup() + + rewardAddr := w.PChainAddress() + if valRewardAddr != "" { + rewardAddr, err = ids.ShortFromString(valRewardAddr) + if err != nil { + return fmt.Errorf("invalid reward address: %w", err) + } + } + + authorityAddr := w.PChainAddress() + if valOwnerAddr != "" { + authorityAddr, err = ids.ShortFromString(valOwnerAddr) + if err != nil { + return fmt.Errorf("invalid owner address: %w", err) + } + } + + stakeNAVAX, err := avaxToNAVAX(valStakeAmount) + if err != nil { + return fmt.Errorf("invalid stake amount: %w", err) + } + if stakeNAVAX < netConfig.MinValidatorStake { + return fmt.Errorf("stake too low for %s: minimum is %.9f AVAX", netConfig.Name, float64(netConfig.MinValidatorStake)/1e9) + } + + delegationFeeShares, err := feeToShares(valDelegationFee) + if err != nil { + return fmt.Errorf("invalid delegation fee: %w", err) + } + autoCompoundShares, err := fractionToShares("auto-compound", valAutoCompound) + if err != nil { + return fmt.Errorf("invalid auto-compound: %w", err) + } + + fmt.Printf("Adding auto-renewed validator %s with %.9f AVAX stake...\n", nodeID, valStakeAmount) + fmt.Printf(" Period: %s\n", period) + fmt.Printf(" Delegation Fee: %.2f%%\n", valDelegationFee*100) + fmt.Printf(" Auto-Compound Rewards: %.2f%%\n", valAutoCompound*100) + fmt.Printf(" Validator Authority: %s\n", authorityAddr) + if nodeURI != "" { + fmt.Printf(" Node Endpoint: %s\n", nodeURI) + } else { + fmt.Println(" BLS PoP Source: --bls-public-key/--bls-pop flags") + } + fmt.Println("Submitting transaction...") + + txID, err := pchain.AddAutoRenewedValidator(ctx, w, pchain.AddAutoRenewedValidatorConfig{ + NodeID: nodeID, + StakeAmt: stakeNAVAX, + RewardAddr: rewardAddr, + ValidatorAuthorityAddr: authorityAddr, + DelegationFee: delegationFeeShares, + AutoCompoundRewardShares: autoCompoundShares, + Period: period, + BLSSigner: nodePoP, + }) + if err != nil { + return err + } + + fmt.Printf("TX ID: %s\n", txID) + return nil + }, +} + +var validatorSetAutoConfigCmd = &cobra.Command{ + Use: "set-auto-config", + Short: "Set auto-renewed validator config", + Long: `Set the next-cycle configuration for an auto-renewed validator.`, + RunE: func(cmd *cobra.Command, args []string) error { + ctx, cancel := getOperationContext() + defer cancel() + + if valSetAutoTxID == "" { + return fmt.Errorf("--tx-id is required") + } + autoRenewedTxID, err := ids.FromString(valSetAutoTxID) + if err != nil { + return fmt.Errorf("invalid tx ID: %w", err) + } + + // --node-id is optional but narrows the validator lookup to a single node. + var nodeID ids.NodeID + if valSetAutoNodeID != "" { + nodeID, err = ids.NodeIDFromString(valSetAutoNodeID) + if err != nil { + return fmt.Errorf("invalid node ID: %w", err) + } + } + + if !cmd.Flags().Changed("period") { + return fmt.Errorf("--period is required") + } + period, err := parseAutoRenewConfigPeriod(valSetAutoPeriod) + if err != nil { + return err + } + + if !cmd.Flags().Changed("auto-compound") { + return fmt.Errorf("--auto-compound is required") + } + autoCompoundShares, err := fractionToShares("auto-compound", valSetAutoCompound) + if err != nil { + return fmt.Errorf("invalid auto-compound: %w", err) + } + + netConfig, err := getNetworkConfig(ctx) + if err != nil { + return fmt.Errorf("failed to get network config: %w", err) + } + if period > 0 && period < netConfig.MinStakeDuration { + return fmt.Errorf("period too short for %s: minimum is %s", netConfig.Name, netConfig.MinStakeDuration) + } + if period > netConfig.MaxStakeDuration { + return fmt.Errorf("period too long for %s: maximum is %s", netConfig.Name, netConfig.MaxStakeDuration) + } + + validatorAuthority, err := pchain.GetAutoRenewedValidatorAuthority(ctx, netConfig.RPCURL, nodeID, autoRenewedTxID) + if err != nil { + return err + } + + // The config owner authorized at add-time is resolved by the builder from + // the wallet backend's owners map, so load a wallet that maps it to the tx. + w, cleanup, err := loadPChainWalletWithOwner(ctx, netConfig, autoRenewedTxID, validatorAuthority) + if err != nil { + return fmt.Errorf("failed to create wallet: %w", err) + } + defer cleanup() + + fmt.Printf("Setting auto-renewed validator config for %s...\n", autoRenewedTxID) + if period == 0 { + fmt.Println(" Period: 0s (exit after current cycle)") + } else { + fmt.Printf(" Period: %s\n", period) + } + fmt.Printf(" Auto-Compound Rewards: %.2f%%\n", valSetAutoCompound*100) + fmt.Println("Submitting transaction...") + + txID, err := pchain.SetAutoRenewedValidatorConfig(ctx, w, pchain.SetAutoRenewedValidatorConfigTxConfig{ + TxID: autoRenewedTxID, + AutoCompoundRewardShares: autoCompoundShares, + Period: period, + }) + if err != nil { + return err + } + + fmt.Printf("TX ID: %s\n", txID) + return nil + }, +} + func parseTimeRange(startStr, durationStr string) (time.Time, time.Time, error) { var start time.Time var err error @@ -227,6 +433,41 @@ func parseTimeRange(startStr, durationStr string) (time.Time, time.Time, error) return start, end, nil } +// parseAutoRenewPeriod parses a positive, whole-second auto-renewal cycle +// duration for add-auto-renewed. +func parseAutoRenewPeriod(periodStr string) (time.Duration, error) { + period, err := time.ParseDuration(periodStr) + if err != nil { + return 0, fmt.Errorf("invalid period: %w", err) + } + if period <= 0 { + return 0, fmt.Errorf("period must be positive") + } + if period%time.Second != 0 { + return 0, fmt.Errorf("period must be a whole number of seconds") + } + return period, nil +} + +// parseAutoRenewConfigPeriod parses a whole-second next-cycle duration for +// set-auto-config. A literal "0" (or "0s") means exit after the current cycle. +func parseAutoRenewConfigPeriod(periodStr string) (time.Duration, error) { + if strings.TrimSpace(periodStr) == "0" { + return 0, nil + } + period, err := time.ParseDuration(periodStr) + if err != nil { + return 0, fmt.Errorf("invalid period: %w", err) + } + if period < 0 { + return 0, fmt.Errorf("period cannot be negative") + } + if period%time.Second != 0 { + return 0, fmt.Errorf("period must be a whole number of seconds") + } + return period, nil +} + // getValidatorPoP returns a BLS proof of possession for validator registration. // Manual mode (default): use --bls-public-key and --bls-pop. // Fallback mode: fetch from --node-endpoint. @@ -272,6 +513,8 @@ func normalizeValidatorNodeURI(addr string) (string, error) { func init() { rootCmd.AddCommand(validatorCmd) validatorCmd.AddCommand(validatorAddCmd) + validatorCmd.AddCommand(validatorAddAutoRenewedCmd) + validatorCmd.AddCommand(validatorSetAutoConfigCmd) validatorCmd.AddCommand(validatorDelegateCmd) // Add validator flags @@ -285,6 +528,24 @@ func init() { validatorAddCmd.Flags().Float64Var(&valDelegationFee, "delegation-fee", 0.02, "Delegation fee (0.02 = 2%)") validatorAddCmd.Flags().StringVar(&valRewardAddr, "reward-address", "", "Reward address (default: own address)") + // Add auto-renewed validator flags + validatorAddAutoRenewedCmd.Flags().StringVar(&valNodeID, "node-id", "", "Node ID to validate (required)") + validatorAddAutoRenewedCmd.Flags().StringVar(&valNodeEndpoint, "node-endpoint", "", "Validator node endpoint (fallback mode) to fetch BLS proof of possession") + validatorAddAutoRenewedCmd.Flags().StringVar(&valBLSPublicKey, "bls-public-key", "", "Validator BLS public key (hex, recommended/manual mode)") + validatorAddAutoRenewedCmd.Flags().StringVar(&valBLSPoP, "bls-pop", "", "Validator BLS proof of possession signature (hex, recommended/manual mode)") + validatorAddAutoRenewedCmd.Flags().Float64Var(&valStakeAmount, "stake", 0, "Stake amount in AVAX (network minimum applies)") + validatorAddAutoRenewedCmd.Flags().StringVar(&valAutoPeriod, "period", "336h", "Auto-renewal cycle duration (for example, 336h for 14 days)") + validatorAddAutoRenewedCmd.Flags().Float64Var(&valDelegationFee, "delegation-fee", 0.02, "Delegation fee (0.02 = 2%)") + validatorAddAutoRenewedCmd.Flags().Float64Var(&valAutoCompound, "auto-compound", 1, "Fraction of rewards to auto-compound (0.3 = 30%, 1 = 100%)") + validatorAddAutoRenewedCmd.Flags().StringVar(&valRewardAddr, "reward-address", "", "Reward address (default: own address)") + validatorAddAutoRenewedCmd.Flags().StringVar(&valOwnerAddr, "owner-address", "", "Address authorized to update auto-renew config (default: own address)") + + // Set auto-renewed validator config flags + validatorSetAutoConfigCmd.Flags().StringVar(&valSetAutoTxID, "tx-id", "", "Original AddAutoRenewedValidatorTx ID (required)") + validatorSetAutoConfigCmd.Flags().StringVar(&valSetAutoNodeID, "node-id", "", "Validator node ID to narrow the authority lookup (optional, recommended)") + validatorSetAutoConfigCmd.Flags().StringVar(&valSetAutoPeriod, "period", "", "Next auto-renewal cycle duration, or 0 to exit after the current cycle (required)") + validatorSetAutoConfigCmd.Flags().Float64Var(&valSetAutoCompound, "auto-compound", 0, "Fraction of rewards to auto-compound (0.3 = 30%, 1 = 100%) (required)") + // Delegate flags validatorDelegateCmd.Flags().StringVar(&valNodeID, "node-id", "", "Node ID to delegate to") validatorDelegateCmd.Flags().Float64Var(&valStakeAmount, "stake", 0, "Stake amount in AVAX (min 25)") diff --git a/cmd/wallet.go b/cmd/wallet.go index 203f4a8..3258a6d 100644 --- a/cmd/wallet.go +++ b/cmd/wallet.go @@ -8,6 +8,8 @@ import ( "github.com/ava-labs/avalanchego/ids" "github.com/ava-labs/avalanchego/utils/constants" + "github.com/ava-labs/avalanchego/vms/platformvm/fx" + "github.com/ava-labs/avalanchego/vms/secp256k1fx" "github.com/ava-labs/platform-cli/pkg/keystore" "github.com/ava-labs/platform-cli/pkg/network" "github.com/ava-labs/platform-cli/pkg/wallet" @@ -280,6 +282,49 @@ func loadPChainWalletWithSubnet(ctx context.Context, netConfig network.Config, s return w, func() {}, nil } +// loadPChainWalletWithOwner creates a P-Chain wallet whose backend maps ownerID +// to owner, enabling owner-authorized transactions (e.g. +// SetAutoRenewedValidatorConfigTx). It fetches P-Chain state once, replacing the +// standard wallet load rather than adding a second round-trip. +func loadPChainWalletWithOwner(ctx context.Context, netConfig network.Config, ownerID ids.ID, owner fx.Owner) (*wallet.Wallet, func(), error) { + if useLedger { + if !wallet.LedgerEnabled { + return nil, nil, fmt.Errorf("ledger support not compiled. Rebuild with: go build -tags ledger") + } + kc, err := wallet.NewLedgerKeychain(ledgerIndex) + if err != nil { + return nil, nil, err + } + w, err := wallet.NewWalletFromKeychainWithOwner(ctx, kc, kc.GetAddress(), netConfig, ownerID, owner) + if err != nil { + kc.Close() + return nil, nil, err + } + return w, kc.Close, nil + } + + keyBytes, err := loadKey() + if err != nil { + return nil, nil, err + } + // Clear key bytes after wallet creation + defer clearBytesWallet(keyBytes) + if netConfig.NetworkID == constants.MainnetID && isEwoqKey(keyBytes) { + return nil, nil, fmt.Errorf("ewoq test key cannot be used on mainnet - this is a well-known key with no security") + } + + key, err := wallet.ToPrivateKey(keyBytes) + if err != nil { + return nil, nil, err + } + kc := secp256k1fx.NewKeychain(key) + w, err := wallet.NewWalletFromKeychainWithOwner(ctx, kc, key.Address(), netConfig, ownerID, owner) + if err != nil { + return nil, nil, err + } + return w, func() {}, nil +} + // loadFullWallet creates a multi-chain wallet (P-Chain + C-Chain). func loadFullWallet(ctx context.Context, netConfig network.Config) (*wallet.FullWallet, func(), error) { if useLedger { diff --git a/e2e/cli_test.go b/e2e/cli_test.go index ecdc65f..973642e 100644 --- a/e2e/cli_test.go +++ b/e2e/cli_test.go @@ -125,7 +125,7 @@ func TestCLIValidatorHelp(t *testing.T) { t.Fatalf("validator help failed: %v", err) } - expected := []string{"add", "delegate"} + expected := []string{"add", "add-auto-renewed", "set-auto-config", "delegate"} for _, cmd := range expected { if !strings.Contains(stdout, cmd) { t.Errorf("validator help missing subcommand: %s", cmd) @@ -306,6 +306,56 @@ func TestCLIValidatorDelegateMissingArgs(t *testing.T) { } } +func TestCLIValidatorAddAutoRenewedHelp(t *testing.T) { + stdout, _, err := runCLI(t, "validator", "add-auto-renewed", "--help") + if err != nil { + t.Fatalf("add-auto-renewed help failed: %v", err) + } + + expected := []string{"node-id", "stake", "period", "auto-compound", "owner-address"} + for _, flag := range expected { + if !strings.Contains(stdout, flag) { + t.Errorf("add-auto-renewed help missing flag: %s", flag) + } + } +} + +func TestCLIValidatorSetAutoConfigHelp(t *testing.T) { + stdout, _, err := runCLI(t, "validator", "set-auto-config", "--help") + if err != nil { + t.Fatalf("set-auto-config help failed: %v", err) + } + + expected := []string{"tx-id", "node-id", "period", "auto-compound"} + for _, flag := range expected { + if !strings.Contains(stdout, flag) { + t.Errorf("set-auto-config help missing flag: %s", flag) + } + } +} + +func TestCLIValidatorAddAutoRenewedMissingArgs(t *testing.T) { + _, stderr, err := runCLI(t, "validator", "add-auto-renewed") + if err == nil { + t.Error("expected error when missing required args") + } + + if !strings.Contains(stderr, "stake") && !strings.Contains(stderr, "required") { + t.Logf("stderr: %s", stderr) + } +} + +func TestCLIValidatorSetAutoConfigMissingArgs(t *testing.T) { + _, stderr, err := runCLI(t, "validator", "set-auto-config") + if err == nil { + t.Error("expected error when missing required args") + } + + if !strings.Contains(stderr, "tx-id") && !strings.Contains(stderr, "required") { + t.Logf("stderr: %s", stderr) + } +} + // ============================================================================= // CLI L1 Command Tests (Error Path - requires valid data) // ============================================================================= diff --git a/go.mod b/go.mod index 6226294..4fba0a2 100644 --- a/go.mod +++ b/go.mod @@ -3,9 +3,9 @@ module github.com/ava-labs/platform-cli go 1.25.11 require ( - github.com/ava-labs/avalanchego v1.14.1 + github.com/ava-labs/avalanchego v1.14.3-0.20260603151011-1339ef45dc6c github.com/ava-labs/ledger-avalanche-go v1.1.0 - github.com/ava-labs/libevm v1.13.15-0.20251210210615-b8e76562a300 + github.com/ava-labs/libevm v1.13.15-0.20260602011657-ad0081e3b988 github.com/spf13/cobra v1.9.1 golang.org/x/crypto v0.50.0 golang.org/x/term v0.42.0 @@ -15,16 +15,16 @@ require ( connectrpc.com/connect v1.18.1 // indirect connectrpc.com/grpcreflect v1.3.0 // indirect github.com/DataDog/zstd v1.5.2 // indirect - github.com/Microsoft/go-winio v0.6.1 // indirect - github.com/StephenButtolph/canoto v0.17.3 // indirect + github.com/Microsoft/go-winio v0.6.2 // indirect + github.com/StephenButtolph/canoto v0.18.0 // indirect github.com/VictoriaMetrics/fastcache v1.12.1 // indirect - github.com/ava-labs/avalanchego/graft/coreth v0.0.0-20251203215505-70148edc6eca // indirect + github.com/ava-labs/avalanchego/graft/coreth v1.14.3-0.20260602193739-919446e8501f // indirect + github.com/ava-labs/avalanchego/graft/evm v1.14.3-0.20260602193739-919446e8501f // indirect github.com/beorn7/perks v1.0.1 // indirect github.com/bits-and-blooms/bitset v1.20.0 // indirect github.com/btcsuite/btcd/btcec/v2 v2.3.5 // indirect github.com/btcsuite/btcd/btcutil v1.1.3 // indirect - github.com/cenkalti/backoff/v4 v4.3.0 // indirect - github.com/cenkalti/backoff/v5 v5.0.2 // indirect + github.com/cenkalti/backoff/v5 v5.0.3 // indirect github.com/cespare/xxhash/v2 v2.3.0 // indirect github.com/cockroachdb/errors v1.9.1 // indirect github.com/cockroachdb/logtags v0.0.0-20230118201751-21c54148d20b // indirect @@ -49,11 +49,11 @@ require ( github.com/go-openapi/jsonreference v0.20.2 // indirect github.com/go-openapi/swag v0.22.3 // indirect github.com/go-viper/mapstructure/v2 v2.4.0 // indirect - github.com/gofrs/flock v0.8.1 // indirect + github.com/gofrs/flock v0.12.1 // indirect github.com/gogo/protobuf v1.3.2 // indirect github.com/golang/protobuf v1.5.4 // indirect github.com/golang/snappy v0.0.5-0.20220116011046-fa5810519dcb // indirect - github.com/google/btree v1.1.2 // indirect + github.com/google/btree v1.1.3 // indirect github.com/google/gnostic-models v0.6.8 // indirect github.com/google/gofuzz v1.2.0 // indirect github.com/google/renameio/v2 v2.0.0 // indirect @@ -61,7 +61,7 @@ require ( github.com/gorilla/mux v1.8.0 // indirect github.com/gorilla/rpc v1.2.0 // indirect github.com/gorilla/websocket v1.5.0 // indirect - github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.1 // indirect + github.com/grpc-ecosystem/grpc-gateway/v2 v2.28.0 // indirect github.com/holiman/bloomfilter/v2 v2.0.3 // indirect github.com/holiman/uint256 v1.2.4 // indirect github.com/imdario/mergo v0.3.16 // indirect @@ -72,8 +72,8 @@ require ( github.com/kr/pretty v0.3.1 // indirect github.com/kr/text v0.2.0 // indirect github.com/mailru/easyjson v0.7.7 // indirect - github.com/mattn/go-runewidth v0.0.13 // indirect - github.com/moby/spdystream v0.2.0 // indirect + github.com/mattn/go-runewidth v0.0.16 // indirect + github.com/moby/spdystream v0.5.1 // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/reflect2 v1.0.2 // indirect github.com/mr-tron/base58 v1.2.0 // indirect @@ -87,7 +87,7 @@ require ( github.com/prometheus/client_model v0.6.2 // indirect github.com/prometheus/common v0.65.0 // indirect github.com/prometheus/procfs v0.16.1 // indirect - github.com/rivo/uniseg v0.2.0 // indirect + github.com/rivo/uniseg v0.4.7 // indirect github.com/rogpeppe/go-internal v1.14.1 // indirect github.com/rs/cors v1.7.0 // indirect github.com/sagikazarmark/locafero v0.9.0 // indirect @@ -95,7 +95,7 @@ require ( github.com/sourcegraph/conc v0.3.0 // indirect github.com/spf13/afero v1.14.0 // indirect github.com/spf13/cast v1.9.2 // indirect - github.com/spf13/pflag v1.0.6 // indirect + github.com/spf13/pflag v1.0.10 // indirect github.com/spf13/viper v1.20.1 // indirect github.com/subosito/gotenv v1.6.0 // indirect github.com/supranational/blst v0.3.14 // indirect @@ -107,31 +107,29 @@ require ( github.com/zondax/hid v0.9.2 // indirect github.com/zondax/ledger-go v1.0.1 // indirect go.opentelemetry.io/auto/sdk v1.2.1 // indirect - go.opentelemetry.io/otel v1.40.0 // indirect - go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.37.0 // indirect + go.opentelemetry.io/otel v1.43.0 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.43.0 // indirect go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.37.0 // indirect - go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.22.0 // indirect - go.opentelemetry.io/otel/metric v1.40.0 // indirect - go.opentelemetry.io/otel/sdk v1.40.0 // indirect - go.opentelemetry.io/otel/trace v1.40.0 // indirect - go.opentelemetry.io/proto/otlp v1.7.0 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.43.0 // indirect + go.opentelemetry.io/otel/metric v1.43.0 // indirect + go.opentelemetry.io/otel/sdk v1.43.0 // indirect + go.opentelemetry.io/otel/trace v1.43.0 // indirect + go.opentelemetry.io/proto/otlp v1.10.0 // indirect go.uber.org/mock v0.5.0 // indirect go.uber.org/multierr v1.11.0 // indirect go.uber.org/zap v1.27.0 // indirect - golang.org/x/exp v0.0.0-20241215155358-4a5509556b9e // indirect - golang.org/x/mod v0.34.0 // indirect + golang.org/x/exp v0.0.0-20250620022241-b7579e27df2b // indirect golang.org/x/net v0.53.0 // indirect - golang.org/x/oauth2 v0.30.0 // indirect + golang.org/x/oauth2 v0.35.0 // indirect golang.org/x/sync v0.20.0 // indirect golang.org/x/sys v0.43.0 // indirect golang.org/x/text v0.36.0 // indirect golang.org/x/time v0.12.0 // indirect - golang.org/x/tools v0.43.0 // indirect - gonum.org/v1/gonum v0.16.0 // indirect - google.golang.org/genproto/googleapis/api v0.0.0-20250707201910-8d1bb00bc6a7 // indirect - google.golang.org/genproto/googleapis/rpc v0.0.0-20250818200422-3122310a409c // indirect - google.golang.org/grpc v1.75.0 // indirect - google.golang.org/protobuf v1.36.8 // indirect + gonum.org/v1/gonum v0.17.0 // indirect + google.golang.org/genproto/googleapis/api v0.0.0-20260401024825-9d38bb4040a9 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20260401024825-9d38bb4040a9 // indirect + google.golang.org/grpc v1.80.0 // indirect + google.golang.org/protobuf v1.36.11 // indirect gopkg.in/inf.v0 v0.9.1 // indirect gopkg.in/natefinch/lumberjack.v2 v2.0.0 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect diff --git a/go.sum b/go.sum index f3c417e..259f3ca 100644 --- a/go.sum +++ b/go.sum @@ -12,11 +12,11 @@ github.com/CloudyKit/jet/v3 v3.0.0/go.mod h1:HKQPgSJmdK8hdoAbKUUWajkHyHo4RaU5rMd github.com/DataDog/zstd v1.5.2 h1:vUG4lAyuPCXO0TLbXvPv7EB7cNK1QV/luu55UHLrrn8= github.com/DataDog/zstd v1.5.2/go.mod h1:g4AWEaM3yOg3HYfnJ3YIawPnVdXJh9QME85blwSAmyw= github.com/Joker/hpp v1.0.0/go.mod h1:8x5n+M1Hp5hC0g8okX3sR3vFQwynaX/UgSOM9MeBKzY= -github.com/Microsoft/go-winio v0.6.1 h1:9/kr64B9VUZrLm5YYwbGtUJnMgqWVOdUAXu6Migciow= -github.com/Microsoft/go-winio v0.6.1/go.mod h1:LRdKpFKfdobln8UmuiYcKPot9D2v6svN5+sAH+4kjUM= +github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY= +github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU= github.com/Shopify/goreferrer v0.0.0-20181106222321-ec9c9a553398/go.mod h1:a1uqRtAwp2Xwc6WNPJEufxJ7fx3npB4UV/JOLmbu5I0= -github.com/StephenButtolph/canoto v0.17.3 h1:lvsnYD4b96vD1knnmp1xCmZqfYpY/jSeRozGdOfdvGI= -github.com/StephenButtolph/canoto v0.17.3/go.mod h1:IcnAHC6nJUfQFVR9y60ko2ecUqqHHSB6UwI9NnBFZnE= +github.com/StephenButtolph/canoto v0.18.0 h1:czLis9aEly5R/ExPwB5X0PJLhXi7Sv4PpffaAJV3KnE= +github.com/StephenButtolph/canoto v0.18.0/go.mod h1:01RsiQp1gnV1eJ6LwygP6buPCLUoAz7jKadQSB0FI0o= github.com/VictoriaMetrics/fastcache v1.12.1 h1:i0mICQuojGDL3KblA7wUNlY5lOK6a4bwt3uRKnkZU40= github.com/VictoriaMetrics/fastcache v1.12.1/go.mod h1:tX04vaqcNoQeGLD+ra5pU5sWkuxnzWhEzLwhP9w653o= github.com/aead/siphash v1.0.1/go.mod h1:Nywa3cDsYNNK3gaciGTWPwHt0wlpNV15vwmswBAUSII= @@ -26,16 +26,18 @@ github.com/allegro/bigcache v1.2.1-0.20190218064605-e24eb225f156/go.mod h1:Cb/ax github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8= github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPdPJAN/hZIm0C4OItdklCFmMRWYpio= github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs= -github.com/ava-labs/avalanchego v1.14.1 h1:955YG175dhyFibiPr1LA1rDw9x8Vt1UcLuNwRLOk1kM= -github.com/ava-labs/avalanchego v1.14.1/go.mod h1:TWuYmX/Wmfdd+9KvFZJHGldQ6SXq0jZ334T0kUOaJsM= -github.com/ava-labs/avalanchego/graft/coreth v0.0.0-20251203215505-70148edc6eca h1:zZIQZhOqKe82SUvEx7IeRVoahjyKI0gfouHPQkvEHeI= -github.com/ava-labs/avalanchego/graft/coreth v0.0.0-20251203215505-70148edc6eca/go.mod h1:y+/5DAxCTLAXdWRxAYN1V8DV0DIF7uHhOOeNa9oASuU= -github.com/ava-labs/firewood-go-ethhash/ffi v0.0.18 h1:Lk4yxNL3iZMRxKZlTKVCHp0Rg7i5QclRei0ZKCgtPac= -github.com/ava-labs/firewood-go-ethhash/ffi v0.0.18/go.mod h1:hR/JSGXxST9B9olwu/NpLXHAykfAyNGfyKnYQqiiOeE= +github.com/ava-labs/avalanchego v1.14.3-0.20260603151011-1339ef45dc6c h1:5v+Cp6AxxWWicn4DcnpbI1Q5+yiti3Bd20dIVVYZQtw= +github.com/ava-labs/avalanchego v1.14.3-0.20260603151011-1339ef45dc6c/go.mod h1:jIUfPDCGYX+oDyM0Ch+EC1hv0uki94xyjNnu1tf1fOQ= +github.com/ava-labs/avalanchego/graft/coreth v1.14.3-0.20260602193739-919446e8501f h1:BJ96PwQIvi+ZVpgLAbSsCyGvtDMBvDhDNrinEUPbS4o= +github.com/ava-labs/avalanchego/graft/coreth v1.14.3-0.20260602193739-919446e8501f/go.mod h1:26DyfT1dixgAro93kATSn0IwMsClMLhdhyfG/wn1ggg= +github.com/ava-labs/avalanchego/graft/evm v1.14.3-0.20260602193739-919446e8501f h1:1X6Vx7YtLBc2y5HqG3u5cXF7w8HNHwtW6jGUCXCHYOg= +github.com/ava-labs/avalanchego/graft/evm v1.14.3-0.20260602193739-919446e8501f/go.mod h1:26znzSsbvyDdC8gt8pzQNdANjD5xaEXYkJsaUQl28O4= +github.com/ava-labs/firewood-go-ethhash/ffi v0.5.0 h1:LXdgVYLV01sn2g4hqZUU8kP3/v3Uf4/1/069rFcc1u8= +github.com/ava-labs/firewood-go-ethhash/ffi v0.5.0/go.mod h1:MoUHYlFrwaflfLpHt8nmQUHoLy2CCHaFNH/n7vazUuI= github.com/ava-labs/ledger-avalanche-go v1.1.0 h1:OkscKtb/gX20HBt8RyAtwXLrQnCEls5SzWGieE7NoNM= github.com/ava-labs/ledger-avalanche-go v1.1.0/go.mod h1:mAlG9ptnPjvNoLGLHXnM3slGY8ewvBJtJNVTEjG8KvI= -github.com/ava-labs/libevm v1.13.15-0.20251210210615-b8e76562a300 h1:9VRvqASGSAnQ9tKVRKGH8Q0Yq8efCwYTBWp0p2creho= -github.com/ava-labs/libevm v1.13.15-0.20251210210615-b8e76562a300/go.mod h1:DqSotSn4Dx/UJV+d3svfW8raR+cH7+Ohl9BpsQ5HlGU= +github.com/ava-labs/libevm v1.13.15-0.20260602011657-ad0081e3b988 h1:h18i7JfNG4qiUlpE1ZZINRa1BZaPVdbNXQxO/2ol4tE= +github.com/ava-labs/libevm v1.13.15-0.20260602011657-ad0081e3b988/go.mod h1:6NxGoR1aLABnfLy+fncXRj0W6rUoUrXghnAWZ+Rhr4o= github.com/aymerick/raymond v2.0.3-0.20180322193309-b565731e1464+incompatible/go.mod h1:osfaiScAUVup+UC9Nfq76eWqDhXlp+4UYaA8uhTBO6g= github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= @@ -66,10 +68,8 @@ github.com/btcsuite/snappy-go v0.0.0-20151229074030-0bdef8d06723/go.mod h1:8woku github.com/btcsuite/snappy-go v1.0.0/go.mod h1:8woku9dyThutzjeg+3xrA5iCpBRH8XEEg3lh6TiUghc= github.com/btcsuite/websocket v0.0.0-20150119174127-31079b680792/go.mod h1:ghJtEyQwv5/p4Mg4C0fgbePVuGr935/5ddU9Z3TmDRY= github.com/btcsuite/winsvc v1.0.0/go.mod h1:jsenWakMcC0zFBFurPLEAyrnc/teJEM1O46fmI40EZs= -github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8= -github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE= -github.com/cenkalti/backoff/v5 v5.0.2 h1:rIfFVxEf1QsI7E1ZHfp/B4DF/6QBAUhmgkxc0H7Zss8= -github.com/cenkalti/backoff/v5 v5.0.2/go.mod h1:rkhZdG3JZukswDf7f0cwqPNk4K0sa+F97BxZthm/crw= +github.com/cenkalti/backoff/v5 v5.0.3 h1:ZN+IMa753KfX5hd8vVaMixjnqRZ3y8CuJKRKj1xcsSM= +github.com/cenkalti/backoff/v5 v5.0.3/go.mod h1:rkhZdG3JZukswDf7f0cwqPNk4K0sa+F97BxZthm/crw= github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= github.com/cespare/cp v0.1.0 h1:SE+dxFebS7Iik5LK0tsi1k9ZCxEaFX4AjQmoyA+1dJk= github.com/cespare/cp v0.1.0/go.mod h1:SOGHArjBr4JWaSDEVpWpo/hNg6RoKrls6Oh40hiwW+s= @@ -178,14 +178,15 @@ github.com/go-openapi/swag v0.22.3 h1:yMBqmnQ0gyZvEb/+KzuWZOXgllrXT4SADYbvDaXHv/ github.com/go-openapi/swag v0.22.3/go.mod h1:UzaqsxGiab7freDnrUUra0MwWfN/q7tE4j+VcZ0yl14= github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod h1:fyg7847qk6SyHyPtNmDHnmrv/HOrqktSC+C9fM+CJOE= github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 h1:tfuBGBXKqDEevZMzYi5KSi8KkcZtzBcTgAUUtapy0OI= -github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572/go.mod h1:9Pwr4B2jHnOSGXyyzV8ROjYa2ojvAY6HCGYYfMoC3Ls= +github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI= +github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8= github.com/go-viper/mapstructure/v2 v2.4.0 h1:EBsztssimR/CONLSZZ04E8qAkxNYq4Qp9LvH92wZUgs= github.com/go-viper/mapstructure/v2 v2.4.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM= github.com/gobwas/httphead v0.0.0-20180130184737-2c6c146eadee/go.mod h1:L0fX3K22YWvt/FAX9NnzrNzcI4wNYi9Yku4O0LKYflo= github.com/gobwas/pool v0.2.0/go.mod h1:q8bcK0KcYlCgd9e7WYLm9LpyS+YeLd8JVDW6WezmKEw= github.com/gobwas/ws v1.0.2/go.mod h1:szmBTxLgaFppYjEmNtny/v3w89xOydFnnZMcgRRu/EM= -github.com/gofrs/flock v0.8.1 h1:+gYjHKf32LDeiEEFhQaotPbLuUXjY5ZqxKgXy7n59aw= -github.com/gofrs/flock v0.8.1/go.mod h1:F1TvTiK9OcQqauNUHlbJvyl9Qa1QvF/gOUDKA14jxHU= +github.com/gofrs/flock v0.12.1 h1:MTLVXXHf8ekldpJk3AKicLij9MdwOWkZ+a/jHHZby9E= +github.com/gofrs/flock v0.12.1/go.mod h1:9zxTsyu5xtJ9DK+1tFZyibEV7y3uwDxPPfbxeeHCoD0= github.com/gogo/googleapis v0.0.0-20180223154316-0cd9801be74a/go.mod h1:gf4bu3Q80BeJ6H1S1vYPm8/ELATdvryBaNFGgqEef3s= github.com/gogo/googleapis v1.4.1/go.mod h1:2lpHqI5OcWCtVElxXnPt+s8oJvMpySlOyM6xDCrzib4= github.com/gogo/protobuf v1.2.0/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= @@ -213,8 +214,8 @@ github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEW github.com/golang/snappy v0.0.5-0.20220116011046-fa5810519dcb h1:PBC98N2aIaM3XXiurYmW7fx4GZkL8feAMVq7nEjURHk= github.com/golang/snappy v0.0.5-0.20220116011046-fa5810519dcb/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= github.com/gomodule/redigo v1.7.1-0.20190724094224-574c33c3df38/go.mod h1:B4C85qUVwatsJoIUNIfCRsp7qO0iAmpGFZ4EELWSbC4= -github.com/google/btree v1.1.2 h1:xf4v41cLI2Z6FxbKm+8Bu+m8ifhj15JuZ9sa0jZCMUU= -github.com/google/btree v1.1.2/go.mod h1:qOPhT0dTNdNzV6Z/lhRX0YXUafgPLFUh+gZMl761Gm4= +github.com/google/btree v1.1.3 h1:CVpQJjYgC4VbzxeGVHfvZrv1ctoYCAI8vbl07Fcxlyg= +github.com/google/btree v1.1.3/go.mod h1:qOPhT0dTNdNzV6Z/lhRX0YXUafgPLFUh+gZMl761Gm4= github.com/google/gnostic-models v0.6.8 h1:yo/ABAfM5IMRsS1VnXjTBvUb61tFIHozhlYvRgGre9I= github.com/google/gnostic-models v0.6.8/go.mod h1:5n7qKqH0f5wFt+aWF8CW6pZLLNOfYuF5OpfBSENuI8U= github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= @@ -244,11 +245,10 @@ github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB7 github.com/gorilla/rpc v1.2.0 h1:WvvdC2lNeT1SP32zrIce5l0ECBfbAlmrmSBsuc57wfk= github.com/gorilla/rpc v1.2.0/go.mod h1:V4h9r+4sF5HnzqbwIez0fKSpANP0zlYd3qR7p36jkTQ= github.com/gorilla/websocket v1.4.1/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= -github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc= github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= -github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.1 h1:X5VWvz21y3gzm9Nw/kaUeku/1+uBhcekkmy4IkffJww= -github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.1/go.mod h1:Zanoh4+gvIgluNqcfMVTJueD4wSS5hT7zTt4Mrutd90= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.28.0 h1:HWRh5R2+9EifMyIHV7ZV+MIZqgz+PMpZ14Jynv3O2Zs= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.28.0/go.mod h1:JfhWUomR1baixubs02l85lZYYOm7LV6om4ceouMv45c= github.com/hashicorp/go-bexpr v0.1.10 h1:9kuI5PFotCboP3dkDYFr/wi0gg0QVbSNz5oFRpxn4uE= github.com/hashicorp/go-bexpr v0.1.10/go.mod h1:oxlubA2vC/gFVfX1A6JGp7ls7uCDlfJn732ehYYg+g0= github.com/hashicorp/go-version v1.2.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA= @@ -326,18 +326,18 @@ github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJ github.com/mattn/go-colorable v0.1.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= github.com/mattn/go-colorable v0.1.8/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= github.com/mattn/go-colorable v0.1.11/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4= -github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= -github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= +github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE= +github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8= github.com/mattn/go-isatty v0.0.7/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= github.com/mattn/go-isatty v0.0.9/go.mod h1:YNRxwqDuOph6SZLI9vUUz6OYw3QyUt7WiY2yME+cCiQ= github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= -github.com/mattn/go-isatty v0.0.17 h1:BTarxUcIeDqL27Mc+vyvdWYSL28zpIhv3RoTdsLMPng= -github.com/mattn/go-isatty v0.0.17/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= +github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= +github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI= -github.com/mattn/go-runewidth v0.0.13 h1:lTGmDsbAYt5DmK6OnoV7EuIF1wEIFAcxld6ypU4OSgU= -github.com/mattn/go-runewidth v0.0.13/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= +github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc= +github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= github.com/mattn/goveralls v0.0.2/go.mod h1:8d1ZMHsd7fW6IRPKQh46F2WRpyib5/X4FOpevwGNQEw= github.com/mediocregopher/radix/v3 v3.4.2/go.mod h1:8FL3F6UQRXHXIBSPUs5h0RybMF8i4n7wVopoX3x7Bv8= github.com/microcosm-cc/bluemonday v1.0.2/go.mod h1:iVP4YcDBq+n/5fb23BhYFvIMq/leAFZyRl6bYmGDlGc= @@ -347,8 +347,8 @@ github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyua github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= github.com/mitchellh/pointerstructure v1.2.0 h1:O+i9nHnXS3l/9Wu7r4NrEdwA2VFTicjUEN1uBnDo34A= github.com/mitchellh/pointerstructure v1.2.0/go.mod h1:BRAsLI5zgXmw97Lf6s25bs8ohIXc3tViBH44KcwB2g4= -github.com/moby/spdystream v0.2.0 h1:cjW1zVyyoiM0T7b6UoySUFqzXMoqRckQtXwGPiBhOM8= -github.com/moby/spdystream v0.2.0/go.mod h1:f7i0iNDQJ059oMTcWxx8MA/zKFIuD/lY+0GqbN2Wy8c= +github.com/moby/spdystream v0.5.1 h1:9sNYeYZUcci9R6/w7KDaFWEWeV4LStVG78Mpyq/Zm/Y= +github.com/moby/spdystream v0.5.1/go.mod h1:xBAYlnt/ay+11ShkdFKNAG7LsyK/tmNBVvVOwrfMgdI= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= @@ -383,16 +383,16 @@ github.com/onsi/ginkgo v1.16.4/go.mod h1:dX+/inL/fNMqNlz0e9LfyB9TswhZpCVdJM/Z6Vv github.com/onsi/ginkgo v1.16.5 h1:8xi0RTUf59SOSfEtZMvwTvXYMzG4gV23XVHOZiXNtnE= github.com/onsi/ginkgo v1.16.5/go.mod h1:+E8gABHa3K6zRBolWtd+ROzc/U5bkGt0FwiG042wbpU= github.com/onsi/ginkgo/v2 v2.1.3/go.mod h1:vw5CSIxN1JObi/U8gcbwft7ZxR2dgaR70JSE3/PpL4c= -github.com/onsi/ginkgo/v2 v2.13.1 h1:LNGfMbR2OVGBfXjvRZIZ2YCTQdGKtPLvuI1rMCCj3OU= -github.com/onsi/ginkgo/v2 v2.13.1/go.mod h1:XStQ8QcGwLyF4HdfcZB8SFOS/MWCgDuXMSBe6zrvLgM= +github.com/onsi/ginkgo/v2 v2.23.3 h1:edHxnszytJ4lD9D5Jjc4tiDkPBZ3siDeJJkUZJJVkp0= +github.com/onsi/ginkgo/v2 v2.23.3/go.mod h1:zXTP6xIp3U8aVuXN8ENK9IXRaTjFnpVB9mGmaSRvxnM= github.com/onsi/gomega v1.4.1/go.mod h1:C1qb7wdrVGGVU+Z6iS04AVkA3Q65CEZX59MT0QO5uiA= github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY= github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo= github.com/onsi/gomega v1.17.0/go.mod h1:HnhC7FXeEQY45zxNK3PPoIUhzk/80Xly9PcubAlGdZY= github.com/onsi/gomega v1.19.0/go.mod h1:LY+I3pBVzYsTBU1AnDwOSxaYi9WoWiqgwooUqq9yPro= -github.com/onsi/gomega v1.29.0 h1:KIA/t2t5UBzoirT4H9tsML45GEbo3ouUnBHsCfD2tVg= -github.com/onsi/gomega v1.29.0/go.mod h1:9sxs+SwGrKI0+PWe4Fxa9tFQQBG5xSsSbMXOI8PPpoQ= +github.com/onsi/gomega v1.36.2 h1:koNYke6TVk6ZmnyHrCXba/T/MoLBXFjeC1PtvYgw0A8= +github.com/onsi/gomega v1.36.2/go.mod h1:DdwyADRjrc825LhMEkD76cHR5+pUnjhUN8GlHlRPHzY= github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic= github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4= github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY= @@ -416,8 +416,9 @@ github.com/prometheus/common v0.65.0 h1:QDwzd+G1twt//Kwj/Ww6E9FQq1iVMmODnILtW1t2 github.com/prometheus/common v0.65.0/go.mod h1:0gZns+BLRQ3V6NdaerOhMbwwRbNh9hkGINtQAsP5GS8= github.com/prometheus/procfs v0.16.1 h1:hZ15bTNuirocR6u0JZ6BAHHmwS1p8B4P6MRqxtzMyRg= github.com/prometheus/procfs v0.16.1/go.mod h1:teAbpZRB1iIAJYREa1LsoWUXykVXA1KlTmWl8x/U+Is= -github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY= github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= +github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ= +github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc= github.com/rogpeppe/go-internal v1.8.1/go.mod h1:JeRgkft04UBgHMgCIwADu4Pn6Mtm5d4nPKWu0nJ5d+o= github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= @@ -454,8 +455,9 @@ github.com/spf13/cobra v1.9.1 h1:CXSaggrXdbHK9CF+8ywj8Amf7PBRmPCOJugH954Nnlo= github.com/spf13/cobra v1.9.1/go.mod h1:nDyEzZ8ogv936Cinf6g1RU9MRY64Ir93oCnqb9wxYW0= github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo= github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= -github.com/spf13/pflag v1.0.6 h1:jFzHGLGAlb3ruxLB8MhbI6A8+AQX/2eW4qeyNZXNp2o= github.com/spf13/pflag v1.0.6/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk= +github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/spf13/viper v1.3.2/go.mod h1:ZiWeW+zYFKm7srdB9IoDzzZXaJaI5eL9QjNiN/DMA2s= github.com/spf13/viper v1.20.1 h1:ZMi+z/lvLyPSCoNtFCpqjy0S4kPbirhpTMwl8BkW9X4= github.com/spf13/viper v1.20.1/go.mod h1:P9Mdzt1zoHIG8m2eZQinpiBjo6kCmZSKBClNNqjJvu4= @@ -525,24 +527,24 @@ github.com/zondax/ledger-go v1.0.1 h1:Ks/2tz/dOF+dbRynfZ0dEhcdL1lqw43Sa0zMXHpQ3a github.com/zondax/ledger-go v1.0.1/go.mod h1:j7IgMY39f30apthJYMd1YsHZRqdyu4KbVmUp0nU78X0= go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64= go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y= -go.opentelemetry.io/otel v1.40.0 h1:oA5YeOcpRTXq6NN7frwmwFR0Cn3RhTVZvXsP4duvCms= -go.opentelemetry.io/otel v1.40.0/go.mod h1:IMb+uXZUKkMXdPddhwAHm6UfOwJyh4ct1ybIlV14J0g= -go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.37.0 h1:Ahq7pZmv87yiyn3jeFz/LekZmPLLdKejuO3NcK9MssM= -go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.37.0/go.mod h1:MJTqhM0im3mRLw1i8uGHnCvUEeS7VwRyxlLC78PA18M= +go.opentelemetry.io/otel v1.43.0 h1:mYIM03dnh5zfN7HautFE4ieIig9amkNANT+xcVxAj9I= +go.opentelemetry.io/otel v1.43.0/go.mod h1:JuG+u74mvjvcm8vj8pI5XiHy1zDeoCS2LB1spIq7Ay0= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.43.0 h1:88Y4s2C8oTui1LGM6bTWkw0ICGcOLCAI5l6zsD1j20k= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.43.0/go.mod h1:Vl1/iaggsuRlrHf/hfPJPvVag77kKyvrLeD10kpMl+A= go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.37.0 h1:EtFWSnwW9hGObjkIdmlnWSydO+Qs8OwzfzXLUPg4xOc= go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.37.0/go.mod h1:QjUEoiGCPkvFZ/MjK6ZZfNOS6mfVEVKYE99dFhuN2LI= -go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.22.0 h1:FyjCyI9jVEfqhUh2MoSkmolPjfh5fp2hnV0b0irxH4Q= -go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.22.0/go.mod h1:hYwym2nDEeZfG/motx0p7L7J1N1vyzIThemQsb4g2qY= -go.opentelemetry.io/otel/metric v1.40.0 h1:rcZe317KPftE2rstWIBitCdVp89A2HqjkxR3c11+p9g= -go.opentelemetry.io/otel/metric v1.40.0/go.mod h1:ib/crwQH7N3r5kfiBZQbwrTge743UDc7DTFVZrrXnqc= -go.opentelemetry.io/otel/sdk v1.40.0 h1:KHW/jUzgo6wsPh9At46+h4upjtccTmuZCFAc9OJ71f8= -go.opentelemetry.io/otel/sdk v1.40.0/go.mod h1:Ph7EFdYvxq72Y8Li9q8KebuYUr2KoeyHx0DRMKrYBUE= -go.opentelemetry.io/otel/sdk/metric v1.40.0 h1:mtmdVqgQkeRxHgRv4qhyJduP3fYJRMX4AtAlbuWdCYw= -go.opentelemetry.io/otel/sdk/metric v1.40.0/go.mod h1:4Z2bGMf0KSK3uRjlczMOeMhKU2rhUqdWNoKcYrtcBPg= -go.opentelemetry.io/otel/trace v1.40.0 h1:WA4etStDttCSYuhwvEa8OP8I5EWu24lkOzp+ZYblVjw= -go.opentelemetry.io/otel/trace v1.40.0/go.mod h1:zeAhriXecNGP/s2SEG3+Y8X9ujcJOTqQ5RgdEJcawiA= -go.opentelemetry.io/proto/otlp v1.7.0 h1:jX1VolD6nHuFzOYso2E73H85i92Mv8JQYk0K9vz09os= -go.opentelemetry.io/proto/otlp v1.7.0/go.mod h1:fSKjH6YJ7HDlwzltzyMj036AJ3ejJLCgCSHGj4efDDo= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.43.0 h1:3iZJKlCZufyRzPzlQhUIWVmfltrXuGyfjREgGP3UUjc= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.43.0/go.mod h1:/G+nUPfhq2e+qiXMGxMwumDrP5jtzU+mWN7/sjT2rak= +go.opentelemetry.io/otel/metric v1.43.0 h1:d7638QeInOnuwOONPp4JAOGfbCEpYb+K6DVWvdxGzgM= +go.opentelemetry.io/otel/metric v1.43.0/go.mod h1:RDnPtIxvqlgO8GRW18W6Z/4P462ldprJtfxHxyKd2PY= +go.opentelemetry.io/otel/sdk v1.43.0 h1:pi5mE86i5rTeLXqoF/hhiBtUNcrAGHLKQdhg4h4V9Dg= +go.opentelemetry.io/otel/sdk v1.43.0/go.mod h1:P+IkVU3iWukmiit/Yf9AWvpyRDlUeBaRg6Y+C58QHzg= +go.opentelemetry.io/otel/sdk/metric v1.43.0 h1:S88dyqXjJkuBNLeMcVPRFXpRw2fuwdvfCGLEo89fDkw= +go.opentelemetry.io/otel/sdk/metric v1.43.0/go.mod h1:C/RJtwSEJ5hzTiUz5pXF1kILHStzb9zFlIEe85bhj6A= +go.opentelemetry.io/otel/trace v1.43.0 h1:BkNrHpup+4k4w+ZZ86CZoHHEkohws8AY+WTX09nk+3A= +go.opentelemetry.io/otel/trace v1.43.0/go.mod h1:/QJhyVBUUswCphDVxq+8mld+AvhXZLhe+8WVFxiFff0= +go.opentelemetry.io/proto/otlp v1.10.0 h1:IQRWgT5srOCYfiWnpqUYz9CVmbO8bFmKcwYxpuCSL2g= +go.opentelemetry.io/proto/otlp v1.10.0/go.mod h1:/CV4QoCR/S9yaPj8utp3lvQPoqMtxXdzn7ozvvozVqk= go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= go.uber.org/mock v0.5.0 h1:KAMbZvZPyBPWgD14IrIQ38QCyjwpvVVV6K/bHl1IwQU= @@ -563,8 +565,8 @@ golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5y golang.org/x/crypto v0.50.0 h1:zO47/JPrL6vsNkINmLoo/PH1gcxpls50DNogFvB5ZGI= golang.org/x/crypto v0.50.0/go.mod h1:3muZ7vA7PBCE6xgPX7nkzzjiUq87kRItoJQM1Yo8S+Q= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= -golang.org/x/exp v0.0.0-20241215155358-4a5509556b9e h1:4qufH0hlUYs6AO6XmZC3GqfDPGSXHVXUFR6OND+iJX4= -golang.org/x/exp v0.0.0-20241215155358-4a5509556b9e/go.mod h1:qj5a5QZpwLU2NLQudwIN5koi3beDhSAlJwa67PuM98c= +golang.org/x/exp v0.0.0-20250620022241-b7579e27df2b h1:M2rDM6z3Fhozi9O7NWsxAkg/yqS/lQJ6PmkyIV3YP+o= +golang.org/x/exp v0.0.0-20250620022241-b7579e27df2b/go.mod h1:3//PLf8L/X+8b4vuAfHzxeRUl04Adcb341+IGKfnqS8= golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= @@ -601,8 +603,8 @@ golang.org/x/net v0.0.0-20220607020251-c690dde0001d/go.mod h1:XRhObCWvk6IyKnWLug golang.org/x/net v0.53.0 h1:d+qAbo5L0orcWAr0a9JweQpjXF19LMXJE8Ey7hwOdUA= golang.org/x/net v0.53.0/go.mod h1:JvMuJH7rrdiCfbeHoo3fCQU24Lf5JJwT9W3sJFulfgs= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= -golang.org/x/oauth2 v0.30.0 h1:dnDm7JmhM45NNpd8FDDeLhK6FwqbOf4MLCM9zb1BOHI= -golang.org/x/oauth2 v0.30.0/go.mod h1:B++QgG3ZKulg6sRPGD/mqlHQs5rB3Ml9erfeDY7xKlU= +golang.org/x/oauth2 v0.35.0 h1:Mv2mzuHuZuY2+bkyWXIHMfhNdJAdwW3FuWeCPYN5GVQ= +golang.org/x/oauth2 v0.35.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -685,8 +687,8 @@ golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8T golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20220517211312-f3a8303e98df/go.mod h1:K8+ghG5WaK9qNqU5K3HdILfMLy1f3aNYFI/wnl100a8= -gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk= -gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E= +gonum.org/v1/gonum v0.17.0 h1:VbpOemQlsSMrYmn7T2OUvQ4dqxQXU+ouZFQsZOx50z4= +gonum.org/v1/gonum v0.17.0/go.mod h1:El3tOrEuMpv2UdMrbNlKEh9vd86bmQ6vqIcDwxEOc1E= google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= google.golang.org/genproto v0.0.0-20180518175338-11a468237815/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= @@ -694,18 +696,18 @@ google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoA google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= google.golang.org/genproto v0.0.0-20210624195500-8bfb893ecb84/go.mod h1:SzzZ/N+nwJDaO1kznhnlzqS8ocJICar6hYhVyhi++24= -google.golang.org/genproto/googleapis/api v0.0.0-20250707201910-8d1bb00bc6a7 h1:FiusG7LWj+4byqhbvmB+Q93B/mOxJLN2DTozDuZm4EU= -google.golang.org/genproto/googleapis/api v0.0.0-20250707201910-8d1bb00bc6a7/go.mod h1:kXqgZtrWaf6qS3jZOCnCH7WYfrvFjkC51bM8fz3RsCA= -google.golang.org/genproto/googleapis/rpc v0.0.0-20250818200422-3122310a409c h1:qXWI/sQtv5UKboZ/zUk7h+mrf/lXORyI+n9DKDAusdg= -google.golang.org/genproto/googleapis/rpc v0.0.0-20250818200422-3122310a409c/go.mod h1:gw1tLEfykwDz2ET4a12jcXt4couGAm7IwsVaTy0Sflo= +google.golang.org/genproto/googleapis/api v0.0.0-20260401024825-9d38bb4040a9 h1:VPWxll4HlMw1Vs/qXtN7BvhZqsS9cdAittCNvVENElA= +google.golang.org/genproto/googleapis/api v0.0.0-20260401024825-9d38bb4040a9/go.mod h1:7QBABkRtR8z+TEnmXTqIqwJLlzrZKVfAUm7tY3yGv0M= +google.golang.org/genproto/googleapis/rpc v0.0.0-20260401024825-9d38bb4040a9 h1:m8qni9SQFH0tJc1X0vmnpw/0t+AImlSvp30sEupozUg= +google.golang.org/genproto/googleapis/rpc v0.0.0-20260401024825-9d38bb4040a9/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8= google.golang.org/grpc v1.12.0/go.mod h1:yo6s7OP7yaDglbqo1J04qKzAhqBH6lvTonzMVmEdcZw= google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY= google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= google.golang.org/grpc v1.38.0/go.mod h1:NREThFqKR1f3iQ6oBuvc5LadQuXVGo9rkm5ZGrQdJfM= -google.golang.org/grpc v1.75.0 h1:+TW+dqTd2Biwe6KKfhE5JpiYIBWq865PhKGSXiivqt4= -google.golang.org/grpc v1.75.0/go.mod h1:JtPAzKiq4v1xcAB2hydNlWI2RnF85XXcV0mhKXr2ecQ= +google.golang.org/grpc v1.80.0 h1:Xr6m2WmWZLETvUNvIUmeD5OAagMw3FiKmMlTdViWsHM= +google.golang.org/grpc v1.80.0/go.mod h1:ho/dLnxwi3EDJA4Zghp7k2Ec1+c2jqup0bFkw07bwF4= google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= @@ -717,8 +719,8 @@ google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpAD google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= -google.golang.org/protobuf v1.36.8 h1:xHScyCOEuuwZEc6UtSOvPbAT4zRh0xcNRYekJwfqyMc= -google.golang.org/protobuf v1.36.8/go.mod h1:fuxRtAxBytpl4zzqUh6/eyUujkJdNiuEkXntxiD/uRU= +google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE= +google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= diff --git a/pkg/network/network.go b/pkg/network/network.go index 80087e9..eed4935 100644 --- a/pkg/network/network.go +++ b/pkg/network/network.go @@ -21,6 +21,7 @@ type Config struct { MinValidatorStake uint64 // Minimum stake to become a validator (in nAVAX) MinDelegatorStake uint64 // Minimum stake to delegate (in nAVAX) MinStakeDuration time.Duration // Minimum staking duration + MaxStakeDuration time.Duration // Maximum staking duration (also bounds auto-renewal cycle length) } // Fuji testnet configuration @@ -28,9 +29,10 @@ var Fuji = Config{ Name: "fuji", NetworkID: 5, RPCURL: "https://api.avax-test.network", - MinValidatorStake: 1_000_000_000, // 1 AVAX - MinDelegatorStake: 1_000_000_000, // 1 AVAX - MinStakeDuration: 24 * time.Hour, // 24 hours + MinValidatorStake: 1_000_000_000, // 1 AVAX + MinDelegatorStake: 1_000_000_000, // 1 AVAX + MinStakeDuration: 24 * time.Hour, // 24 hours + MaxStakeDuration: 365 * 24 * time.Hour, // 1 year } // Mainnet configuration @@ -38,9 +40,10 @@ var Mainnet = Config{ Name: "mainnet", NetworkID: 1, RPCURL: "https://api.avax.network", - MinValidatorStake: 2000_000_000_000, // 2000 AVAX - MinDelegatorStake: 25_000_000_000, // 25 AVAX - MinStakeDuration: 14 * 24 * time.Hour, // 14 days + MinValidatorStake: 2000_000_000_000, // 2000 AVAX + MinDelegatorStake: 25_000_000_000, // 25 AVAX + MinStakeDuration: 14 * 24 * time.Hour, // 14 days + MaxStakeDuration: 365 * 24 * time.Hour, // 1 year } // GetConfig returns the network configuration for the given network name. @@ -135,5 +138,6 @@ func NewCustomConfigWithInsecureHTTP(ctx context.Context, rpcURL string, network MinValidatorStake: minValidatorStake, MinDelegatorStake: minDelegatorStake, MinStakeDuration: minStakeDuration, + MaxStakeDuration: 365 * 24 * time.Hour, // 1 year }, nil } diff --git a/pkg/pchain/acp236_devnetcompat_test.go b/pkg/pchain/acp236_devnetcompat_test.go new file mode 100644 index 0000000..47fd553 --- /dev/null +++ b/pkg/pchain/acp236_devnetcompat_test.go @@ -0,0 +1,347 @@ +//go:build devnetcompat + +package pchain + +import ( + "bytes" + "context" + "errors" + "strings" + "testing" + "time" + + "github.com/ava-labs/avalanchego/ids" + "github.com/ava-labs/avalanchego/snow" + "github.com/ava-labs/avalanchego/utils/constants" + blslocalsigner "github.com/ava-labs/avalanchego/utils/crypto/bls/signer/localsigner" + "github.com/ava-labs/avalanchego/vms/components/avax" + "github.com/ava-labs/avalanchego/vms/platformvm/reward" + "github.com/ava-labs/avalanchego/vms/platformvm/signer" + "github.com/ava-labs/avalanchego/vms/platformvm/txs" + "github.com/ava-labs/avalanchego/vms/secp256k1fx" + "github.com/ava-labs/avalanchego/wallet/subnet/primary/common" +) + +// devnetAutoRenewedValidatorTxIssuer adapts a func to autoRenewedValidatorTxIssuer +// so each test can build, verify and serialize a real ACP-236 tx inline. +type devnetAutoRenewedValidatorTxIssuer func(validatorNodeID ids.NodeID, weight uint64, sig signer.Signer, assetID ids.ID, validationRewardsOwner *secp256k1fx.OutputOwners, delegationRewardsOwner *secp256k1fx.OutputOwners, configOwner *secp256k1fx.OutputOwners, delegationShares uint32, autoCompoundRewardShares uint32, periodSeconds uint64, options ...common.Option) (*txs.Tx, error) + +func (f devnetAutoRenewedValidatorTxIssuer) IssueAddAutoRenewedValidatorTx(validatorNodeID ids.NodeID, weight uint64, sig signer.Signer, assetID ids.ID, validationRewardsOwner *secp256k1fx.OutputOwners, delegationRewardsOwner *secp256k1fx.OutputOwners, configOwner *secp256k1fx.OutputOwners, delegationShares uint32, autoCompoundRewardShares uint32, periodSeconds uint64, options ...common.Option) (*txs.Tx, error) { + return f(validatorNodeID, weight, sig, assetID, validationRewardsOwner, delegationRewardsOwner, configOwner, delegationShares, autoCompoundRewardShares, periodSeconds, options...) +} + +// devnetSetAutoRenewedValidatorConfigTxIssuer adapts a func to +// setAutoRenewedValidatorConfigTxIssuer. +type devnetSetAutoRenewedValidatorConfigTxIssuer func(txID ids.ID, autoCompoundRewardShares uint32, periodSeconds uint64, options ...common.Option) (*txs.Tx, error) + +func (f devnetSetAutoRenewedValidatorConfigTxIssuer) IssueSetAutoRenewedValidatorConfigTx(txID ids.ID, autoCompoundRewardShares uint32, periodSeconds uint64, options ...common.Option) (*txs.Tx, error) { + return f(txID, autoCompoundRewardShares, periodSeconds, options...) +} + +func TestDevnetCompatAddAutoRenewedValidatorTx(t *testing.T) { + ctx := devnetCompatContext() + nodeID := ids.GenerateTestNodeID() + rewardAddr := ids.GenerateTestShortID() + authorityAddr := ids.GenerateTestShortID() + pop := newDevnetCompatPoP(t) + stakeOuts := []*avax.TransferableOutput{ + devnetCompatStakeOut(ctx, 2_000_000_000_000, rewardAddr), + } + cfg := AddAutoRenewedValidatorConfig{ + NodeID: nodeID, + StakeAmt: 2_000_000_000_000, + RewardAddr: rewardAddr, + ValidatorAuthorityAddr: authorityAddr, + DelegationFee: 20_000, + AutoCompoundRewardShares: 750_000, + Period: 14 * 24 * time.Hour, + BLSSigner: pop, + } + callCtx := context.WithValue(context.Background(), testContextKey("devnetcompat"), "add") + + var issuedTx *txs.Tx + issuer := devnetAutoRenewedValidatorTxIssuer(func( + validatorNodeID ids.NodeID, + weight uint64, + gotSigner signer.Signer, + assetID ids.ID, + validationRewardsOwner *secp256k1fx.OutputOwners, + delegationRewardsOwner *secp256k1fx.OutputOwners, + configOwner *secp256k1fx.OutputOwners, + shares uint32, + autoCompoundRewardShares uint32, + periodSeconds uint64, + options ...common.Option, + ) (*txs.Tx, error) { + if common.NewOptions(options).Context().Value(testContextKey("devnetcompat")) != "add" { + t.Fatal("context option was not passed to add-auto-renewed issuer") + } + if validatorNodeID != cfg.NodeID { + t.Fatalf("issuer nodeID = %s, want %s", validatorNodeID, cfg.NodeID) + } + if weight != cfg.StakeAmt { + t.Fatalf("issuer weight = %d, want %d", weight, cfg.StakeAmt) + } + if gotSigner != pop { + t.Fatal("issuer BLS proof pointer mismatch") + } + if assetID != ctx.AVAXAssetID { + t.Fatalf("issuer assetID = %s, want %s", assetID, ctx.AVAXAssetID) + } + if !ownerHasOnly(validationRewardsOwner, rewardAddr) { + t.Fatalf("validation reward owner = %#v, want [%s]", validationRewardsOwner, rewardAddr) + } + if !ownerHasOnly(delegationRewardsOwner, rewardAddr) { + t.Fatalf("delegation reward owner = %#v, want [%s]", delegationRewardsOwner, rewardAddr) + } + if shares != cfg.DelegationFee { + t.Fatalf("issuer delegation shares = %d, want %d", shares, cfg.DelegationFee) + } + if !ownerHasOnly(configOwner, authorityAddr) { + t.Fatalf("validator authority = %#v, want [%s]", configOwner, authorityAddr) + } + if autoCompoundRewardShares != cfg.AutoCompoundRewardShares { + t.Fatalf("issuer auto-compound shares = %d, want %d", autoCompoundRewardShares, cfg.AutoCompoundRewardShares) + } + if periodSeconds != uint64(cfg.Period/time.Second) { + t.Fatalf("issuer period seconds = %d, want %d", periodSeconds, uint64(cfg.Period/time.Second)) + } + + autoTx := &txs.AddAutoRenewedValidatorTx{ + BaseTx: devnetCompatBaseTx(ctx), + ValidatorNodeID: validatorNodeID[:], + Signer: gotSigner, + StakeOuts: stakeOuts, + ValidatorRewardsOwner: validationRewardsOwner, + DelegatorRewardsOwner: delegationRewardsOwner, + ValidatorAuthority: configOwner, + DelegationShares: shares, + AutoCompoundRewardShares: autoCompoundRewardShares, + Period: periodSeconds, + } + autoTx.InitCtx(ctx) + if err := autoTx.SyntacticVerify(ctx); err != nil { + t.Fatalf("AddAutoRenewedValidatorTx failed devnet syntactic verify: %v", err) + } + if !bytes.Equal(autoTx.ValidatorNodeID, cfg.NodeID.Bytes()) { + t.Fatalf("nodeID bytes = %x, want %x", []byte(autoTx.ValidatorNodeID), cfg.NodeID.Bytes()) + } + + issuedTx = initializeDevnetCompatTx(t, autoTx) + parsed, err := txs.Parse(txs.Codec, issuedTx.Bytes()) + if err != nil { + t.Fatalf("failed to parse serialized AddAutoRenewedValidatorTx: %v", err) + } + if _, ok := parsed.Unsigned.(*txs.AddAutoRenewedValidatorTx); !ok { + t.Fatalf("parsed unsigned tx type = %T, want *txs.AddAutoRenewedValidatorTx", parsed.Unsigned) + } + return issuedTx, nil + }) + + gotTxID, err := issueAddAutoRenewedValidatorTx(issuer, ctx.AVAXAssetID, cfg, common.WithContext(callCtx)) + if err != nil { + t.Fatalf("issueAddAutoRenewedValidatorTx returned error: %v", err) + } + if gotTxID != issuedTx.ID() { + t.Fatalf("returned txID = %s, want issued tx ID %s", gotTxID, issuedTx.ID()) + } +} + +func TestDevnetCompatSetAutoRenewedValidatorConfigTx(t *testing.T) { + ctx := devnetCompatContext() + validatorTxID := ids.GenerateTestID() + auth := &secp256k1fx.Input{SigIndices: []uint32{0}} + cfg := SetAutoRenewedValidatorConfigTxConfig{ + TxID: validatorTxID, + AutoCompoundRewardShares: 250_000, + Period: 7 * 24 * time.Hour, + } + callCtx := context.WithValue(context.Background(), testContextKey("devnetcompat"), "set") + + var issuedTx *txs.Tx + issuer := devnetSetAutoRenewedValidatorConfigTxIssuer(func(txID ids.ID, autoCompoundRewardShares uint32, periodSeconds uint64, options ...common.Option) (*txs.Tx, error) { + if common.NewOptions(options).Context().Value(testContextKey("devnetcompat")) != "set" { + t.Fatal("context option was not passed to set-auto-config issuer") + } + if txID != validatorTxID { + t.Fatalf("issuer txID = %s, want %s", txID, validatorTxID) + } + if autoCompoundRewardShares != cfg.AutoCompoundRewardShares { + t.Fatalf("issuer auto-compound shares = %d, want %d", autoCompoundRewardShares, cfg.AutoCompoundRewardShares) + } + if periodSeconds != uint64(cfg.Period/time.Second) { + t.Fatalf("issuer period seconds = %d, want %d", periodSeconds, uint64(cfg.Period/time.Second)) + } + + setTx := &txs.SetAutoRenewedValidatorConfigTx{ + BaseTx: devnetCompatBaseTx(ctx), + TxID: txID, + Auth: auth, + AutoCompoundRewardShares: autoCompoundRewardShares, + Period: periodSeconds, + } + setTx.InitCtx(ctx) + if err := setTx.SyntacticVerify(ctx); err != nil { + t.Fatalf("SetAutoRenewedValidatorConfigTx failed devnet syntactic verify: %v", err) + } + + issuedTx = initializeDevnetCompatTx(t, setTx) + parsed, err := txs.Parse(txs.Codec, issuedTx.Bytes()) + if err != nil { + t.Fatalf("failed to parse serialized SetAutoRenewedValidatorConfigTx: %v", err) + } + if _, ok := parsed.Unsigned.(*txs.SetAutoRenewedValidatorConfigTx); !ok { + t.Fatalf("parsed unsigned tx type = %T, want *txs.SetAutoRenewedValidatorConfigTx", parsed.Unsigned) + } + return issuedTx, nil + }) + + gotTxID, err := issueSetAutoRenewedValidatorConfigTx(issuer, cfg, common.WithContext(callCtx)) + if err != nil { + t.Fatalf("issueSetAutoRenewedValidatorConfigTx returned error: %v", err) + } + if gotTxID != issuedTx.ID() { + t.Fatalf("returned txID = %s, want issued tx ID %s", gotTxID, issuedTx.ID()) + } +} + +func TestDevnetCompatSetAutoRenewedValidatorConfigTxExitCycle(t *testing.T) { + ctx := devnetCompatContext() + cfg := SetAutoRenewedValidatorConfigTxConfig{ + TxID: ids.GenerateTestID(), + AutoCompoundRewardShares: 0, + Period: 0, + } + + issuer := devnetSetAutoRenewedValidatorConfigTxIssuer(func(txID ids.ID, autoCompoundRewardShares uint32, periodSeconds uint64, _ ...common.Option) (*txs.Tx, error) { + if periodSeconds != 0 { + t.Fatalf("exit-cycle period = %d, want 0", periodSeconds) + } + setTx := &txs.SetAutoRenewedValidatorConfigTx{ + BaseTx: devnetCompatBaseTx(ctx), + TxID: txID, + Auth: &secp256k1fx.Input{SigIndices: []uint32{0}}, + AutoCompoundRewardShares: autoCompoundRewardShares, + Period: periodSeconds, + } + setTx.InitCtx(ctx) + if err := setTx.SyntacticVerify(ctx); err != nil { + t.Fatalf("exit-cycle SetAutoRenewedValidatorConfigTx failed devnet syntactic verify: %v", err) + } + return initializeDevnetCompatTx(t, setTx), nil + }) + + if _, err := issueSetAutoRenewedValidatorConfigTx(issuer, cfg); err != nil { + t.Fatalf("exit-cycle issueSetAutoRenewedValidatorConfigTx returned error: %v", err) + } +} + +func TestDevnetCompatACP236ErrorWrapping(t *testing.T) { + expectedErr := errors.New("devnet compat failure") + + addIssuer := devnetAutoRenewedValidatorTxIssuer(func(ids.NodeID, uint64, signer.Signer, ids.ID, *secp256k1fx.OutputOwners, *secp256k1fx.OutputOwners, *secp256k1fx.OutputOwners, uint32, uint32, uint64, ...common.Option) (*txs.Tx, error) { + return nil, expectedErr + }) + _, err := issueAddAutoRenewedValidatorTx(addIssuer, ids.GenerateTestID(), AddAutoRenewedValidatorConfig{Period: time.Second}) + assertWrapped(t, err, expectedErr, "failed to issue AddAutoRenewedValidatorTx") + + setIssuer := devnetSetAutoRenewedValidatorConfigTxIssuer(func(ids.ID, uint32, uint64, ...common.Option) (*txs.Tx, error) { + return nil, expectedErr + }) + _, err = issueSetAutoRenewedValidatorConfigTx(setIssuer, SetAutoRenewedValidatorConfigTxConfig{TxID: ids.GenerateTestID(), Period: time.Second}) + assertWrapped(t, err, expectedErr, "failed to issue SetAutoRenewedValidatorConfigTx") +} + +func TestDevnetCompatACP236DevnetSyntacticRejections(t *testing.T) { + ctx := devnetCompatContext() + + tooManySharesTx := &txs.AddAutoRenewedValidatorTx{ + BaseTx: devnetCompatBaseTx(ctx), + ValidatorNodeID: ids.GenerateTestNodeID().Bytes(), + Signer: newDevnetCompatPoP(t), + StakeOuts: []*avax.TransferableOutput{devnetCompatStakeOut(ctx, 1, ids.GenerateTestShortID())}, + ValidatorRewardsOwner: devnetCompatOwner(ids.GenerateTestShortID()), + DelegatorRewardsOwner: devnetCompatOwner(ids.GenerateTestShortID()), + ValidatorAuthority: devnetCompatOwner(ids.GenerateTestShortID()), + AutoCompoundRewardShares: reward.PercentDenominator + 1, + Period: 1, + } + tooManySharesTx.InitCtx(ctx) + if err := tooManySharesTx.SyntacticVerify(ctx); err == nil { + t.Fatal("AddAutoRenewedValidatorTx expected devnet syntactic rejection for too many auto-compound shares") + } +} + +func newDevnetCompatPoP(t *testing.T) *signer.ProofOfPossession { + t.Helper() + + sk, err := blslocalsigner.New() + if err != nil { + t.Fatalf("failed to create BLS secret key: %v", err) + } + pop, err := signer.NewProofOfPossession(sk) + if err != nil { + t.Fatalf("failed to create BLS proof of possession: %v", err) + } + return pop +} + +func devnetCompatBaseTx(ctx *snow.Context) txs.BaseTx { + return txs.BaseTx{BaseTx: avax.BaseTx{ + NetworkID: ctx.NetworkID, + BlockchainID: ctx.ChainID, + }} +} + +func devnetCompatContext() *snow.Context { + return &snow.Context{ + NetworkID: constants.UnitTestID, + ChainID: constants.PlatformChainID, + AVAXAssetID: ids.GenerateTestID(), + } +} + +func devnetCompatStakeOut(ctx *snow.Context, amount uint64, owner ids.ShortID) *avax.TransferableOutput { + return &avax.TransferableOutput{ + Asset: avax.Asset{ID: ctx.AVAXAssetID}, + Out: &secp256k1fx.TransferOutput{ + Amt: amount, + OutputOwners: *devnetCompatOwner(owner), + }, + } +} + +func devnetCompatOwner(addr ids.ShortID) *secp256k1fx.OutputOwners { + return &secp256k1fx.OutputOwners{ + Threshold: 1, + Addrs: []ids.ShortID{addr}, + } +} + +func ownerHasOnly(owner *secp256k1fx.OutputOwners, addr ids.ShortID) bool { + return owner != nil && + owner.Threshold == 1 && + len(owner.Addrs) == 1 && + owner.Addrs[0] == addr +} + +func initializeDevnetCompatTx(t *testing.T, utx txs.UnsignedTx) *txs.Tx { + t.Helper() + + tx := &txs.Tx{Unsigned: utx} + if err := tx.Initialize(txs.Codec); err != nil { + t.Fatalf("failed to initialize tx: %v", err) + } + return tx +} + +func assertWrapped(t *testing.T, err, target error, message string) { + t.Helper() + + if !errors.Is(err, target) { + t.Fatalf("error = %v, want wrapped %v", err, target) + } + if !strings.Contains(err.Error(), message) { + t.Fatalf("error = %v, want message containing %q", err, message) + } +} diff --git a/pkg/pchain/acp236_test.go b/pkg/pchain/acp236_test.go new file mode 100644 index 0000000..1a1184d --- /dev/null +++ b/pkg/pchain/acp236_test.go @@ -0,0 +1,416 @@ +package pchain + +import ( + "context" + "encoding/json" + "errors" + "net/http" + "net/http/httptest" + "strings" + "testing" + "time" + + "github.com/ava-labs/avalanchego/ids" + "github.com/ava-labs/avalanchego/utils/constants" + "github.com/ava-labs/avalanchego/utils/formatting/address" + "github.com/ava-labs/avalanchego/vms/platformvm/signer" + "github.com/ava-labs/avalanchego/vms/platformvm/txs" + "github.com/ava-labs/avalanchego/vms/secp256k1fx" + "github.com/ava-labs/avalanchego/wallet/subnet/primary/common" +) + +var errAssertable = errors.New("assertable failure") + +// ============================================================================= +// ACP-236 Issuer Stubs +// ============================================================================= + +// stubAutoRenewedValidatorTxIssuer implements autoRenewedValidatorTxIssuer. +type stubAutoRenewedValidatorTxIssuer struct { + tx *txs.Tx + err error + + called bool + gotNodeID ids.NodeID + gotWeight uint64 + gotSigner signer.Signer + gotAssetID ids.ID + gotValidationRewardsOwner *secp256k1fx.OutputOwners + gotDelegationRewardsOwner *secp256k1fx.OutputOwners + gotConfigOwner *secp256k1fx.OutputOwners + gotDelegationShares uint32 + gotAutoCompoundShares uint32 + gotPeriodSeconds uint64 + gotOpts []common.Option +} + +func (s *stubAutoRenewedValidatorTxIssuer) IssueAddAutoRenewedValidatorTx(validatorNodeID ids.NodeID, weight uint64, sig signer.Signer, assetID ids.ID, validationRewardsOwner *secp256k1fx.OutputOwners, delegationRewardsOwner *secp256k1fx.OutputOwners, configOwner *secp256k1fx.OutputOwners, delegationShares uint32, autoCompoundRewardShares uint32, periodSeconds uint64, options ...common.Option) (*txs.Tx, error) { + s.called = true + s.gotNodeID = validatorNodeID + s.gotWeight = weight + s.gotSigner = sig + s.gotAssetID = assetID + s.gotValidationRewardsOwner = validationRewardsOwner + s.gotDelegationRewardsOwner = delegationRewardsOwner + s.gotConfigOwner = configOwner + s.gotDelegationShares = delegationShares + s.gotAutoCompoundShares = autoCompoundRewardShares + s.gotPeriodSeconds = periodSeconds + s.gotOpts = options + return s.tx, s.err +} + +// stubSetAutoRenewedValidatorConfigTxIssuer implements setAutoRenewedValidatorConfigTxIssuer. +type stubSetAutoRenewedValidatorConfigTxIssuer struct { + tx *txs.Tx + err error + + called bool + gotTxID ids.ID + gotAutoCompoundShares uint32 + gotPeriodSeconds uint64 +} + +func (s *stubSetAutoRenewedValidatorConfigTxIssuer) IssueSetAutoRenewedValidatorConfigTx(txID ids.ID, autoCompoundRewardShares uint32, periodSeconds uint64, _ ...common.Option) (*txs.Tx, error) { + s.called = true + s.gotTxID = txID + s.gotAutoCompoundShares = autoCompoundRewardShares + s.gotPeriodSeconds = periodSeconds + return s.tx, s.err +} + +// ============================================================================= +// AddAutoRenewedValidatorTx +// ============================================================================= + +func TestIssueAddAutoRenewedValidatorTx(t *testing.T) { + nodeID := ids.GenerateTestNodeID() + rewardAddr := ids.GenerateTestShortID() + authorityAddr := ids.GenerateTestShortID() + assetID := ids.GenerateTestID() + pop := &signer.ProofOfPossession{} + cfg := AddAutoRenewedValidatorConfig{ + NodeID: nodeID, + StakeAmt: 123, + RewardAddr: rewardAddr, + ValidatorAuthorityAddr: authorityAddr, + DelegationFee: 20_000, + AutoCompoundRewardShares: 500_000, + Period: 14 * 24 * time.Hour, + BLSSigner: pop, + } + txID := ids.GenerateTestID() + + stub := &stubAutoRenewedValidatorTxIssuer{tx: &txs.Tx{TxID: txID}} + ctxKey := testContextKey("auto") + gotTxID, err := issueAddAutoRenewedValidatorTx(stub, assetID, cfg, common.WithContext(context.WithValue(context.Background(), ctxKey, "v"))) + if err != nil { + t.Fatalf("issueAddAutoRenewedValidatorTx() returned error: %v", err) + } + if gotTxID != txID { + t.Fatalf("issueAddAutoRenewedValidatorTx() txID = %s, want %s", gotTxID, txID) + } + if stub.gotNodeID != nodeID { + t.Fatalf("nodeID = %s, want %s", stub.gotNodeID, nodeID) + } + if stub.gotWeight != cfg.StakeAmt { + t.Fatalf("weight = %d, want %d", stub.gotWeight, cfg.StakeAmt) + } + gotPop, ok := stub.gotSigner.(*signer.ProofOfPossession) + if !ok || gotPop != pop { + t.Fatalf("signer = %T (%p), want %p", stub.gotSigner, gotPop, pop) + } + if stub.gotAssetID != assetID { + t.Fatalf("assetID = %s, want %s", stub.gotAssetID, assetID) + } + // Validation and delegation rewards go to the same owner. + if stub.gotValidationRewardsOwner != stub.gotDelegationRewardsOwner { + t.Fatal("validation and delegation rewards owners should be the same pointer") + } + assertSingleOwner(t, "validation rewards owner", stub.gotValidationRewardsOwner, rewardAddr) + assertSingleOwner(t, "config owner", stub.gotConfigOwner, authorityAddr) + if stub.gotDelegationShares != cfg.DelegationFee { + t.Fatalf("delegation shares = %d, want %d", stub.gotDelegationShares, cfg.DelegationFee) + } + if stub.gotAutoCompoundShares != cfg.AutoCompoundRewardShares { + t.Fatalf("auto-compound shares = %d, want %d", stub.gotAutoCompoundShares, cfg.AutoCompoundRewardShares) + } + if want := uint64(cfg.Period / time.Second); stub.gotPeriodSeconds != want { + t.Fatalf("period seconds = %d, want %d", stub.gotPeriodSeconds, want) + } + if len(stub.gotOpts) == 0 { + t.Fatal("expected options to be forwarded to the issuer") + } +} + +func TestIssueAddAutoRenewedValidatorTxRejectsInvalidPeriod(t *testing.T) { + stub := &stubAutoRenewedValidatorTxIssuer{tx: &txs.Tx{TxID: ids.GenerateTestID()}} + _, err := issueAddAutoRenewedValidatorTx(stub, ids.GenerateTestID(), AddAutoRenewedValidatorConfig{Period: 1500 * time.Millisecond}) + if err == nil || !strings.Contains(err.Error(), "period must be a whole number of seconds") { + t.Fatalf("error = %v, want whole-seconds error", err) + } + if stub.called { + t.Fatal("issuer should not be called for an invalid period") + } + + _, err = issueAddAutoRenewedValidatorTx(stub, ids.GenerateTestID(), AddAutoRenewedValidatorConfig{Period: 0}) + if err == nil || !strings.Contains(err.Error(), "period must be positive") { + t.Fatalf("error = %v, want positive-period error", err) + } +} + +func TestIssueAddAutoRenewedValidatorTxWrapsIssuerError(t *testing.T) { + stub := &stubAutoRenewedValidatorTxIssuer{err: errAssertable} + _, err := issueAddAutoRenewedValidatorTx(stub, ids.GenerateTestID(), AddAutoRenewedValidatorConfig{Period: time.Second}) + if err == nil || !strings.Contains(err.Error(), "failed to issue AddAutoRenewedValidatorTx") { + t.Fatalf("error = %v, want wrapped issue error", err) + } +} + +// ============================================================================= +// SetAutoRenewedValidatorConfigTx +// ============================================================================= + +func TestIssueSetAutoRenewedValidatorConfigTx(t *testing.T) { + validatorTxID := ids.GenerateTestID() + issuedTxID := ids.GenerateTestID() + cfg := SetAutoRenewedValidatorConfigTxConfig{ + TxID: validatorTxID, + AutoCompoundRewardShares: 250_000, + Period: 7 * 24 * time.Hour, + } + + stub := &stubSetAutoRenewedValidatorConfigTxIssuer{tx: &txs.Tx{TxID: issuedTxID}} + gotTxID, err := issueSetAutoRenewedValidatorConfigTx(stub, cfg) + if err != nil { + t.Fatalf("issueSetAutoRenewedValidatorConfigTx() returned error: %v", err) + } + if gotTxID != issuedTxID { + t.Fatalf("txID = %s, want %s", gotTxID, issuedTxID) + } + if stub.gotTxID != validatorTxID { + t.Fatalf("builder txID = %s, want %s", stub.gotTxID, validatorTxID) + } + if stub.gotAutoCompoundShares != cfg.AutoCompoundRewardShares { + t.Fatalf("auto-compound shares = %d, want %d", stub.gotAutoCompoundShares, cfg.AutoCompoundRewardShares) + } + if want := uint64(cfg.Period / time.Second); stub.gotPeriodSeconds != want { + t.Fatalf("period seconds = %d, want %d", stub.gotPeriodSeconds, want) + } +} + +func TestIssueSetAutoRenewedValidatorConfigTxAllowsZeroPeriod(t *testing.T) { + stub := &stubSetAutoRenewedValidatorConfigTxIssuer{tx: &txs.Tx{TxID: ids.GenerateTestID()}} + cfg := SetAutoRenewedValidatorConfigTxConfig{ + TxID: ids.GenerateTestID(), + Period: 0, + } + if _, err := issueSetAutoRenewedValidatorConfigTx(stub, cfg); err != nil { + t.Fatalf("issueSetAutoRenewedValidatorConfigTx() returned error: %v", err) + } + if !stub.called { + t.Fatal("issuer should be called for a zero (exit-after-cycle) period") + } + if stub.gotPeriodSeconds != 0 { + t.Fatalf("period seconds = %d, want 0", stub.gotPeriodSeconds) + } +} + +func TestIssueSetAutoRenewedValidatorConfigTxRejectsNegativePeriod(t *testing.T) { + stub := &stubSetAutoRenewedValidatorConfigTxIssuer{tx: &txs.Tx{TxID: ids.GenerateTestID()}} + _, err := issueSetAutoRenewedValidatorConfigTx(stub, SetAutoRenewedValidatorConfigTxConfig{ + TxID: ids.GenerateTestID(), + Period: -time.Second, + }) + if err == nil || !strings.Contains(err.Error(), "period cannot be negative") { + t.Fatalf("error = %v, want negative-period error", err) + } + if stub.called { + t.Fatal("issuer should not be called for a negative period") + } +} + +func TestIssueSetAutoRenewedValidatorConfigTxWrapsIssuerError(t *testing.T) { + stub := &stubSetAutoRenewedValidatorConfigTxIssuer{err: errAssertable} + _, err := issueSetAutoRenewedValidatorConfigTx(stub, SetAutoRenewedValidatorConfigTxConfig{ + TxID: ids.GenerateTestID(), + Period: time.Second, + }) + if err == nil || !strings.Contains(err.Error(), "failed to issue SetAutoRenewedValidatorConfigTx") { + t.Fatalf("error = %v, want wrapped issue error", err) + } +} + +// ============================================================================= +// GetAutoRenewedValidatorAuthority +// ============================================================================= + +func TestGetAutoRenewedValidatorAuthorityUsesValidatorAuthority(t *testing.T) { + targetTxID := ids.GenerateTestID() + otherTxID := ids.GenerateTestID() + rewardAddr := ids.GenerateTestShortID() + authorityAddr := ids.GenerateTestShortID() + + var gotParams string + server := newCurrentValidatorsServer(t, &gotParams, []map[string]any{ + { + "txID": otherTxID.String(), + "validatorAuthority": testAPIOwner(t, ids.GenerateTestShortID(), "0", "1"), + }, + { + "txID": targetTxID.String(), + "validationRewardOwner": testAPIOwner(t, rewardAddr, "0", "1"), + "validatorAuthority": testAPIOwner(t, authorityAddr, "0", "1"), + }, + }) + defer server.Close() + + owner, err := GetAutoRenewedValidatorAuthority(context.Background(), server.URL, ids.EmptyNodeID, targetTxID) + if err != nil { + t.Fatalf("GetAutoRenewedValidatorAuthority() returned error: %v", err) + } + assertSingleOwner(t, "authority", owner, authorityAddr) + if owner.Addrs[0] == rewardAddr { + t.Fatal("authority lookup returned a reward owner instead of validatorAuthority") + } + // With no nodeID the request must not narrow by nodeIDs. + if strings.Contains(gotParams, "NodeID-") { + t.Fatalf("params = %s, expected no nodeIDs filter", gotParams) + } +} + +func TestGetAutoRenewedValidatorAuthorityFiltersByNodeID(t *testing.T) { + targetTxID := ids.GenerateTestID() + nodeID := ids.GenerateTestNodeID() + authorityAddr := ids.GenerateTestShortID() + + var gotParams string + server := newCurrentValidatorsServer(t, &gotParams, []map[string]any{ + { + "txID": targetTxID.String(), + "validatorAuthority": testAPIOwner(t, authorityAddr, "0", "1"), + }, + }) + defer server.Close() + + if _, err := GetAutoRenewedValidatorAuthority(context.Background(), server.URL, nodeID, targetTxID); err != nil { + t.Fatalf("GetAutoRenewedValidatorAuthority() returned error: %v", err) + } + if !strings.Contains(gotParams, nodeID.String()) { + t.Fatalf("params = %s, want nodeIDs filter containing %s", gotParams, nodeID) + } +} + +func TestGetAutoRenewedValidatorAuthorityErrors(t *testing.T) { + targetTxID := ids.GenerateTestID() + + tests := []struct { + name string + validators []map[string]any + wantErr string + }{ + { + name: "not found", + validators: []map[string]any{}, + wantErr: "not found in current validators", + }, + { + name: "missing validatorAuthority", + validators: []map[string]any{{ + "txID": targetTxID.String(), + }}, + wantErr: "did not include validatorAuthority", + }, + { + name: "bad threshold", + validators: []map[string]any{{ + "txID": targetTxID.String(), + "validatorAuthority": testAPIOwner(t, ids.GenerateTestShortID(), "0", "not-a-number"), + }}, + wantErr: "invalid validatorAuthority threshold", + }, + { + name: "bad address", + validators: []map[string]any{{ + "txID": targetTxID.String(), + "validatorAuthority": map[string]any{ + "locktime": "0", + "threshold": "1", + "addresses": []string{"not-an-address"}, + }, + }}, + wantErr: "invalid validatorAuthority addresses", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + var gotParams string + server := newCurrentValidatorsServer(t, &gotParams, tt.validators) + defer server.Close() + + _, err := GetAutoRenewedValidatorAuthority(context.Background(), server.URL, ids.EmptyNodeID, targetTxID) + if err == nil || !strings.Contains(err.Error(), tt.wantErr) { + t.Fatalf("error = %v, want containing %q", err, tt.wantErr) + } + }) + } +} + +// ============================================================================= +// Helpers +// ============================================================================= + +func assertSingleOwner(t *testing.T, label string, owner *secp256k1fx.OutputOwners, addr ids.ShortID) { + t.Helper() + if owner == nil || owner.Locktime != 0 || owner.Threshold != 1 || len(owner.Addrs) != 1 || owner.Addrs[0] != addr { + t.Fatalf("%s = %#v, want locktime 0, threshold 1, addrs [%s]", label, owner, addr) + } +} + +func newCurrentValidatorsServer(t *testing.T, gotParams *string, validators []map[string]any) *httptest.Server { + t.Helper() + + return httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.URL.Path != "/ext/P" { + t.Errorf("request path = %q, want /ext/P", r.URL.Path) + } + var req struct { + Method string `json:"method"` + Params json.RawMessage `json:"params"` + ID any `json:"id"` + } + if err := json.NewDecoder(r.Body).Decode(&req); err != nil { + t.Errorf("failed to decode JSON-RPC request: %v", err) + } + if req.Method != "platform.getCurrentValidators" { + t.Errorf("method = %q, want platform.getCurrentValidators", req.Method) + } + if gotParams != nil { + *gotParams = string(req.Params) + } + + w.Header().Set("Content-Type", "application/json") + _ = json.NewEncoder(w).Encode(map[string]any{ + "jsonrpc": "2.0", + "result": map[string]any{ + "validators": validators, + }, + "id": req.ID, + }) + })) +} + +func testAPIOwner(t *testing.T, addr ids.ShortID, locktime string, threshold string) map[string]any { + t.Helper() + + formattedAddr, err := address.Format("P", constants.GetHRP(constants.UnitTestID), addr.Bytes()) + if err != nil { + t.Fatalf("address.Format() error = %v", err) + } + return map[string]any{ + "locktime": locktime, + "threshold": threshold, + "addresses": []string{formattedAddr}, + } +} diff --git a/pkg/pchain/pchain.go b/pkg/pchain/pchain.go index 3f40f21..2983258 100644 --- a/pkg/pchain/pchain.go +++ b/pkg/pchain/pchain.go @@ -4,11 +4,14 @@ package pchain import ( "context" "fmt" + "strconv" "time" "github.com/ava-labs/avalanchego/ids" "github.com/ava-labs/avalanchego/utils/crypto/bls" + "github.com/ava-labs/avalanchego/utils/formatting/address" "github.com/ava-labs/avalanchego/vms/components/avax" + "github.com/ava-labs/avalanchego/vms/platformvm" "github.com/ava-labs/avalanchego/vms/platformvm/signer" "github.com/ava-labs/avalanchego/vms/platformvm/txs" "github.com/ava-labs/avalanchego/vms/secp256k1fx" @@ -50,6 +53,16 @@ type permissionlessDelegatorTxIssuer interface { IssueAddPermissionlessDelegatorTx(vdr *txs.SubnetValidator, assetID ids.ID, rewardsOwner *secp256k1fx.OutputOwners, options ...common.Option) (*txs.Tx, error) } +// autoRenewedValidatorTxIssuer issues an AddAutoRenewedValidatorTx (ACP-236). +type autoRenewedValidatorTxIssuer interface { + IssueAddAutoRenewedValidatorTx(validatorNodeID ids.NodeID, weight uint64, signer signer.Signer, assetID ids.ID, validationRewardsOwner *secp256k1fx.OutputOwners, delegationRewardsOwner *secp256k1fx.OutputOwners, configOwner *secp256k1fx.OutputOwners, delegationShares uint32, autoCompoundRewardShares uint32, periodSeconds uint64, options ...common.Option) (*txs.Tx, error) +} + +// setAutoRenewedValidatorConfigTxIssuer issues a SetAutoRenewedValidatorConfigTx (ACP-236). +type setAutoRenewedValidatorConfigTxIssuer interface { + IssueSetAutoRenewedValidatorConfigTx(txID ids.ID, autoCompoundRewardShares uint32, periodSeconds uint64, options ...common.Option) (*txs.Tx, error) +} + // createSubnetTxIssuer issues a CreateSubnetTx. type createSubnetTxIssuer interface { IssueCreateSubnetTx(owner *secp256k1fx.OutputOwners, options ...common.Option) (*txs.Tx, error) @@ -255,6 +268,187 @@ func issueAddPermissionlessValidatorTx( return tx.ID(), nil } +// ============================================================================= +// ACP-236 Auto-Renewed Staking +// ============================================================================= + +// AddAutoRenewedValidatorConfig holds configuration for adding an auto-renewed +// primary network validator. +type AddAutoRenewedValidatorConfig struct { + NodeID ids.NodeID + StakeAmt uint64 // in nAVAX + RewardAddr ids.ShortID + ValidatorAuthorityAddr ids.ShortID + DelegationFee uint32 // in parts per million (1_000_000 = 100%) + AutoCompoundRewardShares uint32 // in parts per million (1_000_000 = 100%) + Period time.Duration // auto-renewal cycle duration + BLSSigner *signer.ProofOfPossession // BLS proof of possession for the validator +} + +// AddAutoRenewedValidator adds an auto-renewed validator to the primary network. +func AddAutoRenewedValidator(ctx context.Context, w *wallet.Wallet, cfg AddAutoRenewedValidatorConfig) (ids.ID, error) { + avaxAssetID := w.PWallet().Builder().Context().AVAXAssetID + return issueAddAutoRenewedValidatorTx(w.PWallet(), avaxAssetID, cfg, common.WithContext(ctx)) +} + +func issueAddAutoRenewedValidatorTx( + issuer autoRenewedValidatorTxIssuer, + avaxAssetID ids.ID, + cfg AddAutoRenewedValidatorConfig, + options ...common.Option, +) (ids.ID, error) { + periodSeconds, err := durationToWholeSeconds("period", cfg.Period, false) + if err != nil { + return ids.Empty, err + } + + rewardsOwner := &secp256k1fx.OutputOwners{ + Threshold: 1, + Addrs: []ids.ShortID{cfg.RewardAddr}, + } + validatorAuthority := &secp256k1fx.OutputOwners{ + Threshold: 1, + Addrs: []ids.ShortID{cfg.ValidatorAuthorityAddr}, + } + + tx, err := issuer.IssueAddAutoRenewedValidatorTx( + cfg.NodeID, + cfg.StakeAmt, + cfg.BLSSigner, + avaxAssetID, + rewardsOwner, + rewardsOwner, // delegation rewards go to same owner + validatorAuthority, + cfg.DelegationFee, + cfg.AutoCompoundRewardShares, + periodSeconds, + options..., + ) + if err != nil { + return ids.Empty, fmt.Errorf("failed to issue AddAutoRenewedValidatorTx: %w", err) + } + return tx.ID(), nil +} + +// SetAutoRenewedValidatorConfigTxConfig holds configuration for updating an +// auto-renewed validator's next-cycle configuration. +type SetAutoRenewedValidatorConfigTxConfig struct { + TxID ids.ID + AutoCompoundRewardShares uint32 // in parts per million (1_000_000 = 100%) + Period time.Duration // 0 means exit after the current cycle +} + +// SetAutoRenewedValidatorConfig updates an auto-renewed validator's next-cycle +// configuration. +// +// The wallet must be created with the validator's config-authority owner mapped +// to cfg.TxID (see wallet.NewWalletFromKeychainWithOwner), because the public +// builder resolves the authorizing owner from the wallet backend's owners map +// (builder.authorize -> backend.GetOwner) rather than from chain state. +func SetAutoRenewedValidatorConfig(ctx context.Context, w *wallet.Wallet, cfg SetAutoRenewedValidatorConfigTxConfig) (ids.ID, error) { + return issueSetAutoRenewedValidatorConfigTx(w.PWallet(), cfg, common.WithContext(ctx)) +} + +func issueSetAutoRenewedValidatorConfigTx( + issuer setAutoRenewedValidatorConfigTxIssuer, + cfg SetAutoRenewedValidatorConfigTxConfig, + options ...common.Option, +) (ids.ID, error) { + periodSeconds, err := durationToWholeSeconds("period", cfg.Period, true) + if err != nil { + return ids.Empty, err + } + + tx, err := issuer.IssueSetAutoRenewedValidatorConfigTx( + cfg.TxID, + cfg.AutoCompoundRewardShares, + periodSeconds, + options..., + ) + if err != nil { + return ids.Empty, fmt.Errorf("failed to issue SetAutoRenewedValidatorConfigTx: %w", err) + } + return tx.ID(), nil +} + +// GetAutoRenewedValidatorAuthority returns the config-authority owner for an +// accepted AddAutoRenewedValidatorTx. +// +// When nodeID is non-empty the lookup is narrowed to that node via the +// platform.getCurrentValidators nodeIDs filter, avoiding a full validator-set +// fetch. The typed client does not yet surface validatorAuthority, so the +// reply is decoded with purpose-built structs. +func GetAutoRenewedValidatorAuthority(ctx context.Context, rpcURL string, nodeID ids.NodeID, txID ids.ID) (*secp256k1fx.OutputOwners, error) { + client := platformvm.NewClient(rpcURL) + args := &platformvm.GetCurrentValidatorsArgs{} + if nodeID != ids.EmptyNodeID { + args.NodeIDs = []ids.NodeID{nodeID} + } + reply := &getCurrentValidatorsWithAuthorityReply{} + if err := client.Requester.SendRequest(ctx, "platform.getCurrentValidators", args, reply); err != nil { + return nil, fmt.Errorf("failed to fetch current validators: %w", err) + } + + for _, validator := range reply.Validators { + if validator.TxID != txID.String() { + continue + } + if validator.ValidatorAuthority == nil { + return nil, fmt.Errorf("validator %s did not include validatorAuthority", txID) + } + return validator.ValidatorAuthority.toOutputOwners() + } + return nil, fmt.Errorf("auto-renewed validator %s not found in current validators", txID) +} + +type getCurrentValidatorsWithAuthorityReply struct { + Validators []autoRenewedValidatorWithAuthority `json:"validators"` +} + +type autoRenewedValidatorWithAuthority struct { + TxID string `json:"txID"` + ValidatorAuthority *autoRenewedAPIOwner `json:"validatorAuthority"` +} + +type autoRenewedAPIOwner struct { + Locktime string `json:"locktime"` + Threshold string `json:"threshold"` + Addresses []string `json:"addresses"` +} + +func (o *autoRenewedAPIOwner) toOutputOwners() (*secp256k1fx.OutputOwners, error) { + locktime, err := strconv.ParseUint(o.Locktime, 10, 64) + if err != nil { + return nil, fmt.Errorf("invalid validatorAuthority locktime: %w", err) + } + threshold, err := strconv.ParseUint(o.Threshold, 10, 32) + if err != nil { + return nil, fmt.Errorf("invalid validatorAuthority threshold: %w", err) + } + addrs, err := address.ParseToIDs(o.Addresses) + if err != nil { + return nil, fmt.Errorf("invalid validatorAuthority addresses: %w", err) + } + return &secp256k1fx.OutputOwners{ + Locktime: locktime, + Threshold: uint32(threshold), + Addrs: addrs, + }, nil +} + +func durationToWholeSeconds(name string, duration time.Duration, allowZero bool) (uint64, error) { + if duration < 0 { + return 0, fmt.Errorf("%s cannot be negative", name) + } + if !allowZero && duration == 0 { + return 0, fmt.Errorf("%s must be positive", name) + } + if duration%time.Second != 0 { + return 0, fmt.Errorf("%s must be a whole number of seconds", name) + } + return uint64(duration / time.Second), nil +} + // AddDelegatorConfig holds configuration for adding a delegator. type AddDelegatorConfig struct { NodeID ids.NodeID diff --git a/pkg/wallet/wallet.go b/pkg/wallet/wallet.go index c7c580b..e037800 100644 --- a/pkg/wallet/wallet.go +++ b/pkg/wallet/wallet.go @@ -10,10 +10,15 @@ import ( "github.com/ava-labs/avalanchego/utils/crypto/keychain" "github.com/ava-labs/avalanchego/utils/crypto/secp256k1" "github.com/ava-labs/avalanchego/utils/formatting/address" + "github.com/ava-labs/avalanchego/vms/platformvm/fx" "github.com/ava-labs/avalanchego/vms/secp256k1fx" "github.com/ava-labs/avalanchego/wallet/chain/c" + pchainwallet "github.com/ava-labs/avalanchego/wallet/chain/p" + pbuilder "github.com/ava-labs/avalanchego/wallet/chain/p/builder" + psigner "github.com/ava-labs/avalanchego/wallet/chain/p/signer" pwallet "github.com/ava-labs/avalanchego/wallet/chain/p/wallet" "github.com/ava-labs/avalanchego/wallet/subnet/primary" + walletcommon "github.com/ava-labs/avalanchego/wallet/subnet/primary/common" "github.com/ava-labs/libevm/common" "github.com/ava-labs/platform-cli/pkg/network" ) @@ -93,6 +98,36 @@ func NewWalletFromKeychainWithSubnet(ctx context.Context, kc keychain.Keychain, }, nil } +// NewWalletFromKeychainWithOwner creates a P-Chain wallet whose backend maps +// ownerID -> owner. +// +// This is required for transactions whose authorizing owner is not sourced from +// chain state by the public builder. In particular, the builder for +// SetAutoRenewedValidatorConfigTx resolves the config owner via +// backend.GetOwner(txID), so the owner authorized at add-time must be supplied +// through the backend's owners map. P-Chain state is fetched exactly once here, +// avoiding a second round-trip on top of loading a standard wallet. +func NewWalletFromKeychainWithOwner(ctx context.Context, kc keychain.Keychain, address ids.ShortID, config network.Config, ownerID ids.ID, owner fx.Owner) (*Wallet, error) { + client, pContext, utxos, err := primary.FetchPState(ctx, config.RPCURL, kc.Addresses()) + if err != nil { + return nil, fmt.Errorf("failed to fetch P-Chain wallet state: %w", err) + } + + owners := map[ids.ID]fx.Owner{ownerID: owner} + backend := pwallet.NewBackend(walletcommon.NewChainUTXOs(constants.PlatformChainID, utxos), owners) + pWallet := pwallet.New( + pchainwallet.NewClient(client, backend), + pbuilder.New(kc.Addresses(), pContext, backend), + psigner.New(kc, backend), + ) + + return &Wallet{ + pWallet: pWallet, + config: config, + address: address, + }, nil +} + // PWallet returns the underlying P-Chain wallet. func (w *Wallet) PWallet() pwallet.Wallet { return w.pWallet