From 0c19484dfdf62492fd21ea31c72ff89442ac1a43 Mon Sep 17 00:00:00 2001 From: glyh Date: Mon, 24 Nov 2025 21:06:29 +0800 Subject: [PATCH 01/40] hardfork test: rename graphql package to graphql_client --- .../src/internal/{graphql => client}/client.go | 2 +- .../src/internal/hardfork/config_validation.go | 8 ++++---- .../hardfork_test/src/internal/hardfork/test.go | 6 +++--- .../src/internal/hardfork/validation.go | 16 ++++++++-------- 4 files changed, 16 insertions(+), 16 deletions(-) rename src/app/hardfork_test/src/internal/{graphql => client}/client.go (99%) diff --git a/src/app/hardfork_test/src/internal/graphql/client.go b/src/app/hardfork_test/src/internal/client/client.go similarity index 99% rename from src/app/hardfork_test/src/internal/graphql/client.go rename to src/app/hardfork_test/src/internal/client/client.go index 981a538e1916..8002550f6090 100644 --- a/src/app/hardfork_test/src/internal/graphql/client.go +++ b/src/app/hardfork_test/src/internal/client/client.go @@ -1,4 +1,4 @@ -package graphql +package client import ( "bytes" diff --git a/src/app/hardfork_test/src/internal/hardfork/config_validation.go b/src/app/hardfork_test/src/internal/hardfork/config_validation.go index 037dba4be7b6..c6c5c2103843 100644 --- a/src/app/hardfork_test/src/internal/hardfork/config_validation.go +++ b/src/app/hardfork_test/src/internal/hardfork/config_validation.go @@ -4,12 +4,12 @@ import ( "fmt" "os" - "github.com/MinaProtocol/mina/src/app/hardfork_test/src/internal/graphql" + "github.com/MinaProtocol/mina/src/app/hardfork_test/src/internal/client" ) // ValidatePreforkLedgerHashes validates the generated prefork ledger hashes func (t *HardforkTest) ValidatePreforkLedgerHashes( - latestNonEmptyBlock graphql.BlockData, + latestNonEmptyBlock client.BlockData, genesisEpochStaking string, genesisEpochNext string, latestSnarkedHashPerEpoch map[int]string, @@ -75,7 +75,7 @@ func (t *HardforkTest) ValidatePreforkLedgerHashes( } // ValidateForkConfigData validates the extracted fork config against expected values -func (t *HardforkTest) ValidateForkConfigData(latestNonEmptyBlock graphql.BlockData, forkConfigBytes []byte) error { +func (t *HardforkTest) ValidateForkConfigData(latestNonEmptyBlock client.BlockData, forkConfigBytes []byte) error { forkConfigJson := string(forkConfigBytes) // Validate field values @@ -131,7 +131,7 @@ func (t *HardforkTest) ValidateForkConfigData(latestNonEmptyBlock graphql.BlockD } // ValidateForkRuntimeConfig validates that the runtime config has correct fork data -func (t *HardforkTest) ValidateForkRuntimeConfig(latestNonEmptyBlock graphql.BlockData, configData []byte, forkGenesisTs, mainGenesisTs int64) error { +func (t *HardforkTest) ValidateForkRuntimeConfig(latestNonEmptyBlock client.BlockData, configData []byte, forkGenesisTs, mainGenesisTs int64) error { // Calculate expected genesis slot expectedGenesisSlot := (forkGenesisTs - mainGenesisTs) / int64(t.Config.MainSlot) diff --git a/src/app/hardfork_test/src/internal/hardfork/test.go b/src/app/hardfork_test/src/internal/hardfork/test.go index 7a4b822d1e37..8c8a64caef30 100644 --- a/src/app/hardfork_test/src/internal/hardfork/test.go +++ b/src/app/hardfork_test/src/internal/hardfork/test.go @@ -9,15 +9,15 @@ import ( "syscall" "time" + "github.com/MinaProtocol/mina/src/app/hardfork_test/src/internal/client" "github.com/MinaProtocol/mina/src/app/hardfork_test/src/internal/config" - "github.com/MinaProtocol/mina/src/app/hardfork_test/src/internal/graphql" "github.com/MinaProtocol/mina/src/app/hardfork_test/src/internal/utils" ) // HardforkTest represents the main hardfork test logic type HardforkTest struct { Config *config.Config - Client *graphql.Client + Client *client.Client Logger *utils.Logger ScriptDir string runningCmds []*exec.Cmd @@ -31,7 +31,7 @@ func NewHardforkTest(cfg *config.Config) *HardforkTest { ctx, cancel := context.WithCancel(context.Background()) return &HardforkTest{ Config: cfg, - Client: graphql.NewClient(cfg.HTTPClientTimeoutSeconds, cfg.GraphQLMaxRetries), + Client: client.NewClient(cfg.HTTPClientTimeoutSeconds, cfg.GraphQLMaxRetries), Logger: utils.NewLogger(), ScriptDir: cfg.ScriptDir, runningCmds: make([]*exec.Cmd, 0), diff --git a/src/app/hardfork_test/src/internal/hardfork/validation.go b/src/app/hardfork_test/src/internal/hardfork/validation.go index 80cbcbde8a56..323e33b4078a 100644 --- a/src/app/hardfork_test/src/internal/hardfork/validation.go +++ b/src/app/hardfork_test/src/internal/hardfork/validation.go @@ -4,14 +4,14 @@ import ( "fmt" "time" - "github.com/MinaProtocol/mina/src/app/hardfork_test/src/internal/graphql" + "github.com/MinaProtocol/mina/src/app/hardfork_test/src/internal/client" ) // BlockAnalysisResult holds the results of analyzing blocks type BlockAnalysisResult struct { LatestOccupiedSlot int LatestSnarkedHashPerEpoch map[int]string // map from epoch to snarked ledger hash - LatestNonEmptyBlock graphql.BlockData + LatestNonEmptyBlock client.BlockData GenesisEpochStaking string GenesisEpochNext string } @@ -35,7 +35,7 @@ func (t *HardforkTest) ValidateLatestOccupiedSlot(latestOccupiedSlot int) error } // ValidateLatestNonEmptyBlockSlot checks that the latest non-empty block is before tx end slot -func (t *HardforkTest) ValidateLatestNonEmptyBlockSlot(latestNonEmptyBlock graphql.BlockData) error { +func (t *HardforkTest) ValidateLatestNonEmptyBlockSlot(latestNonEmptyBlock client.BlockData) error { t.Logger.Info("Latest non-empty block: %s, height: %d, slot: %d", latestNonEmptyBlock.StateHash, latestNonEmptyBlock.BlockHeight, latestNonEmptyBlock.Slot) @@ -75,8 +75,8 @@ func (t *HardforkTest) ValidateNoNewBlocks(port int) error { } // CollectBlocks gathers blocks from multiple slots across different ports -func (t *HardforkTest) CollectBlocks(startSlot, endSlot int) ([]graphql.BlockData, error) { - var allBlocks []graphql.BlockData +func (t *HardforkTest) CollectBlocks(startSlot, endSlot int) ([]client.BlockData, error) { + var allBlocks []client.BlockData for i := startSlot; i <= endSlot; i++ { @@ -105,7 +105,7 @@ func (t *HardforkTest) AnalyzeBlocks() (*BlockAnalysisResult, error) { } // Find the first non-empty block to get genesis epoch hashes - var firstEpochBlock graphql.BlockData + var firstEpochBlock client.BlockData for _, block := range blocks { if block.NonEmpty && block.Epoch == 0 { firstEpochBlock = block @@ -154,10 +154,10 @@ func (t *HardforkTest) AnalyzeBlocks() (*BlockAnalysisResult, error) { // FindLatestNonEmptyBlock processes block data to find the latest non-empty block // and collects other important information // This function assumes that there is at least one block with non-zero slot -func (t *HardforkTest) FindLatestNonEmptyBlock(blocks []graphql.BlockData) ( +func (t *HardforkTest) FindLatestNonEmptyBlock(blocks []client.BlockData) ( latestOccupiedSlot int, latestSnarkedHashPerEpoch map[int]string, // map from epoch to snarked ledger hash - latestNonEmptyBlock graphql.BlockData, + latestNonEmptyBlock client.BlockData, err error) { if len(blocks) == 0 { From 80da231ac8307aaaa147df7e54b9d717431cfeb7 Mon Sep 17 00:00:00 2001 From: glyh Date: Mon, 24 Nov 2025 21:28:06 +0800 Subject: [PATCH 02/40] hardfork test: rename cli arg `graphql-max-retries` -> `client-max-retries` --- src/app/hardfork_test/src/internal/app/root.go | 2 +- src/app/hardfork_test/src/internal/config/config.go | 4 ++-- src/app/hardfork_test/src/internal/hardfork/test.go | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/app/hardfork_test/src/internal/app/root.go b/src/app/hardfork_test/src/internal/app/root.go index 0108b155697e..4507bb5b7667 100644 --- a/src/app/hardfork_test/src/internal/app/root.go +++ b/src/app/hardfork_test/src/internal/app/root.go @@ -84,7 +84,7 @@ func init() { rootCmd.Flags().IntVar(&cfg.UserCommandCheckMaxIterations, "user-command-check-max-iterations", cfg.UserCommandCheckMaxIterations, "Max iterations to check for user commands in blocks") rootCmd.Flags().IntVar(&cfg.ForkEarliestBlockMaxRetries, "fork-earliest-block-max-retries", cfg.ForkEarliestBlockMaxRetries, "Maximum number of retries to wait for earliest block in fork network") rootCmd.Flags().IntVar(&cfg.HTTPClientTimeoutSeconds, "http-timeout", cfg.HTTPClientTimeoutSeconds, "HTTP client timeout in seconds for GraphQL requests") - rootCmd.Flags().IntVar(&cfg.GraphQLMaxRetries, "graphql-max-retries", cfg.GraphQLMaxRetries, "Maximum number of retries for GraphQL requests") + rootCmd.Flags().IntVar(&cfg.ClientMaxRetries, "client-max-retries", cfg.ClientMaxRetries, "Maximum number of retries for client requests") // Mark required flags rootCmd.MarkFlagRequired("main-mina-exe") diff --git a/src/app/hardfork_test/src/internal/config/config.go b/src/app/hardfork_test/src/internal/config/config.go index 2c2ce8a4b600..34eef3eb2153 100644 --- a/src/app/hardfork_test/src/internal/config/config.go +++ b/src/app/hardfork_test/src/internal/config/config.go @@ -60,7 +60,7 @@ type Config struct { UserCommandCheckMaxIterations int // Max iterations to check for user commands in blocks ForkEarliestBlockMaxRetries int // Max retries to wait for earliest block in fork network HTTPClientTimeoutSeconds int // HTTP client timeout for GraphQL requests - GraphQLMaxRetries int // Max number of retries for GraphQL requests + ClientMaxRetries int // Max number of retries for client requests } // DefaultConfig returns the default configuration with values @@ -87,7 +87,7 @@ func DefaultConfig() *Config { ForkEarliestBlockMaxRetries: 10, HTTPClientTimeoutSeconds: 600, // ^ fork config take really long time to complete (2-3 minutes) - GraphQLMaxRetries: 5, + ClientMaxRetries: 5, SeedStartPort: 3000, SnarkCoordinatorPort: 7000, diff --git a/src/app/hardfork_test/src/internal/hardfork/test.go b/src/app/hardfork_test/src/internal/hardfork/test.go index 8c8a64caef30..aa4c594339c5 100644 --- a/src/app/hardfork_test/src/internal/hardfork/test.go +++ b/src/app/hardfork_test/src/internal/hardfork/test.go @@ -31,7 +31,7 @@ func NewHardforkTest(cfg *config.Config) *HardforkTest { ctx, cancel := context.WithCancel(context.Background()) return &HardforkTest{ Config: cfg, - Client: client.NewClient(cfg.HTTPClientTimeoutSeconds, cfg.GraphQLMaxRetries), + Client: client.NewClient(cfg.HTTPClientTimeoutSeconds, cfg.ClientMaxRetries), Logger: utils.NewLogger(), ScriptDir: cfg.ScriptDir, runningCmds: make([]*exec.Cmd, 0), From 13f882f5db37334c7fb8e6225b9e64a9e4036593 Mon Sep 17 00:00:00 2001 From: glyh Date: Mon, 24 Nov 2025 21:29:46 +0800 Subject: [PATCH 03/40] hf test go: make client raw gql query private --- .../hardfork_test/src/internal/client/client.go | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/src/app/hardfork_test/src/internal/client/client.go b/src/app/hardfork_test/src/internal/client/client.go index 8002550f6090..fa272949f409 100644 --- a/src/app/hardfork_test/src/internal/client/client.go +++ b/src/app/hardfork_test/src/internal/client/client.go @@ -27,8 +27,8 @@ func NewClient(timeoutSeconds int, maxRetries int) *Client { } } -// Query sends a GraphQL query to the specified port with retry logic -func (c *Client) Query(port int, query string) (gjson.Result, error) { +// query sends a GraphQL query to the specified port with retry logic +func (c *Client) query(port int, query string) (gjson.Result, error) { url := fmt.Sprintf("http://localhost:%d/graphql", port) // Format the query payload @@ -99,7 +99,7 @@ func (c *Client) Query(port int, query string) (gjson.Result, error) { // GetHeight gets the current block height func (c *Client) GetHeight(port int) (int, error) { - result, err := c.Query(port, "bestChain(maxLength: 1) { protocolState { consensusState { blockHeight } } }") + result, err := c.query(port, "bestChain(maxLength: 1) { protocolState { consensusState { blockHeight } } }") if err != nil { return 0, err } @@ -110,7 +110,7 @@ func (c *Client) GetHeight(port int) (int, error) { // GetHeightAndSlotOfEarliest gets the height and slot of the earliest block func (c *Client) GetHeightAndSlotOfEarliest(port int) (height, slot int, err error) { - result, err := c.Query(port, "bestChain { protocolState { consensusState { blockHeight slotSinceGenesis } } }") + result, err := c.query(port, "bestChain { protocolState { consensusState { blockHeight slotSinceGenesis } } }") if err != nil { return 0, 0, err } @@ -123,7 +123,7 @@ func (c *Client) GetHeightAndSlotOfEarliest(port int) (height, slot int, err err // GetForkConfig gets the fork configuration func (c *Client) GetForkConfig(port int) (gjson.Result, error) { - result, err := c.Query(port, "fork_config") + result, err := c.query(port, "fork_config") if err != nil { return gjson.Result{}, err } @@ -133,7 +133,7 @@ func (c *Client) GetForkConfig(port int) (gjson.Result, error) { // BlocksWithUserCommands gets the number of blocks with user commands func (c *Client) BlocksWithUserCommands(port int) (int, error) { - result, err := c.Query(port, "bestChain { commandTransactionCount }") + result, err := c.query(port, "bestChain { commandTransactionCount }") if err != nil { return 0, err } @@ -150,7 +150,7 @@ func (c *Client) BlocksWithUserCommands(port int) (int, error) { } // Blocks gets blocks data as in the original shell script -const BlocksQuery = ` +const Blocksquery = ` bestChain { commandTransactionCount protocolState { @@ -197,7 +197,7 @@ type BlockData struct { // GetBlocks retrieves block data from the node func (c *Client) GetBlocks(port int) ([]BlockData, error) { - result, err := c.Query(port, BlocksQuery) + result, err := c.query(port, Blocksquery) if err != nil { return nil, err } From f8fdb2268b58d63985d6f185131cb65bfaf6fa2e Mon Sep 17 00:00:00 2001 From: glyh Date: Mon, 24 Nov 2025 21:36:18 +0800 Subject: [PATCH 04/40] hf test dhall: remove arg1 as its not used for test --- buildkite/src/Jobs/Test/HardForkTest.dhall | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/buildkite/src/Jobs/Test/HardForkTest.dhall b/buildkite/src/Jobs/Test/HardForkTest.dhall index c26d9e4362ae..9e515248a7e6 100644 --- a/buildkite/src/Jobs/Test/HardForkTest.dhall +++ b/buildkite/src/Jobs/Test/HardForkTest.dhall @@ -54,7 +54,7 @@ in Pipeline.build , privileged = True , useBash = False } - "./scripts/hardfork/build-and-test.sh \$BUILDKITE_BRANCH" + "./scripts/hardfork/build-and-test.sh" ] , label = "hard fork test" , key = "hard-fork-test" From 562c5c6c8b25ade0f1caa3292c36964a56370024 Mon Sep 17 00:00:00 2001 From: glyh Date: Mon, 24 Nov 2025 21:40:04 +0800 Subject: [PATCH 05/40] make prefork network as a configurable argument --- buildkite/src/Jobs/Test/HardForkTest.dhall | 6 +-- scripts/hardfork/build-and-test.sh | 47 ++++++++++++++++++++-- 2 files changed, 46 insertions(+), 7 deletions(-) diff --git a/buildkite/src/Jobs/Test/HardForkTest.dhall b/buildkite/src/Jobs/Test/HardForkTest.dhall index 9e515248a7e6..74db9f39bf39 100644 --- a/buildkite/src/Jobs/Test/HardForkTest.dhall +++ b/buildkite/src/Jobs/Test/HardForkTest.dhall @@ -54,10 +54,10 @@ in Pipeline.build , privileged = True , useBash = False } - "./scripts/hardfork/build-and-test.sh" + "./scripts/hardfork/build-and-test.sh --fork-from origin/master" ] - , label = "hard fork test" - , key = "hard-fork-test" + , label = "hard fork test - legacy mode" + , key = "hard-fork-test-legacy" , target = Size.Integration , soft_fail = Some (B/SoftFail.Boolean False) , docker = None Docker.Type diff --git a/scripts/hardfork/build-and-test.sh b/scripts/hardfork/build-and-test.sh index c5bc8370cbe7..a7a850666341 100755 --- a/scripts/hardfork/build-and-test.sh +++ b/scripts/hardfork/build-and-test.sh @@ -1,8 +1,8 @@ #!/usr/bin/env bash -# This scripts builds master and current branch with nix +# This scripts builds a designated PREFORK branch and current branch with nix # 0. Prepare environment if needed -# 1. Build master as a prefork build; +# 1. Build PREFORK as a prefork build; # 2. Upload to nix cache, the reason for not uploading cache for following 2 # steps is that they change for each PR. # 3. Build current branch as a postfork build; @@ -12,6 +12,45 @@ # Step 0. Prepare environment if needed set -eux -o pipefail +PREFORK="" + +usage() { + echo "Usage: $0 --fork-from " + exit 1 +} + +# ---- argument parsing -------------------------------------------------------- +while [[ $# -gt 0 ]]; do + case "$1" in + --fork-from) + # ensure value exists + if [[ $# -lt 2 ]]; then + echo "Error: --fork-from requires an argument." + usage + fi + PREFORK="$2" + shift 2 + ;; + --help|-h) + usage + ;; + --*) + echo "Unknown option: $1" + usage + ;; + *) + # positional arg — store if needed later + echo "Unexpected argument: $1" + usage + ;; + esac +done + +if [[ -z "$PREFORK" ]]; then + echo "Error: --fork-from must be provided." + usage +fi + NIX_OPTS=( --accept-flake-config --experimental-features 'nix-command flakes' ) if [[ -n "${NIX_CACHE_NAR_SECRET:-}" ]]; then @@ -70,8 +109,8 @@ if [ -n "${BUILDKITE:-}" ]; then git fetch origin fi -# 1. Build master as a prefork build; -git checkout origin/master +# 1. Build PREFORK as a prefork build; +git checkout $PREFORK git submodule update --init --recursive --depth 1 nix "${NIX_OPTS[@]}" build "$PWD?submodules=1#devnet" --out-link "prefork-devnet" From 3c6df4a623f5769f5856203be5a8de1f1d5a4d79 Mon Sep 17 00:00:00 2001 From: glyh Date: Tue, 25 Nov 2025 15:27:06 +0800 Subject: [PATCH 06/40] hf test go: support passing in fork method flag --- scripts/hardfork/build-and-test.sh | 28 ++++++++++- .../hardfork_test/src/internal/app/root.go | 1 + .../src/internal/config/config.go | 2 + .../src/internal/config/fork_method.go | 48 +++++++++++++++++++ 4 files changed, 77 insertions(+), 2 deletions(-) create mode 100644 src/app/hardfork_test/src/internal/config/fork_method.go diff --git a/scripts/hardfork/build-and-test.sh b/scripts/hardfork/build-and-test.sh index a7a850666341..167ca28f3810 100755 --- a/scripts/hardfork/build-and-test.sh +++ b/scripts/hardfork/build-and-test.sh @@ -13,9 +13,10 @@ set -eux -o pipefail PREFORK="" +FORK_METHOD="legacy" usage() { - echo "Usage: $0 --fork-from " + echo "Usage: $0 --fork-from --fork-method " exit 1 } @@ -31,6 +32,23 @@ while [[ $# -gt 0 ]]; do PREFORK="$2" shift 2 ;; + --fork-method) + # ensure value exists + if [[ $# -lt 2 ]]; then + echo "Error: --fork-from requires an argument." + usage + fi + case "$2" in + legacy|advanced-generate-hf-config) + FORK_METHOD="$2" + ;; + *) + echo "Error: --fork-method must be either 'legacy' or 'advanced-generate-hf-config'." + usage + ;; + esac + shift 2 + ;; --help|-h) usage ;; @@ -51,6 +69,11 @@ if [[ -z "$PREFORK" ]]; then usage fi +if [[ "$FORK_METHOD" == "advanced-generate-hf-config" ]]; then + echo "Error: unimplemented fork mode 'advanced-generate-hf-config'" + usage +fi + NIX_OPTS=( --accept-flake-config --experimental-features 'nix-command flakes' ) if [[ -n "${NIX_CACHE_NAR_SECRET:-}" ]]; then @@ -152,6 +175,7 @@ hardfork_test/bin/hardfork_test \ --slot-tx-end "$SLOT_TX_END" \ --slot-chain-end "$SLOT_CHAIN_END" \ --script-dir "$SCRIPT_DIR" \ - --root "$NETWORK_ROOT" + --root "$NETWORK_ROOT" \ + --fork-method "$FORK_METHOD" popd diff --git a/src/app/hardfork_test/src/internal/app/root.go b/src/app/hardfork_test/src/internal/app/root.go index 4507bb5b7667..641d188943cd 100644 --- a/src/app/hardfork_test/src/internal/app/root.go +++ b/src/app/hardfork_test/src/internal/app/root.go @@ -85,6 +85,7 @@ func init() { rootCmd.Flags().IntVar(&cfg.ForkEarliestBlockMaxRetries, "fork-earliest-block-max-retries", cfg.ForkEarliestBlockMaxRetries, "Maximum number of retries to wait for earliest block in fork network") rootCmd.Flags().IntVar(&cfg.HTTPClientTimeoutSeconds, "http-timeout", cfg.HTTPClientTimeoutSeconds, "HTTP client timeout in seconds for GraphQL requests") rootCmd.Flags().IntVar(&cfg.ClientMaxRetries, "client-max-retries", cfg.ClientMaxRetries, "Maximum number of retries for client requests") + rootCmd.Flags().Var(&cfg.ForkMethod, "fork-method", "The implementation of fork") // Mark required flags rootCmd.MarkFlagRequired("main-mina-exe") diff --git a/src/app/hardfork_test/src/internal/config/config.go b/src/app/hardfork_test/src/internal/config/config.go index 34eef3eb2153..2f36f62d64e7 100644 --- a/src/app/hardfork_test/src/internal/config/config.go +++ b/src/app/hardfork_test/src/internal/config/config.go @@ -61,6 +61,8 @@ type Config struct { ForkEarliestBlockMaxRetries int // Max retries to wait for earliest block in fork network HTTPClientTimeoutSeconds int // HTTP client timeout for GraphQL requests ClientMaxRetries int // Max number of retries for client requests + + ForkMethod ForkMethod } // DefaultConfig returns the default configuration with values diff --git a/src/app/hardfork_test/src/internal/config/fork_method.go b/src/app/hardfork_test/src/internal/config/fork_method.go new file mode 100644 index 000000000000..141aed216e18 --- /dev/null +++ b/src/app/hardfork_test/src/internal/config/fork_method.go @@ -0,0 +1,48 @@ +package config + +import ( + "fmt" + "sort" + "strings" +) + +type ForkMethod int + +const ( + Legacy ForkMethod = iota +) + +var forkMethodToString = map[ForkMethod]string{ + Legacy: "legacy", +} + +var stringToForkMethod = map[string]ForkMethod{ + "legacy": Legacy, +} + +func (m *ForkMethod) String() string { + if s, ok := forkMethodToString[*m]; ok { + return s + } + return "unknown" +} + +func validKeys(m map[string]ForkMethod) string { + keys := make([]string, 0, len(m)) + for k := range m { + keys = append(keys, k) + } + sort.Strings(keys) + return strings.Join(keys, "|") +} + +func (m *ForkMethod) Set(s string) error { + v, ok := stringToForkMethod[s] + if !ok { + return fmt.Errorf("invalid mode %q (valid: %s)", s, validKeys(stringToForkMethod)) + } + *m = v + return nil +} + +func (m *ForkMethod) Type() string { return "fork method" } From 58b8ae3ecd815ccdd1020ec0c5f6d139b95c5d65 Mon Sep 17 00:00:00 2001 From: glyh Date: Tue, 25 Nov 2025 15:30:01 +0800 Subject: [PATCH 07/40] simplify & fix error message reporting in hardfork/build-and-test.sh --- scripts/hardfork/build-and-test.sh | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/scripts/hardfork/build-and-test.sh b/scripts/hardfork/build-and-test.sh index 167ca28f3810..fd320fd0ff8e 100755 --- a/scripts/hardfork/build-and-test.sh +++ b/scripts/hardfork/build-and-test.sh @@ -26,7 +26,7 @@ while [[ $# -gt 0 ]]; do --fork-from) # ensure value exists if [[ $# -lt 2 ]]; then - echo "Error: --fork-from requires an argument." + echo "Error: $1 requires an argument." usage fi PREFORK="$2" @@ -35,7 +35,7 @@ while [[ $# -gt 0 ]]; do --fork-method) # ensure value exists if [[ $# -lt 2 ]]; then - echo "Error: --fork-from requires an argument." + echo "Error: $1 requires an argument." usage fi case "$2" in @@ -43,7 +43,7 @@ while [[ $# -gt 0 ]]; do FORK_METHOD="$2" ;; *) - echo "Error: --fork-method must be either 'legacy' or 'advanced-generate-hf-config'." + echo "Error: $1 must be either 'legacy' or 'advanced-generate-hf-config'." usage ;; esac From 5b1088683785db8c84eff27ce63b6d082298df44 Mon Sep 17 00:00:00 2001 From: glyh Date: Tue, 25 Nov 2025 15:32:05 +0800 Subject: [PATCH 08/40] hardfork > build-and-test.sh: unify usage report --- scripts/hardfork/build-and-test.sh | 32 +++++++++++++++--------------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/scripts/hardfork/build-and-test.sh b/scripts/hardfork/build-and-test.sh index fd320fd0ff8e..92a72d7cadd2 100755 --- a/scripts/hardfork/build-and-test.sh +++ b/scripts/hardfork/build-and-test.sh @@ -15,9 +15,16 @@ set -eux -o pipefail PREFORK="" FORK_METHOD="legacy" +USAGE="Usage: $0 --fork-from [--fork-method ]" usage() { - echo "Usage: $0 --fork-from --fork-method " - exit 1 + if (( $# > 0 )); then + echo "$1" >&2 + echo "$USAGE" + exit 1 + else + echo "$USAGE" + exit 0 + fi } # ---- argument parsing -------------------------------------------------------- @@ -26,8 +33,7 @@ while [[ $# -gt 0 ]]; do --fork-from) # ensure value exists if [[ $# -lt 2 ]]; then - echo "Error: $1 requires an argument." - usage + usage "Error: $1 requires an argument." fi PREFORK="$2" shift 2 @@ -35,16 +41,14 @@ while [[ $# -gt 0 ]]; do --fork-method) # ensure value exists if [[ $# -lt 2 ]]; then - echo "Error: $1 requires an argument." - usage + usage "Error: $1 requires an argument." fi case "$2" in legacy|advanced-generate-hf-config) FORK_METHOD="$2" ;; *) - echo "Error: $1 must be either 'legacy' or 'advanced-generate-hf-config'." - usage + usage "Error: $1 must be either 'legacy' or 'advanced-generate-hf-config'." ;; esac shift 2 @@ -53,25 +57,21 @@ while [[ $# -gt 0 ]]; do usage ;; --*) - echo "Unknown option: $1" - usage + usage "Unknown option: $1" ;; *) # positional arg — store if needed later - echo "Unexpected argument: $1" - usage + usage "Unexpected argument: $1" ;; esac done if [[ -z "$PREFORK" ]]; then - echo "Error: --fork-from must be provided." - usage + usage "Error: --fork-from must be provided." fi if [[ "$FORK_METHOD" == "advanced-generate-hf-config" ]]; then - echo "Error: unimplemented fork mode 'advanced-generate-hf-config'" - usage + usage "Error: unimplemented fork mode '$FORK_METHOD'" fi NIX_OPTS=( --accept-flake-config --experimental-features 'nix-command flakes' ) From 8a3d0f1d931f7d34b7440f7067fa150e8f2a9a92 Mon Sep 17 00:00:00 2001 From: glyh Date: Tue, 25 Nov 2025 15:37:55 +0800 Subject: [PATCH 09/40] hf test go: set default for fork method --- src/app/hardfork_test/src/internal/config/config.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/app/hardfork_test/src/internal/config/config.go b/src/app/hardfork_test/src/internal/config/config.go index 2f36f62d64e7..38ba43f602bd 100644 --- a/src/app/hardfork_test/src/internal/config/config.go +++ b/src/app/hardfork_test/src/internal/config/config.go @@ -96,6 +96,8 @@ func DefaultConfig() *Config { WhaleStartPort: 4000, FishStartPort: 5000, NodeStartPort: 6000, + + ForkMethod: Legacy, } } From 69bb4d8d9a647e8a71cc3967b04daf95e9997fd2 Mon Sep 17 00:00:00 2001 From: glyh Date: Tue, 25 Nov 2025 15:39:18 +0800 Subject: [PATCH 10/40] hf test phases: remove redundant phase log --- src/app/hardfork_test/src/internal/hardfork/phases.go | 1 - 1 file changed, 1 deletion(-) diff --git a/src/app/hardfork_test/src/internal/hardfork/phases.go b/src/app/hardfork_test/src/internal/hardfork/phases.go index 9dced89d36e5..f5565bc1753a 100644 --- a/src/app/hardfork_test/src/internal/hardfork/phases.go +++ b/src/app/hardfork_test/src/internal/hardfork/phases.go @@ -147,7 +147,6 @@ func (t *HardforkTest) LegacyForkPhase(analysis *BlockAnalysisResult, forkConfig // Calculate fork genesis timestamp relative to now (before starting fork network) forkGenesisTs := time.Now().Unix() + int64(t.Config.ForkDelay*60) - t.Logger.Info("Phase 3: Generating fork configuration and ledgers...") { preforkGenesisConfigFile := fmt.Sprintf("%s/daemon.json", t.Config.Root) forkHashesFile := "fork_data/hf_ledger_hashes.json" From 0bf52bb2c66c3ba7e32cd43d6cd9700fcb879208 Mon Sep 17 00:00:00 2001 From: glyh Date: Tue, 25 Nov 2025 16:18:41 +0800 Subject: [PATCH 11/40] ledger.go: implement call to advance gen fork config in ledger.go --- .../src/internal/hardfork/ledger.go | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/src/app/hardfork_test/src/internal/hardfork/ledger.go b/src/app/hardfork_test/src/internal/hardfork/ledger.go index 36ac1cddb5ec..412296070a0a 100644 --- a/src/app/hardfork_test/src/internal/hardfork/ledger.go +++ b/src/app/hardfork_test/src/internal/hardfork/ledger.go @@ -81,3 +81,21 @@ func (t *HardforkTest) GenerateForkConfigAndLedgers(analysis *BlockAnalysisResul // Validate modified fork data return t.ValidateForkRuntimeConfig(analysis.LatestNonEmptyBlock, runtimeConfigBytes, forkGenesisTs, mainGenesisTs) } + +func (t *HardforkTest) AdvancedGenerateHardForkConfig(configDir string) error { + cmd := exec.Command(t.Config.MainMinaExe, + "advanced", "generate-hardfork-config", + "--hardfork-config-dir", configDir, + ) + + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + + if err := cmd.Start(); err != nil { + return fmt.Errorf("failed to call mina CLI: %w", err) + } + + cmd.Wait() + + return nil +} From 88fedd999769145837118d55948aa5767d9ea808 Mon Sep 17 00:00:00 2001 From: glyh Date: Mon, 24 Nov 2025 14:56:16 +0800 Subject: [PATCH 12/40] document the purpose & usage of scripts/hardfork/create_runtime_config.sh --- scripts/hardfork/create_runtime_config.sh | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/scripts/hardfork/create_runtime_config.sh b/scripts/hardfork/create_runtime_config.sh index 6652fc5f50f1..7bb2d6c55609 100755 --- a/scripts/hardfork/create_runtime_config.sh +++ b/scripts/hardfork/create_runtime_config.sh @@ -1,17 +1,24 @@ #!/usr/bin/env bash +# NOTE: This script patches the genesis timestamp of a fork config so a new +# network could be schedueld to genesis in some instant in the future. + set -eo pipefail +# ==================== Inputs to this script ==================== +# The fork config we aim to patch on FORK_CONFIG_JSON=${FORK_CONFIG_JSON:=fork_config.json} +# The hashes file generated by runtime-genesis-ledger LEDGER_HASHES_JSON=${LEDGER_HASHES_JSON:=ledger_hashes.json} +# The "base" config file, needed to know the genesis time of the prefork network FORKING_FROM_CONFIG_JSON=${FORKING_FROM_CONFIG_JSON:=genesis_ledgers/mainnet.json} - -# If not given, the genesis timestamp is set to 10 mins into the future +# Genesis timestamp of the postfork network, defaults to 10mins in the future GENESIS_TIMESTAMP=${GENESIS_TIMESTAMP:=$(date -u +"%Y-%m-%dT%H:%M:%SZ" -d "10 mins")} # Pull the original genesis timestamp from the pre-fork config file ORIGINAL_GENESIS_TIMESTAMP=$(jq -r '.genesis.genesis_state_timestamp' "$FORKING_FROM_CONFIG_JSON") OFFSET=$(jq -r '.proof.fork.global_slot_since_genesis' "$FORKING_FROM_CONFIG_JSON") +# =============================================================== if [[ "$OFFSET" == null ]]; then OFFSET=0 From cddb13a82a333739ea9c710afd03621c41bc49e6 Mon Sep 17 00:00:00 2001 From: glyh Date: Mon, 24 Nov 2025 15:01:24 +0800 Subject: [PATCH 13/40] hardfork test > ledger.go: document GenerateForkConfigAndLedgers --- src/app/hardfork_test/src/internal/hardfork/ledger.go | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/app/hardfork_test/src/internal/hardfork/ledger.go b/src/app/hardfork_test/src/internal/hardfork/ledger.go index 412296070a0a..aab61a8873c6 100644 --- a/src/app/hardfork_test/src/internal/hardfork/ledger.go +++ b/src/app/hardfork_test/src/internal/hardfork/ledger.go @@ -64,7 +64,10 @@ func (t *HardforkTest) GenerateAndValidatePreforkLedgers(analysis *BlockAnalysis return nil } -// GenerateForkConfigAndLedgers generates the fork configuration and ledgers +// GenerateForkConfigAndLedgers does the following: +// 1. generate fork ledgers with runtime-genesis-ledger +// 2. patch the genesis time & slot for fork config with create_runtime_config.sh +// 3. perform some base sanity check on the fork config func (t *HardforkTest) GenerateForkConfigAndLedgers(analysis *BlockAnalysisResult, forkConfigPath, forkLedgersDir, forkHashesFile, configFile, preforkGenesisConfigFile string, forkGenesisTs, mainGenesisTs int64) error { // Generate fork ledgers using fork network executable if err := t.GenerateForkLedgers(t.Config.ForkRuntimeGenesisLedger, forkConfigPath, forkLedgersDir, forkHashesFile); err != nil { From 6427263d3be222b22f8eff684ab220ae69e35ed8 Mon Sep 17 00:00:00 2001 From: glyh Date: Tue, 25 Nov 2025 16:50:11 +0800 Subject: [PATCH 14/40] make create-runtime-config.sh syntax stricter --- scripts/hardfork/create_runtime_config.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/hardfork/create_runtime_config.sh b/scripts/hardfork/create_runtime_config.sh index 7bb2d6c55609..85f30ee85553 100755 --- a/scripts/hardfork/create_runtime_config.sh +++ b/scripts/hardfork/create_runtime_config.sh @@ -3,7 +3,7 @@ # NOTE: This script patches the genesis timestamp of a fork config so a new # network could be schedueld to genesis in some instant in the future. -set -eo pipefail +set -eux -o pipefail # ==================== Inputs to this script ==================== # The fork config we aim to patch on From 522673ce782c90480fbc26b25dbf9971afe0985f Mon Sep 17 00:00:00 2001 From: glyh Date: Tue, 25 Nov 2025 19:48:55 +0800 Subject: [PATCH 15/40] hardfork > create_runtime_config.sh: improve jq expression readability --- scripts/hardfork/create_runtime_config.sh | 65 ++++++++++++----------- 1 file changed, 35 insertions(+), 30 deletions(-) diff --git a/scripts/hardfork/create_runtime_config.sh b/scripts/hardfork/create_runtime_config.sh index 85f30ee85553..b4e42c11bb03 100755 --- a/scripts/hardfork/create_runtime_config.sh +++ b/scripts/hardfork/create_runtime_config.sh @@ -35,33 +35,38 @@ SLOT=$((DIFFERENCE_IN_SLOTS+OFFSET)) # jq expression below could be written with less code, # but we aimed for maximum verbosity -jq "{\ - genesis: {\ - genesis_state_timestamp: \"$GENESIS_TIMESTAMP\"\ - },\ - proof: {\ - fork: {\ - state_hash: .proof.fork.state_hash,\ - blockchain_length: .proof.fork.blockchain_length,\ - global_slot_since_genesis: $SLOT,\ - },\ - },\ - ledger: {\ - add_genesis_winner: false,\ - hash: \$hashes[0].ledger.hash,\ - s3_data_hash: \$hashes[0].ledger.s3_data_hash\ - },\ - epoch_data: {\ - staking: {\ - seed: .epoch_data.staking.seed,\ - hash: \$hashes[0].epoch_data.staking.hash,\ - s3_data_hash: \$hashes[0].epoch_data.staking.s3_data_hash\ - },\ - next: {\ - seed: .epoch_data.next.seed,\ - hash: \$hashes[0].epoch_data.next.hash,\ - s3_data_hash: \$hashes[0].epoch_data.next.s3_data_hash\ - }\ - }\ - }" -M \ - --slurpfile hashes "$LEDGER_HASHES_JSON" "$FORK_CONFIG_JSON" +jq -M \ + --arg genesis_timestamp "$GENESIS_TIMESTAMP" \ + --argjson slot "$SLOT" \ + --slurpfile hashes "$LEDGER_HASHES_JSON" \ + ' + { + genesis: { + genesis_state_timestamp: $genesis_timestamp + }, + proof: { + fork: { + state_hash: .proof.fork.state_hash, + blockchain_length: .proof.fork.blockchain_length, + global_slot_since_genesis: $slot + } + }, + ledger: { + add_genesis_winner: false, + hash: $hashes[0].ledger.hash, + s3_data_hash: $hashes[0].ledger.s3_data_hash + }, + epoch_data: { + staking: { + seed: .epoch_data.staking.seed, + hash: $hashes[0].epoch_data.staking.hash, + s3_data_hash: $hashes[0].epoch_data.staking.s3_data_hash + }, + next: { + seed: .epoch_data.next.seed, + hash: $hashes[0].epoch_data.next.hash, + s3_data_hash: $hashes[0].epoch_data.next.s3_data_hash + } + } + } + ' "$FORK_CONFIG_JSON" From 10bd587f697530897d58ac356b84f110bf4a7c1b Mon Sep 17 00:00:00 2001 From: glyh Date: Tue, 25 Nov 2025 20:25:36 +0800 Subject: [PATCH 16/40] hardfork test: implement a script that's designed specifically to patch on config generated by `mina advanced generate-hard-fork-config` --- ...runtime-config-advanced-gen-fork-config.sh | 33 +++++++++++++++++++ 1 file changed, 33 insertions(+) create mode 100755 scripts/hardfork/patch-runtime-config-advanced-gen-fork-config.sh diff --git a/scripts/hardfork/patch-runtime-config-advanced-gen-fork-config.sh b/scripts/hardfork/patch-runtime-config-advanced-gen-fork-config.sh new file mode 100755 index 000000000000..5d7b39a465e0 --- /dev/null +++ b/scripts/hardfork/patch-runtime-config-advanced-gen-fork-config.sh @@ -0,0 +1,33 @@ +#!/usr/bin/env bash + +# NOTE: This script patches the genesis timestamp of a fork config so a new +# network could be schedueld to genesis in some instant in the future. +# It differs from create_runtime_config.sh in that it's aimed at dealing with +# output generated by `mina advanced generate-hard-fork-config` + +set -eux -o pipefail + +# ==================== Inputs to this script ==================== +# The fork config we aim to patch on +FORK_CONFIG_TO_PATCH=${FORK_CONFIG_TO_PATCH:?FORK_CONFIG_TO_PATCH is not provided} +# Prefork network slot time in seconds +PREFORK_SLOT_TIME_SEC=${PREFORK_SLOT_TIME_SEC:?PREFORK_SLOT_TIME_SEC is not provided} +# Delta in slots between the `slot_chain_end` of prefork network and +# `global_slot_since_genesis` of postfork network +HARDFORK_GENESIS_SLOT_DELTA=${HARDFORK_GENESIS_SLOT_DELTA:?HARDFORK_GENESIS_SLOT_DELTA is not provided} +# =============================================================== + +PREFORK_SLOT_CHAIN_END_TIMESTAMP=$(jq -r '.genesis.genesis_state_timestamp' "$FORK_CONFIG_TO_PATCH") +PREFORK_SLOT_CHAIN_END=$(jq -r '.proof.fork.global_slot_since_genesis' "$FORK_CONFIG_TO_PATCH") + +HARDFORK_GENESIS_SEC_DELTA=$((PREFORK_SLOT_TIME_SEC * HARDFORK_GENESIS_SLOT_DELTA)) +POSTFORK_GENESIS_TIMESTAMP=$(date -u -d "$PREFORK_SLOT_CHAIN_END_TIMESTAMP + $HARDFORK_GENESIS_SEC_DELTA seconds" +"%Y-%m-%dT%H:%M:%S.%6NZ") +POSTFORK_GENESIS_SLOT=$((PREFORK_SLOT_CHAIN_END + HARDFORK_GENESIS_SLOT_DELTA)) + +jq -M \ + --arg genesis_timestamp "$POSTFORK_GENESIS_TIMESTAMP" \ + --argjson slot "$POSTFORK_GENESIS_SLOT" \ + ' .genesis.genesis_state_timestamp = $genesis_timestamp + | .proof.fork.global_slot_since_genesis = $slot + ' \ + "$FORK_CONFIG_TO_PATCH" From c593ec4e9427c600ddd919e9a04d63d900e08116 Mon Sep 17 00:00:00 2001 From: glyh Date: Tue, 25 Nov 2025 20:53:26 +0800 Subject: [PATCH 17/40] hardfork test: implement advanced fork procedure --- .../src/internal/hardfork/fork_config.go | 2 +- .../src/internal/hardfork/ledger.go | 11 ++- .../src/internal/hardfork/phases.go | 70 ++++++++++++++++++- 3 files changed, 77 insertions(+), 6 deletions(-) diff --git a/src/app/hardfork_test/src/internal/hardfork/fork_config.go b/src/app/hardfork_test/src/internal/hardfork/fork_config.go index 7aa175039234..1cca6085ea2b 100644 --- a/src/app/hardfork_test/src/internal/hardfork/fork_config.go +++ b/src/app/hardfork_test/src/internal/hardfork/fork_config.go @@ -43,7 +43,7 @@ func (t *HardforkTest) GetForkConfig(port int) ([]byte, error) { } // CreateRuntimeConfig creates the runtime configuration for the fork -func (t *HardforkTest) CreateRuntimeConfig(forkGenesisTimestamp, forkConfigPath, configFile, baseConfigFile, forkHashesFile string) ([]byte, error) { +func (t *HardforkTest) PatchRuntimeConfigLegacy(forkGenesisTimestamp, forkConfigPath, configFile, baseConfigFile, forkHashesFile string) ([]byte, error) { cmd := exec.Command(filepath.Join(t.ScriptDir, "create_runtime_config.sh")) cmd.Env = append(os.Environ(), "GENESIS_TIMESTAMP="+forkGenesisTimestamp, diff --git a/src/app/hardfork_test/src/internal/hardfork/ledger.go b/src/app/hardfork_test/src/internal/hardfork/ledger.go index aab61a8873c6..f88283b3afbe 100644 --- a/src/app/hardfork_test/src/internal/hardfork/ledger.go +++ b/src/app/hardfork_test/src/internal/hardfork/ledger.go @@ -64,11 +64,11 @@ func (t *HardforkTest) GenerateAndValidatePreforkLedgers(analysis *BlockAnalysis return nil } -// GenerateForkConfigAndLedgers does the following: +// PatchForkConfigAndGenerateLedgersLegacy does the following: // 1. generate fork ledgers with runtime-genesis-ledger // 2. patch the genesis time & slot for fork config with create_runtime_config.sh // 3. perform some base sanity check on the fork config -func (t *HardforkTest) GenerateForkConfigAndLedgers(analysis *BlockAnalysisResult, forkConfigPath, forkLedgersDir, forkHashesFile, configFile, preforkGenesisConfigFile string, forkGenesisTs, mainGenesisTs int64) error { +func (t *HardforkTest) PatchForkConfigAndGenerateLedgersLegacy(analysis *BlockAnalysisResult, forkConfigPath, forkLedgersDir, forkHashesFile, configFile, preforkGenesisConfigFile string, forkGenesisTs, mainGenesisTs int64) error { // Generate fork ledgers using fork network executable if err := t.GenerateForkLedgers(t.Config.ForkRuntimeGenesisLedger, forkConfigPath, forkLedgersDir, forkHashesFile); err != nil { return err @@ -76,7 +76,7 @@ func (t *HardforkTest) GenerateForkConfigAndLedgers(analysis *BlockAnalysisResul // Create runtime config forkGenesisTimestamp := config.FormatTimestamp(forkGenesisTs) - runtimeConfigBytes, err := t.CreateRuntimeConfig(forkGenesisTimestamp, forkConfigPath, configFile, preforkGenesisConfigFile, forkHashesFile) + runtimeConfigBytes, err := t.PatchRuntimeConfigLegacy(forkGenesisTimestamp, forkConfigPath, configFile, preforkGenesisConfigFile, forkHashesFile) if err != nil { return err } @@ -100,5 +100,10 @@ func (t *HardforkTest) AdvancedGenerateHardForkConfig(configDir string) error { cmd.Wait() + _, err := os.Stat(fmt.Sprintf("%s/activated", configDir)) + if err != nil { + return fmt.Errorf("failed to check on activated file for advanced generate fork config: %w", err) + } + return nil } diff --git a/src/app/hardfork_test/src/internal/hardfork/phases.go b/src/app/hardfork_test/src/internal/hardfork/phases.go index f5565bc1753a..48451ddb189a 100644 --- a/src/app/hardfork_test/src/internal/hardfork/phases.go +++ b/src/app/hardfork_test/src/internal/hardfork/phases.go @@ -1,8 +1,13 @@ package hardfork import ( + "encoding/json" "fmt" + "math" "os" + "os/exec" + "path/filepath" + "strconv" "time" ) @@ -113,7 +118,6 @@ func (t *HardforkTest) RunForkNetworkPhase(latestPreForkHeight int, forkData For func (t *HardforkTest) LegacyForkPhase(analysis *BlockAnalysisResult, forkConfigBytes []byte, mainGenesisTs int64) (*ForkData, error) { - // Define all localnet file paths if err := os.MkdirAll("fork_data/prefork", 0755); err != nil { return nil, err } @@ -150,7 +154,7 @@ func (t *HardforkTest) LegacyForkPhase(analysis *BlockAnalysisResult, forkConfig { preforkGenesisConfigFile := fmt.Sprintf("%s/daemon.json", t.Config.Root) forkHashesFile := "fork_data/hf_ledger_hashes.json" - if err := t.GenerateForkConfigAndLedgers(analysis, preforkConfig, forkLedgersDir, forkHashesFile, postforkConfig, preforkGenesisConfigFile, forkGenesisTs, mainGenesisTs); err != nil { + if err := t.PatchForkConfigAndGenerateLedgersLegacy(analysis, preforkConfig, forkLedgersDir, forkHashesFile, postforkConfig, preforkGenesisConfigFile, forkGenesisTs, mainGenesisTs); err != nil { return nil, err } } @@ -158,3 +162,65 @@ func (t *HardforkTest) LegacyForkPhase(analysis *BlockAnalysisResult, forkConfig return &ForkData{config: postforkConfig, ledgersDir: forkLedgersDir, genesis: forkGenesisTs}, nil } + +// Uses `mina advanced generate-hardfork-config CLI` +func (t *HardforkTest) AdvancedForkPhase(analysis *BlockAnalysisResult, forkConfigBytes []byte, mainGenesisTs int64) (*ForkData, error) { + + cwd := "" + var err error = nil + if cwd, err = os.Getwd(); err != nil { + return nil, err + } + + forkDataPath := fmt.Sprintf("%s/fork_data", cwd) + if err := os.MkdirAll(forkDataPath, 0755); err != nil { + return nil, err + } + + if err := t.AdvancedGenerateHardForkConfig(forkDataPath); err != nil { + return nil, err + } + + configToPatch := fmt.Sprintf("%s/daemon.json", forkDataPath) + + configJsonString, err := os.ReadFile(configToPatch) + if err != nil { + return nil, err + } + + var configRaw map[string]map[string]string + json.Unmarshal(configJsonString, &configRaw) + + preforkChainEndTs, err := time.Parse(time.RFC3339Nano, configRaw["genesis"]["genesis_state_timestamp"]) + if err != nil { + return nil, err + } + roughForkGenesisTs := time.Now().Add(time.Duration(t.Config.ForkDelay) * time.Minute) + hardforkGenesisSecDelta := roughForkGenesisTs.Sub(preforkChainEndTs).Seconds() + hardforkGenesisSlotDelta := int(math.Ceil(hardforkGenesisSecDelta / float64(t.Config.MainSlot))) + forkSlotDuration := time.Duration(t.Config.MainSlot) * time.Second + forkGenesisTs := preforkChainEndTs.Add(time.Duration(hardforkGenesisSlotDelta) * forkSlotDuration).Unix() + + cmd := exec.Command(filepath.Join(t.ScriptDir, "patch-runtime-config-advanced-gen-fork-config.sh")) + + cmd.Env = append(os.Environ(), + "FORK_CONFIG_TO_PATCH="+configToPatch, + "PREFORK_SLOT_TIME_SEC="+strconv.Itoa(t.Config.MainSlot), + "HARDFORK_GENESIS_SLOT_DELTA="+strconv.Itoa(hardforkGenesisSlotDelta), + ) + cmd.Stderr = os.Stderr + + if err := cmd.Run(); err != nil { + return nil, fmt.Errorf("failed to run patch-runtime-config-advanced-gen-fork-config.sh: %w", err) + } + + patchedConfigString, err := cmd.Output() + + err = os.WriteFile(configToPatch, patchedConfigString, 0644) + if err != nil { + return nil, fmt.Errorf("failed to patch fork config: %w", err) + } + + forkLedgersDir := fmt.Sprintf("%s/genesis", forkDataPath) + return &ForkData{config: configToPatch, ledgersDir: forkLedgersDir, genesis: forkGenesisTs}, nil +} From 7d5234abca67ff3f6f77ca4c13924b4e85ce95f6 Mon Sep 17 00:00:00 2001 From: glyh Date: Tue, 25 Nov 2025 20:58:46 +0800 Subject: [PATCH 18/40] hardfork test: wire advanced fork phase --- .../src/internal/config/fork_method.go | 7 +++++-- .../src/internal/hardfork/phases.go | 2 +- .../src/internal/hardfork/test.go | 19 +++++++++++++++---- 3 files changed, 21 insertions(+), 7 deletions(-) diff --git a/src/app/hardfork_test/src/internal/config/fork_method.go b/src/app/hardfork_test/src/internal/config/fork_method.go index 141aed216e18..31dabbbc586e 100644 --- a/src/app/hardfork_test/src/internal/config/fork_method.go +++ b/src/app/hardfork_test/src/internal/config/fork_method.go @@ -10,14 +10,17 @@ type ForkMethod int const ( Legacy ForkMethod = iota + Advanced ) var forkMethodToString = map[ForkMethod]string{ - Legacy: "legacy", + Legacy: "legacy", + Advanced: "advanced", } var stringToForkMethod = map[string]ForkMethod{ - "legacy": Legacy, + "legacy": Legacy, + "advanced": Advanced, } func (m *ForkMethod) String() string { diff --git a/src/app/hardfork_test/src/internal/hardfork/phases.go b/src/app/hardfork_test/src/internal/hardfork/phases.go index 48451ddb189a..8c636708e9b5 100644 --- a/src/app/hardfork_test/src/internal/hardfork/phases.go +++ b/src/app/hardfork_test/src/internal/hardfork/phases.go @@ -164,7 +164,7 @@ func (t *HardforkTest) LegacyForkPhase(analysis *BlockAnalysisResult, forkConfig } // Uses `mina advanced generate-hardfork-config CLI` -func (t *HardforkTest) AdvancedForkPhase(analysis *BlockAnalysisResult, forkConfigBytes []byte, mainGenesisTs int64) (*ForkData, error) { +func (t *HardforkTest) AdvancedForkPhase(analysis *BlockAnalysisResult, forkConfigBytes []byte) (*ForkData, error) { cwd := "" var err error = nil diff --git a/src/app/hardfork_test/src/internal/hardfork/test.go b/src/app/hardfork_test/src/internal/hardfork/test.go index aa4c594339c5..ad42bcb77390 100644 --- a/src/app/hardfork_test/src/internal/hardfork/test.go +++ b/src/app/hardfork_test/src/internal/hardfork/test.go @@ -123,11 +123,22 @@ func (t *HardforkTest) Run() error { return err } - t.Logger.Info("Phase 2: Forking the legacy way...") + var forkData *ForkData + switch t.Config.ForkMethod { + case config.Legacy: + t.Logger.Info("Phase 2: Forking the legacy way...") + + forkData, err = t.LegacyForkPhase(analysis, forkConfigBytes, mainGenesisTs) + if err != nil { + return err + } + case config.Advanced: + t.Logger.Info("Phase 2: Forking with `mina advanced generate-hardfork-config`...") - forkData, err := t.LegacyForkPhase(analysis, forkConfigBytes, mainGenesisTs) - if err != nil { - return err + forkData, err = t.AdvancedForkPhase(analysis, forkConfigBytes) + if err != nil { + return err + } } t.Logger.Info("Phase 3: Running fork network...") From 720e3bc4028d6ec5eaa8e5f3218ad366c46b8641 Mon Sep 17 00:00:00 2001 From: glyh Date: Tue, 25 Nov 2025 20:59:29 +0800 Subject: [PATCH 19/40] hardfork test: allow advanced fork method to be used in hf test --- scripts/hardfork/build-and-test.sh | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/scripts/hardfork/build-and-test.sh b/scripts/hardfork/build-and-test.sh index 92a72d7cadd2..51f6a74fd9d1 100755 --- a/scripts/hardfork/build-and-test.sh +++ b/scripts/hardfork/build-and-test.sh @@ -44,11 +44,11 @@ while [[ $# -gt 0 ]]; do usage "Error: $1 requires an argument." fi case "$2" in - legacy|advanced-generate-hf-config) + legacy|advanced) FORK_METHOD="$2" ;; *) - usage "Error: $1 must be either 'legacy' or 'advanced-generate-hf-config'." + usage "Error: $1 must be either 'legacy' or 'advanced'." ;; esac shift 2 @@ -70,10 +70,6 @@ if [[ -z "$PREFORK" ]]; then usage "Error: --fork-from must be provided." fi -if [[ "$FORK_METHOD" == "advanced-generate-hf-config" ]]; then - usage "Error: unimplemented fork mode '$FORK_METHOD'" -fi - NIX_OPTS=( --accept-flake-config --experimental-features 'nix-command flakes' ) if [[ -n "${NIX_CACHE_NAR_SECRET:-}" ]]; then From 1d6e6ab823b0f7a45085fd4e4e9f38020d6d219d Mon Sep 17 00:00:00 2001 From: glyh Date: Tue, 25 Nov 2025 21:00:54 +0800 Subject: [PATCH 20/40] hardfork test: add CI job for advanced fork method --- .../src/Jobs/Test/HardForkTestAdvanced.dhall | 67 +++++++++++++++++++ ...orkTest.dhall => HardForkTestLegacy.dhall} | 2 +- 2 files changed, 68 insertions(+), 1 deletion(-) create mode 100644 buildkite/src/Jobs/Test/HardForkTestAdvanced.dhall rename buildkite/src/Jobs/Test/{HardForkTest.dhall => HardForkTestLegacy.dhall} (98%) diff --git a/buildkite/src/Jobs/Test/HardForkTestAdvanced.dhall b/buildkite/src/Jobs/Test/HardForkTestAdvanced.dhall new file mode 100644 index 000000000000..cc546f115a45 --- /dev/null +++ b/buildkite/src/Jobs/Test/HardForkTestAdvanced.dhall @@ -0,0 +1,67 @@ +let ContainerImages = ../../Constants/ContainerImages.dhall + +let Cmd = ../../Lib/Cmds.dhall + +let S = ../../Lib/SelectFiles.dhall + +let Pipeline = ../../Pipeline/Dsl.dhall + +let PipelineTag = ../../Pipeline/Tag.dhall + +let PipelineScope = ../../Pipeline/Scope.dhall + +let JobSpec = ../../Pipeline/JobSpec.dhall + +let Command = ../../Command/Base.dhall + +let Docker = ../../Command/Docker/Type.dhall + +let Size = ../../Command/Size.dhall + +let B = ../../External/Buildkite.dhall + +let B/SoftFail = B.definitions/commandStep/properties/soft_fail/Type + +in Pipeline.build + Pipeline.Config::{ + , spec = JobSpec::{ + , dirtyWhen = + [ S.strictlyStart (S.contains "src") + , S.exactly "buildkite/src/Jobs/Test/HardForkTest" "dhall" + , S.strictlyStart (S.contains "scripts/hardfork") + , S.strictlyStart (S.contains "nix") + , S.exactly "flake" "nix" + , S.exactly "flake" "lock" + , S.exactly "default" "nix" + ] + , path = "Test" + , name = "HardForkTestAdvanced" + , scope = PipelineScope.AllButPullRequest + , tags = + [ PipelineTag.Type.Long + , PipelineTag.Type.Test + , PipelineTag.Type.Stable + , PipelineTag.Type.Hardfork + ] + } + , steps = + [ Command.build + Command.Config::{ + , commands = + [ Cmd.runInDocker + Cmd.Docker::{ + , image = ContainerImages.nixos + , privileged = True + , useBash = False + } + "./scripts/hardfork/build-and-test.sh --fork-from origin/master --fork-method advanced" + ] + , label = "hard fork test - advanced mode" + , key = "hard-fork-test-advanced" + , target = Size.Integration + , soft_fail = Some (B/SoftFail.Boolean False) + , docker = None Docker.Type + , timeout_in_minutes = Some +420 + } + ] + } diff --git a/buildkite/src/Jobs/Test/HardForkTest.dhall b/buildkite/src/Jobs/Test/HardForkTestLegacy.dhall similarity index 98% rename from buildkite/src/Jobs/Test/HardForkTest.dhall rename to buildkite/src/Jobs/Test/HardForkTestLegacy.dhall index 74db9f39bf39..12455d3f0583 100644 --- a/buildkite/src/Jobs/Test/HardForkTest.dhall +++ b/buildkite/src/Jobs/Test/HardForkTestLegacy.dhall @@ -35,7 +35,7 @@ in Pipeline.build , S.exactly "default" "nix" ] , path = "Test" - , name = "HardForkTest" + , name = "HardForkTestLegacy" , scope = PipelineScope.AllButPullRequest , tags = [ PipelineTag.Type.Long From ed48489465fbac7254f3bbfbca4177f178509619 Mon Sep 17 00:00:00 2001 From: glyh Date: Tue, 25 Nov 2025 21:02:02 +0800 Subject: [PATCH 21/40] FIX(hardfork test advanced): use compatible as prefork network as the RPC haven't entered master yet --- buildkite/src/Jobs/Test/HardForkTestAdvanced.dhall | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/buildkite/src/Jobs/Test/HardForkTestAdvanced.dhall b/buildkite/src/Jobs/Test/HardForkTestAdvanced.dhall index cc546f115a45..f9190c6098e2 100644 --- a/buildkite/src/Jobs/Test/HardForkTestAdvanced.dhall +++ b/buildkite/src/Jobs/Test/HardForkTestAdvanced.dhall @@ -54,7 +54,7 @@ in Pipeline.build , privileged = True , useBash = False } - "./scripts/hardfork/build-and-test.sh --fork-from origin/master --fork-method advanced" + "./scripts/hardfork/build-and-test.sh --fork-from origin/compatible --fork-method advanced" ] , label = "hard fork test - advanced mode" , key = "hard-fork-test-advanced" From f892d4d54db4d237607e23bbeb91eaed8b7d14a0 Mon Sep 17 00:00:00 2001 From: glyh Date: Tue, 25 Nov 2025 21:18:05 +0800 Subject: [PATCH 22/40] FIX(hardfork test advanced): set a proper client port for advanced fork config generation --- src/app/hardfork_test/src/internal/hardfork/ledger.go | 4 +++- src/app/hardfork_test/src/internal/hardfork/phases.go | 2 +- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/src/app/hardfork_test/src/internal/hardfork/ledger.go b/src/app/hardfork_test/src/internal/hardfork/ledger.go index f88283b3afbe..da3a9fd1ae56 100644 --- a/src/app/hardfork_test/src/internal/hardfork/ledger.go +++ b/src/app/hardfork_test/src/internal/hardfork/ledger.go @@ -4,6 +4,7 @@ import ( "fmt" "os" "os/exec" + "strconv" "github.com/MinaProtocol/mina/src/app/hardfork_test/src/internal/config" ) @@ -85,10 +86,11 @@ func (t *HardforkTest) PatchForkConfigAndGenerateLedgersLegacy(analysis *BlockAn return t.ValidateForkRuntimeConfig(analysis.LatestNonEmptyBlock, runtimeConfigBytes, forkGenesisTs, mainGenesisTs) } -func (t *HardforkTest) AdvancedGenerateHardForkConfig(configDir string) error { +func (t *HardforkTest) AdvancedGenerateHardForkConfig(configDir string, clientPort int) error { cmd := exec.Command(t.Config.MainMinaExe, "advanced", "generate-hardfork-config", "--hardfork-config-dir", configDir, + "--daemon-port", strconv.Itoa(clientPort), ) cmd.Stdout = os.Stdout diff --git a/src/app/hardfork_test/src/internal/hardfork/phases.go b/src/app/hardfork_test/src/internal/hardfork/phases.go index 8c636708e9b5..c6648efaa5ad 100644 --- a/src/app/hardfork_test/src/internal/hardfork/phases.go +++ b/src/app/hardfork_test/src/internal/hardfork/phases.go @@ -177,7 +177,7 @@ func (t *HardforkTest) AdvancedForkPhase(analysis *BlockAnalysisResult, forkConf return nil, err } - if err := t.AdvancedGenerateHardForkConfig(forkDataPath); err != nil { + if err := t.AdvancedGenerateHardForkConfig(forkDataPath, t.AnyPortOfType(PORT_CLIENT)); err != nil { return nil, err } From 8cbfa7681a9266422973e010969ece903a8bd14e Mon Sep 17 00:00:00 2001 From: glyh Date: Tue, 25 Nov 2025 22:22:00 +0800 Subject: [PATCH 23/40] hf test go: bump down a bunch of parameters to make test run faster & lighter --- scripts/hardfork/build-and-test.sh | 4 ++-- src/app/hardfork_test/src/internal/config/config.go | 8 ++++---- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/scripts/hardfork/build-and-test.sh b/scripts/hardfork/build-and-test.sh index 51f6a74fd9d1..f33db0aacdda 100755 --- a/scripts/hardfork/build-and-test.sh +++ b/scripts/hardfork/build-and-test.sh @@ -158,8 +158,8 @@ nix "${NIX_OPTS[@]}" build "$PWD?submodules=1#hardfork_test" --out-link "hardfor # 5. Execute hardfork_test on them. -SLOT_TX_END=${SLOT_TX_END:-$((40))} -SLOT_CHAIN_END=${SLOT_CHAIN_END:-$((SLOT_TX_END+5))} +SLOT_TX_END=33 +SLOT_CHAIN_END=$((SLOT_TX_END+4)) NETWORK_ROOT=$(mktemp -d --tmpdir hardfork-network.XXXXXXX) diff --git a/src/app/hardfork_test/src/internal/config/config.go b/src/app/hardfork_test/src/internal/config/config.go index 38ba43f602bd..665d2025e5aa 100644 --- a/src/app/hardfork_test/src/internal/config/config.go +++ b/src/app/hardfork_test/src/internal/config/config.go @@ -74,12 +74,12 @@ func DefaultConfig() *Config { BestChainQueryFrom: 25, MainSlot: 30, ForkSlot: 30, - MainDelay: 8, - ForkDelay: 8, + MainDelay: 7, + ForkDelay: 7, NumWhales: 2, NumFish: 1, - NumNodes: 1, - PaymentInterval: 10, + NumNodes: 0, + PaymentInterval: 20, ShutdownTimeoutMinutes: 10, PollingIntervalSeconds: 5, ForkConfigRetryDelaySeconds: 60, From 818ab4c5ce7e6dd788eac7bd3b1fea5fbf15b338 Mon Sep 17 00:00:00 2001 From: glyh Date: Tue, 25 Nov 2025 23:49:27 +0800 Subject: [PATCH 24/40] fix(advanced hardfork test): ensure cli command is called before network is shutdown --- .../src/internal/hardfork/phases.go | 28 +++++------ .../src/internal/hardfork/test.go | 47 +++++++++++++------ 2 files changed, 46 insertions(+), 29 deletions(-) diff --git a/src/app/hardfork_test/src/internal/hardfork/phases.go b/src/app/hardfork_test/src/internal/hardfork/phases.go index c6648efaa5ad..15bcdc699937 100644 --- a/src/app/hardfork_test/src/internal/hardfork/phases.go +++ b/src/app/hardfork_test/src/internal/hardfork/phases.go @@ -11,13 +11,15 @@ import ( "time" ) +type HFHandler func(*HardforkTest, *BlockAnalysisResult) error + // RunMainNetworkPhase runs the main network and validates its operation // and returns the fork config bytes and block analysis result -func (t *HardforkTest) RunMainNetworkPhase(mainGenesisTs int64) ([]byte, *BlockAnalysisResult, error) { +func (t *HardforkTest) RunMainNetworkPhase(mainGenesisTs int64, beforeShutdown HFHandler) (*BlockAnalysisResult, error) { // Start the main network mainNetCmd, err := t.RunMainNetwork(mainGenesisTs) if err != nil { - return nil, nil, err + return nil, err } defer t.gracefulShutdown(mainNetCmd, "Main network") @@ -28,44 +30,42 @@ func (t *HardforkTest) RunMainNetworkPhase(mainGenesisTs int64) ([]byte, *BlockA // Check block height at slot BestChainQueryFrom blockHeight, err := t.Client.GetHeight(t.AnyPortOfType(PORT_REST)) if err != nil { - return nil, nil, err + return nil, err } t.Logger.Info("Block height is %d at slot %d.", blockHeight, t.Config.BestChainQueryFrom) // Validate slot occupancy if err := t.ValidateSlotOccupancy(0, blockHeight); err != nil { - return nil, nil, err + return nil, err } // Analyze blocks and get genesis epoch data analysis, err := t.AnalyzeBlocks() if err != nil { - return nil, nil, err + return nil, err } // Validate max slot if err := t.ValidateLatestOccupiedSlot(analysis.LatestOccupiedSlot); err != nil { - return nil, nil, err + return nil, err } // Validate latest block slot if err := t.ValidateLatestNonEmptyBlockSlot(analysis.LatestNonEmptyBlock); err != nil { - return nil, nil, err + return nil, err } // Validate no new blocks are created after chain end if err := t.ValidateNoNewBlocks(t.AnyPortOfType(PORT_REST)); err != nil { - return nil, nil, err + return nil, err } - // Extract fork config before nodes shutdown - forkConfigBytes, err := t.GetForkConfig(t.AnyPortOfType(PORT_REST)) - if err != nil { - return nil, nil, err + if err := beforeShutdown(t, analysis); err != nil { + return nil, err } - return forkConfigBytes, analysis, nil + return analysis, nil } type ForkData struct { @@ -164,7 +164,7 @@ func (t *HardforkTest) LegacyForkPhase(analysis *BlockAnalysisResult, forkConfig } // Uses `mina advanced generate-hardfork-config CLI` -func (t *HardforkTest) AdvancedForkPhase(analysis *BlockAnalysisResult, forkConfigBytes []byte) (*ForkData, error) { +func (t *HardforkTest) AdvancedForkPhase(analysis *BlockAnalysisResult) (*ForkData, error) { cwd := "" var err error = nil diff --git a/src/app/hardfork_test/src/internal/hardfork/test.go b/src/app/hardfork_test/src/internal/hardfork/test.go index ad42bcb77390..905991c14815 100644 --- a/src/app/hardfork_test/src/internal/hardfork/test.go +++ b/src/app/hardfork_test/src/internal/hardfork/test.go @@ -118,31 +118,48 @@ func (t *HardforkTest) Run() error { // Phase 1: Run and validate main network t.Logger.Info("Phase 1: Running main network...") - forkConfigBytes, analysis, err := t.RunMainNetworkPhase(mainGenesisTs) - if err != nil { - return err - } + forkDataChan := make(chan ForkData, 1) - var forkData *ForkData + var beforeShutdown HFHandler switch t.Config.ForkMethod { case config.Legacy: - t.Logger.Info("Phase 2: Forking the legacy way...") - forkData, err = t.LegacyForkPhase(analysis, forkConfigBytes, mainGenesisTs) - if err != nil { - return err + beforeShutdown = func(t *HardforkTest, analysis *BlockAnalysisResult) error { + t.Logger.Info("Phase 2: Forking the legacy way...") + + forkConfigBytes, err := t.GetForkConfig(t.AnyPortOfType(PORT_REST)) + if err != nil { + return err + } + var forkData *ForkData + forkData, err = t.LegacyForkPhase(analysis, forkConfigBytes, mainGenesisTs) + if err != nil { + return err + } + forkDataChan <- *forkData + return nil } - case config.Advanced: - t.Logger.Info("Phase 2: Forking with `mina advanced generate-hardfork-config`...") - forkData, err = t.AdvancedForkPhase(analysis, forkConfigBytes) - if err != nil { - return err + case config.Advanced: + beforeShutdown = func(t *HardforkTest, analysis *BlockAnalysisResult) error { + t.Logger.Info("Phase 2: Forking with `mina advanced generate-hardfork-config`...") + + forkData, err := t.AdvancedForkPhase(analysis) + if err != nil { + return err + } + forkDataChan <- *forkData + return nil } } + analysis, err := t.RunMainNetworkPhase(mainGenesisTs, beforeShutdown) + if err != nil { + return err + } + t.Logger.Info("Phase 3: Running fork network...") - if err := t.RunForkNetworkPhase(analysis.LatestNonEmptyBlock.BlockHeight, *forkData, mainGenesisTs); err != nil { + if err := t.RunForkNetworkPhase(analysis.LatestNonEmptyBlock.BlockHeight, <-forkDataChan, mainGenesisTs); err != nil { return err } From 971f0cde2f44eef5e7317d85a32ee95f07861ab8 Mon Sep 17 00:00:00 2001 From: glyh Date: Wed, 26 Nov 2025 11:46:39 +0800 Subject: [PATCH 25/40] fix(advanced hardfork test): don't prepare fork config dir as advanced hf test require that dir to be absent --- src/app/hardfork_test/src/internal/hardfork/phases.go | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/app/hardfork_test/src/internal/hardfork/phases.go b/src/app/hardfork_test/src/internal/hardfork/phases.go index 15bcdc699937..533e9b95ce31 100644 --- a/src/app/hardfork_test/src/internal/hardfork/phases.go +++ b/src/app/hardfork_test/src/internal/hardfork/phases.go @@ -173,9 +173,6 @@ func (t *HardforkTest) AdvancedForkPhase(analysis *BlockAnalysisResult) (*ForkDa } forkDataPath := fmt.Sprintf("%s/fork_data", cwd) - if err := os.MkdirAll(forkDataPath, 0755); err != nil { - return nil, err - } if err := t.AdvancedGenerateHardForkConfig(forkDataPath, t.AnyPortOfType(PORT_CLIENT)); err != nil { return nil, err From ccbd0f3554b2b603759275846df3087f969f7fff Mon Sep 17 00:00:00 2001 From: glyh Date: Wed, 26 Nov 2025 11:48:56 +0800 Subject: [PATCH 26/40] hf test go: add a note for further refactoring on legacy fork --- src/app/hardfork_test/src/internal/hardfork/test.go | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/app/hardfork_test/src/internal/hardfork/test.go b/src/app/hardfork_test/src/internal/hardfork/test.go index 905991c14815..fdaa6ee29ea1 100644 --- a/src/app/hardfork_test/src/internal/hardfork/test.go +++ b/src/app/hardfork_test/src/internal/hardfork/test.go @@ -125,6 +125,9 @@ func (t *HardforkTest) Run() error { case config.Legacy: beforeShutdown = func(t *HardforkTest, analysis *BlockAnalysisResult) error { + // TODO: refactor so that: + // 1. stuff here go into LegacyForkPhase + // 2. fork validation is agnostic to fork method and it's checked regardless of fork method t.Logger.Info("Phase 2: Forking the legacy way...") forkConfigBytes, err := t.GetForkConfig(t.AnyPortOfType(PORT_REST)) From 428a806321297a2500b92e5f74bdd2a39882754d Mon Sep 17 00:00:00 2001 From: glyh Date: Wed, 26 Nov 2025 21:29:00 +0800 Subject: [PATCH 27/40] report if inherit_with doesn't have existing config --- scripts/mina-local-network/mina-local-network.sh | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/scripts/mina-local-network/mina-local-network.sh b/scripts/mina-local-network/mina-local-network.sh index ccbb08878b26..da9d6d54950d 100755 --- a/scripts/mina-local-network/mina-local-network.sh +++ b/scripts/mina-local-network/mina-local-network.sh @@ -748,6 +748,10 @@ load_config() { inherit_with:*) local replaced_config_file IFS=',' read -r replaced_config_file OVERRIDE_GENSIS_LEDGER <<< "${config_mode#inherit_with:}" + if [ ! -f "${replaced_config_file}" ]; then + echo "Error: Config file '${replaced_config_file}' does not exist, can't inherit_with." >&2 + exit 1 + fi cp -f "${replaced_config_file}" "${config_file}" ;; esac From 028948be53ab864cf16bbf9e36fbe2dd6dd7007e Mon Sep 17 00:00:00 2001 From: glyh Date: Wed, 26 Nov 2025 21:31:05 +0800 Subject: [PATCH 28/40] mina-local-network.sh: print the conifg that's inherited --- scripts/mina-local-network/mina-local-network.sh | 3 +++ 1 file changed, 3 insertions(+) diff --git a/scripts/mina-local-network/mina-local-network.sh b/scripts/mina-local-network/mina-local-network.sh index da9d6d54950d..82c20616f58d 100755 --- a/scripts/mina-local-network/mina-local-network.sh +++ b/scripts/mina-local-network/mina-local-network.sh @@ -751,6 +751,9 @@ load_config() { if [ ! -f "${replaced_config_file}" ]; then echo "Error: Config file '${replaced_config_file}' does not exist, can't inherit_with." >&2 exit 1 + else + echo "Inheriting config at ${replaced_config_file}:" + cat "${replaced_config_file}" fi cp -f "${replaced_config_file}" "${config_file}" ;; From eb7e25ba911ed248cdf0e67d70980b33998ba798 Mon Sep 17 00:00:00 2001 From: glyh Date: Sun, 30 Nov 2025 17:28:41 +0800 Subject: [PATCH 29/40] FIX: hardfork: run patch hf config script once not twice! --- src/app/hardfork_test/src/internal/hardfork/phases.go | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/src/app/hardfork_test/src/internal/hardfork/phases.go b/src/app/hardfork_test/src/internal/hardfork/phases.go index 533e9b95ce31..9acf9f286302 100644 --- a/src/app/hardfork_test/src/internal/hardfork/phases.go +++ b/src/app/hardfork_test/src/internal/hardfork/phases.go @@ -207,11 +207,10 @@ func (t *HardforkTest) AdvancedForkPhase(analysis *BlockAnalysisResult) (*ForkDa ) cmd.Stderr = os.Stderr - if err := cmd.Run(); err != nil { - return nil, fmt.Errorf("failed to run patch-runtime-config-advanced-gen-fork-config.sh: %w", err) - } - patchedConfigString, err := cmd.Output() + if err != nil { + return nil, fmt.Errorf("failed to run script: %w", err) + } err = os.WriteFile(configToPatch, patchedConfigString, 0644) if err != nil { From ae858b6e7d533917ed9b2bc197e102c560a4e4ef Mon Sep 17 00:00:00 2001 From: glyh Date: Sun, 30 Nov 2025 17:38:34 +0800 Subject: [PATCH 30/40] hardfork test advanced: set `--generate-fork-validation` to false to speed up fork config generation --- src/app/hardfork_test/src/internal/hardfork/ledger.go | 1 + 1 file changed, 1 insertion(+) diff --git a/src/app/hardfork_test/src/internal/hardfork/ledger.go b/src/app/hardfork_test/src/internal/hardfork/ledger.go index da3a9fd1ae56..73d34f551b50 100644 --- a/src/app/hardfork_test/src/internal/hardfork/ledger.go +++ b/src/app/hardfork_test/src/internal/hardfork/ledger.go @@ -91,6 +91,7 @@ func (t *HardforkTest) AdvancedGenerateHardForkConfig(configDir string, clientPo "advanced", "generate-hardfork-config", "--hardfork-config-dir", configDir, "--daemon-port", strconv.Itoa(clientPort), + "--generate-fork-validation", "false", ) cmd.Stdout = os.Stdout From a43ce0c6fd42b7ddbf68eabaa231f177f3ecc271 Mon Sep 17 00:00:00 2001 From: glyh Date: Tue, 2 Dec 2025 12:39:25 +0800 Subject: [PATCH 31/40] hardfork test advanced: remove patching genesis time stamp after generated fork config --- ...runtime-config-advanced-gen-fork-config.sh | 33 ------------------ .../src/internal/hardfork/phases.go | 34 ++----------------- 2 files changed, 3 insertions(+), 64 deletions(-) delete mode 100755 scripts/hardfork/patch-runtime-config-advanced-gen-fork-config.sh diff --git a/scripts/hardfork/patch-runtime-config-advanced-gen-fork-config.sh b/scripts/hardfork/patch-runtime-config-advanced-gen-fork-config.sh deleted file mode 100755 index 5d7b39a465e0..000000000000 --- a/scripts/hardfork/patch-runtime-config-advanced-gen-fork-config.sh +++ /dev/null @@ -1,33 +0,0 @@ -#!/usr/bin/env bash - -# NOTE: This script patches the genesis timestamp of a fork config so a new -# network could be schedueld to genesis in some instant in the future. -# It differs from create_runtime_config.sh in that it's aimed at dealing with -# output generated by `mina advanced generate-hard-fork-config` - -set -eux -o pipefail - -# ==================== Inputs to this script ==================== -# The fork config we aim to patch on -FORK_CONFIG_TO_PATCH=${FORK_CONFIG_TO_PATCH:?FORK_CONFIG_TO_PATCH is not provided} -# Prefork network slot time in seconds -PREFORK_SLOT_TIME_SEC=${PREFORK_SLOT_TIME_SEC:?PREFORK_SLOT_TIME_SEC is not provided} -# Delta in slots between the `slot_chain_end` of prefork network and -# `global_slot_since_genesis` of postfork network -HARDFORK_GENESIS_SLOT_DELTA=${HARDFORK_GENESIS_SLOT_DELTA:?HARDFORK_GENESIS_SLOT_DELTA is not provided} -# =============================================================== - -PREFORK_SLOT_CHAIN_END_TIMESTAMP=$(jq -r '.genesis.genesis_state_timestamp' "$FORK_CONFIG_TO_PATCH") -PREFORK_SLOT_CHAIN_END=$(jq -r '.proof.fork.global_slot_since_genesis' "$FORK_CONFIG_TO_PATCH") - -HARDFORK_GENESIS_SEC_DELTA=$((PREFORK_SLOT_TIME_SEC * HARDFORK_GENESIS_SLOT_DELTA)) -POSTFORK_GENESIS_TIMESTAMP=$(date -u -d "$PREFORK_SLOT_CHAIN_END_TIMESTAMP + $HARDFORK_GENESIS_SEC_DELTA seconds" +"%Y-%m-%dT%H:%M:%S.%6NZ") -POSTFORK_GENESIS_SLOT=$((PREFORK_SLOT_CHAIN_END + HARDFORK_GENESIS_SLOT_DELTA)) - -jq -M \ - --arg genesis_timestamp "$POSTFORK_GENESIS_TIMESTAMP" \ - --argjson slot "$POSTFORK_GENESIS_SLOT" \ - ' .genesis.genesis_state_timestamp = $genesis_timestamp - | .proof.fork.global_slot_since_genesis = $slot - ' \ - "$FORK_CONFIG_TO_PATCH" diff --git a/src/app/hardfork_test/src/internal/hardfork/phases.go b/src/app/hardfork_test/src/internal/hardfork/phases.go index 9acf9f286302..945833c05a46 100644 --- a/src/app/hardfork_test/src/internal/hardfork/phases.go +++ b/src/app/hardfork_test/src/internal/hardfork/phases.go @@ -3,11 +3,7 @@ package hardfork import ( "encoding/json" "fmt" - "math" "os" - "os/exec" - "path/filepath" - "strconv" "time" ) @@ -185,38 +181,14 @@ func (t *HardforkTest) AdvancedForkPhase(analysis *BlockAnalysisResult) (*ForkDa return nil, err } + // TODO: validate fork config here instead of using a generic map to read it. var configRaw map[string]map[string]string json.Unmarshal(configJsonString, &configRaw) - preforkChainEndTs, err := time.Parse(time.RFC3339Nano, configRaw["genesis"]["genesis_state_timestamp"]) + forkGenesisTs, err := time.Parse(time.RFC3339Nano, configRaw["genesis"]["genesis_state_timestamp"]) if err != nil { return nil, err } - roughForkGenesisTs := time.Now().Add(time.Duration(t.Config.ForkDelay) * time.Minute) - hardforkGenesisSecDelta := roughForkGenesisTs.Sub(preforkChainEndTs).Seconds() - hardforkGenesisSlotDelta := int(math.Ceil(hardforkGenesisSecDelta / float64(t.Config.MainSlot))) - forkSlotDuration := time.Duration(t.Config.MainSlot) * time.Second - forkGenesisTs := preforkChainEndTs.Add(time.Duration(hardforkGenesisSlotDelta) * forkSlotDuration).Unix() - - cmd := exec.Command(filepath.Join(t.ScriptDir, "patch-runtime-config-advanced-gen-fork-config.sh")) - - cmd.Env = append(os.Environ(), - "FORK_CONFIG_TO_PATCH="+configToPatch, - "PREFORK_SLOT_TIME_SEC="+strconv.Itoa(t.Config.MainSlot), - "HARDFORK_GENESIS_SLOT_DELTA="+strconv.Itoa(hardforkGenesisSlotDelta), - ) - cmd.Stderr = os.Stderr - - patchedConfigString, err := cmd.Output() - if err != nil { - return nil, fmt.Errorf("failed to run script: %w", err) - } - - err = os.WriteFile(configToPatch, patchedConfigString, 0644) - if err != nil { - return nil, fmt.Errorf("failed to patch fork config: %w", err) - } - forkLedgersDir := fmt.Sprintf("%s/genesis", forkDataPath) - return &ForkData{config: configToPatch, ledgersDir: forkLedgersDir, genesis: forkGenesisTs}, nil + return &ForkData{config: configToPatch, ledgersDir: forkLedgersDir, genesis: forkGenesisTs.Unix()}, nil } From b1d050bf71194f9f6bf3df83b65e1bd36d45f315 Mon Sep 17 00:00:00 2001 From: glyh Date: Tue, 2 Dec 2025 12:43:18 +0800 Subject: [PATCH 32/40] mina local network: support overriding daemon.hard_fork_genesis_slot_delta in daemon config --- scripts/mina-local-network/mina-local-network.sh | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/scripts/mina-local-network/mina-local-network.sh b/scripts/mina-local-network/mina-local-network.sh index 82c20616f58d..82dad67216de 100755 --- a/scripts/mina-local-network/mina-local-network.sh +++ b/scripts/mina-local-network/mina-local-network.sh @@ -55,6 +55,7 @@ DEMO_MODE=false SLOT_TX_END= SLOT_CHAIN_END= +HARDFORK_GENESIS_SLOT_DELTA= # ================================================ # Globals (assigned during execution of script) @@ -140,6 +141,7 @@ help() { | Default: None -sce |--slot-chain-end | When set, stop producing blocks from this chain on. | Default: None +-hfd |--hardfork-genesis-slot-delta | When set override the value `hard_fork_genesis_slot_delta` in daemon config. -r |--root | When set, override the root working folder (i.e. the value of ROOT) for this script. WARN: this script will clean up anything inside that folder when initializing any run! | Default: ${ROOT} -h |--help | Displays this help message @@ -505,6 +507,10 @@ while [[ "$#" -gt 0 ]]; do SLOT_CHAIN_END="${2}" shift ;; + -hfd |--hardfork-genesis-slot-delta) + HARDFORK_GENESIS_SLOT_DELTA="${2}" + shift + ;; -r | --root) ROOT="${2}" shift @@ -803,6 +809,11 @@ if [ ! -z "${SLOT_CHAIN_END}" ]; then jq-inplace ".daemon.slot_chain_end=${SLOT_CHAIN_END}" "${CONFIG}" fi +if [ ! -z "${HARDFORK_GENESIS_SLOT_DELTA}" ]; then + echo 'Modifying configuration to override hardfork genesis slot delta...' + jq-inplace ".daemon.hard_fork_genesis_slot_delta=${HARDFORK_GENESIS_SLOT_DELTA}" "${CONFIG}" +fi + # ================================================ # Launch the Nodes From 04231c3739240189223be44e61efd0f43c1fae38 Mon Sep 17 00:00:00 2001 From: glyh Date: Tue, 2 Dec 2025 12:58:57 +0800 Subject: [PATCH 33/40] hardfork test: set hardfork-genesis-slot-delta in main network if using advanced mode --- .../src/internal/hardfork/network.go | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/src/app/hardfork_test/src/internal/hardfork/network.go b/src/app/hardfork_test/src/internal/hardfork/network.go index 3064345385ce..1a0f7e1e151e 100644 --- a/src/app/hardfork_test/src/internal/hardfork/network.go +++ b/src/app/hardfork_test/src/internal/hardfork/network.go @@ -2,6 +2,7 @@ package hardfork import ( "fmt" + "math" "math/rand" "os" "os/exec" @@ -94,13 +95,24 @@ func (t *HardforkTest) RunMainNetwork(mainGenesisTs int64) (*exec.Cmd, error) { mainGenesisTimestamp := config.FormatTimestamp(mainGenesisTs) - return t.startLocalNetwork(t.Config.MainMinaExe, "main", []string{ + args := []string{ "--update-genesis-timestamp", fmt.Sprintf("fixed:%s", mainGenesisTimestamp), "--config", "reset", "--override-slot-time", strconv.Itoa(t.Config.MainSlot * 1000), "--slot-transaction-end", strconv.Itoa(t.Config.SlotTxEnd), "--slot-chain-end", strconv.Itoa(t.Config.SlotChainEnd), - }) + } + + switch t.Config.ForkMethod { + case config.Advanced: + forkDelay := time.Duration(t.Config.ForkDelay) * time.Minute + hardforkGenesisDelta := int(math.Ceil(forkDelay.Seconds() / float64(t.Config.MainSlot))) + args = append(args, "--hardfork-genesis-slot-delta", strconv.Itoa(hardforkGenesisDelta)) + case config.Legacy: + // Do nothing as we'll patch the slot at the end of main network + } + + return t.startLocalNetwork(t.Config.MainMinaExe, "main", args) } // RunForkNetwork starts the fork network with hardfork configuration From 086b584e58faf13a15ce94264e563483899ce59b Mon Sep 17 00:00:00 2001 From: glyh Date: Tue, 2 Dec 2025 13:00:22 +0800 Subject: [PATCH 34/40] FIX(hardfork test): in fork network, don't delay the genesis. It's already delayed in legacy mode when patching genesis config. And in advanced mode we've have a calculated genesis that shouldn't be overridden --- src/app/hardfork_test/src/internal/hardfork/network.go | 1 - 1 file changed, 1 deletion(-) diff --git a/src/app/hardfork_test/src/internal/hardfork/network.go b/src/app/hardfork_test/src/internal/hardfork/network.go index 1a0f7e1e151e..2ec3a9ad3d81 100644 --- a/src/app/hardfork_test/src/internal/hardfork/network.go +++ b/src/app/hardfork_test/src/internal/hardfork/network.go @@ -118,7 +118,6 @@ func (t *HardforkTest) RunMainNetwork(mainGenesisTs int64) (*exec.Cmd, error) { // RunForkNetwork starts the fork network with hardfork configuration func (t *HardforkTest) RunForkNetwork(configFile, forkLedgersDir string) (*exec.Cmd, error) { return t.startLocalNetwork(t.Config.ForkMinaExe, "fork", []string{ - "--update-genesis-timestamp", fmt.Sprintf("delay_sec:%d", t.Config.ForkDelay*60), "--config", fmt.Sprintf("inherit_with:%s,%s", configFile, forkLedgersDir), "--override-slot-time", strconv.Itoa(t.Config.ForkSlot * 1000)}, ) From 002944db171bbcb8af5d99c834fccaaf00e97bed Mon Sep 17 00:00:00 2001 From: glyh Date: Tue, 2 Dec 2025 13:59:59 +0800 Subject: [PATCH 35/40] bump up fork delay now that we're not waiting double fork delay before genesis of fork network --- src/app/hardfork_test/src/internal/config/config.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/app/hardfork_test/src/internal/config/config.go b/src/app/hardfork_test/src/internal/config/config.go index 665d2025e5aa..6cf9942100a0 100644 --- a/src/app/hardfork_test/src/internal/config/config.go +++ b/src/app/hardfork_test/src/internal/config/config.go @@ -75,7 +75,7 @@ func DefaultConfig() *Config { MainSlot: 30, ForkSlot: 30, MainDelay: 7, - ForkDelay: 7, + ForkDelay: 9, NumWhales: 2, NumFish: 1, NumNodes: 0, From 7d2ffbd6072d320247b316c7ef3e8bcf161b6970 Mon Sep 17 00:00:00 2001 From: glyh Date: Mon, 1 Dec 2025 20:56:54 +0800 Subject: [PATCH 36/40] legacy hf test: refactor hashes, pre&post-patch config validation --- .../internal/hardfork/config_validation.go | 252 ++++++++++-------- .../src/internal/hardfork/json_validation.go | 167 ------------ .../src/internal/hardfork/ledger.go | 19 +- .../src/internal/hardfork/phases.go | 22 +- 4 files changed, 151 insertions(+), 309 deletions(-) delete mode 100644 src/app/hardfork_test/src/internal/hardfork/json_validation.go diff --git a/src/app/hardfork_test/src/internal/hardfork/config_validation.go b/src/app/hardfork_test/src/internal/hardfork/config_validation.go index c6c5c2103843..f0eeb08cf75a 100644 --- a/src/app/hardfork_test/src/internal/hardfork/config_validation.go +++ b/src/app/hardfork_test/src/internal/hardfork/config_validation.go @@ -1,20 +1,38 @@ package hardfork import ( + "bytes" + "encoding/json" "fmt" "os" + "time" "github.com/MinaProtocol/mina/src/app/hardfork_test/src/internal/client" ) -// ValidatePreforkLedgerHashes validates the generated prefork ledger hashes -func (t *HardforkTest) ValidatePreforkLedgerHashes( +type HashAndS3 struct { + S3DataHash string `json:"s3_data_hash"` + Hash string `json:"hash"` +} + +type RuntimeGenesisLedgerHashes struct { + EpochData struct { + Staking HashAndS3 `json:"staking"` + Next HashAndS3 `json:"next"` + } `json:"epoch_data"` + Ledger HashAndS3 `json:"ledger"` +} + +func (t *HardforkTest) ValidateRuntimeGenesisLedgerHashes( latestNonEmptyBlock client.BlockData, genesisEpochStaking string, genesisEpochNext string, latestSnarkedHashPerEpoch map[int]string, - preforkHashesFile string, + ledgerHashesFile string, ) error { + + t.Logger.Info("Validating ledger hashes file generated by runtime-genesis-ledger") + // Calculate slot_tx_end_epoch // 48 as specififed by mina-local-network.sh slotTxEndEpoch := latestNonEmptyBlock.Slot / 48 @@ -31,163 +49,161 @@ func (t *HardforkTest) ValidatePreforkLedgerHashes( } // Read prefork hashes from file - preforkHashesData, err := os.ReadFile(preforkHashesFile) + ledgerHashesBytes, err := os.ReadFile(ledgerHashesFile) if err != nil { - return fmt.Errorf("failed to read prefork hashes file: %w", err) + return fmt.Errorf("failed to read ledger hashes file: %w", err) } - preforkHashesJson := string(preforkHashesData) + var hashes RuntimeGenesisLedgerHashes + dec := json.NewDecoder(bytes.NewReader(ledgerHashesBytes)) + dec.DisallowUnknownFields() - // Validate field values - if err := validateStringField(preforkHashesJson, "epoch_data.staking.hash", expectedStakingHash); err != nil { - return err + if err := dec.Decode(&hashes); err != nil { + return fmt.Errorf("failed to unmarshal ledger hashes file: %w", err) } - if err := validateStringField(preforkHashesJson, "epoch_data.next.hash", expectedNextHash); err != nil { - return err - } - if err := validateStringField(preforkHashesJson, "ledger.hash", latestNonEmptyBlock.StagedHash); err != nil { - return err - } - - ledger_fields := []string{"hash", "s3_data_hash"} - // Validate object structure - ensure only expected fields are present - if err := t.validateObjectFields(preforkHashesJson, "epoch_data.staking", ledger_fields); err != nil { - return err - } - if err := t.validateObjectFields(preforkHashesJson, "epoch_data.next", ledger_fields); err != nil { - return err - } - if err := t.validateObjectFields(preforkHashesJson, "ledger", ledger_fields); err != nil { - return err + if hashes.EpochData.Staking.Hash != expectedStakingHash { + return fmt.Errorf("Expected epoch_data.staking.hash `%s`, got `%s`", expectedStakingHash, hashes.EpochData.Staking.Hash) } - if err := t.validateObjectFields(preforkHashesJson, "epoch_data", []string{"staking", "next"}); err != nil { - return err + + if hashes.EpochData.Next.Hash != expectedNextHash { + return fmt.Errorf("Expected epoch_data.next.hash `%s`, got `%s`", expectedNextHash, hashes.EpochData.Next.Hash) } - // Validate root object contains only expected top-level fields - if err := t.validateRootObjectFields(preforkHashesJson, []string{"epoch_data", "ledger"}); err != nil { - return err + if hashes.Ledger.Hash != latestNonEmptyBlock.StagedHash { + return fmt.Errorf("Expected ledger.hash `%s`, got `%s`", latestNonEmptyBlock.StagedHash, hashes.Ledger.Hash) } - t.Logger.Info("Prefork ledger hashes validated successfully") + t.Logger.Info("Ledger hashes file validated successfully") return nil } -// ValidateForkConfigData validates the extracted fork config against expected values -func (t *HardforkTest) ValidateForkConfigData(latestNonEmptyBlock client.BlockData, forkConfigBytes []byte) error { - forkConfigJson := string(forkConfigBytes) +type EpochDataPrepatch struct { + Seed string `json:"seed"` + Accounts any `json:"accounts"` +} - // Validate field values - if err := validateIntField(forkConfigJson, "proof.fork.blockchain_length", latestNonEmptyBlock.BlockHeight); err != nil { - return err - } - if err := validateIntField(forkConfigJson, "proof.fork.global_slot_since_genesis", latestNonEmptyBlock.Slot); err != nil { - return err - } - if err := validateStringField(forkConfigJson, "proof.fork.state_hash", latestNonEmptyBlock.StateHash); err != nil { - return err - } - if err := validateStringField(forkConfigJson, "epoch_data.next.seed", latestNonEmptyBlock.NextEpochSeed); err != nil { - return err - } - if err := validateStringField(forkConfigJson, "epoch_data.staking.seed", latestNonEmptyBlock.CurEpochSeed); err != nil { - return err - } - if err := validateStringField(forkConfigJson, "ledger.hash", latestNonEmptyBlock.StagedHash); err != nil { - return err - } - if err := validateBoolField(forkConfigJson, "ledger.add_genesis_winner", false); err != nil { - return err +type LegacyPrepatchForkConfigView struct { + Proof struct { + Fork struct { + BlockChainLength int `json:"blockchain_length"` + GlobalSlotSinceGenesis int `json:"global_slot_since_genesis"` + StateHash string `json:"state_hash"` + } `json:"fork"` + } `json:"proof"` + EpochData struct { + Staking EpochDataPrepatch `json:"staking"` + Next EpochDataPrepatch `json:"next"` + } `json:"epoch_data"` + Ledger struct { + Hash string `json:"hash"` + Accounts any `json:"accounts"` + AddGenesisWinner bool `json:"add_genesis_winner"` + } `json:"ledger"` +} + +func (t *HardforkTest) ValidateLegacyPrepatchForkConfig(latestNonEmptyBlock client.BlockData, forkConfigBytes []byte) error { + + t.Logger.Info("Validating legacy prepatch fork config") + + var config LegacyPrepatchForkConfigView + dec := json.NewDecoder(bytes.NewReader(forkConfigBytes)) + dec.DisallowUnknownFields() + + if err := dec.Decode(&config); err != nil { + return fmt.Errorf("failed to unmarshal legacy prepatch fork config: %w", err) } - // Validate object structure - ensure only expected fields are present - if err := t.validateObjectFields(forkConfigJson, "proof.fork", []string{"blockchain_length", "global_slot_since_genesis", "state_hash"}); err != nil { - return err + if config.Proof.Fork.BlockChainLength != latestNonEmptyBlock.BlockHeight { + return fmt.Errorf("Expected proof.fork.blockchain_length to be %d, got %d", latestNonEmptyBlock.BlockHeight, config.Proof.Fork.BlockChainLength) } - if err := t.validateObjectFields(forkConfigJson, "epoch_data.staking", []string{"seed", "accounts"}); err != nil { - return err + + if config.Proof.Fork.GlobalSlotSinceGenesis != latestNonEmptyBlock.Slot { + return fmt.Errorf("Expected proof.fork.global_slot_since_genesis to be %d, got %d", latestNonEmptyBlock.Slot, config.Proof.Fork.GlobalSlotSinceGenesis) } - if err := t.validateObjectFields(forkConfigJson, "epoch_data.next", []string{"seed", "accounts"}); err != nil { - return err + + if config.Proof.Fork.StateHash != latestNonEmptyBlock.StateHash { + return fmt.Errorf("Expected proof.fork.state_hash to be `%s`, got `%s`", latestNonEmptyBlock.StateHash, config.Proof.Fork.StateHash) } - if err := t.validateObjectFields(forkConfigJson, "ledger", []string{"hash", "accounts", "add_genesis_winner"}); err != nil { - return err + + if config.EpochData.Staking.Seed != latestNonEmptyBlock.CurEpochSeed { + return fmt.Errorf("Expected proof.epoch_data.staking.seed to be `%s`, got `%s`", latestNonEmptyBlock.CurEpochSeed, config.EpochData.Staking.Seed) } - if err := t.validateObjectFields(forkConfigJson, "epoch_data", []string{"staking", "next"}); err != nil { - return err + + if config.EpochData.Next.Seed != latestNonEmptyBlock.NextEpochSeed { + return fmt.Errorf("Expected proof.epoch_data.next.seed to be `%s`, got `%s`", latestNonEmptyBlock.NextEpochSeed, config.EpochData.Next.Seed) } - if err := t.validateObjectFields(forkConfigJson, "proof", []string{"fork"}); err != nil { - return err + + if config.Ledger.Hash != latestNonEmptyBlock.StagedHash { + return fmt.Errorf("Expected ledger.hash to be `%s`, got `%s`", latestNonEmptyBlock.StagedHash, config.Ledger.Hash) } - // Validate root object contains only expected top-level fields - if err := t.validateRootObjectFields(forkConfigJson, []string{"proof", "epoch_data", "ledger"}); err != nil { - return err + if config.Ledger.AddGenesisWinner != false { + return fmt.Errorf("Expected ledger.add_genesis_winner to be false, got true") } - t.Logger.Info("Fork config data validated successfully") + t.Logger.Info("Legacy prepatch fork config validated successfully") return nil } -// ValidateForkRuntimeConfig validates that the runtime config has correct fork data -func (t *HardforkTest) ValidateForkRuntimeConfig(latestNonEmptyBlock client.BlockData, configData []byte, forkGenesisTs, mainGenesisTs int64) error { +type EpochDataPatched struct { + Hash string `json:"hash"` + S3DataHash string `json:"s3_data_hash"` + Seed string `json:"seed"` +} + +type LegacyPostpatchForkConfigView struct { + Proof struct { + Fork struct { + BlockChainLength int `json:"blockchain_length"` + GlobalSlotSinceGenesis int64 `json:"global_slot_since_genesis"` + StateHash string `json:"state_hash"` + } `json:"fork"` + } `json:"proof"` + EpochData struct { + Staking EpochDataPatched `json:"staking"` + Next EpochDataPatched `json:"next"` + } `json:"epoch_data"` + Genesis struct { + GenesisStateTimeStamp time.Time `json:"genesis_state_timestamp"` + } + Ledger struct { + Hash string `json:"hash"` + S3DataHash string `json:"s3_data_hash"` + AddGenesisWinner bool `json:"add_genesis_winner"` + } `json:"ledger"` +} + +func (t *HardforkTest) ValidateLegacyPostpatchForkConfig(latestNonEmptyBlock client.BlockData, forkConfigBytes []byte, forkGenesisTs, mainGenesisTs int64) error { + + t.Logger.Info("Validating legacy postpatch fork config") // Calculate expected genesis slot expectedGenesisSlot := (forkGenesisTs - mainGenesisTs) / int64(t.Config.MainSlot) - configJson := string(configData) + var config LegacyPostpatchForkConfigView + dec := json.NewDecoder(bytes.NewReader(forkConfigBytes)) + dec.DisallowUnknownFields() - // Validate field values - if err := validateIntField(configJson, "proof.fork.blockchain_length", latestNonEmptyBlock.BlockHeight); err != nil { - return err - } - if err := validateInt64Field(configJson, "proof.fork.global_slot_since_genesis", expectedGenesisSlot); err != nil { - return err - } - if err := validateStringField(configJson, "proof.fork.state_hash", latestNonEmptyBlock.StateHash); err != nil { - return err - } - if err := validateUnixTimestampField(configJson, "genesis.genesis_state_timestamp", forkGenesisTs); err != nil { - return err - } - if err := t.validateObjectFields(configJson, "genesis", []string{"genesis_state_timestamp"}); err != nil { - return err + if err := dec.Decode(&config); err != nil { + return fmt.Errorf("failed to unmarshal fork config: %w", err) } - // Validate object structure - ensure only expected fields are present - if err := t.validateObjectFields(configJson, "proof.fork", []string{"blockchain_length", "global_slot_since_genesis", "state_hash"}); err != nil { - return err - } - if err := t.validateObjectFields(configJson, "proof", []string{"fork"}); err != nil { - return err + if config.Proof.Fork.BlockChainLength != latestNonEmptyBlock.BlockHeight { + return fmt.Errorf("Expected proof.fork.blockchain_length to be %d, got %d", latestNonEmptyBlock.BlockHeight, config.Proof.Fork.BlockChainLength) } - epochFields := []string{"hash", "s3_data_hash", "seed"} - // Validate object structure - ensure only expected fields are present - if err := t.validateObjectFields(configJson, "epoch_data.staking", epochFields); err != nil { - return err - } - if err := t.validateObjectFields(configJson, "epoch_data.next", epochFields); err != nil { - return err - } - if err := t.validateObjectFields(configJson, "epoch_data", []string{"staking", "next"}); err != nil { - return err + if config.Proof.Fork.GlobalSlotSinceGenesis != expectedGenesisSlot { + return fmt.Errorf("Expected proof.fork.global_slot_since_genesis to be %d, got %d", expectedGenesisSlot, config.Proof.Fork.GlobalSlotSinceGenesis) } - // Validate ledger.add_genesis_winner is false - if err := validateBoolField(configJson, "ledger.add_genesis_winner", false); err != nil { - return err + if config.Proof.Fork.StateHash != latestNonEmptyBlock.StateHash { + return fmt.Errorf("Expected proof.fork.state_hash to be `%s`, got `%s`", latestNonEmptyBlock.StateHash, config.Proof.Fork.StateHash) } - ledgerFields := []string{"hash", "s3_data_hash", "add_genesis_winner"} - if err := t.validateObjectFields(configJson, "ledger", ledgerFields); err != nil { - return err + if config.Genesis.GenesisStateTimeStamp.Unix() != forkGenesisTs { + return fmt.Errorf("Expected genesis.genesis_state_timestamp to be `%d`(unix timestamp), got `%s`(RFC3339)", forkGenesisTs, config.Genesis.GenesisStateTimeStamp.Format(time.RFC3339)) } - // Validate root object contains only expected top-level fields - if err := t.validateRootObjectFields(configJson, []string{"proof", "epoch_data", "ledger", "genesis"}); err != nil { - return err - } - t.Logger.Info("Config for the fork is correct, starting a new network") + t.Logger.Info("Legacy postpatch fork config validated successfully") return nil } diff --git a/src/app/hardfork_test/src/internal/hardfork/json_validation.go b/src/app/hardfork_test/src/internal/hardfork/json_validation.go deleted file mode 100644 index 1c3a0bb8d837..000000000000 --- a/src/app/hardfork_test/src/internal/hardfork/json_validation.go +++ /dev/null @@ -1,167 +0,0 @@ -package hardfork - -import ( - "fmt" - "time" - - "github.com/tidwall/gjson" -) - -// validateStringField validates that a string field exists and matches the expected value -func validateStringField(json, path, expected string) error { - result := gjson.Get(json, path) - if !result.Exists() { - return fmt.Errorf("missing field: %s", path) - } - actual := result.String() - if actual != expected { - return fmt.Errorf("%s mismatch: expected %s, got %s", path, expected, actual) - } - return nil -} - -// validateIntField validates that an integer field exists and matches the expected value -func validateIntField(json, path string, expected int) error { - result := gjson.Get(json, path) - if !result.Exists() { - return fmt.Errorf("missing field: %s", path) - } - actual := result.Int() - if actual != int64(expected) { - return fmt.Errorf("%s mismatch: expected %d, got %d", path, expected, actual) - } - return nil -} - -// validateInt64Field validates that an int64 field exists and matches the expected value -func validateInt64Field(json, path string, expected int64) error { - result := gjson.Get(json, path) - if !result.Exists() { - return fmt.Errorf("missing field: %s", path) - } - actual := result.Int() - if actual != expected { - return fmt.Errorf("%s mismatch: expected %d, got %d", path, expected, actual) - } - return nil -} - -// validateBoolField validates that a boolean field exists and matches the expected value -func validateBoolField(json, path string, expected bool) error { - result := gjson.Get(json, path) - if !result.Exists() { - return fmt.Errorf("missing field: %s", path) - } - actual := result.Bool() - if actual != expected { - return fmt.Errorf("%s mismatch: expected %v, got %v", path, expected, actual) - } - return nil -} - -// validateUnixTimestampField validates that a timestamp field matches the expected Unix timestamp -// The field must be stored as an RFC3339 formatted string -func validateUnixTimestampField(json, path string, expectedUnixTs int64) error { - result := gjson.Get(json, path) - if !result.Exists() { - return fmt.Errorf("missing field: %s", path) - } - - if result.Type != gjson.String { - return fmt.Errorf("%s must be a string", path) - } - - timestampStr := result.String() - // Try parsing as RFC3339 timestamp - t, err := time.Parse(time.RFC3339, timestampStr) - if err != nil { - // Also try a common variant with space instead of 'T' - t, err = time.Parse("2006-01-02 15:04:05-07:00", timestampStr) - if err != nil { - return fmt.Errorf("%s is not a valid RFC3339 timestamp: %v", path, err) - } - } - - actualUnixTs := t.Unix() - if actualUnixTs != expectedUnixTs { - return fmt.Errorf("%s mismatch: expected %d, got %d", path, expectedUnixTs, actualUnixTs) - } - return nil -} - -// validateObjectFields validates that an object contains only the expected fields -func (t *HardforkTest) validateObjectFields(json, path string, expectedFields []string) error { - obj := gjson.Get(json, path) - if !obj.Exists() { - return fmt.Errorf("missing object: %s", path) - } - if !obj.IsObject() { - return fmt.Errorf("%s is not an object", path) - } - - // Create a map of expected fields for quick lookup - expectedMap := make(map[string]bool) - for _, field := range expectedFields { - expectedMap[field] = true - } - - // Check all fields in the object - fieldCount := 0 - var unexpectedFields []string - obj.ForEach(func(key, value gjson.Result) bool { - fieldCount++ - fieldName := key.String() - if !expectedMap[fieldName] { - unexpectedFields = append(unexpectedFields, fieldName) - t.Logger.Error("Unexpected field in %s: %s", path, fieldName) - } - return true // continue iteration - }) - - if len(unexpectedFields) > 0 { - return fmt.Errorf("%s contains unexpected fields: %v", path, unexpectedFields) - } - - if fieldCount != len(expectedFields) { - return fmt.Errorf("%s should contain exactly %d field(s), found %d", path, len(expectedFields), fieldCount) - } - - return nil -} - -// validateRootObjectFields validates that the root-level JSON contains only expected fields -func (t *HardforkTest) validateRootObjectFields(json string, expectedFields []string) error { - rootResult := gjson.Parse(json) - if !rootResult.IsObject() { - return fmt.Errorf("root is not an object") - } - - // Create a map of expected fields for quick lookup - expectedMap := make(map[string]bool) - for _, field := range expectedFields { - expectedMap[field] = true - } - - // Check all fields in the root object - fieldCount := 0 - var unexpectedFields []string - rootResult.ForEach(func(key, value gjson.Result) bool { - fieldCount++ - fieldName := key.String() - if !expectedMap[fieldName] { - unexpectedFields = append(unexpectedFields, fieldName) - t.Logger.Error("Unexpected field in root object: %s", fieldName) - } - return true - }) - - if len(unexpectedFields) > 0 { - return fmt.Errorf("root object contains unexpected fields: %v", unexpectedFields) - } - - if fieldCount != len(expectedFields) { - return fmt.Errorf("root object should contain exactly %d field(s), found %d", len(expectedFields), fieldCount) - } - - return nil -} diff --git a/src/app/hardfork_test/src/internal/hardfork/ledger.go b/src/app/hardfork_test/src/internal/hardfork/ledger.go index 73d34f551b50..8fe95e7cd954 100644 --- a/src/app/hardfork_test/src/internal/hardfork/ledger.go +++ b/src/app/hardfork_test/src/internal/hardfork/ledger.go @@ -43,26 +43,19 @@ func (t *HardforkTest) GenerateForkLedgers(executablePath, forkConfigPath, ledge return nil } -// GenerateAndValidatePreforkLedgers validates fork config and ledgers -// Note: Fork config extraction happens in RunMainNetworkPhase before nodes shutdown -func (t *HardforkTest) GenerateAndValidatePreforkLedgers(analysis *BlockAnalysisResult, forkConfigPath, preforkLedgersDir, preforkHashesFile string) error { +func (t *HardforkTest) GenerateAndValidateHashesAndLedgers(analysis *BlockAnalysisResult, forkConfigPath, preforkLedgersDir, prepatchForkConfig string) error { // Generate prefork ledgers using main network executable - if err := t.GenerateForkLedgers(t.Config.MainRuntimeGenesisLedger, forkConfigPath, preforkLedgersDir, preforkHashesFile); err != nil { + if err := t.GenerateForkLedgers(t.Config.MainRuntimeGenesisLedger, forkConfigPath, preforkLedgersDir, prepatchForkConfig); err != nil { return err } - // Validate prefork ledger hashes - if err := t.ValidatePreforkLedgerHashes( + return t.ValidateRuntimeGenesisLedgerHashes( analysis.LatestNonEmptyBlock, analysis.GenesisEpochStaking, analysis.GenesisEpochNext, analysis.LatestSnarkedHashPerEpoch, - preforkHashesFile, - ); err != nil { - return err - } - - return nil + prepatchForkConfig, + ) } // PatchForkConfigAndGenerateLedgersLegacy does the following: @@ -83,7 +76,7 @@ func (t *HardforkTest) PatchForkConfigAndGenerateLedgersLegacy(analysis *BlockAn } // Validate modified fork data - return t.ValidateForkRuntimeConfig(analysis.LatestNonEmptyBlock, runtimeConfigBytes, forkGenesisTs, mainGenesisTs) + return t.ValidateLegacyPostpatchForkConfig(analysis.LatestNonEmptyBlock, runtimeConfigBytes, forkGenesisTs, mainGenesisTs) } func (t *HardforkTest) AdvancedGenerateHardForkConfig(configDir string, clientPort int) error { diff --git a/src/app/hardfork_test/src/internal/hardfork/phases.go b/src/app/hardfork_test/src/internal/hardfork/phases.go index 945833c05a46..970a0a790229 100644 --- a/src/app/hardfork_test/src/internal/hardfork/phases.go +++ b/src/app/hardfork_test/src/internal/hardfork/phases.go @@ -114,35 +114,35 @@ func (t *HardforkTest) RunForkNetworkPhase(latestPreForkHeight int, forkData For func (t *HardforkTest) LegacyForkPhase(analysis *BlockAnalysisResult, forkConfigBytes []byte, mainGenesisTs int64) (*ForkData, error) { - if err := os.MkdirAll("fork_data/prefork", 0755); err != nil { + if err := os.MkdirAll("fork_data/prepatch", 0755); err != nil { return nil, err } // Define all fork_data file paths - preforkConfig := "fork_data/prefork/config.json" + prepatchConfig := "fork_data/prepatch/config.json" // Validate fork config data - if err := t.ValidateForkConfigData(analysis.LatestNonEmptyBlock, forkConfigBytes); err != nil { + if err := t.ValidateLegacyPrepatchForkConfig(analysis.LatestNonEmptyBlock, forkConfigBytes); err != nil { return nil, err } // Write fork config to file - if err := os.WriteFile(preforkConfig, forkConfigBytes, 0644); err != nil { + if err := os.WriteFile(prepatchConfig, forkConfigBytes, 0644); err != nil { return nil, err } { - preforkLedgersDir := "fork_data/prefork/hf_ledgers" - preforkHashesFile := "fork_data/prefork/hf_ledger_hashes.json" - if err := t.GenerateAndValidatePreforkLedgers(analysis, preforkConfig, preforkLedgersDir, preforkHashesFile); err != nil { + preforkLedgersDir := "fork_data/prepatch/hf_ledgers" + preforkHashesFile := "fork_data/prepatch/hf_ledger_hashes.json" + if err := t.GenerateAndValidateHashesAndLedgers(analysis, prepatchConfig, preforkLedgersDir, preforkHashesFile); err != nil { return nil, err } } - if err := os.MkdirAll("fork_data/postfork", 0755); err != nil { + if err := os.MkdirAll("fork_data/postpatch", 0755); err != nil { return nil, err } - postforkConfig := "fork_data/postfork/config.json" - forkLedgersDir := "fork_data/postfork/hf_ledgers" + postforkConfig := "fork_data/postpatch/config.json" + forkLedgersDir := "fork_data/postpatch/hf_ledgers" // Calculate fork genesis timestamp relative to now (before starting fork network) forkGenesisTs := time.Now().Unix() + int64(t.Config.ForkDelay*60) @@ -150,7 +150,7 @@ func (t *HardforkTest) LegacyForkPhase(analysis *BlockAnalysisResult, forkConfig { preforkGenesisConfigFile := fmt.Sprintf("%s/daemon.json", t.Config.Root) forkHashesFile := "fork_data/hf_ledger_hashes.json" - if err := t.PatchForkConfigAndGenerateLedgersLegacy(analysis, preforkConfig, forkLedgersDir, forkHashesFile, postforkConfig, preforkGenesisConfigFile, forkGenesisTs, mainGenesisTs); err != nil { + if err := t.PatchForkConfigAndGenerateLedgersLegacy(analysis, prepatchConfig, forkLedgersDir, forkHashesFile, postforkConfig, preforkGenesisConfigFile, forkGenesisTs, mainGenesisTs); err != nil { return nil, err } } From 7004d47ce4749c61694fdf79779ecae4b533278b Mon Sep 17 00:00:00 2001 From: glyh Date: Tue, 2 Dec 2025 15:55:18 +0800 Subject: [PATCH 37/40] hf test go legacy: factor out final config validation --- .../src/internal/hardfork/ledger.go | 12 ++----- .../src/internal/hardfork/phases.go | 33 ++++++++++--------- 2 files changed, 21 insertions(+), 24 deletions(-) diff --git a/src/app/hardfork_test/src/internal/hardfork/ledger.go b/src/app/hardfork_test/src/internal/hardfork/ledger.go index 8fe95e7cd954..d4acc927d4d3 100644 --- a/src/app/hardfork_test/src/internal/hardfork/ledger.go +++ b/src/app/hardfork_test/src/internal/hardfork/ledger.go @@ -62,21 +62,15 @@ func (t *HardforkTest) GenerateAndValidateHashesAndLedgers(analysis *BlockAnalys // 1. generate fork ledgers with runtime-genesis-ledger // 2. patch the genesis time & slot for fork config with create_runtime_config.sh // 3. perform some base sanity check on the fork config -func (t *HardforkTest) PatchForkConfigAndGenerateLedgersLegacy(analysis *BlockAnalysisResult, forkConfigPath, forkLedgersDir, forkHashesFile, configFile, preforkGenesisConfigFile string, forkGenesisTs, mainGenesisTs int64) error { +func (t *HardforkTest) PatchForkConfigAndGenerateLedgersLegacy(analysis *BlockAnalysisResult, forkConfigPath, forkLedgersDir, forkHashesFile, configFile, preforkGenesisConfigFile string, forkGenesisTs, mainGenesisTs int64) ([]byte, error) { // Generate fork ledgers using fork network executable if err := t.GenerateForkLedgers(t.Config.ForkRuntimeGenesisLedger, forkConfigPath, forkLedgersDir, forkHashesFile); err != nil { - return err + return nil, err } // Create runtime config forkGenesisTimestamp := config.FormatTimestamp(forkGenesisTs) - runtimeConfigBytes, err := t.PatchRuntimeConfigLegacy(forkGenesisTimestamp, forkConfigPath, configFile, preforkGenesisConfigFile, forkHashesFile) - if err != nil { - return err - } - - // Validate modified fork data - return t.ValidateLegacyPostpatchForkConfig(analysis.LatestNonEmptyBlock, runtimeConfigBytes, forkGenesisTs, mainGenesisTs) + return t.PatchRuntimeConfigLegacy(forkGenesisTimestamp, forkConfigPath, configFile, preforkGenesisConfigFile, forkHashesFile) } func (t *HardforkTest) AdvancedGenerateHardForkConfig(configDir string, clientPort int) error { diff --git a/src/app/hardfork_test/src/internal/hardfork/phases.go b/src/app/hardfork_test/src/internal/hardfork/phases.go index 970a0a790229..d917c0671903 100644 --- a/src/app/hardfork_test/src/internal/hardfork/phases.go +++ b/src/app/hardfork_test/src/internal/hardfork/phases.go @@ -129,33 +129,36 @@ func (t *HardforkTest) LegacyForkPhase(analysis *BlockAnalysisResult, forkConfig if err := os.WriteFile(prepatchConfig, forkConfigBytes, 0644); err != nil { return nil, err } - { - preforkLedgersDir := "fork_data/prepatch/hf_ledgers" - preforkHashesFile := "fork_data/prepatch/hf_ledger_hashes.json" - if err := t.GenerateAndValidateHashesAndLedgers(analysis, prepatchConfig, preforkLedgersDir, preforkHashesFile); err != nil { - return nil, err - } + + prepatchLedgersDir := "fork_data/prepatch/hf_ledgers" + prepatchHashesFile := "fork_data/prepatch/hf_ledger_hashes.json" + if err := t.GenerateAndValidateHashesAndLedgers(analysis, prepatchConfig, prepatchLedgersDir, prepatchHashesFile); err != nil { + return nil, err } if err := os.MkdirAll("fork_data/postpatch", 0755); err != nil { return nil, err } - postforkConfig := "fork_data/postpatch/config.json" - forkLedgersDir := "fork_data/postpatch/hf_ledgers" + postpatchConfig := "fork_data/postpatch/config.json" + postpatchLedgersDir := "fork_data/postpatch/hf_ledgers" // Calculate fork genesis timestamp relative to now (before starting fork network) forkGenesisTs := time.Now().Unix() + int64(t.Config.ForkDelay*60) - { - preforkGenesisConfigFile := fmt.Sprintf("%s/daemon.json", t.Config.Root) - forkHashesFile := "fork_data/hf_ledger_hashes.json" - if err := t.PatchForkConfigAndGenerateLedgersLegacy(analysis, prepatchConfig, forkLedgersDir, forkHashesFile, postforkConfig, preforkGenesisConfigFile, forkGenesisTs, mainGenesisTs); err != nil { - return nil, err - } + preforkGenesisConfigFile := fmt.Sprintf("%s/daemon.json", t.Config.Root) + forkHashesFile := "fork_data/hf_ledger_hashes.json" + + patchedConfigBytes, err := t.PatchForkConfigAndGenerateLedgersLegacy(analysis, prepatchConfig, postpatchLedgersDir, forkHashesFile, postpatchConfig, preforkGenesisConfigFile, forkGenesisTs, mainGenesisTs) + if err != nil { + return nil, err + } + err = t.ValidateLegacyPostpatchForkConfig(analysis.LatestNonEmptyBlock, patchedConfigBytes, forkGenesisTs, mainGenesisTs) + if err != nil { + return nil, err } - return &ForkData{config: postforkConfig, ledgersDir: forkLedgersDir, genesis: forkGenesisTs}, nil + return &ForkData{config: postpatchConfig, ledgersDir: postpatchLedgersDir, genesis: forkGenesisTs}, nil } From 47d8f4ee95663c3fcc56610a2783e2a9c1f84d3e Mon Sep 17 00:00:00 2001 From: glyh Date: Tue, 2 Dec 2025 15:59:55 +0800 Subject: [PATCH 38/40] hf test go advanced: implement validation for fork config --- .../src/internal/config/config.go | 6 ++++++ .../internal/hardfork/config_validation.go | 16 ++++++++-------- .../src/internal/hardfork/network.go | 5 +---- .../src/internal/hardfork/phases.go | 19 +++++++++---------- .../src/internal/hardfork/test.go | 2 +- 5 files changed, 25 insertions(+), 23 deletions(-) diff --git a/src/app/hardfork_test/src/internal/config/config.go b/src/app/hardfork_test/src/internal/config/config.go index 6cf9942100a0..6389ccdaf3a2 100644 --- a/src/app/hardfork_test/src/internal/config/config.go +++ b/src/app/hardfork_test/src/internal/config/config.go @@ -2,6 +2,7 @@ package config import ( "fmt" + "math" "os" "time" ) @@ -143,6 +144,11 @@ func (c *Config) Validate() error { return nil } +func (c *Config) HfSlotDelta() int { + + forkDelayDuration := time.Duration(c.ForkDelay) * time.Minute + return int(math.Ceil(forkDelayDuration.Seconds() / float64(c.MainSlot))) +} // FormatTimestamp formats a UNIX timestamp into the format used by the shell script func FormatTimestamp(unixTs int64) string { diff --git a/src/app/hardfork_test/src/internal/hardfork/config_validation.go b/src/app/hardfork_test/src/internal/hardfork/config_validation.go index f0eeb08cf75a..b17c8faeead5 100644 --- a/src/app/hardfork_test/src/internal/hardfork/config_validation.go +++ b/src/app/hardfork_test/src/internal/hardfork/config_validation.go @@ -146,13 +146,13 @@ func (t *HardforkTest) ValidateLegacyPrepatchForkConfig(latestNonEmptyBlock clie return nil } -type EpochDataPatched struct { +type EpochData struct { Hash string `json:"hash"` S3DataHash string `json:"s3_data_hash"` Seed string `json:"seed"` } -type LegacyPostpatchForkConfigView struct { +type FinalForkConfigView struct { Proof struct { Fork struct { BlockChainLength int `json:"blockchain_length"` @@ -161,8 +161,8 @@ type LegacyPostpatchForkConfigView struct { } `json:"fork"` } `json:"proof"` EpochData struct { - Staking EpochDataPatched `json:"staking"` - Next EpochDataPatched `json:"next"` + Staking EpochData `json:"staking"` + Next EpochData `json:"next"` } `json:"epoch_data"` Genesis struct { GenesisStateTimeStamp time.Time `json:"genesis_state_timestamp"` @@ -174,13 +174,13 @@ type LegacyPostpatchForkConfigView struct { } `json:"ledger"` } -func (t *HardforkTest) ValidateLegacyPostpatchForkConfig(latestNonEmptyBlock client.BlockData, forkConfigBytes []byte, forkGenesisTs, mainGenesisTs int64) error { +func (t *HardforkTest) ValidateFinalForkConfig(latestNonEmptyBlock client.BlockData, forkConfigBytes []byte, forkGenesisTs, mainGenesisTs int64) error { - t.Logger.Info("Validating legacy postpatch fork config") + t.Logger.Info("Validating final fork config") // Calculate expected genesis slot expectedGenesisSlot := (forkGenesisTs - mainGenesisTs) / int64(t.Config.MainSlot) - var config LegacyPostpatchForkConfigView + var config FinalForkConfigView dec := json.NewDecoder(bytes.NewReader(forkConfigBytes)) dec.DisallowUnknownFields() @@ -204,6 +204,6 @@ func (t *HardforkTest) ValidateLegacyPostpatchForkConfig(latestNonEmptyBlock cli return fmt.Errorf("Expected genesis.genesis_state_timestamp to be `%d`(unix timestamp), got `%s`(RFC3339)", forkGenesisTs, config.Genesis.GenesisStateTimeStamp.Format(time.RFC3339)) } - t.Logger.Info("Legacy postpatch fork config validated successfully") + t.Logger.Info("Final fork config validated successfully") return nil } diff --git a/src/app/hardfork_test/src/internal/hardfork/network.go b/src/app/hardfork_test/src/internal/hardfork/network.go index 2ec3a9ad3d81..e9281f4858e9 100644 --- a/src/app/hardfork_test/src/internal/hardfork/network.go +++ b/src/app/hardfork_test/src/internal/hardfork/network.go @@ -2,7 +2,6 @@ package hardfork import ( "fmt" - "math" "math/rand" "os" "os/exec" @@ -105,9 +104,7 @@ func (t *HardforkTest) RunMainNetwork(mainGenesisTs int64) (*exec.Cmd, error) { switch t.Config.ForkMethod { case config.Advanced: - forkDelay := time.Duration(t.Config.ForkDelay) * time.Minute - hardforkGenesisDelta := int(math.Ceil(forkDelay.Seconds() / float64(t.Config.MainSlot))) - args = append(args, "--hardfork-genesis-slot-delta", strconv.Itoa(hardforkGenesisDelta)) + args = append(args, "--hardfork-genesis-slot-delta", strconv.Itoa(t.Config.HfSlotDelta())) case config.Legacy: // Do nothing as we'll patch the slot at the end of main network } diff --git a/src/app/hardfork_test/src/internal/hardfork/phases.go b/src/app/hardfork_test/src/internal/hardfork/phases.go index d917c0671903..fb38b1e80945 100644 --- a/src/app/hardfork_test/src/internal/hardfork/phases.go +++ b/src/app/hardfork_test/src/internal/hardfork/phases.go @@ -1,7 +1,6 @@ package hardfork import ( - "encoding/json" "fmt" "os" "time" @@ -153,7 +152,7 @@ func (t *HardforkTest) LegacyForkPhase(analysis *BlockAnalysisResult, forkConfig if err != nil { return nil, err } - err = t.ValidateLegacyPostpatchForkConfig(analysis.LatestNonEmptyBlock, patchedConfigBytes, forkGenesisTs, mainGenesisTs) + err = t.ValidateFinalForkConfig(analysis.LatestNonEmptyBlock, patchedConfigBytes, forkGenesisTs, mainGenesisTs) if err != nil { return nil, err } @@ -163,7 +162,7 @@ func (t *HardforkTest) LegacyForkPhase(analysis *BlockAnalysisResult, forkConfig } // Uses `mina advanced generate-hardfork-config CLI` -func (t *HardforkTest) AdvancedForkPhase(analysis *BlockAnalysisResult) (*ForkData, error) { +func (t *HardforkTest) AdvancedForkPhase(analysis *BlockAnalysisResult, mainGenesisTs int64) (*ForkData, error) { cwd := "" var err error = nil @@ -177,21 +176,21 @@ func (t *HardforkTest) AdvancedForkPhase(analysis *BlockAnalysisResult) (*ForkDa return nil, err } - configToPatch := fmt.Sprintf("%s/daemon.json", forkDataPath) + forkConfig := fmt.Sprintf("%s/daemon.json", forkDataPath) - configJsonString, err := os.ReadFile(configToPatch) + forkConfigBytes, err := os.ReadFile(forkConfig) if err != nil { return nil, err } - // TODO: validate fork config here instead of using a generic map to read it. - var configRaw map[string]map[string]string - json.Unmarshal(configJsonString, &configRaw) + forkGenesisSlot := t.Config.SlotChainEnd + t.Config.HfSlotDelta() + forkGenesisTs := mainGenesisTs + int64(forkGenesisSlot*t.Config.MainSlot) - forkGenesisTs, err := time.Parse(time.RFC3339Nano, configRaw["genesis"]["genesis_state_timestamp"]) + err = t.ValidateFinalForkConfig(analysis.LatestNonEmptyBlock, forkConfigBytes, forkGenesisTs, mainGenesisTs) if err != nil { return nil, err } + forkLedgersDir := fmt.Sprintf("%s/genesis", forkDataPath) - return &ForkData{config: configToPatch, ledgersDir: forkLedgersDir, genesis: forkGenesisTs.Unix()}, nil + return &ForkData{config: forkConfig, ledgersDir: forkLedgersDir, genesis: forkGenesisTs}, nil } diff --git a/src/app/hardfork_test/src/internal/hardfork/test.go b/src/app/hardfork_test/src/internal/hardfork/test.go index fdaa6ee29ea1..bb7fe3483633 100644 --- a/src/app/hardfork_test/src/internal/hardfork/test.go +++ b/src/app/hardfork_test/src/internal/hardfork/test.go @@ -147,7 +147,7 @@ func (t *HardforkTest) Run() error { beforeShutdown = func(t *HardforkTest, analysis *BlockAnalysisResult) error { t.Logger.Info("Phase 2: Forking with `mina advanced generate-hardfork-config`...") - forkData, err := t.AdvancedForkPhase(analysis) + forkData, err := t.AdvancedForkPhase(analysis, mainGenesisTs) if err != nil { return err } From 3d881c22568f35cd514e3bc0f31f63d213235eea Mon Sep 17 00:00:00 2001 From: glyh Date: Tue, 2 Dec 2025 17:42:44 +0800 Subject: [PATCH 39/40] FIX(hf test go legacy): patch timestamp to RFC3339 format so it could be parsed by config validation --- src/app/hardfork_test/src/internal/config/config.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/app/hardfork_test/src/internal/config/config.go b/src/app/hardfork_test/src/internal/config/config.go index 6389ccdaf3a2..99249085f901 100644 --- a/src/app/hardfork_test/src/internal/config/config.go +++ b/src/app/hardfork_test/src/internal/config/config.go @@ -153,7 +153,7 @@ func (c *Config) HfSlotDelta() int { // FormatTimestamp formats a UNIX timestamp into the format used by the shell script func FormatTimestamp(unixTs int64) string { t := time.Unix(unixTs, 0).UTC() - return t.Format("2006-01-02 15:04:05+00:00") + return t.Format(time.RFC3339) } // validateExecutable checks if a file exists and has executable permissions From 6c403dcc2188921b91396615c9fca93ac1e04b15 Mon Sep 17 00:00:00 2001 From: glyh Date: Tue, 2 Dec 2025 22:23:07 +0800 Subject: [PATCH 40/40] mina local network: show what's being overriden --- scripts/mina-local-network/mina-local-network.sh | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/scripts/mina-local-network/mina-local-network.sh b/scripts/mina-local-network/mina-local-network.sh index 82dad67216de..74e476c7078f 100755 --- a/scripts/mina-local-network/mina-local-network.sh +++ b/scripts/mina-local-network/mina-local-network.sh @@ -795,22 +795,22 @@ update_genesis_timestamp() { update_genesis_timestamp "${UPDATE_GENESIS_TIMESTAMP}" if [ ! -z "${OVERRIDE_SLOT_TIME_MS}" ]; then - echo 'Modifying configuration to override slot time...' + echo "Setting proof.block_window_duration_ms to ${OVERRIDE_SLOT_TIME_MS}..." jq-inplace ".proof.block_window_duration_ms=${OVERRIDE_SLOT_TIME_MS}" "${CONFIG}" fi if [ ! -z "${SLOT_TX_END}" ]; then - echo 'Modifying configuration to override slot transaction end...' + echo "Setting daemon.slot_tx_end to ${SLOT_TX_END}..." jq-inplace ".daemon.slot_tx_end=${SLOT_TX_END}" "${CONFIG}" fi if [ ! -z "${SLOT_CHAIN_END}" ]; then - echo 'Modifying configuration to override slot chain end...' + echo "Setting daemon.slot_chain_end to ${SLOT_CHAIN_END}..." jq-inplace ".daemon.slot_chain_end=${SLOT_CHAIN_END}" "${CONFIG}" fi if [ ! -z "${HARDFORK_GENESIS_SLOT_DELTA}" ]; then - echo 'Modifying configuration to override hardfork genesis slot delta...' + echo "Setting daemon.hard_fork_genesis_slot_delta to ${HARDFORK_GENESIS_SLOT_DELTA}..." jq-inplace ".daemon.hard_fork_genesis_slot_delta=${HARDFORK_GENESIS_SLOT_DELTA}" "${CONFIG}" fi