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 계약, 이는 위에서 설명한 바와 같이 새로운 사용자 정의 계정 계약을 생성/배포하는 책임을 집니다.
  • 사용자 작업(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에서 찾을 수 있습니다.