Skip to content

Commit 98e1ca9

Browse files
committed
update README.md
1 parent 8a36be1 commit 98e1ca9

2 files changed

Lines changed: 465 additions & 119 deletions

File tree

.cursor/rules/code_standards.mdc

Lines changed: 231 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,25 @@
11
---
22
description: Spamoor Code Standards and Conventions
3+
globs:
34
alwaysApply: 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
3047
type 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

3868
type Scenario struct {
@@ -42,79 +72,253 @@ type Scenario struct {
4272
}
4373

4474
var 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
69264
import (
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

Comments
 (0)