11---
22description: Spamoor Code Standards and Conventions
3+ globs:
34alwaysApply: false
45---
56
67# Spamoor Code Standards
78
9+ ## Critical Development Rules
10+
11+ 🚨 **NEVER use the root wallet directly in scenarios** - it's shared across all running scenarios and direct usage will cause nonce conflicts.
12+
13+ 🚨 **NEVER use go-ethereum's bound contracts for transactions** - they manage nonces independently and will conflict with Spamoor's nonce tracking. Always use `BuildBoundTx` pattern.
14+
15+ 🚨 **ALWAYS spread transactions across multiple wallets** - Ethereum clients have limits on pending transactions per sender (typically 64-1000).
16+
17+ 🚨 **ALWAYS respect context cancellation** - scenarios must stop all operations when context is cancelled.
18+
19+ 🚨 **ALWAYS call onComplete() in ProcessNextTxFn** - required for scenario.RunTransactionScenario transaction counting.
20+
21+ 🚨 **NEVER assume receipt is non-nil in OnComplete** - handle cancellation and replacement transaction scenarios properly.
22+
823## Go Language Standards
924
1025### Naming Conventions
@@ -14,25 +29,40 @@ alwaysApply: false
1429- **Variables**: camelCase for local, PascalCase for exported package-level
1530- **Constants**: PascalCase for exported, camelCase for unexported
1631- **Scenario names**: lowercase with hyphens (e.g., `deploy-destruct`)
32+ - **Well-known wallets**: lowercase with hyphens (e.g., `deployer`, `token-owner`)
1733
1834### File Organization
1935- One main type per file when possible
2036- Group related functionality in the same file
2137- Use descriptive filenames that match primary functionality
2238- Scenario directories: `scenarios/<scenario-name>/`
2339- Each scenario has its own package named after the scenario
40+ - Contract bindings in `contract/` subdirectory
2441
2542### Code Structure Patterns
2643
2744#### Scenario Implementation
2845```go
29- // Standard scenario structure
46+ // Standard scenario structure with all common options
3047type ScenarioOptions struct {
31- // Common fields present in most scenarios
32- TotalCount uint64 `yaml:"total_count"`
33- Throughput uint64 `yaml:"throughput"`
34- MaxPending uint64 `yaml:"max_pending"`
35- // Scenario-specific fields
48+ // Transaction Control
49+ TotalCount uint64 `yaml:"total_count"` // Total transactions (0 = unlimited)
50+ Throughput uint64 `yaml:"throughput"` // Transactions per slot
51+ MaxPending uint64 `yaml:"max_pending"` // Maximum concurrent pending
52+ MaxWallets uint64 `yaml:"max_wallets"` // Maximum child wallets
53+
54+ // Gas Configuration
55+ BaseFee uint64 `yaml:"base_fee"` // Base fee in gwei
56+ TipFee uint64 `yaml:"tip_fee"` // Priority fee in gwei
57+ GasLimit uint64 `yaml:"gas_limit"` // Gas limit per transaction
58+
59+ // Client Control
60+ ClientGroup string `yaml:"client_group"` // Preferred client group
61+
62+ // Logging
63+ LogTxs bool `yaml:"log_txs"` // Log individual transactions
64+
65+ // Scenario-specific options...
3666}
3767
3868type Scenario struct {
@@ -42,79 +72,253 @@ type Scenario struct {
4272}
4373
4474var ScenarioName = "scenario-name"
75+ var ScenarioDefaultOptions = ScenarioOptions{
76+ TotalCount: 0,
77+ Throughput: 10,
78+ MaxPending: 0,
79+ MaxWallets: 0,
80+ BaseFee: 20,
81+ TipFee: 2,
82+ GasLimit: 21000,
83+ ClientGroup: "",
84+ LogTxs: false,
85+ }
86+ ```
87+
88+ #### Scenario Registration
89+ ```go
90+ // In scenarios/scenarios.go
91+ var ScenarioDescriptors = []*scenario.Descriptor{
92+ &myscenario.ScenarioDescriptor,
93+ }
94+
95+ // In your scenario package
96+ var ScenarioDescriptor = scenario.Descriptor{
97+ Name: ScenarioName,
98+ Description: "Clear description of scenario purpose",
99+ DefaultOptions: ScenarioDefaultOptions,
100+ NewScenario: newScenario,
101+ }
45102```
46103
47104#### Error Handling
48105- Use explicit error returns: `func() (ReturnType, error)`
49106- Wrap errors with context: `fmt.Errorf("operation failed: %w", err)`
50107- Log errors at appropriate levels (Error, Warn, Info, Debug)
51108- Use structured logging with logrus
109+ - Handle nil receipts in transaction callbacks
52110
53111#### Configuration
54112- Use YAML tags for all configuration structs
55113- Support both command-line flags and YAML configuration
56114- Use spf13/pflag for command-line parsing
57115- Provide sensible defaults for all options
116+ - Validate option combinations in Init()
117+
118+ ### Transaction Patterns
119+
120+ #### Contract Interactions (BuildBoundTx)
121+ ```go
122+ // CORRECT: Use BuildBoundTx for contract calls
123+ tx, err := wallet.BuildBoundTx(ctx, &txbuilder.TxMetadata{
124+ GasTipCap: uint256.MustFromBig(tipCap),
125+ GasFeeCap: uint256.MustFromBig(feeCap),
126+ Gas: gasLimit,
127+ }, func(transactOpts *bind.TransactOpts) (*types.Transaction, error) {
128+ return contract.Transfer(transactOpts, toAddr, amount)
129+ })
130+
131+ // WRONG: Never use bound contracts with a separate transaction context directly
132+ // tx, err := contract.Transfer(transactOpts, toAddr, amount) // DON'T DO THIS
133+ ```
134+
135+ #### Transaction Building
136+ ```go
137+ // Always use txbuilder package
138+ import "github.com/ethpandaops/spamoor/txbuilder"
139+
140+ txData := &txbuilder.TxMetadata{
141+ GasTipCap: uint256.NewInt(tipCap),
142+ GasFeeCap: uint256.NewInt(feeCap),
143+ Gas: gasLimit,
144+ To: &targetAddr,
145+ Value: uint256.NewInt(amount),
146+ Data: callData,
147+ }
148+
149+ // Create appropriate transaction type
150+ dynFeeTx, err := txbuilder.DynFeeTx(txData) // EIP-1559
151+ blobTx, err := txbuilder.BlobTx(txData, blobs) // EIP-4844
152+ ```
153+
154+ ### Wallet Management Patterns
155+
156+ #### Wallet Pool Configuration
157+ ```go
158+ func (s *Scenario) Init(options *scenario.Options) error {
159+ // Configure child wallets based on throughput
160+ if s.options.MaxWallets > 0 {
161+ s.walletPool.SetWalletCount(s.options.MaxWallets)
162+ } else if s.options.TotalCount > 0 {
163+ maxWallets := s.options.TotalCount / 50
164+ if maxWallets < 10 {
165+ maxWallets = 10
166+ } else if maxWallets > 1000 {
167+ maxWallets = 1000
168+ }
169+ s.walletPool.SetWalletCount(maxWallets)
170+ } else {
171+ walletCount := s.options.Throughput * 2
172+ if walletCount > 1000 {
173+ walletCount = 1000
174+ }
175+ s.walletPool.SetWalletCount(walletCount)
176+ }
177+
178+ // Configure funding
179+ s.walletPool.SetRefillAmount(utils.EtherToWei(uint256.NewInt(5)))
180+ s.walletPool.SetRefillBalance(utils.EtherToWei(uint256.NewInt(1)))
181+
182+ // Add well-known wallets
183+ s.walletPool.AddWellKnownWallet(&spamoor.WellKnownWalletConfig{
184+ Name: "deployer",
185+ RefillAmount: utils.EtherToWei(uint256.NewInt(50)),
186+ RefillBalance: utils.EtherToWei(uint256.NewInt(10)),
187+ VeryWellKnown: false, // Scenario-specific
188+ })
189+
190+ return nil
191+ }
192+ ```
193+
194+ #### Wallet Selection
195+ ```go
196+ // For high throughput, use pending-count-based selection
197+ wallet := s.walletPool.GetWallet(spamoor.SelectWalletByPendingTxCount, txIdx)
198+
199+ // For predictable selection
200+ wallet := s.walletPool.GetWallet(spamoor.SelectWalletByIndex, txIdx)
201+
202+ // For well-known wallets
203+ deployerWallet := s.walletPool.GetWellKnownWallet("deployer")
204+ ```
205+
206+ ### Transaction Submission Patterns
207+
208+ #### Asynchronous Submission
209+ ```go
210+ err := txpool.SendTransaction(ctx, wallet, signedTx, &spamoor.SendTransactionOptions{
211+ Client: client,
212+ Rebroadcast: true,
213+ OnComplete: func(tx *types.Transaction, receipt *types.Receipt, err error) {
214+ // ALWAYS call onComplete when using RunTransactionScenario
215+ onComplete()
216+
217+ if err != nil {
218+ s.logger.Warnf("Transaction error: %v", err)
219+ return
220+ }
221+ },
222+ OnConfirm: func(tx *types.Transaction, receipt *types.Receipt) {
223+ txFees := utils.GetTransactionFees(tx, receipt)
224+
225+ if receipt.Status == types.ReceiptStatusSuccessful {
226+ s.handleSuccess(tx, receipt, txFees)
227+ } else {
228+ s.logger.Warnf("Transaction reverted: %s", tx.Hash().Hex())
229+ }
230+ },
231+ })
232+ ```
233+
234+ #### Batch Submission
235+ ```go
236+ // Single wallet batch
237+ receipts, err := txpool.SendTransactionBatch(ctx, wallet, transactions, &spamoor.BatchOptions{
238+ SendTransactionOptions: spamoor.SendTransactionOptions{
239+ Client: client,
240+ Rebroadcast: true,
241+ },
242+ PendingLimit: 50,
243+ })
244+
245+ // Multi-wallet batch (preferred for high throughput)
246+ receipts, err := txpool.SendMultiTransactionBatch(ctx, walletTxs, &spamoor.BatchOptions{
247+ SendTransactionOptions: spamoor.SendTransactionOptions{
248+ Rebroadcast: true,
249+ },
250+ PendingLimit: 50,
251+ })
252+ ```
58253
59254### Documentation Standards
60255- Public functions/types must have godoc comments
61256- Comments should explain WHY, not just WHAT
62257- Each scenario must have a comprehensive README.md
63258- Use examples in documentation when helpful
64- - API endpoints must be documented with Swagger annotations
259+ - Document scenario-specific options clearly
260+ - Include troubleshooting section in READMEs
65261
66262### Import Organization
67263```go
68- // Standard library imports first
69264import (
265+ // Standard library imports first
70266 "context"
71267 "fmt"
268+ "math/big"
72269 "time"
73- )
74270
75- // Third-party imports second (grouped by organization)
76- import (
271+ // Third-party imports second (grouped by organization)
272+ "github.com/ethereum/go-ethereum/accounts/abi/bind"
77273 "github.com/ethereum/go-ethereum/common"
78274 "github.com/ethereum/go-ethereum/core/types"
79-
275+ "github.com/holiman/uint256"
80276 "github.com/sirupsen/logrus"
81277 "github.com/spf13/pflag"
82-
83278 "gopkg.in/yaml.v3"
84- )
85279
86- // Local imports last
87- import (
280+ // Local imports last
88281 "github.com/ethpandaops/spamoor/scenario"
89282 "github.com/ethpandaops/spamoor/spamoor"
283+ "github.com/ethpandaops/spamoor/txbuilder"
284+ "github.com/ethpandaops/spamoor/utils"
90285)
91286```
92287
93288### Logging Standards
94289- Use structured logging with logrus
95- - Create named loggers: `logger := logrus.WithField("component ", "scenario-name" )`
290+ - Create named loggers: `logger := logrus.WithField("scenario ", ScenarioName )`
96291- Log levels:
97292 - **Error**: System failures, critical issues
98- - **Warn**: Recoverable problems, deprecated usage
293+ - **Warn**: Recoverable problems, failed transactions
99294 - **Info**: Important operations, scenario lifecycle
100295 - **Debug**: Detailed operational information
101296 - **Trace**: Very detailed debugging information
102-
103- ### Testing Patterns
104- - Unit tests for utility functions
105- - Integration tests for scenario functionality
106- - Use table-driven tests when appropriate
107- - Mock external dependencies (RPC clients, etc.)
108- - Test files: `*_test.go`
297+ - Add transaction context to logs: wallet, nonce, client
109298
110299### Concurrency Patterns
111300- Use context.Context for cancellation and timeouts
112301- Protect shared state with mutexes
113302- Use channels for communication between goroutines
114- - Follow Go concurrency best practices (share memory by communicating)
303+ - Follow Go concurrency best practices
304+ - Use RunTransactionScenario for transaction based scenarios
305+ - Use appropiate transaction send mechanism (SendTransaction / SendMultiTransactionBatch / SendTransactionBatch)
306+ - Respect client pending transaction limits
115307
116308### Constants and Configuration
117309- Define magic numbers as named constants
118310- Group related constants in const blocks
119311- Use iota for enumerated values
120- - Configuration should be environment-aware
312+ - Configuration should be environment-aware- Use standard option names across scenarios
313+
314+ ### Best Practices Summary
315+ 1. **Always use RunTransactionScenario helper** for transaction scenarios
316+ 2. **Configure adequate wallet count** based on throughput needs
317+ 3. **Use BuildBoundTx** for all contract interactions
318+ 4. **Handle nil receipts** in transaction callbacks
319+ 5. **Enable rebroadcast** for important transactions
320+ 6. **Update balances manually** for internal transfers
321+ 7. **Spread load across clients** using appropriate selection
322+ 8. **Monitor wallet funding** through logs
323+ 9. **Validate configurations** in Init()
324+ 10. **Always call onComplete()** in ProcessNextTxFn
0 commit comments