taskrunner

package
v1.2.1 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: May 21, 2026 License: MIT Imports: 23 Imported by: 0

README

TaskRunner

Execute configurable task sequences with initialization and recurring execution phases.

⚠️ IMPORTANT: This scenario requires task configuration via --tasks or --tasks-file. It will not work without tasks defined.

Usage

spamoor taskrunner [flags]

Configuration

Base Settings (required)
  • --privkey - Private key of the sending wallet
  • --rpchost - RPC endpoint(s) to send transactions to
Task Configuration (required)
  • --tasks - Inline task configuration (YAML/JSON string)
  • --tasks-file - Path to task configuration file or URL (http/https)
  • --await-txs - Send and await each transaction individually instead of batching
Volume Control (either -c or -t required)
  • -c, --count - Total number of task execution cycles
  • -t, --throughput - Task execution cycles per slot
  • --max-pending - Maximum number of running execution cycles with pending transactions
Transaction Settings
  • --basefee - Max fee per gas in gwei (default: 20) - inherited by all tasks
  • --tipfee - Max tip per gas in gwei (default: 2) - inherited by all tasks
  • --rebroadcast - Seconds to wait before rebroadcasting (default: 120)
Wallet Management
  • --max-wallets - Maximum number of child wallets to use
Client Settings
  • --client-group - Client group to use for sending transactions
Debug Options
  • --timeout - Maximum scenario runtime (e.g. '1h', '30m', '5s')
  • --log-txs - Log all submitted transactions

Examples

Deploy contract and call function repeatedly:

spamoor taskrunner -p "<PRIVKEY>" -h http://rpc-host:8545 -c 100 \
  --tasks '{
    "execution": [
      {
        "type": "deploy",
        "name": "test", 
        "data": {"contract_code": "0x6080..."}
      },
      {
        "type": "call",
        "data": {
          "target": "{contract:test}",
          "call_data": "0x6057361d{random:32}"
        }
      }
    ]
  }'

Load tasks from file:

spamoor taskrunner -p "<PRIVKEY>" -h http://rpc-host:8545 -t 5 \
  --tasks-file ./tasks.yaml

Load tasks from URL:

spamoor taskrunner -p "<PRIVKEY>" -h http://rpc-host:8545 -c 1000 \
  --tasks-file https://example.com/test-tasks.yaml

Advanced Usage

Transaction Execution Modes

Batch Mode (default): All tasks in an execution cycle are built first, then sent as a batch for maximum throughput. This creates temporary nonce gaps during building.

Await Mode (--await-txs): Each task is built, sent, and awaited individually before proceeding to the next task. This ensures:

  • No nonce gaps during execution (critical for concurrent operations)
  • Sequential execution within each cycle
  • Each task waits for confirmation before the next task is built
  • Easier debugging and transaction tracking
  • Lower throughput but guaranteed ordering and no nonce conflicts

Example with sequential execution:

spamoor taskrunner -p "<PRIVKEY>" -h http://rpc-host:8545 -c 100 \
  --await-txs --tasks-file ./sequential-tasks.yaml
Task Configuration Format

Tasks are configured in YAML or JSON with two phases. All tasks automatically inherit the global transaction settings (--basefee, --tipfee) from the scenario options:

# Initialization tasks (run once)
init:
  - type: deploy
    name: factory
    data:
      contract_file: "./factory.bin"
      gas_limit: 3000000

# Execution tasks (run repeatedly)  
execution:
  - type: deploy
    name: token
    data:
      contract_file: "./erc20.bin"
      contract_args: "0x000000000000000000000000000000000000000000000000000000000000001e"
      
  - type: call
    data:
      target: "{contract:token}"
      call_abi_file: "./erc20-abi.json" 
      call_fn_name: "transfer"
      call_args: ["{randomaddr}", "{random:1000000}"]
Task Types
Deploy Task

Deploys a contract and optionally registers it in the contract registry.

- type: deploy
  name: token        # Optional: register contract as "token"
  data:
    contract_code: "0x608060405234801561001057600080fd5b50..."  # Inline bytecode
    # OR
    contract_file: "./contracts/erc20.bin"                       # File path or URL (http/https)
    
    contract_args: "0x000000000000000000000000000000000000000000000000000000000000001e"  # Constructor args
    gas_limit: 4000000    # Gas limit for deployment
    amount: 1000          # ETH to send in gwei (optional)
Call Task

Calls a function on a deployed contract.

- type: call
  name: transfer     # Optional task name
  data:
    target: "{contract:token}"        # Contract reference
    # OR
    target: "0x742d35Cc9B3Ed5F9..."  # Direct address
    
    # Method 1: Raw calldata
    call_data: "0xa9059cbb000000000000000000000000742d35cc9b3ed5f9..."
    
    # Method 2: ABI + function
    call_abi: '[{"inputs":[...],"name":"transfer",...}]'  # Inline ABI
    # OR
    call_abi_file: "./contracts/erc20-abi.json"           # ABI file or URL (http/https)
    call_fn_name: "transfer"                              # Function name
    call_args: ["{randomaddr}", "1000000"]                # Function arguments
    
    gas_limit: 100000
    amount: 0         # ETH to send in gwei (optional)
Placeholders

TaskRunner supports dynamic placeholders in arguments and bytecode:

Basic Placeholders
  • {random} - Random uint256 value
  • {random:N} - Random value between 0 and N
  • {randomaddr} - Random Ethereum address
  • {txid} - Current transaction ID
  • {stepid} - Current step index within execution cycle
Contract Address Placeholders
  • {contract:name} - Reference to deployed contract address
  • {contract:name:nonce} - Child contract address calculation using CREATE opcode

The child address calculation uses Ethereum's standard CREATE address formula: childAddress = keccak256(rlp.encode(parentAddress, nonce))[12:]

Examples:

# Basic contract reference
call_args: ["{contract:token}", "{random:1000000}"]

# Child contract addresses
call_args: ["{contract:factory:1}", "{contract:factory:2}"]  # First and second child contracts

# In deploy bytecode (automatically strips 0x prefix)
contract_code: "608060405234801561001057600080fd5b50{contract:factory:1}..."

# In constructor arguments (automatically strips 0x prefix)  
contract_args: "000000000000000000000000{contract:parent:0}0000000000000000000000000000000000000000000000000000000000000001"

Address Formatting:

  • Call tasks: Contract addresses include 0x prefix (standard format)
  • Deploy tasks: Contract addresses in contract_code and contract_args automatically strip 0x prefix for bytecode compatibility
Contract Registry

The contract registry maintains deployed contract addresses across phases:

  1. Init Phase: Contracts deployed here are available throughout execution
  2. Execution Phase: Each cycle has its own registry that inherits from init
  3. Contract References: Use {contract:name} to reference deployed contracts
Wallet Management

TaskRunner uses different wallet strategies for each phase:

  1. Init Phase: Uses a well-known wallet (taskrunner-init) for all initialization tasks

    • Ensures init contracts have predictable addresses
    • Avoids conflicts with numbered wallets used in execution
    • Sequential execution with consistent wallet state
  2. Execution Phase: Uses numbered wallets distributed across cycles

    • Each execution cycle can use different wallets for parallel processing
    • Follows standard spamoor wallet selection patterns
    • Enables high throughput execution

Registry Inheritance:

Init Registry (Global)
├── Contract "factory" → 0x123...
└── Contract "helper" → 0x456...

Execution Registry (Per Cycle)  
├── Inherits: factory, helper
├── Contract "token" → 0x789...    # Deployed this cycle
└── Contract "nft" → 0xabc...      # Deployed this cycle
Advanced Examples
Multi-Contract Deployment
init:
  - type: deploy
    name: factory
    data:
      contract_file: "./factory.bin"
      gas_limit: 3000000

execution:
  - type: call
    data:
      target: "{contract:factory}"
      call_abi_file: "./factory-abi.json"
      call_fn_name: "createToken"
      call_args: ["{random}", "{randomaddr}"]
      
  - type: call  
    data:
      target: "{contract:factory}"
      call_fn_name: "mint"
      call_args: ["{randomaddr}", "{random:1000000}"]
Complex ERC20 Testing
init:
  - type: deploy
    name: token
    data:
      contract_file: "./erc20.bin"
      contract_args: "0x0000000000000000000000000000000000000000000000056bc75e2d630eb20000"

execution:
  - type: call
    data:
      target: "{contract:token}"
      call_abi_file: "./erc20-abi.json"
      call_fn_name: "transfer" 
      call_args: ["{randomaddr}", "{random:1000000}"]
      
  - type: call
    data:
      target: "{contract:token}"
      call_abi_file: "./erc20-abi.json"
      call_fn_name: "approve"
      call_args: ["{randomaddr}", "{random:2000000}"]
Factory Pattern with Child Contracts
init:
  - type: deploy
    name: factory
    data:
      contract_file: "./factory.bin"

execution:
  # Deploy child contracts through factory
  - type: call
    data:
      target: "{contract:factory}"
      call_abi_file: "./factory-abi.json"
      call_fn_name: "createChild"
      call_args: ["{random}", "{randomaddr}"]
      
  # Interact with the first child contract deployed by factory
  - type: call
    data:
      target: "{contract:factory:1}"  # First child (nonce 1)
      call_abi_file: "./child-abi.json"
      call_fn_name: "initialize"
      call_args: ["{contract:factory}", "{random:1000}"]
      
  # Deploy another child and interact with the second one
  - type: call
    data:
      target: "{contract:factory}"
      call_fn_name: "createChild" 
      call_args: ["{random}", "{randomaddr}"]
      
  - type: call
    data:
      target: "{contract:factory:2}"  # Second child (nonce 2)
      call_fn_name: "setParent"
      call_args: ["{contract:factory}"]
Proxy Pattern with Predictable Addresses
init:
  - type: deploy
    name: implementation
    data:
      contract_file: "./implementation.bin"
      
  - type: deploy
    name: proxy_factory
    data:
      contract_file: "./proxy_factory.bin"
      # Embed implementation address in proxy bytecode
      contract_args: "000000000000000000000000{contract:implementation}"

execution:
  # Create proxy pointing to implementation
  - type: call
    data:
      target: "{contract:proxy_factory}"
      call_fn_name: "createProxy"
      call_args: ["{randomaddr}"]  # owner
      
  # Call implementation through first proxy
  - type: call
    data:
      target: "{contract:proxy_factory:1}"  # First proxy created
      call_abi_file: "./implementation-abi.json"
      call_fn_name: "setValue"
      call_args: ["{random:1000000}"]

When to Use TaskRunner

Use TaskRunner when:

  • Testing complex multi-step contract interaction patterns
  • Need to maintain state between transaction cycles
  • Require flexible, configurable transaction sequences
  • Other scenarios are too rigid or specific

Don't use TaskRunner for:

  • Simple EOA transfers (use eoatx)
  • Basic contract deployments (use deploytx)
  • Standard token transfers (use erctx)
  • Single-function contract calls (use calltx)

Error Handling

  • Configuration Errors: Invalid YAML/JSON or missing required fields cause immediate failure
  • Task Validation: Each task is validated before execution starts
  • Transaction Failures: Failed transactions are logged but don't stop the scenario
  • Contract References: Unknown contract references cause immediate task failure

Performance Tips

  1. Minimize Init Tasks: Init phase runs sequentially using a single well-known wallet - keep it minimal
  2. Use Multiple Wallets: Distribute execution load across --max-wallets for better throughput
  3. Choose Execution Mode:
    • Use batch mode (default) for maximum throughput
    • Use await mode (--await-txs) when tasks must execute sequentially or for debugging
  4. Wallet Strategy: Init phase isolation prevents wallet conflicts during high-throughput execution
  5. Optimize Gas Limits: Set appropriate gas limits to avoid failures

Troubleshooting

"no tasks configuration provided"
  • Cause: Neither --tasks nor --tasks-file was specified
  • Fix: Provide task configuration via one of these flags
"contract 'name' not found in registry"
  • Cause: Referencing a contract that wasn't deployed or named incorrectly
  • Fix: Ensure contract is deployed in init phase or earlier in execution phase
"failed to parse tasks as YAML or JSON"
  • Cause: Malformed configuration syntax
  • Fix: Validate YAML/JSON syntax using online validators
High transaction failure rates
  • Cause: Insufficient gas limits or network congestion
  • Fix: Increase gas limits or reduce throughput

Documentation

Index

Constants

This section is empty.

Variables

View Source
var ScenarioDefaultOptions = ScenarioOptions{
	TotalCount:  0,
	Throughput:  10,
	MaxPending:  0,
	MaxWallets:  0,
	Rebroadcast: 1,
	BaseFee:     20,
	TipFee:      2,
	TasksConfig: "",
	TasksFile:   "",
	AwaitTxs:    false,
	Timeout:     "",
	ClientGroup: "",
	LogTxs:      false,
}
View Source
var ScenarioDescriptor = scenario.Descriptor{
	Name:           ScenarioName,
	Description:    "Execute configurable task sequences with initialization and recurring execution phases",
	DefaultOptions: ScenarioDefaultOptions,
	NewScenario:    newScenario,
}
View Source
var ScenarioName = "taskrunner"

Functions

func ProcessBasicPlaceholders added in v1.1.7

func ProcessBasicPlaceholders(str string, txIdx uint64, stepIdx int, stripPrefix bool) (string, error)

ProcessBasicPlaceholders processes {txid}, {stepid}, {random}, {random:N}, {randomaddr} placeholders If stripPrefix is true, removes 0x prefix from addresses (for use in bytecode/calldata)

func ProcessContractPlaceholders added in v1.1.7

func ProcessContractPlaceholders(str string, registry *ContractRegistry, stripPrefix bool) (string, error)

ProcessContractPlaceholders processes {contract:name} and {contract:name:nonce} placeholders If stripPrefix is true, removes the 0x prefix from addresses (for use in bytecode/calldata)

func ValidateConfigString

func ValidateConfigString(configStr string) error

ValidateConfigString validates a configuration string without full parsing

Types

type BaseTask

type BaseTask struct {
	Type string
	Name string
}

BaseTask provides common functionality for all task types

func (*BaseTask) GetName

func (t *BaseTask) GetName() string

GetName returns the task name

func (*BaseTask) GetType

func (t *BaseTask) GetType() string

GetType returns the task type

type CallTask

type CallTask struct {
	BaseTask
	Target      string        `yaml:"target" json:"target"`               // Contract address or {contract:name}
	CallData    string        `yaml:"call_data" json:"call_data"`         // Raw hex calldata
	CallABI     string        `yaml:"call_abi" json:"call_abi"`           // JSON ABI string
	CallABIFile string        `yaml:"call_abi_file" json:"call_abi_file"` // Path to ABI file or URL (http/https)
	CallFnName  string        `yaml:"call_fn_name" json:"call_fn_name"`   // Function name
	CallArgs    []interface{} `yaml:"call_args" json:"call_args"`         // Function arguments
	GasLimit    uint64        `yaml:"gas_limit" json:"gas_limit"`         // Gas limit
	Amount      uint64        `yaml:"amount" json:"amount"`               // ETH amount to send (in gwei)
}

CallTask represents a contract function call task

func (*CallTask) BuildTransaction

func (t *CallTask) BuildTransaction(ctx context.Context, wallet *spamoor.Wallet, registry *ContractRegistry, execCtx *TaskExecutionContext) (*types.Transaction, error)

BuildTransaction creates a contract call transaction

func (*CallTask) Validate

func (t *CallTask) Validate() error

Validate checks if the call task configuration is valid

type ContractRegistry

type ContractRegistry struct {
	// contains filtered or unexported fields
}

ContractRegistry manages deployed contract addresses for task references

func NewContractRegistry

func NewContractRegistry() *ContractRegistry

NewContractRegistry creates a new empty contract registry

func (*ContractRegistry) Clone

func (r *ContractRegistry) Clone() *ContractRegistry

Clone creates a new registry that inherits from this one This is used to create execution-scoped registries that can see init contracts

func (*ContractRegistry) Get

func (r *ContractRegistry) Get(name string) (common.Address, bool)

Get retrieves a contract address by name It first checks the local registry, then the parent registry

func (*ContractRegistry) Has

func (r *ContractRegistry) Has(name string) bool

Has checks if a contract with the given name exists

func (*ContractRegistry) ListAllContracts

func (r *ContractRegistry) ListAllContracts() map[string]common.Address

ListAllContracts returns all contracts including from parent registries

func (*ContractRegistry) ListContracts

func (r *ContractRegistry) ListContracts() map[string]common.Address

ListContracts returns all contract names and addresses in this registry (excluding parent registry)

func (*ContractRegistry) ResolveReference

func (r *ContractRegistry) ResolveReference(ref string) (common.Address, error)

ResolveReference resolves a contract reference string to an address Supports both direct addresses and contract references like {contract:name}

func (*ContractRegistry) Set

func (r *ContractRegistry) Set(name string, address common.Address)

Set stores a contract address with the given name

type DeployTask

type DeployTask struct {
	BaseTask
	ContractCode string `yaml:"contract_code" json:"contract_code"` // Hex-encoded bytecode
	ContractFile string `yaml:"contract_file" json:"contract_file"` // Path to bytecode file or URL (http/https)
	ContractArgs string `yaml:"contract_args" json:"contract_args"` // Constructor arguments (hex)
	GasLimit     uint64 `yaml:"gas_limit" json:"gas_limit"`         // Gas limit for deployment
	Amount       uint64 `yaml:"amount" json:"amount"`               // ETH amount to send (in gwei)
}

DeployTask represents a contract deployment task

func (*DeployTask) BuildTransaction

func (t *DeployTask) BuildTransaction(ctx context.Context, wallet *spamoor.Wallet, registry *ContractRegistry, execCtx *TaskExecutionContext) (*types.Transaction, error)

BuildTransaction creates a contract deployment transaction

func (*DeployTask) Validate

func (t *DeployTask) Validate() error

Validate checks if the deploy task configuration is valid

type RawTasksConfig

type RawTasksConfig struct {
	InitTasks      []*TaskConfig `yaml:"init" json:"init"`
	ExecutionTasks []*TaskConfig `yaml:"execution" json:"execution"`
}

RawTasksConfig represents the raw configuration before parsing tasks

type Scenario

type Scenario struct {
	// contains filtered or unexported fields
}

func (*Scenario) Flags

func (s *Scenario) Flags(flags *pflag.FlagSet) error

func (*Scenario) Init

func (s *Scenario) Init(options *scenario.Options) error

func (*Scenario) Run

func (s *Scenario) Run(ctx context.Context) error

type ScenarioOptions

type ScenarioOptions struct {
	TotalCount  uint64  `yaml:"total_count"`
	Throughput  uint64  `yaml:"throughput"`
	MaxPending  uint64  `yaml:"max_pending"`
	MaxWallets  uint64  `yaml:"max_wallets"`
	Rebroadcast uint64  `yaml:"rebroadcast"`
	BaseFee     float64 `yaml:"base_fee"`
	TipFee      float64 `yaml:"tip_fee"`
	BaseFeeWei  string  `yaml:"base_fee_wei"`
	TipFeeWei   string  `yaml:"tip_fee_wei"`

	TasksConfig string `yaml:"tasks_config"` // Inline YAML/JSON task configuration
	TasksFile   string `yaml:"tasks_file"`   // Path to task configuration file or URL
	AwaitTxs    bool   `yaml:"await_txs"`    // Send and await each transaction individually

	Timeout     string `yaml:"timeout"`
	ClientGroup string `yaml:"client_group"`
	LogTxs      bool   `yaml:"log_txs"`
}

type Task

type Task interface {
	// GetType returns the task type identifier
	GetType() string
	// GetName returns the task name (optional, for registry)
	GetName() string
	// Validate checks if the task configuration is valid
	Validate() error
	// BuildTransaction creates a transaction for this task
	BuildTransaction(ctx context.Context, wallet *spamoor.Wallet, registry *ContractRegistry, execCtx *TaskExecutionContext) (*types.Transaction, error)
}

Task represents a single executable task in the TaskRunner scenario

func CreateTask

func CreateTask(config *TaskConfig) (Task, error)

CreateTask creates a task instance from configuration

func NewCallTask

func NewCallTask(name string, data map[string]interface{}) (Task, error)

NewCallTask creates a new call task from configuration

func NewDeployTask

func NewDeployTask(name string, data map[string]interface{}) (Task, error)

NewDeployTask creates a new deploy task from configuration

type TaskConfig

type TaskConfig struct {
	Type string                 `yaml:"type" json:"type"`
	Name string                 `yaml:"name" json:"name"`
	Data map[string]interface{} `yaml:"data" json:"data"`
}

TaskConfig represents the generic configuration for any task

type TaskExecutionContext

type TaskExecutionContext struct {
	BaseFee    float64             // Max fee per gas in gwei
	TipFee     float64             // Max tip per gas in gwei
	BaseFeeWei string              // Max fee per gas in wei (overrides BaseFee for L2 sub-gwei fees)
	TipFeeWei  string              // Max tip per gas in wei (overrides TipFee for L2 sub-gwei fees)
	TxPool     *spamoor.TxPool     // For fee calculation
	WalletPool *spamoor.WalletPool // For gas estimation helpers (EstimateDeployGas, etc.)
	Client     *spamoor.Client     // Live client for RPC-based gas estimation
}

TaskExecutionContext contains context information for task execution

type TaskFactory

type TaskFactory func(name string, data map[string]interface{}) (Task, error)

TaskFactory is a function that creates a task from configuration

type TasksConfig

type TasksConfig struct {
	InitTasks      []Task `yaml:"init" json:"init"`
	ExecutionTasks []Task `yaml:"execution" json:"execution"`
}

TasksConfig represents the complete task configuration

func ParseTasksConfig

func ParseTasksConfig(data []byte) (*TasksConfig, error)

ParseTasksConfig parses task configuration from YAML or JSON data

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL