Where we left off
The previous AA post explained why Shell Chain implements account abstraction at the protocol layer rather than as an overlay. This post explains how Phase 1 is actually built.
The wire format
A batch transaction is identified by tx_type = 0x7E (decimal 126). The
full structure, in Rust notation:
pub struct BatchTransaction {
pub nonce: u64,
pub gas: u64,
pub inner_calls: Vec<InnerCall>, // max AA_MAX_INNER_CALLS = 16
pub paymaster: Option<Address>,
pub paymaster_signature: Option<Bytes>,
pub validity_window: Option<(u64, u64)>, // (not_before, not_after) Unix secs
pub signature: PqSignature,
}
pub struct InnerCall {
pub to: Address,
pub value: U256,
pub data: Bytes,
pub gas_limit: u64,
}
The encoding is RLP, prepended with the 0x7E type byte — the same envelope
used by EIP-2718 typed transactions on Ethereum. This means block body
parsing code needs only a single branch on the first byte to distinguish
batch from legacy transactions.
Signing domain isolation
A subtle but critical design decision: the hash that the sender signs over a batch transaction must not be forgeable from a v0.17.0 single-transaction signature (and vice versa).
We achieve this with a domain byte:
// v0.17.0 single tx
tx_hash = keccak256(0x01 || RLP(tx_fields))
// v0.18.0 batch tx
bundle_hash = keccak256(0x42 || RLP(tx) || RLP(bundle_signing_fields))
bundle_signing_fields is the ordered tuple of inner call hashes,
paymaster address, and validity window. This means a valid batch signature
is computationally useless for signing a legacy tx, and a valid legacy
signature cannot be reused for a batch.
Paymaster admission
When a batch tx with a paymaster field arrives at the mempool:
- Balance check: the paymaster address must hold at least
gas * gas_priceat the current state root. - Signature check:
verify_ml_dsa65(paymaster_pubkey, paymaster_sig, keccak256("paymaster" || bundle_hash || validity_window))must pass. - Validity window:
not_before ≤ now ≤ not_after(nodes allow ±30s clock skew).
If any of these fail, the tx is rejected at the gate — it never enters the
mempool. This is intentionally stricter than Ethereum's eth_sendRawTransaction,
which defers validation to block inclusion time.
At execution time, gas is debited from the paymaster account, not the sender. The sender's nonce is still incremented (preventing replay), but the sender's balance is untouched as long as the paymaster has sufficient funds.
Execution semantics
The executor processes inner calls in order:
for (i, call) in inner_calls.iter().enumerate() {
let outcome = evm.execute(call);
if outcome.is_revert() {
// Roll back all state changes from calls 0..=i
// Deduct gas consumed up to the revert point from paymaster (or sender)
return BatchOutcome::Revert { inner_index: i, reason: outcome.revert_reason() };
}
logs.extend(outcome.logs);
}
The atomicity guarantee is straightforward: if call 3 of 5 reverts, the journal is rewound to the pre-batch snapshot. Gas consumed up to the revert is still charged (to prevent DoS via cheap reverts).
This is different from Ethereum's ERC-4337 model, where inner ops each get their own gas accounting. Shell Chain's model is simpler: one nonce, one gas budget, one signature, all-or-nothing.
SDK usage
import { buildBatchTransaction, signTransaction } from 'shell-sdk';
const batch = buildBatchTransaction([
{ to: tokenAddr, value: 0n, data: encodeTransfer(alice, 100n), gasLimit: 60_000n },
{ to: dexAddr, value: 0n, data: encodeSwap(tokenAddr, 50n), gasLimit: 80_000n },
]);
// Optional: attach a paymaster
const sponsored = buildSponsoredTransaction(batch, paymasterAddr, paymasterSig);
const signed = await signer.signTransaction(sponsored);
await provider.sendTransaction(signed);
The SDK normalizes all ML-DSA-65 naming variants ("Dilithium3",
"MlDsa65", "ML-DSA-65") to the canonical NIST name internally. Existing
code that used "Dilithium3" continues to work.
What Phase 2 will add
Phase 2 (v0.19.0 target) will extend the paymaster model to support
contract-based paymasters — smart contracts that implement a validation
interface, enabling programmable gas sponsorship policies. This is the
equivalent of ERC-4337's IPaymaster, but integrated at the protocol layer
with PQ-native signatures.
Session keys and guardian recovery will also land in Phase 2.