— ERC-4337 ACCOUNT ABSTRACTION —
A deep-dive into the WebXcom Account Abstraction architecture — how user accounts map to AA wallets, how slot ownership works, gas sponsorship via VerifyingPaymaster, and private key rotation. WebXcom 계정추상화(AA) 아키텍처 심층 가이드 — 사용자 계정과 AA 지갑의 매핑, 슬롯 소유권, VerifyingPaymaster를 통한 가스비 대납, 프라이빗키 교체 시스템을 다룹니다.
This guide covers the 6 core pillars of the WebXcom AA system. 이 가이드는 WebXcom AA 시스템의 6가지 핵심 주제를 다룹니다.
When a user signs up via Google OAuth or email, the WebXcom platform automatically provisions the full blockchain identity stack — no seed phrases, no wallet extensions, no Web3 knowledge required from the user. 사용자가 Google OAuth 또는 이메일로 가입하면, WebXcom 플랫폼은 전체 블록체인 신원 스택을 자동으로 프로비저닝합니다 — 시드 구문, 지갑 확장 프로그램, Web3 지식이 사용자에게 일절 필요하지 않습니다.
user_uuid in the DB →
the platform generates a owner_eoa (an Externally Owned Account whose private key is encrypted and stored server-side) →
which in turn controls a SimpleAccount (the ERC-4337 AA wallet, deployed via CREATE2 for a deterministic counterfactual address).
Google/Email 계정 → DB에서 user_uuid로 식별 →
플랫폼이 owner_eoa (프라이빗키가 암호화되어 서버에 저장되는 EOA)를 생성 →
이 EOA가 SimpleAccount (CREATE2를 통해 결정론적 카운터팩추얼 주소로 배포되는 ERC-4337 AA 지갑)를 제어합니다.
The complete account hierarchy: 전체 계정 계층 구조:
Google / Email Account
└── user_uuid (DB Primary Key)
└── owner_eoa (Platform-generated EOA, private key encrypted in DB)
└── SimpleAccount (AA Wallet, Counterfactual Address via CREATE2)
└── content_address (Slot/Deck address)
Counterfactual Address (CREATE2) — The AA wallet address is computed deterministically from the factory address, the owner EOA address, and a salt value. This means the address is known before the contract is actually deployed on-chain. The first transaction that touches this wallet will trigger deployment automatically via the initCode field of the UserOperation.
카운터팩추얼 주소 (CREATE2) — AA 지갑 주소는 팩토리 주소, owner EOA 주소, salt 값으로 결정론적으로 계산됩니다. 즉 컨트랙트가 온체인에 실제 배포되기 전에 주소를 미리 알 수 있습니다. 이 지갑에 대한 첫 번째 트랜잭션이 UserOperation의 initCode 필드를 통해 자동으로 배포를 트리거합니다.
owner_eoa private key. The platform holds it encrypted in the database. The AA wallet (SimpleAccount) is the user's on-chain identity — all transactions originate from this contract address.
사용자는 owner_eoa 프라이빗키를 직접 관리하지 않습니다. 플랫폼이 암호화하여 데이터베이스에 보관합니다. AA 지갑(SimpleAccount)이 사용자의 온체인 신원이며 — 모든 트랜잭션은 이 컨트랙트 주소에서 시작됩니다.
In the WebXcom system, the AA wallet address is the user's canonical on-chain identity. When the SimpleAccount executes a call on any contract, msg.sender is the SimpleAccount's address — not the owner EOA.
WebXcom 시스템에서 AA 지갑 주소는 사용자의 정규 온체인 신원입니다. SimpleAccount가 컨트랙트를 호출하면, msg.sender는 owner EOA가 아닌 SimpleAccount의 주소입니다.
Each slot (deck) also has its own dedicated AA wallet called content_address. This is computed with a different salt (C_SALT) using the same owner EOA, creating a separate SimpleAccount for content-level operations.
각 슬롯(덱)도 content_address라는 전용 AA 지갑을 가집니다. 동일한 owner EOA에 다른 salt(C_SALT)를 사용하여 계산되며, 콘텐츠 수준의 작업을 위한 별도의 SimpleAccount를 생성합니다.
| Entity | Address Type | Salt | Description |
|---|---|---|---|
| User | SimpleAccount | W_SALT |
Main AA wallet — the user's primary on-chain identity and msg.sender
메인 AA 지갑 — 사용자의 주요 온체인 신원이자 msg.sender
|
| Slot / Deck | SimpleAccount | C_SALT | Content AA wallet — dedicated address for the slot/deck content 콘텐츠 AA 지갑 — 슬롯/덱 콘텐츠 전용 주소 |
Ownership assignment — When a slot is purchased or minted, the dex contract's buy_slot(aaAddress, slot, ...) function registers the user's AA wallet address as the slot owner. On-chain ownership is determined solely by what is recorded in the dex contract.
소유권 할당 — 슬롯이 구매 또는 민팅되면, dex 컨트랙트의 buy_slot(aaAddress, slot, ...) 함수가 사용자의 AA 지갑 주소를 슬롯 소유자로 등록합니다. 온체인 소유권은 dex 컨트랙트에 기록된 내용에 의해서만 결정됩니다.
// On-chain ownership check (Solidity)
// dex contract stores: mapping(uint256 => address) public slotOwner;
function buy_slot(address aaAddress, uint256 slot, ...) external {
// ... payment logic ...
slotOwner[slot] = aaAddress; // User's SimpleAccount(W_SALT)
}
// When querying ownership:
// slotOwner[slot] == user's AA wallet address (SimpleAccount)
// Address computation (off-chain)
// Both addresses are derived from the SAME owner EOA but with different salts
const userAAAddress = computeAddress(factory, ownerEoa, W_SALT); // Main wallet
const contentAddress = computeAddress(factory, ownerEoa, C_SALT); // Slot/Deck wallet
// CREATE2: address = keccak256(0xff ++ factory ++ salt ++ keccak256(initCode))[12:]
W_SALT) handles identity and ownership. The content address (C_SALT) provides a dedicated namespace for slot/deck-level operations, keeping concerns separated on-chain.
사용자의 메인 AA 지갑(W_SALT)은 신원과 소유권을 처리합니다. 콘텐츠 주소(C_SALT)는 슬롯/덱 수준의 작업을 위한 전용 네임스페이스를 제공하여, 온체인에서 관심사를 분리합니다.
Slot ownership transfer is executed as an AA UserOperation. The platform handles the entire flow — the user simply initiates the request, and the platform signs and submits the on-chain transaction on their behalf. 슬롯 소유권 이전은 AA UserOperation으로 실행됩니다. 플랫폼이 전체 플로우를 처리하며 — 사용자는 요청만 시작하면, 플랫폼이 대신 서명하고 온체인 트랜잭션을 제출합니다.
| Parameter | Type | Required | Description |
|---|---|---|---|
| state | string | Required | Session state token for request validation 요청 검증을 위한 세션 상태 토큰 |
| cu_id | string | Required | Content unit identifier 콘텐츠 유닛 식별자 |
| from_uuid | string | Required | Current owner's user_uuid 현재 소유자의 user_uuid |
| to_uuid | string | Required | New owner's user_uuid 새 소유자의 user_uuid |
| meta_idx | number | Required | Metadata index of the slot 슬롯의 메타데이터 인덱스 |
Server-to-server flow — The request travels from A서버 (OAUTH/NestJS) via gRPC to B서버 (Oauth-blockchain), which constructs and submits the AA UserOperation. 서버 간 플로우 — 요청은 A서버(OAUTH/NestJS)에서 gRPC를 통해 B서버(Oauth-blockchain)로 전달되며, B서버가 AA UserOperation을 구성하고 제출합니다.
// Request body
POST /idex-platform/exchange_token_owner
{
"state": "session-state-token",
"cu_id": "content-unit-id",
"from_uuid": "sender-user-uuid",
"to_uuid": "receiver-user-uuid",
"meta_idx": 42
}
The transfer flow end-to-end: 엔드투엔드 트랜스퍼 플로우:
User requests transfer (from_uuid → to_uuid)
↓
A서버: /idex-platform/exchange_token_owner
↓ gRPC
B서버: dapp_exchange_own_initPermissionSelf
↓
AA UserOperation: SimpleAccount.execute(dex.init_self(aaAddress, slot, newOwnerAAAddress))
↓ EntryPoint.handleOps
On-chain: Slot ownership changed
Users on the WebXcom platform never pay gas fees. Every on-chain operation — slot purchases, transfers, ownership changes — is sponsored by the platform's VerifyingPaymaster. This is a core design decision that enables a Web2-like user experience on Web3 infrastructure. WebXcom 플랫폼의 사용자는 가스비를 절대 부담하지 않습니다. 슬롯 구매, 트랜스퍼, 소유권 변경 등 모든 온체인 작업은 플랫폼의 VerifyingPaymaster가 대납합니다. 이는 Web3 인프라 위에서 Web2와 같은 사용자 경험을 제공하기 위한 핵심 설계 결정입니다.
The complete UserOperation lifecycle: UserOperation의 전체 라이프사이클:
executeViaAA()
1. Build UserOperation
├── sender = aaAddress (SimpleAccount)
├── initCode = factory.createAccount(owner, salt) // only if first tx
├── callData = SimpleAccount.execute(target, value, data)
├── callGasLimit / verificationGasLimit / preVerificationGas
└── paymasterAndData = (empty, filled in step 2)
2. signPaymasterData()
├── Platform signs: hash(userOp, validUntil, validAfter)
├── validUntil = now + 5 minutes
├── validAfter = now
└── paymasterAndData = paymaster + validUntil + validAfter + signature
3. signUserOperation()
├── owner_eoa signs the full UserOp hash
└── signature = owner_eoa.sign(entryPoint.getUserOpHash(userOp))
4. submitUserOperation()
└── EntryPoint.handleOps([userOp], beneficiary)
↓
EntryPoint validates Paymaster signature ✓
EntryPoint validates owner signature ✓
SimpleAccount.execute() runs ✓
Gas deducted from Paymaster's EntryPoint deposit
initCode contains the factory call that deploys the SimpleAccount contract. The EntryPoint executes this before validation. All subsequent transactions use initCode = '0x' (empty) since the contract already exists.
첫 번째 UserOperation에서 initCode는 SimpleAccount 컨트랙트를 배포하는 팩토리 호출을 포함합니다. EntryPoint가 검증 전에 이를 실행합니다. 이후 모든 트랜잭션은 컨트랙트가 이미 존재하므로 initCode = '0x'(빈 값)를 사용합니다.
// initCode construction (first tx only)
const initCode = ethers.utils.solidityPack(
['address', 'bytes'],
[
FACTORY_ADDRESS,
factory.interface.encodeFunctionData('createAccount', [ownerEoa, salt])
]
);
// Subsequent transactions
const initCode = '0x'; // Contract already deployed
validUntil timestamp set to now + 5 minutes. After this window, the signature is rejected by the EntryPoint. This prevents captured UserOperations from being replayed later.
페이마스터 서명에는 현재 시간 + 5분으로 설정된 validUntil 타임스탬프가 포함됩니다. 이 시간이 지나면 EntryPoint에서 서명이 거부됩니다. 이를 통해 캡처된 UserOperation의 재생 공격을 방지합니다.
| UserOp Field | First Transaction | Subsequent Transactions |
|---|---|---|
| sender | AA address (counterfactual, not yet deployed) AA 주소 (카운터팩추얼, 아직 미배포) | AA address (deployed contract) AA 주소 (배포된 컨트랙트) |
| initCode | factory + createAccount(owner, salt) |
'0x' |
| nonce | 0 |
Auto-incremented by EntryPoint EntryPoint에 의해 자동 증가 |
| paymasterAndData |
paymaster address + validUntil + validAfter + paymaster signature
|
|
The PIN is a user-facing security mechanism that bridges the gap between Web2 familiarity and Web3 key management. It is not the private key of the AA wallet. Rather, the PIN is one of three cryptographic shares required to reconstruct the owner EOA private key — using an XOR 3-of-3 threshold scheme. No single party ever holds the complete private key. PIN은 Web2의 친숙함과 Web3 키 관리 사이의 간극을 메우는 사용자 대면 보안 메커니즘입니다. AA 지갑의 프라이빗키가 아닙니다. PIN은 XOR 3-of-3 임계값 방식으로 owner EOA 프라이빗키를 복원하기 위해 필요한 세 개의 암호학적 조각 중 하나입니다. 어떤 단일 주체도 완전한 프라이빗키를 단독으로 보유하지 않습니다.
When a PIN wallet is created, the owner EOA private key is split into three shares via bitwise XOR. All three shares must be combined to recover the original key: PIN 지갑이 생성될 때, owner EOA 프라이빗키는 비트 XOR을 통해 세 개의 조각으로 분할됩니다. 원래 키를 복원하려면 세 조각 모두 필요합니다:
fullPrivateKey = share_pin ⊕ share_server ⊕ share_user
share_pin : PBKDF2-SHA256(pin_hash, 'webxcom-pin-salt', 100_000 iter, 32 bytes)
— derived on-demand from user's PIN; never stored anywhere
share_server : crypto.randomBytes(32)
— stored in B서버 DB (Key table, b_key column)
share_user : fullPrivateKey ⊕ share_pin ⊕ share_server
— returned to client; stored in Rails encrypted session cookie
| Share | Holder 보관 주체 | Storage 저장 위치 | How recovered 복원 방법 |
|---|---|---|---|
| share_pin | Derived from user's PIN 사용자 PIN에서 파생 | Never stored — re-derived on each use 저장하지 않음 — 매번 재파생 | User enters PIN → PBKDF2 derivation 사용자 PIN 입력 → PBKDF2 파생 |
| share_server | B서버 (Blockchain Service) B서버 (블록체인 서비스) |
B서버 DB — Key.b_key
B서버 DB — Key.b_key
|
Loaded from DB on every transaction 트랜잭션마다 DB에서 로드 |
| share_user | Browser (webxcom client) 브라우저 (webxcom 클라이언트) |
Rails encrypted session cookie (session[:key_share_user])
Rails 암호화 세션 쿠키 (session[:key_share_user])
|
Sent to A서버 with every PIN-gated request PIN 인증 요청마다 A서버로 전송 |
share_server — useless without share_user (browser session) and the user's PIN. Similarly, a stolen session cookie yields only share_user — the other two shares remain unknown. The full private key exists in plaintext only inside the B서버 process memory during transaction signing, and is securely wiped immediately after use (Buffer.fill(0)).
B서버 DB에 접근한 공격자는 share_server만 얻습니다 — share_user(브라우저 세션)와 사용자 PIN 없이는 무용지물입니다. 마찬가지로 탈취된 세션 쿠키는 share_user만 노출합니다 — 나머지 두 조각은 알 수 없습니다. 완전한 프라이빗키는 트랜잭션 서명 시 B서버 프로세스 메모리 내에서만 잠시 존재하며, 사용 직후 안전하게 초기화됩니다 (Buffer.fill(0)).
PIN registration happens during account signup. The platform creates the AA wallet and XOR-splits the private key in a single atomic operation. PIN 등록은 계정 가입 시 발생합니다. 플랫폼은 단일 원자적 연산으로 AA 지갑을 생성하고 프라이빗키를 XOR 분할합니다.
// webxcom → A서버
POST /v1/pin/register
{
"user_uuid": 12345,
"pin_hash": "SHA-256(pin + pin_salt)", // plaintext PIN never transmitted
"pin_salt": "<64-char hex>"
}
// A서버 → B서버 (gRPC)
CreateWalletWithPin({ account_idx, pin_hash })
// B서버 internal:
// 1. Ethers.Wallet.createRandom() → mnemonic, privateKey, ownerEoaAddress
// 2. AAService.computeAAAddress() → aaAddress (counterfactual, CREATE2)
// 3. splitKeyXor3(privateKey, pin_hash):
// share_pin = PBKDF2(pin_hash, 'webxcom-pin-salt', 100000, 32, sha256)
// share_server = crypto.randomBytes(32)
// share_user = privateKey ⊕ share_pin ⊕ share_server
// 4. DB transaction (atomic):
// Wallet.create({ account_idx, address: aaAddress, owner_eoa, wallet_type: 'AA' })
// Key.create({ a_key: UUID(account_idx, U_SALT), b_key: share_server })
// Key.create({ a_key: UUID(account_idx, U_SALT)+'_MN', b_key: AES256(mnemonic) })
// 5. privateKey buffer wiped (Buffer.fill(0))
// A서버 response → webxcom
{
"success": true,
"data": [{
"address": "0xAA...", // SimpleAccount address
"mnemonic": "<AES-256-CTR encrypted>",
"key_share_user": "0x..." // share_user — stored in Rails session
}]
}
Every sensitive operation (transfer, ownership change, etc.) requires the user to enter their PIN. The three shares are combined in-memory inside B서버 to reconstruct the private key for signing, then immediately wiped. 모든 민감한 작업(이체, 소유권 변경 등)은 사용자 PIN 입력이 필요합니다. 세 조각은 B서버 내부 메모리에서 합산되어 서명용 프라이빗키를 복원하며, 즉시 초기화됩니다.
// webxcom → A서버
POST /v1/pin/verify
{
"user_uuid": 12345,
"pin_hash": "SHA-256(entered_pin + session[:pin_salt])",
"pin_salt": "session[:pin_salt]",
"key_share_user": "session[:key_share_user]"
}
// A서버 → B서버 (gRPC)
ExecutePinTransaction({ account_idx, pin_hash, key_share_user, tx_data })
// B서버 internal:
// 1. share_server = Key.findOne({ a_key: UUID(account_idx, U_SALT) }).b_key
// 2. restoreKeyXor3(key_share_user, pin_hash, share_server):
// share_pin = PBKDF2(pin_hash, 'webxcom-pin-salt', 100000, 32, sha256)
// fullKey = share_user ⊕ share_pin ⊕ share_server
// 3. Sign UserOperation with fullKey
// 4. fullKey buffer wiped (Buffer.fill(0))
// 5. Submit via EntryPoint
If the user remembers their mnemonic (12-word recovery phrase) but forgets their PIN, they can reset the PIN without changing the owner EOA. The existing owner EOA private key is re-split with the new PIN hash. 사용자가 니모닉(12단어 복구 구문)을 기억하지만 PIN을 잊어버린 경우, owner EOA를 변경하지 않고 PIN을 재설정할 수 있습니다. 기존 owner EOA 프라이빗키가 새 PIN 해시로 재분할됩니다.
// POST /v1/pin/reset
{
"user_uuid": 12345,
"mnemonic": "word1 word2 ... word12", // user's recovery phrase
"new_pin_hash": "SHA-256(new_pin + new_salt)",
"new_pin_salt": "<64-char hex>"
}
// B서버 internal (verifyMnemonic):
// 1. Decrypt stored mnemonic (Key[UUID_MN]) → compare with user input
// 2. Derive privateKey from mnemonic (Ethers.fromMnemonic)
// 3. splitKeyXor3(privateKey, new_pin_hash) → new share_server, new share_user
// 4. Key[UUID].b_key ← new share_server
// 5. Return new key_share_user (owner EOA unchanged)
| Aspect | Traditional Web3 전통적 Web3 | WebXcom (XOR 3-of-3 PIN) WebXcom (XOR 3-of-3 PIN) |
|---|---|---|
| Daily Auth 일상 인증 | Wallet extension + signature prompt 지갑 확장 프로그램 + 서명 프롬프트 | 6-digit PIN (familiar Web2 pattern) 6자리 PIN (친숙한 Web2 패턴) |
| Key Storage 키 저장 | Single encrypted key in custody 단일 암호화 키 보관 | XOR 3-of-3: share split across browser session, B서버 DB, and user PIN XOR 3-of-3: 조각이 브라우저 세션, B서버 DB, 사용자 PIN으로 분산 |
| Key Backup 키 백업 | User must store 12-24 word seed phrase 사용자가 12-24 단어 시드 구문 보관 필요 | Mnemonic stored AES-256-CTR encrypted in B서버 DB (for PIN reset only) 니모닉은 B서버 DB에 AES-256-CTR 암호화 저장 (PIN 재설정 전용) |
| PIN/Key Loss PIN/키 분실 | Permanent loss of funds 자금의 영구적 손실 | PIN reset via mnemonic; full key rotation via email verification (Section 6) 니모닉으로 PIN 재설정; 이메일 인증으로 전체 키 교체 (섹션 6) |
| Custodial Risk 수탁 위험 | Custodian holds full key 수탁자가 전체 키 보유 | No single party holds full key — 3-of-3 required 단일 주체가 전체 키 미보유 — 3-of-3 필요 |
| User Knowledge 사용자 지식 | Must understand blockchain, gas, wallets 블록체인, 가스, 지갑 이해 필요 | Zero blockchain knowledge needed 블록체인 지식 불필요 |
POST /v1/pin/reset): User provides their 12-word mnemonic. B서버 validates it, re-splits the same private key with the new PIN. Owner EOA is unchanged; AA wallet address is unchanged.POST /v1/pin/recover): User verifies identity via email. B서버 generates a new owner EOA, calls SimpleAccount.changeOwner(newOwnerEoa) on-chain, and re-splits the new private key. AA wallet address remains unchanged. See Section 6 for details.
옵션 A — 니모닉 재설정 (POST /v1/pin/reset): 사용자가 12단어 니모닉을 제공합니다. B서버가 검증 후 동일한 프라이빗키를 새 PIN으로 재분할합니다. Owner EOA 불변, AA 지갑 주소 불변.POST /v1/pin/recover): 사용자가 이메일로 신원 확인. B서버가 새 owner EOA를 생성하고 SimpleAccount.changeOwner(newOwnerEoa)를 온체인 호출 후, 새 프라이빗키를 재분할합니다. AA 지갑 주소 불변. 자세한 내용은 섹션 6 참조.
One of the most powerful properties of Account Abstraction is the ability to rotate the signing key without changing the wallet address. Since the AA wallet is a smart contract (SimpleAccount), its owner field can be updated to point to a new EOA — while the contract address (and thus the on-chain identity) remains permanent.
계정추상화의 가장 강력한 속성 중 하나는 지갑 주소를 변경하지 않고 서명 키를 교체할 수 있다는 것입니다. AA 지갑은 스마트 컨트랙트(SimpleAccount)이므로, owner 필드를 새 EOA를 가리키도록 업데이트할 수 있습니다 — 컨트랙트 주소(즉 온체인 신원)는 영구적으로 유지됩니다.
Key rotation is triggered when the user can no longer recover their PIN via mnemonic (lost both PIN and mnemonic, or suspects key compromise). The user verifies identity via email, and the platform generates a completely new owner EOA, rotates ownership on-chain, and XOR-re-splits the new key with the new PIN. 키 교체는 사용자가 니모닉으로도 PIN을 복구할 수 없는 경우(PIN과 니모닉 모두 분실, 또는 키 유출 의심)에 트리거됩니다. 사용자가 이메일로 신원 확인을 완료하면, 플랫폼은 완전히 새로운 owner EOA를 생성하고, 온체인 소유권을 교체하며, 새 PIN으로 새 키를 XOR 재분할합니다.
This calls the B서버 gRPC service: 이는 B서버 gRPC 서비스를 호출합니다:
// webxcom → A서버 (after email verification)
POST /v1/pin/recover
{
"user_uuid": 12345,
"pin_hash": "SHA-256(new_pin + new_salt)",
"pin_salt": "<64-char hex>"
}
// A서버 → B서버 (gRPC)
RecoverWalletKeyWithPin({ account_idx, pin_hash })
Step-by-step breakdown inside B서버 (recover_wallet_key_with_pin):
B서버 내부 단계별 상세 설명 (recover_wallet_key_with_pin):
| Step | Actor | Action |
|---|---|---|
| 1 | B서버 |
Load current state: retrieve share_server from Key[UUID] and decrypt Key[UUID_MN] to get stored mnemonic
현재 상태 로드: Key[UUID]에서 share_server 조회, Key[UUID_MN] 복호화하여 니모닉 확인
|
| 2 | B서버 |
Validate current owner: derive old owner EOA from stored mnemonic, verify it matches Wallet.owner_eoa; if on-chain check needed, verify SimpleAccount.owner
현재 소유자 검증: 저장된 니모닉에서 기존 owner EOA 파생, Wallet.owner_eoa와 일치 확인; 온체인 검증이 필요하면 SimpleAccount.owner 확인
|
| 3 | B서버 |
Generate new owner EOA: Ethers.Wallet.createRandom() → new mnemonic, new private key, new EOA address
새 owner EOA 생성: Ethers.Wallet.createRandom() → 새 니모닉, 새 프라이빗키, 새 EOA 주소
|
| 4 | B서버 |
Build UserOperation: callData = SimpleAccount.changeOwner(newOwnerEoa); old owner EOA signs this UserOp (still the current on-chain signer)
UserOperation 구성: callData = SimpleAccount.changeOwner(newOwnerEoa); 기존 owner EOA가 서명 (여전히 현재 온체인 서명자)
|
| 5 | EntryPoint |
Validates old owner signature → executes SimpleAccount.owner = newOwnerEoa on-chain
기존 owner 서명 검증 → 온체인 SimpleAccount.owner = newOwnerEoa 실행
|
| 6 | B서버 |
XOR re-split new private key: splitKeyXor3(newPrivateKey, new_pin_hash) → new share_server, new share_user
새 프라이빗키 XOR 재분할: splitKeyXor3(newPrivateKey, new_pin_hash) → 새 share_server, 새 share_user
|
| 7 | B서버 |
Atomic DB update: Key[UUID].b_key ← new share_server, Key[UUID_MN].b_key ← AES256(new mnemonic), Wallet.owner_eoa ← new EOA address
원자적 DB 업데이트: Key[UUID].b_key ← 새 share_server, Key[UUID_MN].b_key ← AES256(새 니모닉), Wallet.owner_eoa ← 새 EOA 주소
|
| 8 | B서버 |
Secure cleanup: wipe all key buffers (Buffer.fill(0)); return { address (unchanged), mnemonic_enc, key_share_user }
보안 정리: 모든 키 버퍼 초기화 (Buffer.fill(0)); { address (불변), mnemonic_enc, key_share_user } 반환
|
changeOwner succeeds but the DB transaction fails, the system reconciles by checking SimpleAccount.owner on-chain before any subsequent operation. B서버 always verifies the on-chain owner matches the DB owner_eoa at the start of recover_wallet_key_with_pin and will recompute the AA address if needed.
키 교체는 온체인과 오프체인 레이어 간에 원자적이지 않습니다. 온체인 changeOwner가 성공하고 DB 트랜잭션이 실패하면, 이후 작업 전에 온체인 SimpleAccount.owner를 확인하여 조정합니다. B서버는 recover_wallet_key_with_pin 시작 시 온체인 소유자와 DB owner_eoa가 일치하는지 항상 검증하며, 필요하면 AA 주소를 재계산합니다.
The on-chain state before and after rotation: 교체 전후의 온체인 상태:
// Before rotation
SimpleAccount (0xAA11...):
owner = 0xOLD_EOA_111... (old signing key — reconstructed via old XOR shares)
// After rotation
SimpleAccount (0xAA11...): ← ADDRESS UNCHANGED
owner = 0xNEW_EOA_222... (new signing key — new XOR 3-of-3 split)
// B서버 DB after rotation
Key[UUID].b_key = new_share_server (old share_server overwritten)
Key[UUID_MN].b_key = AES256(new_mnemonic)
Wallet.owner_eoa = 0xNEW_EOA_222...
// Browser session after rotation
session[:key_share_user] = new_share_user (old share_user invalidated)
session[:pin_hash] = new_pin_hash
// All on-chain references to 0xAA11... remain valid
// Slot ownership, token balances, approvals — all preserved
| Scenario 시나리오 | Method 방법 | Owner EOA Owner EOA | On-chain tx? 온체인 트랜잭션? |
|---|---|---|---|
| Forgot PIN, have mnemonic PIN 분실, 니모닉 보유 | POST /v1/pin/reset |
Unchanged 변경 없음 | No 아니오 |
| Forgot PIN + mnemonic, or key compromise PIN + 니모닉 분실, 또는 키 유출 의심 | POST /v1/pin/recover |
New (rotated) 새로 교체 |
Yes — changeOwner
예 — changeOwner
|