IoTeX의 계정 추상화에 대한 필수 가이드: p256 서명에 대한 실용 가이드
우리 커뮤니티가 IoTeX 개선 제안 14를 전폭적으로 찬성 투표하면서, 계정 추상화가 드디어 IoTeX 메인넷과 테스트넷에 활성화되었으며, 그 기능은 이제 모든 생태계 개발자에게 제공됩니다. 그렇다면 AA는 무엇이며, 어떻게 작동하고, 다음 애플리케이션에서 어떻게 사용할 수 있을까요?
간단한 복습
계정 추상화 (AA)는 ERC-4337에 의해 정의된 바와 같이 "사용자가 EOA 대신 임의의 검증 로직을 포함하는 스마트 계약 지갑을 기본 계정으로 사용할 수 있게 해줍니다." ERC-4337은 사용자가 스마트 계약을 기본 계좌로 사용할 수 있도록 하는 등 많은 사용자 경험의 이점을 도입합니다.
ERC-4337은 블록체인 위에서 실행되며 블록체인 자체에 대한 변경이 필요하지 않습니다. 현재 IoTeX의 계정 추상화 코드는 ERC-4337 0.6.0 릴리스 버전을 기반으로 합니다.
AA 인프라의 구성 요소
AA 인프라의 구성 요소는 다음과 같습니다:
- 번들러 서비스: 메인넷을 위한 하나의 엔드포인트 (https://bundler.w3bstream.com)와 테스트넷을 위한 하나의 엔드포인트 (https://bundler.testnet.w3bstream.com)가 있습니다. 번들러는 여러 개의 추상화된 사용자 작업을 단일 거래로 집계하여 기본 블록체인이 처리할 수 있도록 하는 오프체인 노드입니다. 이 거래는
EntryPoint계약이라고 하는 다른 고정 구성 요소로 전송됩니다. EntryPoint계약: IoTeX에 배포된EntryPoint계약이 두 개 있습니다. 하나는 메인넷용 (0xc3527348De07d591c9d567ce1998eFA2031B8675)이고, 다른 하나는 테스트넷용 (0xc3527348De07d591c9d567ce1998eFA2031B8675)입니다.EntryPoint계약은 특정 특별 계약인AccountFactory계약을 생성/배포하는 책임을 집니다. 이 계약은 특정 목적으로 사용할 수 있는 특정 계좌(지갑 계약)를 생성하는 책임을집니다.
계정 추상화를 사용하여 새로운 사용자 정의 계정을 생성하기 위해서는 dApp 개발자가 애플리케이션의 필요에 따라 생성해야 할 특정 구성 요소가 있습니다:
Account계약, 이는validateUserOp메서드에서 검증 로직을 구현하며, 사용자 작업에서 요구할 수 있는 실행 로직을 포함합니다.AccountFactory계약, 이는 위에서 설명한 바와 같이 새로운 사용자 정의 계정 계약을 생성/배포하는 책임을 집니다.- 사용자 작업(build user ops)을 구성하는 클라이언트 코드, 이는
AccountFactory에서 구현된 검증 규칙과 호환됩니다. - 강제목적지인 paymaster는 AA 아키텍처의 선택적 부분입니다. IoTeX는 테스트넷의 경우에만 https://paymaster.testnet.w3bstream.com에서 paymaster 서비스를 제공합니다. paymaster의 역할은 사용자 작업을 실행하는 데 필요한 가스를 후원하는 것으로, 이를 완전히 후원하거나 사용자가 다양한 토큰으로 이를 지불하는 것을 허용합니다.
예시: P256AccountFactory
첫 번째 예로, 우리는 개발자가 "p256" 암호화로 서명된 사용자 작업을 확인할 수 있는 계정 계약을 생성할 수 있도록 하는 공식 P256AccountFactory 계약을 제공합니다 (메인넷 0xD98d2B6cBca981c777037c5784721d8179D7030b 및 테스트넷 0x508Db1A73FcBA98594679aD4f5d8D0B880BbdaFB). 이는 사용자들이 생체 인식으로 거래에 서명하거나 시드 문구 없이 이동할 수 있게 해주고, 심지어는 그들의 장치가 전용 보안 칩을 지원하는 경우 더 나은 보안을 제공하는 유용한 기능입니다 (예: 안드로이드의 Secure Element와 애플의 Secure Enclave 등). P256AccountFactory의 소스 코드는 https://github.com/iotexproject/account-abstraction-contracts/blob/main/contracts/accounts/secp256r1/P256AccountFactory.sol에서 찾을 수 있으며, 오픈 소스 계정 추상화 계약은 여기 https://github.com/iotexproject/account-abstraction-contracts/tree/main의 EIP-4337의 원저자에 의해 구현된 것을 바탕으로 합니다.
P256AccountFactory는 또한 VerifyingPaymaster 계약 (https://github.com/iotexproject/account-abstraction-contracts/blob/main/contracts/paymaster/VerifyingPaymaster.sol) 및 paymaster 계약을 위한 지불 증명을 생성하는 오프체인 서비스 엔드포인트 (https://paymaster.testnet.w3bstream.com, 테스트넷 전용)로 구성된 paymaster 서비스의 관리를 지원합니다.
아래 코드는 JavaScript 클라이언트에서 p256 계정 구현과 상호작용하여 계정을 생성하는 방법을 보여줍니다:
async function main() {
// 배포된 계약 로드
const factory = (await ethers.getContract("P256AccountFactory")) as P256AccountFactory
const entryPoint = (await ethers.getContract("EntryPoint")) as EntryPoint
// 사용자 작업을 전송하는 EOA 계정
const bundler = new ethers.Wallet(process.env.BUNDLER!, ethers.provider)
// secp256r1 키쌍 로드
const keyContent = fs.readFileSync(path.join(__dirname, "key.pem"))
const keyPair = ecPem.loadPrivateKey(keyContent)
const publicKey = "0x" + keyPair.getPublicKey("hex").substring(2)
const index = 0
const account = await factory.getAddress(publicKey, index)
// 계정 생성 사용자 작업 생성
const initCode = hexConcat([ factory.address, factory.interface.encodeFunctionData("createAccount", [publicKey, index]),
])
const createOp = {
sender: account,
initCode: initCode,
}
const fullCreateOp = await fillUserOp(createOp, entryPoint)
// 가스를 위해 IOTX 지분
const stake = await entryPoint.balanceOf(account)
if (stake.isZero()) {
console.log(`계정 ${account}을(를) 위한 가스 입금`)
const tx = await entryPoint
.connect(bundler)
.depositTo(account, { value: ethers.utils.parseEther("10") })
await tx.wait()
}
// secp256r1 곡선을 사용하여 사용자 작업 서명
const chainId = (await ethers.provider.getNetwork()).chainId
const signedOp = await signOp(
fullCreateOp,
entryPoint.address,
chainId,
new P2565Signer(keyPair)
)
// 사용자 작업 시뮬레이션
const err = await entryPoint.callStatic.simulateValidation(signedOp).catch((e) => e)
if (err.errorName === "FailedOp") {
console.error(`시뮬레이션 op 오류 ${err.errorArgs.at(-1)}`)
return
} else if (err.errorName !== "ValidationResult") {
console.error(`알 수 없는 오류 ${err}`)
return
}
console.log(`시뮬레이션 op 성공`)
// 사용자 작업을 EntryPoint에 전송
const tx = await entryPoint.connect(bundler).handleOps([signedOp], bundler.address)
console.log(`계정 생성 tx: ${tx.hash}, 계정: ${account}`)
}
다음 코드는 번들러 서비스와 paymaster를 사용하여 IOTX를 전송하는 방법을 보여줍니다:
async function main() {
const factory = (await ethers.getContract("P256AccountFactory")) as P256AccountFactory
const accountTpl = await ethers.getContractFactory("P256Account")
const entryPoint = (await ethers.getContract("EntryPoint")) as EntryPoint
const paymaster = await ethers.getContract("VerifyingPaymaster")
const bundler = new JsonRpcProvider("http://localhost:4337")
const signer = new ethers.Wallet(process.env.PRIVATE_KEY!)
const keyContent = fs.readFileSync(path.join(__dirname, "key.pem"))
const keyPair = ecPem.loadPrivateKey(keyContent)
const publicKey = "0x" + keyPair.getPublicKey("hex").substring(2)
const index = 0
const account = await factory.getAddress(publicKey, index)
const callData = accountTpl.interface.encodeFunctionData("execute", [
"0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266",
ethers.utils.parseEther("0.1"),
"0x",
])
const transferOp = {
sender: account,
callData,
preVerificationGas: 50000,
}
const fullCreateOp = await fillUserOp(transferOp, entryPoint)
fullCreateOp.paymasterAndData = hexConcat([
paymaster.address,
defaultAbiCoder.encode(["uint48", "uint48"], [0, 0]),
"0x" + "00".repeat(65),
])
const validAfter = Math.floor(new Date().getTime() / 1000)
const validUntil = validAfter + 86400 // 하루
const pendingOpHash = await paymaster.getHash(fullCreateOp, validUntil, validAfter)
const paymasterSignature = await signer.signMessage(arrayify(pendingOpHash))
fullCreateOp.paymasterAndData = hexConcat([
paymaster.address,
defaultAbiCoder.encode(["uint48", "uint48"], [validUntil, validAfter]),
paymasterSignature,
])
const chainId = (await ethers.provider.getNetwork()).chainId
const signedOp = await signOp(
fullCreateOp,
entryPoint.address,
chainId,
new P2565Signer(keyPair)
)
const err = await entryPoint.callStatic.simulateValidation(signedOp).catch((e) => e)
if (err.errorName === "FailedOp") {
console.error(`시뮬레이션 op 오류 ${err.errorArgs.at(-1)}`)
return
} else if (err.errorName !== "ValidationResult") {
console.error(`알 수 없는 오류 ${err}`)
return
}
console.log(`시뮬레이션 op 성공`)
const hexifiedUserOp = deepHexlify(await resolveProperties(signedOp))
const result = await bundler.send("eth_sendUserOperation", [hexifiedUserOp, entryPoint.address])
console.log(`번들러를 사용한 전송 성공 opHash: ${result}`)
}
JavaScript 클라이언트에서 p256 계정 구현과 상호작용하는 방법에 대한 나머지 예시는 https://github.com/iotexproject/account-abstraction-contracts/tree/main/scripts/secp256r1에서 찾을 수 있습니다.