Skip to content

Prioritized and injected transaction sequences - #1605

Open
elopez wants to merge 3 commits into
feat/interworker-busfrom
feat/prioritized-sequences
Open

Prioritized and injected transaction sequences#1605
elopez wants to merge 3 commits into
feat/interworker-busfrom
feat/prioritized-sequences

Conversation

@elopez

@elopez elopez commented Aug 14, 2026

Copy link
Copy Markdown
Member

A prototype is a SolCall with holes: a function name plus an argument
list where Nothing marks an argument the fuzzer should fill in. This PR
teaches the generator to build transactions from prototypes, and teaches a
fuzzing worker to accept a sequence of them over the inter-worker bus and
spend part of its budget fuzzing that ordering instead of a corpus-mutated one.

The point is to let something outside the campaign say "this ordering looks
interesting" — an ordering the corpus mutators are unlikely to stumble into on
their own — without pinning everything else about the sequence.

Nothing sends these commands yet; the client that does arrives with the MCP
server. Until then the entry points are genTxFromPrototype and
genPrioritizedSeq.

Commits

Worth reviewing in order — the first is a pure refactor and carries no
behaviour change:

  1. refactor: factor the shared tail out of genTxgenTx resolved the
    deployed contracts, picked one and generated a call, then wrapped it in a
    Tx. Only the middle step is about choosing what to call, so the two ends
    become callableContracts and toTx, with genRandomCall in between.
  2. feat: generate a transaction from a call prototypeSolCallPrototype,
    genTxFromPrototype, matchingContracts, genPrototypeCall.
  3. feat: prioritize injected call sequences during fuzzing
    genPrioritizedSeq, WorkerState.prioritizedSequences, the FuzzSequence
    and ClearPrioritization commands, and the randseq split.

Notes on the design

genPrioritizedSeq generates the prototype calls in order with up to
maxInterleavedTxs random transactions between consecutive ones, prefixes the
result with the start of a corpus entry so the ordering also runs from a state
the campaign has already reached, then pads or truncates to seqLen. Worker 0
always takes the empty prefix, so the ordering stays covered from the initial
state too.

Matching a prototype against the deployed contracts is on name and arity
only
— an argument left open carries no type to compare against, so
same-arity overloads all qualify and one is picked at random. This is
documented at matchingContracts. When nothing exposes the named function the
generator falls back to a fully random transaction, so a prototype naming a
function that isn't there costs diversity rather than stalling the worker.

SolCallPrototype lives in Types.Signature next to SolCall, so
Transaction, Campaign and InterWorker share one name for it.
genPrioritizedSeq sits next to genTxFromPrototype in Echidna.Transaction,
and the two new command handlers go in Worker/Command.hs alongside the
sampling ones rather than back into runFuzzWorker's where block.

Two things to flag

  • prioritizedSequences is uncapped, unlike sampledFunctions which
    maxSampledFunctions bounds. It only grows via bus commands and randseq
    walks it with rElem on the hot path, so a client injecting repeatedly grows
    it without bound; ClearPrioritization is the only reset. Happy to add a cap
    for symmetry with the sampling handler if reviewers prefer.
  • Truncation can cut prototype calls. When the expansion alone exceeds
    seqLen, take seqLen drops its tail, so a prototype longer than seqLen is
    only partly exercised. Pre-existing behaviour, preserved here.

Verification

cabal build is warning-free, hlint lib src is clean (the one remaining hint
is pre-existing in SourceMapping.hs), and all 190 tests pass. Each commit
builds on its own, so the branch stays bisectable.

Since nothing enables the feature until the MCP server lands, the new path was
also driven directly:

  • every generated sequence is exactly seqLen, with the prototype calls present
    in order and with the fixed arguments they were given;
  • a prototype naming a missing function falls back without ever producing that
    call, still at seqLen;
  • worker 0 never takes a corpus prefix, worker 1 does;
  • end to end, a real campaign with FuzzSequence pushed over the bus breaks an
    ordering-dependent property that a 20,000-tx baseline run does not.

elopez and others added 3 commits August 14, 2026 16:15
genTx does three things in sequence: resolve the deployed contracts to
the ABIs Echidna knows how to call, pick one of them and generate a call
to it, then wrap that call in a transaction with a random sender, value
and delay. Only the middle step is about choosing what to call; the two
ends are the same for any generator that has already decided.

Split them into callableContracts, genRandomCall and toTx, leaving genTx
as the composition of the three. No behaviour changes -- this is the
groundwork for a generator that picks its own call and reuses both ends
rather than repeating them.

Co-authored-by: gustavo-grieco <gustavo.grieco+github@gmail.com>
A prototype is a SolCall with holes: a function name and an argument
list where Nothing marks an argument left for the fuzzer to fill in. It
is how something outside the campaign says "call transfer, I don't care
with what", or "call transfer with this recipient and any amount".

genTxFromPrototype resolves one against the deployed contracts and hands
the result to the same tail genTx uses. Matching is on name and arity
only: an argument left open carries no type to compare against, so
same-arity overloads all qualify and one is picked at random.
matchingContracts cuts each contract down to just the signatures that
match, which keeps the subsequent pick total -- there is no filtered
list that can turn out to be empty after a contract was already chosen.

When nothing deployed exposes such a function it falls back to a fully
random transaction, so a prototype naming a function that isn't there
costs diversity rather than stalling the worker.

Nothing produces prototypes yet; the command that injects them follows.

Co-authored-by: gustavo-grieco <gustavo.grieco+github@gmail.com>
Add the other half of prototypes: a worker can be handed a sequence of
them together with a probability, and will fuzz that sequence instead of
a corpus-mutated one that often. This is what lets something outside the
campaign spend part of a worker's budget on a specific ordering -- an
ordering it has reason to believe is interesting, but that the corpus
mutators are unlikely to stumble into.

FuzzSequence and ClearPrioritization join the commands a fuzzing worker
accepts over the bus, and what they inject accumulates in
WorkerState.prioritizedSequences. randseq now chooses between the
prioritized path and the standard one; its previous body moves to
genStandardSeq unchanged.

genPrioritizedSeq turns a sequence of prototypes into a real one. The
calls are generated in order with up to maxInterleavedTxs random
transactions between consecutive ones, so what gets pinned is the
ordering rather than the whole sequence. It is then prefixed with the
start of a corpus entry, so the ordering also runs from a state the
campaign has already reached -- except on worker 0, which always takes
the empty prefix so the initial state stays covered. The result is
padded with random transactions, or truncated, to respect seqLen.

Nothing sends these commands yet; the client that does arrives with the
MCP server.

Co-authored-by: gustavo-grieco <gustavo.grieco+github@gmail.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant