-
Notifications
You must be signed in to change notification settings - Fork 27
(feat) Introduce contract-deployment bloating scenario #41
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Conversation
Includes the scenarios contemplated within statebloat as well as any extra data about them such as gas cost metrics.
This scenario deploys contracts that are exactly 24kB in size (EIP-170 limit) to maximize state growth while minimizing gas cost. 1. Generates a contract with exactly 24,576 bytes of runtime code 2. Deploys the contract using CREATE 3. Each deployment adds: - 24,576 bytes of runtime code - Account trie node - Total state growth: ~24.7kB per deployment - 32,000 gas for CREATE - 20,000 gas for new account - 200 gas per byte for code deposit (24,576 bytes) - Total: 4,967,200 gas per deployment
- Remove redundant throughput flag and consolidate to contracts-per-tx - Remove count flag and related code for cleaner interface - Update wallet count calculation to use contracts-per-tx - Keep only two deployment rate control methods: - contracts-per-tx: direct control of contracts per transaction - gas-per-block: calculate contracts based on target gas The scenario now runs indefinitely until stopped, with cleaner and more focused configuration options.
- Add validation to ensure contract deployments don't exceed block gas limit - Check both gas-per-block and contracts-per-tx against block gas limit - Add clear error messages when gas limits are exceeded - Move validation to Run() function to access context
- Introduce multiple test cases for contract deployment: using contracts per block, gas per block, and handling invalid configurations. - Update config opts to replace `contracts-per-tx` with `contracts-per-block` for better clarity. - Add error handling for scenarios where neither gas per block nor contracts per block is set.
- Refactor gas fee parameters to align with EIP-1559 standards. - Initialize wallet pool in the contract deployment test.
|
Heya @CPerezz, I've reviewed the document, and it looks like most of the cheap state growth scenarios are already supported by the existing spamoor scenarios:
The Reversible state bloat attack vectors sound like they can be fully achieved with the |
|
Hey @pk910 apologies for the late reply. Mainly the deployment contract scenario was quite different from what I need. Yes, we both deploy contracts but I needed those to be the same ABI but different bytecode by using a random salt on deploymet such that the bytecode storage for clients gets bloated too (identical bytecodes aren't duplicated ofc). Also, this scenario grants quite some logging utilities needed later to use the wallets to bloat the KV stores with new addresses getting tokens. I'm completelly OK if you prefer to not merge this as is super similar. |
6c63e86 to
f92a90e
Compare
715d413 to
13604ed
Compare
- Introduced setup and teardown for contract deployment tests. - Updated scenario options to include a maximum transactions limit. St we can test for a single tx at a time and shortening testing time.
…to max (EIP170) - Introduced multiple dummy functions in the StateBloatToken contract to artificially inflate bytecode size. - Updated ABI and binary files to reflect changes in the contract structure.
- Updated the contract deployment scenario to log deployed contract addresses and gas used. - Added functionality to write deployed addresses to a JSON file for easier tracking. - Removed gas-per-block validation and adjusted wallet count logic. - Improved README with instructions for running against a local Anvil node without Go tests.
…ction tracking - Introduced a batch deployment strategy that calculates the number of contracts fitting within the block gas limit. - Added detailed tracking for deployed contracts, including gas used and bytecode size, with structured loggin. - Improved nonce management and transaction retry logic.
… logging - Added functionality to save a mapping of private keys to contract addresses in a deployments.json file after each contract deployment. - Refactored the final summary logging to focus on the deployments.json file, removing the previous detailed contract saving logic. - imporved logging to provide insights into the total number of deployers and contracts processed.
…mprove transaction handling - Removed unused imports and redundant fields. - Simplified transaction processing by releasing locks earlier. - Improved logging for transaction sending and contract deployment confirmation. - Cleaned up nonce management by directly fetching the nonce from the client.
…djustment - Added functionality to dynamically adjust transaction fees based on current network conditions. - Implemented retry logic for transaction sending with exponential backoff for base fee errors.
…s_limit - Introduced BlockDeploymentStats struct to track deployment statistics per block, including contract count, total gas used, and total bytecode size. - Implemented real-time block monitoring for logging deployment summaries. - Updated contract bytecode size calculations to reflect actual deployed bytecode. - Adjusted transaction processing intervals for improved efficiency.
6df98c4 to
139b18e
Compare
Signed-off-by: CPerezz <37264926+CPerezz@users.noreply.github.com>
…based rate limiting - Set MaxPending default to 100 to fix scenario hanging at startup (was 0, preventing any transactions) - Remove hardcoded 12-second block time assumption that caused deployments only every 2 blocks - Implement proper block-based rate limiting that waits for actual block changes - Set rate limit to 2x expected throughput for better network utilization - Send transactions at expected throughput rate per block instead of arbitrary delays
…nt scenario - Replace incorrect import 'scenariotypes' with 'scenario' package - Update ScenarioDescriptor to use correct scenario.Descriptor type - Fix Init method signature to match scenario.Scenario interface - Update wallet private key access to use GetWallet().GetPrivateKey() - Remove unused Config() method not part of the interface - Fix walletPool references in Init method
|
Seems I screwed up a bit when splitting into multiple PRs. With these two commits the scenario should now be ready. |
| // startBlockMonitor starts a background goroutine that monitors for new blocks | ||
| // and logs block deployment summaries immediately when blocks are mined | ||
| func (s *Scenario) startBlockMonitor(ctx context.Context) { | ||
| monitorCtx, cancel := context.WithCancel(ctx) | ||
| s.blockMonitorCancel = cancel | ||
| s.blockMonitorDone = make(chan struct{}) | ||
|
|
||
| go func() { | ||
| defer close(s.blockMonitorDone) | ||
|
|
||
| client := s.walletPool.GetClient(spamoor.SelectClientByIndex, 0, s.options.ClientGroup) | ||
| if client == nil { | ||
| s.logger.Warn("No client available for block monitoring") | ||
| return | ||
| } | ||
|
|
||
| ethClient := client.GetEthClient() | ||
| ticker := time.NewTicker(2 * time.Second) // Poll every 2 seconds | ||
| defer ticker.Stop() | ||
|
|
||
| for { | ||
| select { | ||
| case <-monitorCtx.Done(): | ||
| return | ||
| case <-ticker.C: | ||
| // Get current block number | ||
| latestBlock, err := ethClient.BlockByNumber(monitorCtx, nil) | ||
| if err != nil { | ||
| s.logger.WithError(err).Debug("Failed to get latest block for monitoring") | ||
| continue | ||
| } | ||
|
|
||
| currentBlockNumber := latestBlock.Number().Uint64() | ||
|
|
||
| // Log any completed blocks that haven't been logged yet | ||
| s.blockStatsMutex.Lock() | ||
| for bn := s.lastLoggedBlock + 1; bn < currentBlockNumber; bn++ { | ||
| if stats, exists := s.blockStats[bn]; exists && stats.ContractCount > 0 { | ||
| avgGasPerByte := float64(stats.TotalGasUsed) / float64(max(stats.TotalBytecodeSize, 1)) | ||
|
|
||
| s.contractsMutex.Lock() | ||
| totalContracts := len(s.deployedContracts) | ||
| s.contractsMutex.Unlock() | ||
|
|
||
| s.logger.WithFields(logrus.Fields{ | ||
| "block_number": bn, | ||
| "contracts_deployed": stats.ContractCount, | ||
| "total_gas_used": stats.TotalGasUsed, | ||
| "total_bytecode_size": stats.TotalBytecodeSize, | ||
| "avg_gas_per_byte": fmt.Sprintf("%.2f", avgGasPerByte), | ||
| "total_contracts": totalContracts, | ||
| }).Info("Block deployment summary") | ||
|
|
||
| s.lastLoggedBlock = bn | ||
| } | ||
| } | ||
| s.blockStatsMutex.Unlock() | ||
| } | ||
| } | ||
| }() | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
this is basically what the TxPool logic does and shouldn't be duplicated.
you can get the receipts for each deployment in the SubmitTransactionOptions.OnConfirm callback and do the statistics summary there,
or subscribe to block updates via txPool.SubscribeToBlockUpdates, which will give you a per block event with all transactions / receipts that are relevant for the current scenario.
| func (s *Scenario) getChainID(ctx context.Context) (*big.Int, error) { | ||
| s.chainIDOnce.Do(func() { | ||
| client := s.walletPool.GetClient(spamoor.SelectClientByIndex, 0, s.options.ClientGroup) | ||
| if client == nil { | ||
| s.chainIDError = fmt.Errorf("no client available for chain ID") | ||
| return | ||
| } | ||
| s.chainID, s.chainIDError = client.GetChainId(ctx) | ||
| }) | ||
| return s.chainID, s.chainIDError | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
the ChainId is cached and can be accessed via s.walletPool.GetChainId()
| // waitForPendingTxSlot waits until we have capacity for another transaction | ||
| func (s *Scenario) waitForPendingTxSlot(ctx context.Context) { | ||
| for { | ||
| s.pendingTxsMutex.RLock() | ||
| count := len(s.pendingTxs) | ||
| s.pendingTxsMutex.RUnlock() | ||
|
|
||
| if count < int(s.options.MaxPending) { | ||
| return | ||
| } | ||
|
|
||
| // Check and clean up confirmed transactions | ||
| s.processPendingTransactions(ctx) | ||
| time.Sleep(1 * time.Second) | ||
| } | ||
| } | ||
|
|
||
| // processPendingTransactions checks for transaction confirmations and updates state | ||
| func (s *Scenario) processPendingTransactions(ctx context.Context) { | ||
| s.pendingTxsMutex.Lock() | ||
|
|
||
| client := s.walletPool.GetClient(spamoor.SelectClientByIndex, 0, s.options.ClientGroup) | ||
| if client == nil { | ||
| s.pendingTxsMutex.Unlock() | ||
| return | ||
| } | ||
|
|
||
| ethClient := client.GetEthClient() | ||
| var confirmedTxs []common.Hash | ||
| var timedOutTxs []common.Hash | ||
| var successfulDeployments []struct { | ||
| ContractAddress common.Address | ||
| PrivateKey *ecdsa.PrivateKey | ||
| Receipt *types.Receipt | ||
| TxHash common.Hash | ||
| } | ||
|
|
||
| for txHash, pendingTx := range s.pendingTxs { | ||
| // Check if transaction is too old (1 minute timeout) | ||
| if time.Since(pendingTx.Timestamp) > 1*time.Minute { | ||
| s.logger.Warnf("Transaction %s timed out after 1 minute, removing from pending", txHash.Hex()) | ||
| timedOutTxs = append(timedOutTxs, txHash) | ||
| continue | ||
| } | ||
|
|
||
| receipt, err := ethClient.TransactionReceipt(ctx, txHash) | ||
| if err != nil { | ||
| // Transaction still pending or error retrieving receipt | ||
| continue | ||
| } | ||
|
|
||
| confirmedTxs = append(confirmedTxs, txHash) | ||
|
|
||
| // Process successful deployment | ||
| if receipt.Status == 1 && receipt.ContractAddress != (common.Address{}) { | ||
| successfulDeployments = append(successfulDeployments, struct { | ||
| ContractAddress common.Address | ||
| PrivateKey *ecdsa.PrivateKey | ||
| Receipt *types.Receipt | ||
| TxHash common.Hash | ||
| }{ | ||
| ContractAddress: receipt.ContractAddress, | ||
| PrivateKey: pendingTx.PrivateKey, | ||
| Receipt: receipt, | ||
| TxHash: txHash, | ||
| }) | ||
| } | ||
| } | ||
|
|
||
| // Remove confirmed transactions from pending map | ||
| for _, txHash := range confirmedTxs { | ||
| delete(s.pendingTxs, txHash) | ||
| } | ||
|
|
||
| // Remove timed out transactions from pending map | ||
| for _, txHash := range timedOutTxs { | ||
| delete(s.pendingTxs, txHash) | ||
| } | ||
|
|
||
| s.pendingTxsMutex.Unlock() | ||
|
|
||
| // Process successful deployments after releasing the lock | ||
| for _, deployment := range successfulDeployments { | ||
| s.recordDeployedContract(deployment.ContractAddress, deployment.PrivateKey, deployment.Receipt, deployment.TxHash) | ||
| } | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
this duplicates the transaction tracking logic and shouldn't be necessary.
You'll get the receipt in the SendTransactionOptions.OnConfirm callback, so you should just call s.recordDeployedContract from there.
| PrivateKey: fmt.Sprintf("0x%x", crypto.FromECDSA(privateKey)), | ||
| } | ||
|
|
||
| s.deployedContracts = append(s.deployedContracts, deployment) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This feels like a memory leak, as the deployedContracts slice can grow over hours and cause OOM kills at some point.
Consider adding those entries in a append-style file.
maybe as yaml array, so you can easily append lines without reading & re-writing the whole file:
- {"ContractAddress": "0x...", "PrivateKey": "0x..."}
- {"ContractAddress": "0x...", "PrivateKey": "0x..."}
...| // Create or update current block stats (removed the old logging logic) | ||
| if s.blockStats[blockNumber] == nil { | ||
| s.blockStats[blockNumber] = &BlockDeploymentStats{ | ||
| BlockNumber: blockNumber, | ||
| } | ||
| s.logger.WithField("block_number", blockNumber).Debug("Created new block stats") | ||
| } | ||
|
|
||
| blockStat := s.blockStats[blockNumber] | ||
| blockStat.ContractCount++ | ||
| blockStat.TotalGasUsed += receipt.GasUsed | ||
| blockStat.TotalBytecodeSize += bytecodeSize | ||
|
|
||
| s.logger.WithFields(logrus.Fields{ | ||
| "block_number": blockNumber, | ||
| "contracts_in_block": blockStat.ContractCount, | ||
| "gas_used": blockStat.TotalGasUsed, | ||
| "bytecode_size": blockStat.TotalBytecodeSize, | ||
| }).Debug("Updated block stats") |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
shouldn't be necessary, subscribe to block notifications as mentioned above if you want per block stats.
| // Calculate rate limiting based on block gas limit if max-transactions is 0 | ||
| var maxTxsPerBlock uint64 | ||
| var useRateLimiting bool | ||
|
|
||
| if s.options.MaxTransactions == 0 { | ||
| blockGasLimit := currentBlock.GasLimit() | ||
| // TODO: This should be a constant. | ||
| estimatedGasPerContract := uint64(4949468) // Updated estimate based on contract size reduction | ||
| expectedThroughput := blockGasLimit / estimatedGasPerContract | ||
| maxTxsPerBlock = expectedThroughput * 2 // Set rate limit to 2x expected throughput | ||
| useRateLimiting = true | ||
|
|
||
| s.logger.Infof("Rate limiting enabled: block gas limit %d, gas per contract %d, expected throughput %d, rate limit %d txs/block", | ||
| blockGasLimit, estimatedGasPerContract, expectedThroughput, maxTxsPerBlock) | ||
| } | ||
|
|
||
| txIdxCounter := uint64(0) | ||
| totalTxCount := atomic.Uint64{} | ||
| blockTxCount := uint64(0) | ||
| lastBlockNumber := currentBlock.Number().Uint64() | ||
|
|
||
| for { | ||
| // Check if we've reached max transactions (if set) | ||
| if s.options.MaxTransactions > 0 && txIdxCounter >= s.options.MaxTransactions { | ||
| s.logger.Infof("reached maximum number of transactions (%d)", s.options.MaxTransactions) | ||
| break | ||
| } | ||
|
|
||
| // Rate limiting logic | ||
| if useRateLimiting { | ||
| // If we've sent expected throughput transactions, wait for next block | ||
| if blockTxCount >= maxTxsPerBlock/2 { // Send throughput amount, not rate limit amount | ||
| s.logger.Infof("Sent %d txs, waiting for next block", blockTxCount) | ||
|
|
||
| // Wait for block number to change | ||
| for { | ||
| currentBlock, err := ethClient.BlockByNumber(ctx, nil) | ||
| if err == nil && currentBlock.Number().Uint64() > lastBlockNumber { | ||
| lastBlockNumber = currentBlock.Number().Uint64() | ||
| blockTxCount = 0 | ||
| break | ||
| } | ||
| time.Sleep(100 * time.Millisecond) | ||
| } | ||
| } | ||
| } | ||
|
|
||
| // Wait for available slot | ||
| s.waitForPendingTxSlot(ctx) | ||
|
|
||
| // Send a single transaction | ||
| err := s.sendTransaction(ctx, txIdxCounter) | ||
| if err != nil { | ||
| s.logger.Warnf("failed to send transaction %d: %v", txIdxCounter, err) | ||
| time.Sleep(1 * time.Second) | ||
| continue | ||
| } | ||
|
|
||
| txIdxCounter++ | ||
| totalTxCount.Add(1) | ||
| blockTxCount++ | ||
|
|
||
| // Process pending transactions periodically with 1 second intervals | ||
| if txIdxCounter%10 == 0 { | ||
| s.processPendingTransactions(ctx) | ||
|
|
||
| s.contractsMutex.Lock() | ||
| contractCount := len(s.deployedContracts) | ||
| s.contractsMutex.Unlock() | ||
|
|
||
| s.logger.Infof("Progress: sent %d txs, deployed %d contracts", txIdxCounter, contractCount) | ||
| } | ||
|
|
||
| // Small delay to prevent overwhelming the RPC | ||
| time.Sleep(100 * time.Millisecond) | ||
| } | ||
|
|
||
| // Wait for all pending transactions to complete with 1 second intervals | ||
| s.logger.Info("Waiting for remaining transactions to complete...") | ||
| for { | ||
| s.processPendingTransactions(ctx) | ||
|
|
||
| s.pendingTxsMutex.RLock() | ||
| pendingCount := len(s.pendingTxs) | ||
| s.pendingTxsMutex.RUnlock() | ||
|
|
||
| if pendingCount == 0 { | ||
| break | ||
| } | ||
|
|
||
| s.logger.Infof("Waiting for %d pending transactions...", pendingCount) | ||
| time.Sleep(1 * time.Second) // Changed from 2 seconds to 1 second | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
this duplicates the scenario.RunTransactionScenario helper, that is shared across almost all scenarios to avoid duplicating the core transaction count / throughput & pending limitations.
it should be something like this:
err := scenario.RunTransactionScenario(ctx, scenario.TransactionScenarioOptions{
TotalCount: s.options.MaxTransactions,
Throughput: maxTxsPerBlock,
MaxPending: maxPending,
Timeout: timeout,
WalletPool: s.walletPool,
Logger: s.logger,
ProcessNextTxFn: func(ctx context.Context, txIdx uint64, onComplete func()) (func(), error) {
tx, err := s.sendTransaction(ctx, txIdxCounter, onComplete) // ensure onComplete gets called when the tx completes (either an error before/on submission or confirmation)
return func() {
if err != nil {
logger.Warnf("could not send transaction: %v", err)
} else if s.options.LogTxs {
logger.Infof("sent tx #%6d: %v", txIdx+1, tx.Hash().String())
} else {
logger.Debugf("sent tx #%6d: %v", txIdx+1, tx.Hash().String())
}
}, err
},
})| for attempt := 0; attempt < maxRetries; attempt++ { | ||
| err := s.attemptTransaction(ctx, txIdx, attempt) | ||
| if err == nil { | ||
| return nil | ||
| } | ||
|
|
||
| // Check if it's a base fee error | ||
| if strings.Contains(err.Error(), "max fee per gas less than block base fee") { | ||
| s.logger.Warnf("Transaction %d base fee too low, adjusting fees and retrying (attempt %d/%d)", | ||
| txIdx, attempt+1, maxRetries) | ||
|
|
||
| // Update fees based on current network conditions | ||
| if updateErr := s.updateDynamicFees(ctx); updateErr != nil { | ||
| s.logger.Warnf("Failed to update dynamic fees: %v", updateErr) | ||
| } | ||
|
|
||
| time.Sleep(time.Duration(attempt+1) * 500 * time.Millisecond) // Exponential backoff | ||
| continue | ||
| } | ||
|
|
||
| // For other errors, return immediately | ||
| return err | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
this gas increasing retry loop shouldn't be necessary. Send transactions with a static base fee. You might get a empty block if the base fee is too high, but that's fine, as otherwise the base fee grows exponentially and you can't afford the fee over long term even with nearly unlimited funds.
| func (s *Scenario) updateDynamicFees(ctx context.Context) error { | ||
| client := s.walletPool.GetClient(spamoor.SelectClientByIndex, 0, s.options.ClientGroup) | ||
| if client == nil { | ||
| return fmt.Errorf("no client available") | ||
| } | ||
|
|
||
| ethClient := client.GetEthClient() | ||
|
|
||
| // Get the latest block to check current base fee | ||
| latestBlock, err := ethClient.BlockByNumber(ctx, nil) | ||
| if err != nil { | ||
| return fmt.Errorf("failed to get latest block: %w", err) | ||
| } | ||
|
|
||
| if latestBlock.BaseFee() != nil { | ||
| // Convert base fee from wei to gwei | ||
| currentBaseFeeGwei := new(big.Int).Div(latestBlock.BaseFee(), big.NewInt(1000000000)) | ||
|
|
||
| newBaseFeeGwei := new(big.Int).Add(currentBaseFeeGwei, big.NewInt(100)) | ||
|
|
||
| s.options.BaseFee = newBaseFeeGwei.Uint64() | ||
|
|
||
| // Also increase tip fee slightly to ensure competitive priority | ||
| if s.options.TipFee+1 > 3 { | ||
| s.options.TipFee = s.options.TipFee + 1 | ||
| } else { | ||
| s.options.TipFee = 2 // Minimum 3 gwei tip | ||
| } | ||
|
|
||
| s.logger.Infof("Updated dynamic fees - Base fee: %d gwei, Tip fee: %d gwei (network base fee: %s gwei)", | ||
| s.options.BaseFee, s.options.TipFee, currentBaseFeeGwei.String()) | ||
| } | ||
|
|
||
| return nil | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
the block base fee is tracked in the TxPool and can be requested via TxPool.GetCurrentBaseFee / TxPool.GetCurrentBaseFeeWithInit
| addr := crypto.PubkeyToAddress(wallet.GetWallet().GetPrivateKey().PublicKey) | ||
| nonce, err := client.GetEthClient().PendingNonceAt(ctx, addr) | ||
| if err != nil { | ||
| return fmt.Errorf("failed to get nonce for %s: %w", addr.Hex(), err) | ||
| } | ||
|
|
||
| // Create transaction auth | ||
| auth, err := bind.NewKeyedTransactorWithChainID(wallet.GetWallet().GetPrivateKey(), chainID) | ||
| if err != nil { | ||
| return fmt.Errorf("failed to create auth: %w", err) | ||
| } | ||
|
|
||
| auth.Nonce = big.NewInt(int64(nonce)) | ||
| auth.Value = big.NewInt(0) | ||
| auth.GasLimit = 5200000 // Fixed gas limit for contract deployment | ||
|
|
||
| // Set EIP-1559 fee parameters | ||
| if s.options.BaseFee > 0 { | ||
| auth.GasFeeCap = new(big.Int).Mul(big.NewInt(int64(s.options.BaseFee)), big.NewInt(1000000000)) | ||
| } | ||
| if s.options.TipFee > 0 { | ||
| auth.GasTipCap = new(big.Int).Mul(big.NewInt(int64(s.options.TipFee)), big.NewInt(1000000000)) | ||
| } | ||
|
|
||
| // Generate random salt for unique contract | ||
| salt := make([]byte, 32) | ||
| _, err = rand.Read(salt) | ||
| if err != nil { | ||
| return fmt.Errorf("failed to generate salt: %w", err) | ||
| } | ||
| saltInt := new(big.Int).SetBytes(salt) | ||
|
|
||
| // Deploy the contract | ||
| ethClient := client.GetEthClient() | ||
| if ethClient == nil { | ||
| return fmt.Errorf("failed to get eth client") | ||
| } | ||
|
|
||
| _, tx, _, err := contract.DeployContract(auth, ethClient, saltInt) | ||
| if err != nil { | ||
| return fmt.Errorf("failed to deploy contract: %w", err) | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nonce tracking needs to be done within the local wallet, otherwise you'll get duplicate transactions as previous transactions are still being gossiped or otherwise processed.
the spamoor.Wallet provides appropriate methods to build transactions with a thread safe tracked nonce:
feeCap, tipCap, err := s.walletPool.GetTxPool().GetSuggestedFees(client, s.options.BaseFee, s.options.TipFee)
if err != nil {
return nil, client, err
}
// Generate random salt for unique contract
salt := make([]byte, 32)
_, err = rand.Read(salt)
if err != nil {
return fmt.Errorf("failed to generate salt: %w", err)
}
saltInt := new(big.Int).SetBytes(salt)
tx, err := wallet.BuildBoundTx(ctx, &txbuilder.TxMetadata{
GasFeeCap: uint256.MustFromBig(feeCap),
GasTipCap: uint256.MustFromBig(tipCap),
Gas: 2000000,
Value: uint256.NewInt(0),
}, func(transactOpts *bind.TransactOpts) (*types.Transaction, error) {
_, deployTx, _, err := contract.DeployContract(transactOpts, client.GetEthClient(), saltInt)
return deployTx, err
})| @@ -0,0 +1 @@ | |||
| [{"inputs":[{"internalType":"uint256","name":"_salt","type":"uint256"}],"stateMutability":"nonpayable","type":"constructor"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"address","name":"owner","type":"address"},{"indexed":true,"internalType":"address","name":"spender","type":"address"},{"indexed":false,"internalType":"uint256","name":"value","type":"uint256"}],"name":"Approval","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"address","name":"from","type":"address"},{"indexed":true,"internalType":"address","name":"to","type":"address"},{"indexed":false,"internalType":"uint256","name":"value","type":"uint256"}],"name":"Transfer","type":"event"},{"inputs":[{"internalType":"address","name":"","type":"address"},{"internalType":"address","name":"","type":"address"}],"name":"allowance","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"spender","type":"address"},{"internalType":"uint256","name":"value","type":"uint256"}],"name":"approve","outputs":[{"internalType":"bool","name":"","type":"bool"}],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"","type":"address"}],"name":"balanceOf","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"decimals","outputs":[{"internalType":"uint8","name":"","type":"uint8"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"dummy1","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"pure","type":"function"},{"inputs":[],"name":"dummy10","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"pure","type":"function"},{"inputs":[],"name":"dummy11","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"pure","type":"function"},{"inputs":[],"name":"dummy12","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"pure","type":"function"},{"inputs":[],"name":"dummy13","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"pure","type":"function"},{"inputs":[],"name":"dummy14","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"pure","type":"function"},{"inputs":[],"name":"dummy15","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"pure","type":"function"},{"inputs":[],"name":"dummy16","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"pure","type":"function"},{"inputs":[],"name":"dummy17","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"pure","type":"function"},{"inputs":[],"name":"dummy18","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"pure","type":"function"},{"inputs":[],"name":"dummy19","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"pure","type":"function"},{"inputs":[],"name":"dummy2","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"pure","type":"function"},{"inputs":[],"name":"dummy20","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"pure","type":"function"},{"inputs":[],"name":"dummy21","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"pure","type":"function"},{"inputs":[],"name":"dummy22","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"pure","type":"function"},{"inputs":[],"name":"dummy23","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"pure","type":"function"},{"inputs":[],"name":"dummy24","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"pure","type":"function"},{"inputs":[],"name":"dummy25","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"pure","type":"function"},{"inputs":[],"name":"dummy26","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"pure","type":"function"},{"inputs":[],"name":"dummy27","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"pure","type":"function"},{"inputs":[],"name":"dummy28","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"pure","type":"function"},{"inputs":[],"name":"dummy29","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"pure","type":"function"},{"inputs":[],"name":"dummy3","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"pure","type":"function"},{"inputs":[],"name":"dummy30","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"pure","type":"function"},{"inputs":[],"name":"dummy31","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"pure","type":"function"},{"inputs":[],"name":"dummy32","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"pure","type":"function"},{"inputs":[],"name":"dummy33","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"pure","type":"function"},{"inputs":[],"name":"dummy34","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"pure","type":"function"},{"inputs":[],"name":"dummy35","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"pure","type":"function"},{"inputs":[],"name":"dummy36","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"pure","type":"function"},{"inputs":[],"name":"dummy37","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"pure","type":"function"},{"inputs":[],"name":"dummy38","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"pure","type":"function"},{"inputs":[],"name":"dummy39","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"pure","type":"function"},{"inputs":[],"name":"dummy4","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"pure","type":"function"},{"inputs":[],"name":"dummy40","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"pure","type":"function"},{"inputs":[],"name":"dummy41","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"pure","type":"function"},{"inputs":[],"name":"dummy42","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"pure","type":"function"},{"inputs":[],"name":"dummy43","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"pure","type":"function"},{"inputs":[],"name":"dummy44","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"pure","type":"function"},{"inputs":[],"name":"dummy45","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"pure","type":"function"},{"inputs":[],"name":"dummy46","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"pure","type":"function"},{"inputs":[],"name":"dummy47","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"pure","type":"function"},{"inputs":[],"name":"dummy48","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"pure","type":"function"},{"inputs":[],"name":"dummy49","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"pure","type":"function"},{"inputs":[],"name":"dummy5","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"pure","type":"function"},{"inputs":[],"name":"dummy50","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"pure","type":"function"},{"inputs":[],"name":"dummy51","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"pure","type":"function"},{"inputs":[],"name":"dummy52","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"pure","type":"function"},{"inputs":[],"name":"dummy53","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"pure","type":"function"},{"inputs":[],"name":"dummy54","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"pure","type":"function"},{"inputs":[],"name":"dummy55","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"pure","type":"function"},{"inputs":[],"name":"dummy56","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"pure","type":"function"},{"inputs":[],"name":"dummy57","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"pure","type":"function"},{"inputs":[],"name":"dummy58","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"pure","type":"function"},{"inputs":[],"name":"dummy59","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"pure","type":"function"},{"inputs":[],"name":"dummy6","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"pure","type":"function"},{"inputs":[],"name":"dummy60","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"pure","type":"function"},{"inputs":[],"name":"dummy61","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"pure","type":"function"},{"inputs":[],"name":"dummy62","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"pure","type":"function"},{"inputs":[],"name":"dummy63","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"pure","type":"function"},{"inputs":[],"name":"dummy64","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"pure","type":"function"},{"inputs":[],"name":"dummy65","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"pure","type":"function"},{"inputs":[],"name":"dummy7","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"pure","type":"function"},{"inputs":[],"name":"dummy8","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"pure","type":"function"},{"inputs":[],"name":"dummy9","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"pure","type":"function"},{"inputs":[],"name":"name","outputs":[{"internalType":"string","name":"","type":"string"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"salt","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"symbol","outputs":[{"internalType":"string","name":"","type":"string"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"totalSupply","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"to","type":"address"},{"internalType":"uint256","name":"value","type":"uint256"}],"name":"transfer","outputs":[{"internalType":"bool","name":"","type":"bool"}],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"from","type":"address"},{"internalType":"address","name":"to","type":"address"},{"internalType":"uint256","name":"value","type":"uint256"}],"name":"transferFrom","outputs":[{"internalType":"bool","name":"","type":"bool"}],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"from","type":"address"},{"internalType":"address","name":"to","type":"address"},{"internalType":"uint256","name":"value","type":"uint256"}],"name":"transferFrom1","outputs":[{"internalType":"bool","name":"","type":"bool"}],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"from","type":"address"},{"internalType":"address","name":"to","type":"address"},{"internalType":"uint256","name":"value","type":"uint256"}],"name":"transferFrom10","outputs":[{"internalType":"bool","name":"","type":"bool"}],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"from","type":"address"},{"internalType":"address","name":"to","type":"address"},{"internalType":"uint256","name":"value","type":"uint256"}],"name":"transferFrom11","outputs":[{"internalType":"bool","name":"","type":"bool"}],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"from","type":"address"},{"internalType":"address","name":"to","type":"address"},{"internalType":"uint256","name":"value","type":"uint256"}],"name":"transferFrom12","outputs":[{"internalType":"bool","name":"","type":"bool"}],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"from","type":"address"},{"internalType":"address","name":"to","type":"address"},{"internalType":"uint256","name":"value","type":"uint256"}],"name":"transferFrom13","outputs":[{"internalType":"bool","name":"","type":"bool"}],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"from","type":"address"},{"internalType":"address","name":"to","type":"address"},{"internalType":"uint256","name":"value","type":"uint256"}],"name":"transferFrom14","outputs":[{"internalType":"bool","name":"","type":"bool"}],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"from","type":"address"},{"internalType":"address","name":"to","type":"address"},{"internalType":"uint256","name":"value","type":"uint256"}],"name":"transferFrom15","outputs":[{"internalType":"bool","name":"","type":"bool"}],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"from","type":"address"},{"internalType":"address","name":"to","type":"address"},{"internalType":"uint256","name":"value","type":"uint256"}],"name":"transferFrom16","outputs":[{"internalType":"bool","name":"","type":"bool"}],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"from","type":"address"},{"internalType":"address","name":"to","type":"address"},{"internalType":"uint256","name":"value","type":"uint256"}],"name":"transferFrom17","outputs":[{"internalType":"bool","name":"","type":"bool"}],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"from","type":"address"},{"internalType":"address","name":"to","type":"address"},{"internalType":"uint256","name":"value","type":"uint256"}],"name":"transferFrom18","outputs":[{"internalType":"bool","name":"","type":"bool"}],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"from","type":"address"},{"internalType":"address","name":"to","type":"address"},{"internalType":"uint256","name":"value","type":"uint256"}],"name":"transferFrom19","outputs":[{"internalType":"bool","name":"","type":"bool"}],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"from","type":"address"},{"internalType":"address","name":"to","type":"address"},{"internalType":"uint256","name":"value","type":"uint256"}],"name":"transferFrom2","outputs":[{"internalType":"bool","name":"","type":"bool"}],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"from","type":"address"},{"internalType":"address","name":"to","type":"address"},{"internalType":"uint256","name":"value","type":"uint256"}],"name":"transferFrom3","outputs":[{"internalType":"bool","name":"","type":"bool"}],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"from","type":"address"},{"internalType":"address","name":"to","type":"address"},{"internalType":"uint256","name":"value","type":"uint256"}],"name":"transferFrom4","outputs":[{"internalType":"bool","name":"","type":"bool"}],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"from","type":"address"},{"internalType":"address","name":"to","type":"address"},{"internalType":"uint256","name":"value","type":"uint256"}],"name":"transferFrom5","outputs":[{"internalType":"bool","name":"","type":"bool"}],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"from","type":"address"},{"internalType":"address","name":"to","type":"address"},{"internalType":"uint256","name":"value","type":"uint256"}],"name":"transferFrom6","outputs":[{"internalType":"bool","name":"","type":"bool"}],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"from","type":"address"},{"internalType":"address","name":"to","type":"address"},{"internalType":"uint256","name":"value","type":"uint256"}],"name":"transferFrom7","outputs":[{"internalType":"bool","name":"","type":"bool"}],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"from","type":"address"},{"internalType":"address","name":"to","type":"address"},{"internalType":"uint256","name":"value","type":"uint256"}],"name":"transferFrom8","outputs":[{"internalType":"bool","name":"","type":"bool"}],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"from","type":"address"},{"internalType":"address","name":"to","type":"address"},{"internalType":"uint256","name":"value","type":"uint256"}],"name":"transferFrom9","outputs":[{"internalType":"bool","name":"","type":"bool"}],"stateMutability":"nonpayable","type":"function"}] No newline at end of file | |||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
please exclude the .abi & .bin from the repository and copy/amend the compile.sh from another scenario.
that way all contracts are built the same way, which is way easier to maintain.
|
Heya @CPerezz, |
🏭 Contract Deployment State Bloat
This scenario deploys contracts that are exactly 24kB in size (EIP-170 limit) to maximize state growth while minimizing gas cost.
How it Works
⛽ Gas Cost Breakdown
Batch Strategy
The scenario automatically calculates how many contracts can fit in one block:
This ensures optimal utilization of block space while maintaining predictable transaction inclusion patterns.
🚀 Usage
Build
Run
Key Flags
--max-transactions- Total number of contracts to deploy (0 = infinite, default: 0)--max-wallets- Max child wallets to use (0 = root wallet only, default: 0)--basefee- Base fee per gas in gwei (default: 10)--tipfee- Tip fee per gas in gwei (default: 2)Example with Anvil node