Shell Chain Native Account Abstraction Guide
Shell Chain implements account abstraction at the protocol layer. Every user
account is treated as a smart account from the start: the chain validates
post-quantum signatures natively, uses 32-byte native addresses end-to-end inside the PQVM, and exposes each address as 0x + 64 lowercase hex.
See also: Quickstart Guide · JSON-RPC API Reference · Post-Quantum Cryptography Guide
1. What "native AA" means on Shell Chain
Shell Chain does not rely on ERC-4337's EntryPoint / Bundler architecture.
Instead, transaction validation is part of the base protocol:
- Default path: built-in post-quantum signature validation
- Upgradeable path: account-specific validation contract logic
- Stable account identity: address stays the same across key rotation
- PQVM execution: the PQVM operates on Shell's full 32-byte addresses (no truncation to 20 bytes)
In practice, this means the chain can support:
- first-use account creation from a PQ public key
- key rotation without changing account identity
- custom validation logic such as multisig or social recovery
2. Address format
2.1 Native 32-byte address derivation
Shell Chain derives account addresses from the signing algorithm and public key:
preimage = version(1 byte) || algo_id(1 byte) || pubkey(n bytes)
address = blake3(preimage)
version = 0x01for the first derivation schemealgo_id = SignatureType::as_u8()- the final address is the full 32-byte BLAKE3 output
There is no internal 20-byte address model. The PQVM, RPC layer, genesis format, and storage all operate on the same native 32-byte address.
2.2 External address encoding
Shell Chain exposes one valid representation of each 32-byte address:
0x<64 lowercase hex characters>
Example:
- canonical form:
0x0b871be37a4af6348d28a90dbecc96820aa50d5178b7cbd7ced3d710e5a7eda9
2.3 Why Shell Chain keeps one canonical form
The 0x + 64 lowercase hex format is canonical for JSON-RPC, genesis, CLI, SDK APIs, and human-facing displays.
It refers to the full 32-byte native address.
3. Validation model
Shell Chain uses a three-layer validation flow.
| Layer | Trigger | Validation rule | Purpose |
|---|---|---|---|
| Layer 1 | First transaction from an account with no state entry | Re-derive tx.from from (version, algo_id, pubkey) and verify signature |
Account creation / first-use safety |
| Layer 2 | Existing account with validation_code_hash = None |
Verify pubkey_hash and PQ signature |
Normal operation with key rotation support |
| Layer 3 | Existing account with validation_code_hash = Some(hash) |
Call account-specific validation logic in the PQVM | Multisig / recovery / custom policies |
3.1 Layer 1 — first-use validation
When the account does not yet exist in world state:
- the node requires
sender_pubkey - it derives the expected address from
(version, algo_id, pubkey) - it checks that the derived address matches
tx.from - it verifies the PQ signature
This is the only stage where address derivation itself is re-checked.
3.2 Layer 2 — default existing-account validation
Once an account exists and uses the built-in validator path:
- the node resolves the sender public key
- it compares
blake3(pubkey)withaccount.pq_pubkey_hash - it verifies the PQ signature
At this stage the chain no longer needs to re-derive the address from the new public key, which is what makes key rotation without address changes possible.
3.3 Layer 3 — custom validator path
If account.validation_code_hash is set, the chain delegates validation to
account-specific PQVM logic instead of the built-in PQ verifier.
This is the hook for advanced account policies such as:
- multisig
- social recovery
- time locks
- contract-defined signature / authorization gates
4. Custom validator contract interface
Shell Chain's native AA path calls a validation function with the transaction hash and signature material:
interface IAccountValidator {
function validateTransaction(
bytes32 txHash,
bytes calldata sig,
bytes calldata pubkey
) external returns (bytes1);
}
Validation call behavior
- target: the account address being validated
- gas cap:
500_000 - input:
validateTransaction(bytes32,bytes,bytes) - execution model: isolated validation dry-run against a world-state snapshot
- replay guard: protocol nonce equality is still enforced before execution
Current limitation
Custom validators currently receive only txHash, sig, and pubkey.
That is enough for custom signature / policy checks, but not enough to
safely replace canonical nonce handling. Because of that, Shell Chain keeps the
protocol nonce check as the baseline replay guard even when
validation_code_hash is set. Richer custom nonce schemes are deferred until
the validator ABI carries more transaction context.
Validation succeeds when the return value is interpreted as true / valid. Current node logic accepts the common "magic valid" encodings:
- raw
0x01 - ABI-encoded
bool(true) - ABI-encoded
bytes1(0x01)
This call path is implemented in:
crates/evm/src/aa_validation.rscrates/evm/src/tx_validation.rscontracts/DefaultPQValidator.sol
5. Key rotation and validator upgrades
The long-term AA model includes a protocol-managed account controller for:
rotateKey(pubkey, algo_id)setValidationCode(code_hash)clearValidationCode()
Why address rotation is not required
Shell Chain checks address derivation only when the account is first created.
After that, validation depends on the account's stored pq_pubkey_hash or
custom validator configuration.
That means a user can:
- keep the same account address
- rotate to a new keypair
- even move to a different supported PQ algorithm
without changing the account's on-chain identity.
Current status
The validation dispatcher, AccountManager system-contract flow, and reference validator contract are all landed. The remaining AA work is focused on wider workspace regression and final rollout validation.
6. How this differs from ERC-4337
| Topic | Shell Chain native AA | ERC-4337 |
|---|---|---|
| Validation location | Protocol-level | EntryPoint contract |
| Bundler required | No | Yes |
| Separate alt-mempool | No | Usually yes |
| Default validator | Built into the chain | Wallet contract-defined |
| Address format | 0x + 64 lowercase hex (32-byte native address) |
0x... (20-byte) |
Shell Chain's model is closer to a native smart-account chain than to an Ethereum add-on AA layer.
7. Rollout boundary for testnet
Native AA + 32-byte native addressing is a release-boundary change.
For the first public M9-compatible testnet:
- a one-time testnet reset is required
- genesis must be rebuilt with the new 32-byte address space
- old world-state / RocksDB / mempool data is not migrated in place
The same private keys may be reused, but their exported Shell Chain addresses must be re-derived under the new address scheme.
8. Implementation status
| Area | Status | Notes |
|---|---|---|
| PQ address derivation (`blake3(algo_id | pubkey)`) | |
| RPC / CLI / genesis address migration | ✅ Implemented | User-facing outputs now use 32-byte 0x hex |
| AA validation dispatcher core | ✅ Implemented | Layer 1 / Layer 2 / Layer 3 routing exists |
| Custom validator dry-run path | ✅ Implemented | Snapshot-based EVM validation with gas cap |
| Mempool / production ingress integration | ✅ Implemented | Revalidation and block-production paths are wired |
AccountManager (rotateKey, setValidationCode) |
✅ Implemented | Native system-contract flow is live and tested |
| Reference validator contract | ✅ Implemented | contracts/DefaultPQValidator.sol + compiled runtime fixture |
9. Developer pointers
If you want to trace the implementation in code:
crates/primitives/src/address.rs— address derivation and 32-byte hex encodingcrates/evm/src/aa_validation.rs— native AA dispatcher and custom-validator pathcrates/evm/src/tx_validation.rs— transaction validation entry pointscrates/mempool/src/pool.rs— mempool-side validation integration
10. Summary
Shell Chain's AA model combines:
- protocol-native smart-account validation
- post-quantum key material
- 32-byte
0x+ 64 hex chain-specific addresses - future-safe key rotation and validator upgrades
- batch transactions and native paymaster (v0.18.0 Phase 1)
The goal is to make account abstraction the default account model, not an optional overlay.
11. Batch Transactions (v0.18.0 Phase 1)
Shell Chain v0.18.0 introduces AA batch transactions — a single PQ-signed transaction that executes multiple inner calls atomically.
11.1 Transaction type 0x7E
Batch transactions use tx type 0x7E (126). The payload carries:
pub struct InnerCall {
pub to: Option<Address>, // None = contract creation
pub value: U256,
pub data: Bytes,
pub gas_limit: u64, // per-inner advisory cap; sum ≤ outer gas_limit
}
pub struct AaBundle {
pub inner_calls: Vec<InnerCall>,
pub paymaster: Option<Address>,
pub paymaster_signature: Option<Bytes>,
}
AaBundle is an optional trailing field on SignedTransaction. The base
Transaction struct is unchanged, preserving backward compatibility.
11.2 Execution semantics
- All inner calls share one nonce (the outer transaction nonce)
- All inner calls share one gas budget (the outer
gas_limit) - Execution is atomic: if any inner call reverts, the entire batch reverts
- Each inner call produces its own receipt logs
- Per-inner
gas_limitis an advisory cap; the node still enforces the sum
Maximum inner calls: 16 per batch.
11.3 Building a batch transaction with the SDK
import { buildBatchTransaction, buildSignedTransaction } from "shell-sdk";
const batch = buildBatchTransaction([
{ to: "0xContractA", value: 0n, data: calldata1, gasLimit: 100_000n },
{ to: "0xContractB", value: 1_000_000n, data: "0x", gasLimit: 21_000n },
]);
const signed = await buildSignedTransaction(adapter, {
...batch,
aaBundle: batch.aaBundle,
nonce: await provider.getNonce(address),
gasPrice: await provider.getGasPrice(),
gasLimit: 250_000n,
chainId: 1n,
});
await provider.sendTransaction(signed);
11.4 Estimating batch gas
Use shell_estimateBatch before submitting:
const estimate = await provider.estimateBatch({
from: address,
innerCalls: [
{ to: "0xContractA", value: "0x0", data: calldata1, gasLimit: "0x186a0" },
],
});
console.log(estimate.total_gas); // hex string
12. Sponsored Gas / Paymaster (v0.18.0 Phase 1)
Shell Chain v0.18.0 introduces native paymaster accounts. A paymaster is an ordinary chain account that authorizes paying gas on behalf of another account.
12.1 How paymaster authorization works
A batch transaction (or regular transaction) includes:
paymaster: Address— the account that will pay gaspaymaster_signature: Bytes— ECDSA/PQ signature by the paymaster over the canonical transaction hash
The node validates the paymaster signature during mempool admission:
- Checks that the paymaster address has sufficient balance
- Verifies the paymaster signature against the canonical hash
- On execution, deducts gas cost from the paymaster account instead of the sender
Scope (v0.18.0): paymaster is always a native EOA-like account. Smart-contract paymasters with arbitrary policy code are deferred to v0.19.0.
12.2 Checking paymaster policy
const policy = await provider.getPaymasterPolicy("0xpaymasterAddress");
// { address, hasPqPubkey, balance, policy: "eoa-open", maxGasSponsorship: null }
12.3 Checking sponsorship after execution
const info = await provider.isSponsored("0xtxhash");
// { found: true, location: "chain", isAaBundle: true, sponsored: true,
// paymaster: "0x...", sender: "0x...", innerCallCount: 2 }
12.4 Security and hash domains
Batch and sponsored transactions use a distinct hash domain from regular
transactions. This prevents any v0.17 or earlier signature from being replayed
as a v0.18.0 AA bundle. The hash domain is defined in
crates/primitives/src/transaction.rs.
13. Implementation pointers (v0.18.0)
| File | Purpose |
|---|---|
crates/primitives/src/transaction.rs |
AaBundle, InnerCall wire structs; hash domain |
crates/evm/src/executor.rs |
Batch execution loop; inner-call receipts |
crates/evm/src/tx_validation.rs |
Paymaster balance + signature pre-check |
crates/rpc/src/api.rs |
shell_estimateBatch, shell_getPaymasterPolicy, shell_isSponsored |
tests/e2e/aa_batch_test.rs |
E2E batch execution tests |
tests/e2e/aa_sponsored_test.rs |
E2E paymaster tests |