Skip to content
Draft
Show file tree
Hide file tree
Changes from 5 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
6 changes: 6 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -216,6 +216,9 @@ jobs:
- name: Check image build for avalanchego test setup
shell: bash
run: ./scripts/run_task.sh test-build-antithesis-images-avalanchego
env:
# Use GitHub run ID as a consistent random seed for CI testing
ANTITHESIS_RANDOM_SEED: ${{ github.run_id }}
test_build_antithesis_xsvm_images:
name: Build Antithesis xsvm images
runs-on: ubuntu-latest
Expand All @@ -225,6 +228,9 @@ jobs:
- name: Check image build for xsvm test setup
shell: bash
run: ./scripts/run_task.sh test-build-antithesis-images-xsvm
env:
# Use GitHub run ID as a consistent random seed for CI testing
ANTITHESIS_RANDOM_SEED: ${{ github.run_id }}
e2e_bootstrap_monitor:
name: Run bootstrap monitor e2e tests
runs-on: ubuntu-latest
Expand Down
4 changes: 4 additions & 0 deletions .github/workflows/publish_antithesis_images.yml
Original file line number Diff line number Diff line change
Expand Up @@ -36,9 +36,13 @@ jobs:
env:
IMAGE_PREFIX: ${{ env.REGISTRY }}/${{ env.REPOSITORY }}
IMAGE_TAG: ${{ github.event.inputs.image_tag || 'latest' }}
# Use GitHub run ID for deterministic randomization across Antithesis containers
ANTITHESIS_RANDOM_SEED: ${{ github.run_id }}

- name: Build and push images for xsvm test setup
run: ./scripts/run_task.sh build-antithesis-images-xsvm
env:
IMAGE_PREFIX: ${{ env.REGISTRY }}/${{ env.REPOSITORY }}
IMAGE_TAG: ${{ github.event.inputs.image_tag || 'latest' }}
# Use GitHub run ID for deterministic randomization across Antithesis containers
ANTITHESIS_RANDOM_SEED: ${{ github.run_id }}
6 changes: 6 additions & 0 deletions .github/workflows/trigger-antithesis-avalanchego.yml
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,11 @@ on:
default: latest
required: true
type: string
random_seed:
description: 'Random seed for genesis randomization (leave empty for default config)'
default: ''
required: false
type: string

jobs:
antithesis_avalanchego:
Expand All @@ -39,3 +44,4 @@ jobs:
additional_parameters: |-
custom.duration=${{ github.event.inputs.duration || '7.5' }}
custom.workload=avalanchego
${{ github.event.inputs.random_seed && format('custom.antithesis_random_seed={0}', github.event.inputs.random_seed) || '' }}
6 changes: 6 additions & 0 deletions .github/workflows/trigger-antithesis-xsvm.yml
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,11 @@ on:
default: latest
required: true
type: string
random_seed:
description: 'Random seed for genesis randomization (leave empty for default config)'
default: ''
required: false
type: string

jobs:
antithesis_xsvm:
Expand All @@ -39,3 +44,4 @@ jobs:
additional_parameters: |-
custom.duration=${{ github.event.inputs.duration || '7.5' }}
custom.workload=xsvm
${{ github.event.inputs.random_seed && format('custom.antithesis_random_seed={0}', github.event.inputs.random_seed) || '' }}
164 changes: 164 additions & 0 deletions tests/fixture/tmpnet/genesis.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,9 @@ import (
"encoding/json"
"errors"
"math/big"
"math/rand"
"os"
"strconv"
"time"

"github.com/ava-labs/libevm/core"
Expand All @@ -20,6 +23,7 @@ import (
"github.com/ava-labs/avalanchego/utils/crypto/secp256k1"
"github.com/ava-labs/avalanchego/utils/formatting/address"
"github.com/ava-labs/avalanchego/utils/units"
"github.com/ava-labs/avalanchego/vms/components/gas"
"github.com/ava-labs/avalanchego/vms/platformvm/reward"
)

Expand Down Expand Up @@ -191,3 +195,163 @@ func stakersForNodes(networkID uint32, nodes []*Node) ([]genesis.UnparsedStaker,

return initialStakers, nil
}

// NewRandomizedTestGenesis creates a test genesis with randomized parameters
// using the ANTITHESIS_RANDOM_SEED environment variable for consistent randomization
// across all containers in antithesis tests.
func NewRandomizedTestGenesis(
networkID uint32,
nodes []*Node,
keysToFund []*secp256k1.PrivateKey,
) (*genesis.UnparsedConfig, error) {
// Get the base genesis config first
config, err := NewTestGenesis(networkID, nodes, keysToFund)
if err != nil {
return nil, stacktrace.Wrap(err)
}

// Check for antithesis random seed
antithesisSeed := os.Getenv("ANTITHESIS_RANDOM_SEED")
if antithesisSeed == "" {
// No randomization requested, return the original config
return config, nil
}

// Parse the seed and create a deterministic random source
seed, err := strconv.ParseInt(antithesisSeed, 10, 64)
if err != nil {
return nil, stacktrace.Errorf("failed to parse ANTITHESIS_RANDOM_SEED: %w", err)
}

// Create a deterministic random source
rng := rand.New(rand.NewSource(seed)) // #nosec G404
Copy link

Copilot AI Sep 26, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Using math/rand for cryptographic purposes is not secure. Consider using crypto/rand for security-sensitive randomization, especially for network parameters that could affect consensus.

Copilot uses AI. Check for mistakes.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we care about this?


// Randomize the genesis parameters
if err := randomizeGenesisParams(rng, config); err != nil {
return nil, stacktrace.Errorf("failed to randomize genesis params: %w", err)
}

return config, nil
}

// randomizeGenesisParams randomizes various genesis parameters
func randomizeGenesisParams(rng *rand.Rand, config *genesis.UnparsedConfig) error {
// Parse C-Chain genesis to modify it
var cChainGenesis core.Genesis
if err := json.Unmarshal([]byte(config.CChainGenesis), &cChainGenesis); err != nil {
return stacktrace.Errorf("failed to unmarshal C-Chain genesis: %w", err)
}

// Randomize gas limit (between 50M and 200M)
cChainGenesis.GasLimit = uint64(50_000_000 + rng.Intn(150_000_000))

// Randomize initial stake duration (between 12 hours and 48 hours)
minStakeDuration := 12 * 60 * 60 // 12 hours in seconds
maxStakeDuration := 48 * 60 * 60 // 48 hours in seconds
stakeDurationRange := maxStakeDuration - minStakeDuration
config.InitialStakeDuration = uint64(minStakeDuration + rng.Intn(stakeDurationRange))

// Randomize initial stake duration offset (between 30 minutes and 3 hours)
minOffset := 30 * 60 // 30 minutes in seconds
maxOffset := 3 * 60 * 60 // 3 hours in seconds
offsetRange := maxOffset - minOffset
config.InitialStakeDurationOffset = uint64(minOffset + rng.Intn(offsetRange))

// Serialize the modified C-Chain genesis back
cChainGenesisBytes, err := json.Marshal(&cChainGenesis)
if err != nil {
return stacktrace.Errorf("failed to marshal C-Chain genesis: %w", err)
}
config.CChainGenesis = string(cChainGenesisBytes)

return nil
}

// RandomizedParams creates randomized network parameters for testing
// using the ANTITHESIS_RANDOM_SEED environment variable.
func RandomizedParams(rng *rand.Rand, baseParams genesis.Params) genesis.Params {
// Create a copy of the base params
params := baseParams

// Randomize P-Chain minimum gas price
// Range: 1 to 1000 nAVAX (1 to 1000 * 10^9 wei equivalent)
minPrice := 1 + rng.Intn(1000)
params.TxFeeConfig.DynamicFeeConfig.MinPrice = gas.Price(minPrice)

// Randomize validator fee minimum price
// Range: 1 to 1000 nAVAX
validatorMinPrice := 1 + rng.Intn(1000)
params.TxFeeConfig.ValidatorFeeConfig.MinPrice = gas.Price(uint64(validatorMinPrice) * units.NanoAvax)

// Randomize transaction fees
// Base transaction fee: 0.1 to 10 milliAVAX
baseFeeMultiplier := 1 + rng.Intn(100) // 1 to 100
params.TxFeeConfig.TxFee = uint64(baseFeeMultiplier) * (units.MilliAvax / 10)

// Create asset transaction fee: 0.5 to 50 milliAVAX
createAssetFeeMultiplier := 5 + rng.Intn(500) // 5 to 500 (0.5 to 50 milliAVAX)
params.TxFeeConfig.CreateAssetTxFee = uint64(createAssetFeeMultiplier) * (units.MilliAvax / 10)

// Randomize gas capacity and throughput parameters
// Max capacity: 500K to 2M
params.TxFeeConfig.DynamicFeeConfig.MaxCapacity = gas.Gas(500_000 + rng.Intn(1_500_000))

// Max per second: 50K to 200K
maxPerSecond := 50_000 + rng.Intn(150_000)
params.TxFeeConfig.DynamicFeeConfig.MaxPerSecond = gas.Gas(maxPerSecond)

// Target per second: 25% to 75% of max per second
targetRatio := 25 + rng.Intn(51) // 25 to 75
params.TxFeeConfig.DynamicFeeConfig.TargetPerSecond = gas.Gas(maxPerSecond * targetRatio / 100)

// Randomize validator fee capacity and target
validatorCapacity := 10_000 + rng.Intn(40_000) // 10K to 50K
params.TxFeeConfig.ValidatorFeeConfig.Capacity = gas.Gas(validatorCapacity)

// Target: 25% to 75% of capacity
validatorTargetRatio := 25 + rng.Intn(51) // 25 to 75
params.TxFeeConfig.ValidatorFeeConfig.Target = gas.Gas(validatorCapacity * validatorTargetRatio / 100)

// Randomize staking parameters
// Min validator stake: 1 to 5 KiloAVAX
minValidatorStakeMultiplier := 1 + rng.Intn(5)
params.StakingConfig.MinValidatorStake = uint64(minValidatorStakeMultiplier) * units.KiloAvax

// Max validator stake: 2 to 10 MegaAVAX
maxValidatorStakeMultiplier := 2 + rng.Intn(9)
params.StakingConfig.MaxValidatorStake = uint64(maxValidatorStakeMultiplier) * units.MegaAvax

// Min delegator stake: 5 to 100 AVAX
minDelegatorStakeMultiplier := 5 + rng.Intn(96)
params.StakingConfig.MinDelegatorStake = uint64(minDelegatorStakeMultiplier) * units.Avax

// Min delegation fee: 1% to 10%
minDelegationFeePercent := 1 + rng.Intn(10)
params.StakingConfig.MinDelegationFee = uint32(minDelegationFeePercent * 10000) // Convert to basis points

return params
}

// GetRandomizedParams returns randomized network parameters if ANTITHESIS_RANDOM_SEED is set,
// otherwise returns the original parameters for the given network.
func GetRandomizedParams(networkID uint32) genesis.Params {
baseParams := genesis.Params{
TxFeeConfig: genesis.GetTxFeeConfig(networkID),
StakingConfig: genesis.GetStakingConfig(networkID),
}

// Check for antithesis random seed
antithesisSeed := os.Getenv("ANTITHESIS_RANDOM_SEED")
if antithesisSeed == "" {
return baseParams
}

// Parse the seed and create a deterministic random source
seed, err := strconv.ParseInt(antithesisSeed, 10, 64)
if err != nil {
return baseParams
}

rng := rand.New(rand.NewSource(seed)) // #nosec G404
Copy link

Copilot AI Sep 26, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Using math/rand for cryptographic purposes is not secure. Consider using crypto/rand for security-sensitive randomization, especially for network parameters that could affect consensus.

Copilot uses AI. Check for mistakes.

return RandomizedParams(rng, baseParams)
}
146 changes: 146 additions & 0 deletions tests/fixture/tmpnet/genesis_randomized_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@
// Copyright (C) 2019-2025, Ava Labs, Inc. All rights reserved.
// See the file LICENSE for licensing terms.

package tmpnet

import (
"testing"

"github.com/stretchr/testify/require"

"github.com/ava-labs/avalanchego/genesis"
)

const networkID = uint32(147147)

func TestNewRandomizedTestGenesis(t *testing.T) {
require := require.New(t)

// Test without ANTITHESIS_RANDOM_SEED - should behave like normal genesis
nodes := NewNodesOrPanic(5)
keys, err := NewPrivateKeys(3)
require.NoError(err)

// Normal genesis without randomization
originalGenesis, err := NewTestGenesis(networkID, nodes, keys)
require.NoError(err)

// Randomized genesis without env var should be the same
randomizedGenesis, err := NewRandomizedTestGenesis(networkID, nodes, keys)
require.NoError(err)

// Should behave the same when no seed is set
require.Equal(originalGenesis.NetworkID, randomizedGenesis.NetworkID)
require.Len(randomizedGenesis.Allocations, len(originalGenesis.Allocations))

// Test with ANTITHESIS_RANDOM_SEED set
t.Setenv("ANTITHESIS_RANDOM_SEED", "12345")

randomizedGenesis2, err := NewRandomizedTestGenesis(networkID, nodes, keys)
require.NoError(err)

// Should still have same basic structure
require.Equal(originalGenesis.NetworkID, randomizedGenesis2.NetworkID)
require.Len(randomizedGenesis2.Allocations, len(originalGenesis.Allocations))

// But may have different timing parameters
require.NotEqual(randomizedGenesis.InitialStakeDuration, randomizedGenesis2.InitialStakeDuration)
require.NotEqual(randomizedGenesis.InitialStakeDurationOffset, randomizedGenesis2.InitialStakeDurationOffset)
}

func TestRandomizedParams(t *testing.T) {
require := require.New(t)

// Test with local params as base
baseParams := genesis.LocalParams

t.Run("without_env_var", func(t *testing.T) {
// Ensure no env var is set for this subtest
t.Setenv("ANTITHESIS_RANDOM_SEED", "")

params := GetRandomizedParams(networkID)

// Should return original config when no env var
require.Equal(baseParams.TxFeeConfig.TxFee, params.TxFee)
require.Equal(baseParams.StakingConfig.MinValidatorStake, params.MinValidatorStake)
})

t.Run("with_env_var", func(t *testing.T) {
// Test with environment variable
t.Setenv("ANTITHESIS_RANDOM_SEED", "54321")

randomizedParams := GetRandomizedParams(networkID)

// Should have randomized values
require.NotEqual(baseParams.TxFeeConfig.DynamicFeeConfig.MinPrice, randomizedParams.DynamicFeeConfig.MinPrice)
require.NotEqual(baseParams.StakingConfig.MinValidatorStake, randomizedParams.MinValidatorStake)

// Test determinism - same seed should produce same results
randomizedParams2 := GetRandomizedParams(networkID)

require.Equal(randomizedParams.DynamicFeeConfig.MinPrice, randomizedParams2.DynamicFeeConfig.MinPrice)
require.Equal(randomizedParams.MinValidatorStake, randomizedParams2.MinValidatorStake)
})
}

func TestRandomizedParamsValidation(t *testing.T) {
require := require.New(t)

// Test with valid seeds
testCases := []struct {
seed string
name string
}{
{"123456789", "positive integer"},
{"0", "zero"},
{"999999999999", "large number"},
}

for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
t.Setenv("ANTITHESIS_RANDOM_SEED", tc.seed)

// Should not panic or error
params := GetRandomizedParams(networkID)

// Should have valid randomized values
require.Greater(params.DynamicFeeConfig.MinPrice, genesis.LocalParams.DynamicFeeConfig.MinPrice)
require.Positive(params.MinValidatorStake)
})
}
}

func TestRandomizedGenesisWithDifferentSeeds(t *testing.T) {
require := require.New(t)

nodes := NewNodesOrPanic(3)
keys, err := NewPrivateKeys(2)
require.NoError(err)

// Test with different seeds produce different results
seeds := []string{"111", "222", "333"}
allParams := make([]genesis.Params, 0, len(seeds))

for _, seed := range seeds {
t.Setenv("ANTITHESIS_RANDOM_SEED", seed)

// Test randomized params
params := GetRandomizedParams(networkID)
allParams = append(allParams, params)

// Test randomized genesis creation works
genesis, err := NewRandomizedTestGenesis(networkID, nodes, keys)
require.NoError(err)
require.NotNil(genesis)
require.Equal(networkID, genesis.NetworkID)
}

// Verify different seeds produce different values (at least one pair should be different)
pricesAllSame := allParams[0].DynamicFeeConfig.MinPrice == allParams[1].DynamicFeeConfig.MinPrice &&
allParams[1].DynamicFeeConfig.MinPrice == allParams[2].DynamicFeeConfig.MinPrice
require.False(pricesAllSame, "All P-chain min gas prices should not be the same")

stakesAllSame := allParams[0].MinValidatorStake == allParams[1].MinValidatorStake &&
allParams[1].MinValidatorStake == allParams[2].MinValidatorStake
require.False(stakesAllSame, "All min validator stakes should not be the same")
}
Loading
Loading