Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
252 changes: 134 additions & 118 deletions src/app/hardfork_test/src/internal/hardfork/config_validation.go
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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
}
Loading