diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index dbfe906bd039..d546657f589d 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -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 @@ -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 diff --git a/.github/workflows/publish_antithesis_images.yml b/.github/workflows/publish_antithesis_images.yml index 9506126b698a..48704b5b8d7b 100644 --- a/.github/workflows/publish_antithesis_images.yml +++ b/.github/workflows/publish_antithesis_images.yml @@ -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 }} diff --git a/.github/workflows/trigger-antithesis-avalanchego.yml b/.github/workflows/trigger-antithesis-avalanchego.yml index d0dd75793f8b..c506c0fffd80 100644 --- a/.github/workflows/trigger-antithesis-avalanchego.yml +++ b/.github/workflows/trigger-antithesis-avalanchego.yml @@ -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: @@ -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) || '' }} diff --git a/.github/workflows/trigger-antithesis-xsvm.yml b/.github/workflows/trigger-antithesis-xsvm.yml index e5efddeb4b2f..1abe9edb3a19 100644 --- a/.github/workflows/trigger-antithesis-xsvm.yml +++ b/.github/workflows/trigger-antithesis-xsvm.yml @@ -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: @@ -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) || '' }} diff --git a/tests/fixture/tmpnet/genesis.go b/tests/fixture/tmpnet/genesis.go index 5a99c505d008..fcf85f4657d7 100644 --- a/tests/fixture/tmpnet/genesis.go +++ b/tests/fixture/tmpnet/genesis.go @@ -7,6 +7,9 @@ import ( "encoding/json" "errors" "math/big" + "math/rand" + "os" + "strconv" "time" "github.com/ava-labs/libevm/core" @@ -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" ) @@ -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 + + // 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(uint64(minPrice) * units.NanoAvax) + + // 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 + return RandomizedParams(rng, baseParams) +} diff --git a/tests/fixture/tmpnet/genesis_randomized_test.go b/tests/fixture/tmpnet/genesis_randomized_test.go new file mode 100644 index 000000000000..5e36c76e5b93 --- /dev/null +++ b/tests/fixture/tmpnet/genesis_randomized_test.go @@ -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") +} diff --git a/tests/fixture/tmpnet/network.go b/tests/fixture/tmpnet/network.go index 9623a01c14a0..31d7fbca6785 100644 --- a/tests/fixture/tmpnet/network.go +++ b/tests/fixture/tmpnet/network.go @@ -294,6 +294,11 @@ func (n *Network) EnsureDefaultConfig(ctx context.Context, log logging.Logger) e return stacktrace.Wrap(errMissingRuntimeConfig) } + // Apply randomized network parameters if ANTITHESIS_RANDOM_SEED is set + if antithesisSeed := os.Getenv("ANTITHESIS_RANDOM_SEED"); antithesisSeed != "" { + n.applyRandomizedFlags(log, antithesisSeed) + } + return nil } @@ -362,6 +367,11 @@ func (n *Network) DefaultGenesis() (*genesis.UnparsedConfig, error) { } keysToFund = append(keysToFund, n.PreFundedKeys...) + // Check if we should use randomized genesis for antithesis testing + if os.Getenv("ANTITHESIS_RANDOM_SEED") != "" { + return NewRandomizedTestGenesis(defaultNetworkID, n.Nodes, keysToFund) + } + return NewTestGenesis(defaultNetworkID, n.Nodes, keysToFund) } @@ -1132,3 +1142,59 @@ func MetricsLinkForNetwork(networkUUID string, startTime string, endTime string) endTime, ) } + +// applyRandomizedFlags applies randomized network parameters using the provided seed +func (n *Network) applyRandomizedFlags(log logging.Logger, antithesisSeed string) { + // Validate the seed format (the actual randomization is done in GetRandomizedParams) + if _, err := strconv.ParseInt(antithesisSeed, 10, 64); err != nil { + log.Warn("failed to parse ANTITHESIS_RANDOM_SEED, skipping randomization", zap.Error(err)) + return + } + + // TODO(jonathanoppenheimer): is there a better way to apply these values? + randomizedParams := GetRandomizedParams(n.GetNetworkID()) + configMappings := []struct { + flagKey string + value uint64 + }{ + // Dynamic fee parameters + {config.DynamicFeesMinGasPriceKey, uint64(randomizedParams.DynamicFeeConfig.MinPrice)}, + {config.DynamicFeesMaxGasCapacityKey, uint64(randomizedParams.DynamicFeeConfig.MaxCapacity)}, + {config.DynamicFeesMaxGasPerSecondKey, uint64(randomizedParams.DynamicFeeConfig.MaxPerSecond)}, + {config.DynamicFeesTargetGasPerSecondKey, uint64(randomizedParams.DynamicFeeConfig.TargetPerSecond)}, + {config.DynamicFeesExcessConversionConstantKey, uint64(randomizedParams.DynamicFeeConfig.ExcessConversionConstant)}, + + // Validator fee parameters + {config.ValidatorFeesCapacityKey, uint64(randomizedParams.ValidatorFeeConfig.Capacity)}, + {config.ValidatorFeesTargetKey, uint64(randomizedParams.ValidatorFeeConfig.Target)}, + {config.ValidatorFeesMinPriceKey, uint64(randomizedParams.ValidatorFeeConfig.MinPrice)}, + {config.ValidatorFeesExcessConversionConstantKey, uint64(randomizedParams.ValidatorFeeConfig.ExcessConversionConstant)}, + + // Static fee parameters + {config.TxFeeKey, randomizedParams.TxFee}, + {config.CreateAssetTxFeeKey, randomizedParams.CreateAssetTxFee}, + + // Staking parameters + {config.MinValidatorStakeKey, randomizedParams.MinValidatorStake}, + {config.MaxValidatorStakeKey, randomizedParams.MaxValidatorStake}, + {config.MinDelegatorStakeKey, randomizedParams.MinDelegatorStake}, + {config.MinDelegatorFeeKey, uint64(randomizedParams.MinDelegationFee)}, + } + + // Apply all mappings + for _, mapping := range configMappings { + n.DefaultFlags[mapping.flagKey] = strconv.FormatUint(mapping.value, 10) + } + + log.Info("applied randomized network parameters", + zap.String("seed", antithesisSeed), + zap.Uint64("minGasPrice", uint64(randomizedParams.DynamicFeeConfig.MinPrice)), + zap.Uint64("validatorMinPrice", uint64(randomizedParams.ValidatorFeeConfig.MinPrice)), + zap.Uint64("txFee", randomizedParams.TxFee), + zap.Uint64("createAssetTxFee", randomizedParams.CreateAssetTxFee), + zap.Uint64("minValidatorStake", randomizedParams.MinValidatorStake), + zap.Uint64("maxValidatorStake", randomizedParams.MaxValidatorStake), + zap.Uint64("minDelegatorStake", randomizedParams.MinDelegatorStake), + zap.Uint32("minDelegationFee", randomizedParams.MinDelegationFee), + ) +}