Subscribe

IoTeX의 계정 추상화 필수 가이드: p256 서명 실전 가이드

IoTeX의 계정 추상화 필수 가이드: p256 서명 실전 가이드

커뮤니티가 IoTeX Improvement Proposal 14에 압도적으로 찬성 투표함에 따라, 계정 추상화(Account Abstraction)가 마침내 IoTeX 메인넷과 테스트넷에 도입되었으며, 이제 모든 생태계 개발자가 해당 기능을 사용할 수 있습니다. 그렇다면 AA란 무엇이고, 어떻게 작동하며, 다음 애플리케이션에서 어떻게 사용할 수 있을까요?

간단한 복습

ERC-4337에서 정의하는 계정 추상화(AA)는 “사용자가 기본 계정으로 EOA 대신 임의의 검증 로직을 포함하는 스마트 컨트랙트 지갑을 사용할 수 있게” 합니다. ERC-4337은 많은 사용자 경험 개선 사항을 도입하며, 특히 사람들이 스마트 컨트랙트를 기본 계정으로 사용할 수 있게 해준다는 점이 가장 주목할 만합니다.

ERC-4337은 블록체인 위에서 실행되며 블록체인 자체에 어떠한 변경도 필요로 하지 않습니다. 현재 IoTeX 계정 추상화 코드는 ERC-4337 0.6.0 릴리스 버전을 기반으로 합니다.

AA 인프라의 구성 요소

AA

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

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

계정 추상화를 사용하여 새로운 커스텀 계정을 만들기 위해서는, dApp 개발자가 자신의 애플리케이션 요구 사항에 맞게 만들어야 하는 몇 가지 구성 요소가 있습니다.

  • validateUserOp 메서드에 검증 로직과 사용자 작업(user operation)에 필요한 실행 로직을 구현하는 Account 컨트랙트.
  • 위에서 언급했듯이 새로운 커스텀 계정 컨트랙트를 생성/배포하는 역할을 하는 AccountFactory 컨트랙트.
  • AccountFactory에 구현된 검증 규칙과 호환되는 사용자 작업을 구성하는 클라이언트 코드.
  • 페이마스터(paymaster)는 AA 아키텍처의 선택적 구성 요소입니다. IoTeX는 테스트넷에 한해 https://paymaster.testnet.w3bstream.com에서 페이마스터 서비스를 제공합니다. 페이마스터의 역할은 사용자 작업 실행에 필요한 가스를 지원하는 것으로, 완전히 대신 지불하거나 사용자가 다양한 토큰으로 지불할 수 있도록 허용합니다.

예시: P256AccountFactory

첫 번째 예시로, 개발자가 이더리움 및 IoTeX 네이티브 “secp256k1” 타원 곡선이 아닌 “p256” 암호화 방식으로 서명된 사용자 작업을 검증할 수 있는 계정 컨트랙트를 생성할 수 있도록 하는 공식 P256AccountFactory 컨트랙트(메인넷 0xD98d2B6cBca981c777037c5784721d8179D7030b, 테스트넷 0x508Db1A73FcBA98594679aD4f5d8D0B880BbdaFB)를 제공합니다. 이는 개발자가 사용자가 생체 인증으로 트랜잭션에 서명하거나, 시드 문구(seed phrase)를 사용하지 않거나, 심지어 기기에 전용 보안 칩(예: Android의 Secure Element, Apple의 Secure Enclave 등)이 탑재된 경우 더 우수한 보안을 제공하는 애플리케이션을 만들 수 있게 해주기 때문에 매우 유용합니다. P256AccountFactorycan의 소스 코드는 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() {
    // load deployed contracts
    const factory = (await ethers.getContract("P256AccountFactory")) as P256AccountFactory
    const entryPoint = (await ethers.getContract("EntryPoint")) as EntryPoint

    // an EOA account for send UserOperations
    const bundler = new ethers.Wallet(process.env.BUNDLER!, ethers.provider)

    // load secp256r1 keypair
    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)

    // create create account UserOperation
    const initCode = hexConcat([        factory.address,        factory.interface.encodeFunctionData("createAccount", [publicKey, index]),
    ])
    const createOp = {
        sender: account,
        initCode: initCode,
    }

    const fullCreateOp = await fillUserOp(createOp, entryPoint)

    // stake IOTX for gas
    const stake = await entryPoint.balanceOf(account)
    if (stake.isZero()) {
        console.log(`deposit gas for account ${account}`)
        const tx = await entryPoint
            .connect(bundler)
            .depositTo(account, { value: ethers.utils.parseEther("10") })
        await tx.wait()
    }

    // sign UserOperation using secp256r1 curve
    const chainId = (await ethers.provider.getNetwork()).chainId
    const signedOp = await signOp(
        fullCreateOp,
        entryPoint.address,
        chainId,
        new P2565Signer(keyPair)
    )

    // simulate UserOperation
    const err = await entryPoint.callStatic.simulateValidation(signedOp).catch((e) => e)
    if (err.errorName === "FailedOp") {
        console.error(`simulate op error ${err.errorArgs.at(-1)}`)
        return
    } else if (err.errorName !== "ValidationResult") {
        console.error(`unknow error ${err}`)
        return
    }
    console.log(`simulate op success`)

    // send UserOpersion to EntryPoint
    const tx = await entryPoint.connect(bundler).handleOps([signedOp], bundler.address)
    console.log(`create account tx: ${tx.hash}, account: ${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 // one day
    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(`simulate op error ${err.errorArgs.at(-1)}`)
        return
    } else if (err.errorName !== "ValidationResult") {
        console.error(`unknow error ${err}`)
        return
    }
    console.log(`simulate op success`)

    const hexifiedUserOp = deepHexlify(await resolveProperties(signedOp))
    const result = await bundler.send("eth_sendUserOperation", [hexifiedUserOp, entryPoint.address])
    console.log(`transfer use bundler success opHash: ${result}`)
}

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

IoTeX