Integrate the SDK
Make your contract a valid Gas Killer target.
The HTTP API returns a ready-to-sign verifyAndUpdate transaction. That call
only exists if your contract inherits the Gas Killer Solidity SDK, so the
contract side comes first: the router refuses to accept a task for a target that
does not advertise the SDK interface.
A valid target needs three things. Miss any one and the failure shows up at a different stage:
| Requirement | Enforced by | Symptom when missing |
|---|---|---|
Inherits GasKillerSDK | Router, at submit time | Task rejected — the target does not report the interface |
avsAddress and schnorrRegistry wired to the live deployment | Router, while building the payload | Task fails — the router will not return a payload that cannot settle |
Mutating functions marked trackState | Nothing — it is your replay protection | Silent: stale payloads stay applicable |
The router proves the settlement call before handing it back, so a mis-wired target fails as a task with the underlying error rather than as a reverted transaction you paid for. Get the addresses right up front anyway, see Configuration.
Requirements
- Solidity ^0.8.27 and Foundry.
- An EVM at Cancun or later.
TransitionGuarduses EIP-1153 transient storage and has no pre-Cancun fallback path, so deploying to a chain without it breaks settlement itself rather than just the guard. The public testnet settles on Sepolia, which qualifies.
Install
forge install gas-killer/solidity-sdkAdd the remapping to remappings.txt (or foundry.toml):
gas-killer-sdk/=lib/solidity-sdk/src/The SDK pins evm_version = "cancun" in its own foundry.toml. Set the same in
yours.
A minimal target
Everything an integration needs, and nothing else. Counter keeps one value and
one tracked function that recomputes it; in a real contract that function is
whatever is too expensive to run on-chain.
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.27;
import {GasKillerSDK} from "gas-killer-sdk/GasKillerSDK.sol";
contract Counter is GasKillerSDK {
/// Declared first, so `total` occupies storage slot 0 and `runs` slot 1.
/// The signed diff writes raw slots, see Tracked functions.
uint256 public total;
uint256 public runs;
event Recomputed(uint256 indexed runs, uint256 total);
constructor(address avs, address schnorrRegistry) {
_setAvsAddress(avs);
_setSchnorrRegistry(schnorrRegistry);
}
/// The expensive path. On-chain this is the reference implementation the
/// operators reproduce off-chain; you never have to call it on-chain.
function recompute(uint32 rounds) external trackState {
uint256 acc = total;
for (uint32 i = 0; i < rounds; i++) {
acc = uint256(keccak256(abi.encode(acc, i)));
}
total = acc;
runs += 1;
emit Recomputed(runs, total);
}
}That is the whole contract-side integration. verifyAndUpdate,
verifyAndUpdateBatch, stateTransitionCount(), supportsInterface, and the
transition guard all come from the base.
Deploy and verify
Pass the live addresses from Configuration:
forge create src/Counter.sol:Counter \
--rpc-url "$RPC_URL" --private-key "$PRIVATE_KEY" \
--constructor-args "$AVS_ADDRESS" "$SCHNORR_REGISTRY_ADDRESS"Then check all four of these before submitting a task. Each maps to a failure you would otherwise debug from a revert:
The router will accept it. Must return true, or task submission is rejected:
cast call "$TARGET" "supportsInterface(bytes4)(bool)" 0x82b35a01 --rpc-url "$RPC_URL"The wiring took. Both must equal the values you passed:
cast call "$TARGET" "avsAddress()(address)" --rpc-url "$RPC_URL"
cast call "$TARGET" "schnorrRegistry()(address)" --rpc-url "$RPC_URL"The registry is the live operator set. This is the check that catches a stale address, see Verify your own wiring.
The counter starts at zero. Confirms StateTracker is reachable:
cast call "$TARGET" "stateTransitionCount()(uint256)" --rpc-url "$RPC_URL"Then submit a task
With the contract deployed, the API side is unchanged from the
Quickstart: POST /tasks with your target address and the
ABI-encoded calldata for the tracked function, poll until ready, submit the
payload.
call_data: Array.from(
Buffer.from(
encodeFunctionData({ abi, functionName: "recompute", args: [1000] }).slice(2),
"hex",
).values(),
)The operators execute recompute(1000) off-chain, sign the resulting diff, and
the payload you get back is a verifyAndUpdate call that writes total, runs,
and the Recomputed log directly, without running the loop on-chain.
Coming from the BLS SDK
Earlier SDK releases verified BLS signatures (constructor argument
blsSigChecker, interface ID 0x93de4531). The operators now sign with
aggregate Schnorr only, so a target built on that release no longer settles. To
move one across, update the SDK, swap _setBlsSignatureChecker for
_setSchnorrRegistry, and redeploy against the addresses in
Configuration. The task digest, trackState
and the state update encoding are unchanged, so the contract's own logic stays
as it is.
Where to go next
- Configuration — the addresses to wire, how to verify them, and how to stay reconfigurable.
- Tracked functions — what
trackStateguarantees, storage layout rules, and what keeps a function extractable. - Reference — full API surface, state update types, and a revert-selector lookup table.
