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:

In practice, this means the chain can support:


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)

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:

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:

  1. the node requires sender_pubkey
  2. it derives the expected address from (version, algo_id, pubkey)
  3. it checks that the derived address matches tx.from
  4. 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:

  1. the node resolves the sender public key
  2. it compares blake3(pubkey) with account.pq_pubkey_hash
  3. 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:


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

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:

This call path is implemented in:


5. Key rotation and validator upgrades

The long-term AA model includes a protocol-managed account controller for:

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:

  1. keep the same account address
  2. rotate to a new keypair
  3. 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:

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:


10. Summary

Shell Chain's AA model combines:

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

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:

The node validates the paymaster signature during mempool admission:

  1. Checks that the paymaster address has sufficient balance
  2. Verifies the paymaster signature against the canonical hash
  3. 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