Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
88 changes: 88 additions & 0 deletions generator/scenarios/StorageRW.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
package scenarios

import (
"math/big"

"github.com/ethereum/go-ethereum/accounts/abi/bind"
"github.com/ethereum/go-ethereum/common"
ethtypes "github.com/ethereum/go-ethereum/core/types"
"github.com/ethereum/go-ethereum/ethclient"

"github.com/sei-protocol/sei-load/config"
"github.com/sei-protocol/sei-load/generator/bindings"
"github.com/sei-protocol/sei-load/types"
)

const StorageRW = "storagerw"

// Fixed slot and empty pad for the scaffold; PLT-465 makes these per-tx.
var (
storageRWSlot = big.NewInt(0)
storageRWPad = []byte{}
)

// StorageRWScenario implements the TxGenerator interface for StorageRWv1 contract operations
type StorageRWScenario struct {
*ContractScenarioBase[bindings.StorageRWv1]
contract *bindings.StorageRWv1
}

// NewStorageRWScenario creates a new StorageRW scenario
func NewStorageRWScenario(cfg config.Scenario) TxGenerator {
scenario := &StorageRWScenario{}
scenario.ContractScenarioBase = NewContractScenarioBase[bindings.StorageRWv1](scenario, cfg)
return scenario
}

// Name returns the name of the scenario.
func (s *StorageRWScenario) Name() string {
return StorageRW
}

// DeployContract implements ContractDeployer interface - deploys StorageRWv1.
// StorageRWv1 is mapping-backed and takes no constructor arguments; the keyspace
// is generator-side.
func (s *StorageRWScenario) DeployContract(opts *bind.TransactOpts, client *ethclient.Client) (common.Address, *ethtypes.Transaction, error) {
address, tx, _, err := bindings.DeployStorageRWv1(opts, client)
return address, tx, err
}

// GetBindFunc implements ContractDeployer interface - returns the binding function
func (s *StorageRWScenario) GetBindFunc() ContractBindFunc[bindings.StorageRWv1] {
return bindings.NewStorageRWv1
}

// SetContract implements ContractDeployer interface - stores the contract instance
func (s *StorageRWScenario) SetContract(contract *bindings.StorageRWv1) {
s.contract = contract
}

// Attach implements TxGenerator interface - attaches to an existing contract
func (s *StorageRWScenario) Attach(config *config.LoadConfig, address common.Address) error {
// Call base Attach to set deployed flag and config
if err := s.ContractScenarioBase.Attach(config, address); err != nil {
return err
}

var client *ethclient.Client
var err error
if !config.MockDeploy {
client, err = ethclient.Dial(config.Endpoints[0])
if err != nil {
return err
}
}

s.contract, err = bindings.NewStorageRWv1(address, client)
return err
}

// CreateContractTransaction implements ContractDeployer interface - creates a
// fixed StorageRWv1 rmw transaction. See package doc for the scaffold and gas
// rationale.
func (s *StorageRWScenario) CreateContractTransaction(auth *bind.TransactOpts, scenario *types.TxScenario) (*ethtypes.Transaction, error) {
// 50k fits rmw (SLOAD+SSTORE) with headroom; see package doc for sizing.
// PLT-465 revisits with the distribution-driven pad.
auth.GasLimit = 50000
return s.contract.Rmw(auth, storageRWSlot, storageRWPad)
}
76 changes: 76 additions & 0 deletions generator/scenarios/StorageRW_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
package scenarios_test

import (
"testing"

"github.com/stretchr/testify/require"

"github.com/sei-protocol/sei-load/config"
"github.com/sei-protocol/sei-load/generator/bindings"
"github.com/sei-protocol/sei-load/generator/scenarios"
"github.com/sei-protocol/sei-load/types"
)

// rmwSelector is the 4-byte function selector for StorageRWv1.rmw(uint256,bytes).
// It is the ABI-derived discriminator the produced calldata must start with.
var rmwSelector = []byte{0x22, 0x74, 0x6b, 0x07}

// TestStorageRWFactoryRegistration proves the scenario is reachable by name
// through the factory.
func TestStorageRWFactoryRegistration(t *testing.T) {
gen := scenarios.CreateScenario(config.Scenario{Name: scenarios.StorageRW})
require.NotNil(t, gen)
require.Equal(t, scenarios.StorageRW, gen.Name())
}

// TestStorageRWDeployAndGenerate proves the deploy + send path end-to-end under
// mock deploy: the scenario binds StorageRWv1, attaches at a known address, and
// produces a valid fixed rmw transaction targeting that contract.
func TestStorageRWDeployAndGenerate(t *testing.T) {
cfg := &config.LoadConfig{
ChainID: 7777,
MockDeploy: true,
Endpoints: []string{"http://localhost:8545"},
}

gen := scenarios.CreateScenario(config.Scenario{Name: scenarios.StorageRW})

// Mirror generator.mockDeployAll: attach the bound contract at a known address.
contractAddr := types.GenerateAccounts(1)[0].Address
require.NoError(t, gen.Attach(cfg, contractAddr))

// Build the tx scenario the way the weighted generator does: a funded sender.
sender := types.GenerateAccounts(1)[0]
txScenario := &types.TxScenario{
Name: scenarios.StorageRW,
Sender: sender,
}

loadTx := gen.Generate(txScenario)
require.NotNil(t, loadTx)
require.NotNil(t, loadTx.EthTx)

// The produced tx must target the deployed contract...
require.NotNil(t, loadTx.EthTx.To())
require.Equal(t, contractAddr, *loadTx.EthTx.To())

// ...and carry rmw calldata against the fixed slot 0.
data := loadTx.EthTx.Data()
require.GreaterOrEqual(t, len(data), 4)
require.Equal(t, rmwSelector, data[:4])

// Pin the fixed scaffold calldata: rmw(uint256 slot, bytes _pad) with
// slot == 0 and an empty pad. ABI head is the slot operand (32B) then the
// bytes offset (0x40); the tail is the bytes length (0). All zero except the
// 0x40 offset, so the full body is 96 bytes.
body := data[4:]
require.Len(t, body, 96)
wantBody := make([]byte, 96)
wantBody[63] = 0x40 // offset to the _pad bytes argument
require.Equal(t, wantBody, body)

// Sanity: the selector we assert against matches the binding's ABI.
parsed, err := bindings.StorageRWv1MetaData.GetAbi()
require.NoError(t, err)
require.Equal(t, rmwSelector, parsed.Methods["rmw"].ID)
}
62 changes: 62 additions & 0 deletions generator/scenarios/doc.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
// Package scenarios defines the load scenarios seiload can generate, and the
// shared scaffolding that lets a contract-backed scenario describe only what is
// unique to its contract.
//
// # The contract-scenario pattern
//
// Every scenario satisfies TxGenerator (Name/Generate/Attach/Deploy). Non-contract
// scenarios (the EVMTransfer family) implement it directly; contract scenarios
// compose ContractScenarioBase[T], which factors out the deploy-wait-bind flow
// and the per-tx auth construction so the concrete scenario only supplies its
// contract specifics.
//
// A contract scenario embeds *ContractScenarioBase[T] (T being the generated
// binding) and implements ContractDeployer[T]:
//
// - DeployContract — deploy the contract for this run.
// - GetBindFunc — return the binding's constructor so the base can bind the
// deployed (or attached) address.
// - SetContract — receive the bound instance for later CreateContractTransaction
// calls.
// - CreateContractTransaction — build one load transaction against the contract.
//
// The base owns the rest: DeployScenario deploys, waits for the receipt, asserts
// success, then binds and hands back the instance via SetContract; AttachScenario
// binds an already-deployed address the same way; CreateTransaction builds the
// per-tx auth and delegates to CreateContractTransaction.
//
// # MockDeploy attach
//
// Under config.MockDeploy a scenario attaches to a known address without a live
// endpoint, so the bind backend is nil. This is the path the tests and
// generator.mockDeployAll exercise: bind at an address, produce calldata, but
// never send. CreateContractTransaction must therefore stay pure (it shapes a
// transaction; it does not touch the chain).
//
// # Factory registration
//
// scenarioFactories maps a lowercase scenario name to its constructor, and
// CreateScenario resolves a config.Scenario by name. Non-contract entries are
// hand-written; contract entries below the AUTO-GENERATED marker in factory.go
// are emitted by `make generate` from the contract bindings — do not edit that
// block by hand.
//
// # StorageRW scaffold
//
// StorageRW issues a read-modify-write against StorageRWv1 to exercise the SLOAD
// + SSTORE storage path under load. PLT-461 lands it as a scaffold: every
// transaction targets one fixed slot with an empty calldata pad, which is enough
// to prove the deploy/send path. The per-tx slot/value/pad distribution arrives
// in PLT-465.
//
// Gas sizing. The rmw is an SLOAD + SSTORE on a single slot: ~26k gas warm, but
// ~44k on a cold first touch (the cold-SLOAD and the zero-to-nonzero SSTORE both
// charge their higher rates). The scaffold pins GasLimit to 50k: it covers the
// cold-first-touch case with headroom for the (currently empty) pad, and packs
// roughly 4x denser than the 200k default in CreateTransactionOpts. Density
// matters on a gas-limit-admission chain, where a block admits transactions up
// to its gas limit regardless of gas actually used — an oversized limit reserves
// block space the rmw never spends and throttles achievable throughput. PLT-465
// revisits the limit once the calldata pad is distribution-driven, since pad size
// changes calldata gas.
package scenarios
1 change: 1 addition & 0 deletions generator/scenarios/factory.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ var scenarioFactories = map[string]ScenarioFactory{
ERC20Noop: NewERC20NoopScenario,
ERC20: NewERC20Scenario,
ERC721: NewERC721Scenario,
StorageRW: NewStorageRWScenario,

// DO NOT EDIT ABOVE THIS LINE - AUTO-GENERATED CONTENT
}
Expand Down
Loading