IoTeX에서 계정 추상화에 대한 필수 가이드: p256 서명에 대한 실용 가이드

The Essential Guide to Account Abstraction on IoTeX: A Practical Guide to p256 Signatures

커뮤니티가 IoTeX 개선 제안 14에 만장일치로 투표함에 따라, 계정 추상화가 마침내 IoTeX 메인넷과 테스트넷에 등장하였고, 그 기능이 이제 모든 생태계 개발자를 위해 제공됩니다. 그렇다면 AA란 무엇이며, 어떻게 작동하며, 다음 애플리케이션에서 어떻게 사용할 수 있을까요?

간단한 개요

계정 추상화(AA)는 ERC-4337에 정의되어 있으며, "사용자가 EOA 대신 임의의 검증 로직이 포함된 스마트 계약 지갑을 주요 계정으로 사용할 수 있도록 허용합니다." ERC-4337은 사용자가 스마트 계약을 주요 계정으로 사용할 수 있도록 해 주는 등 많은 사용자 경험 이점을 소개합니다.

ERC-4337은 블록체인 위에서 작동하며 블록체인 자체에 대한 변경이 필요하지 않습니다. 현재 IoTeX 계정 추상화 코드는 ERC-4337 0.6.0 릴리스 버전을 기반으로 하고 있습니다.

AA 인프라의 구성 요소

AA

AA 인프라의 구성 요소는 다음과 같습니다:

  • 번들러 서비스: 메인넷에 대한 하나의 엔드포인트 (https://bundler.w3bstream.com)와 테스트넷에 대한 하나의 엔드포인트 (https://bundler.testnet.w3bstream.com). 번들러는 여러 개의 추상화된 사용자 작업을 단일 트랜잭션으로 집계하여 기본 블록체인이 처리할 수 있도록 하는 오프체인 노드입니다. 이 트랜잭션은 EntryPoint 계약이라고 하는 다른 고정 구성 요소에 전송됩니다.
  • EntryPoint 계약: IoTeX에 배포된 두 개의 EntryPoint 계약이 있습니다: 하나는 메인넷용 (0xc3527348De07d591c9d567ce1998eFA2031B8675)이고, 하나는 테스트넷용 (0xc3527348De07d591c9d567ce1998eFA2031B8675)입니다. EntryPoint 계약은 특정 특별 계약(AccountFactory 계약이라고 함)을 생성하고 배포하는 역할을 하며, 이 계약은 특정 용도로 사용할 수 있는 특정 계정(지갑 계약)을 생성하는 역할을 맡고 있습니다.

계정 추상화를 사용하여 새로운 맞춤형 계정을 생성하기 위해서는 dApp 개발자가 애플리케이션의 요구에 따라 생성해야 할 특정 구성 요소가 있습니다:

  • Account 계약, 이는 validateUserOp 메서드에 검증 로직을 구현하고, 사용자 작업에서 요구할 수 있는 실행 로직을 포함합니다.
  • AccountFactory 계약, 이는 위에서 언급한 대로 새로운 맞춤 계정 계약의 생성/배포를 책임집니다.
  • AccountFactory에 구현된 검증 규칙과 호환되는 사용자 작업을 구축하는 클라이언트 코드.
  • 강조하는 페이마스터는 AA 아키텍처의 선택적 부분입니다. IoTeX는 테스트넷에 대해서만 https://paymaster.testnet.w3bstream.com에서 페이마스터 서비스를 제공합니다. 페이마스터의 역할은 사용자 작업을 실행하는 데 필요한 가스를 후원하는 것으로, 완전히 후원하거나 사용자가 다양한 토큰으로 지불할 수 있게 허용합니다.

예제: P256AccountFactory

첫 번째 예로, 우리는 개발자가 사용자 작업을 "p256" 암호화로 서명할 수 있는 계정 계약을 생성할 수 있도록 해주는 공식 P256AccountFactory 계약(메인넷 0xD98d2B6cBca981c777037c5784721d8179D7030b 및 테스트넷 0x508Db1A73FcBA98594679aD4f5d8D0B880BbdaFB)을 제공했습니다. 이는 개발자가 사용자가 바이오 매트릭스로 트랜잭션에 서명하거나, 시드 문구에서 벗어나거나, 전용 보안 칩(예: Android의 보안 요소 및 Apple의 보안 인클레이브 등)이 지원되는 경우 더욱 안전하게 사용할 수 있는 애플리케이션을 생성할 수 있도록 합니다. P256AccountFactory의 원본 코드는 https://github.com/iotexproject/account-abstraction-contracts/blob/main/contracts/accounts/secp256r1/P256AccountFactory.sol에서 찾을 수 있으며, 오픈 소스 계정 추상화 계약은 EIP-4337의 원래 저자에 의해 공식 이더리움 구현에 의존합니다. https://github.com/iotexproject/account-abstraction-contracts/tree/main에서 확인하실 수 있습니다.

P256AccountFactory는 또한 페이마스터 서비스 관리도 지원하며, 이는 두 구성 요소로 구성됩니다: VerifyingPaymaster 계약 (https://github.com/iotexproject/account-abstraction-contracts/blob/main/contracts/paymaster/VerifyingPaymaster.sol)와 페이마스터 계약에 대한 결제 증명을 생성하는 오프체인 서비스 엔드포인트 (https://paymaster.testnet.w3bstream.com, 테스트넷 전용).

아래 코드는 자바스크립트 클라이언트에서 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(`시뮬레이션 오류 ${err.errorArgs.at(-1)}`)
        return
    } else if (err.errorName !== "ValidationResult") {
        console.error(`알 수 없는 오류 ${err}`)
        return
    }
    console.log(`시뮬레이션 성공`)

    // EntryPoint에 사용자 작업 전송
    const tx = await entryPoint.connect(bundler).handleOps([signedOp], bundler.address)
    console.log(`계정 생성 tx: ${tx.hash}, 계정: ${account}`)
}

다음 코드는 번들러 서비스 및 페이마스터를 사용하여 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(`시뮬레이션 오류 ${err.errorArgs.at(-1)}`)
        return
    } else if (err.errorName !== "ValidationResult") {
        console.error(`알 수 없는 오류 ${err}`)
        return
    }
    console.log(`시뮬레이션 성공`)

    const hexifiedUserOp = deepHexlify(await resolveProperties(signedOp))
    const result = await bundler.send("eth_sendUserOperation", [hexifiedUserOp, entryPoint.address])
    console.log(`번들러 사용 전송 성공 opHash: ${result}`)
}

자바스크립트 클라이언트에서 p256 계정 구현과 상호작용하는 방법에 대한 나머지 예제는 https://github.com/iotexproject/account-abstraction-contracts/tree/main/scripts/secp256r1에서 확인하실 수 있습니다.