AI와 오프체인 데이터를 위한 서명 쿼럼 오라클

“실전 오라클” 시리즈 4편(마지막) — 스마트 컨트랙트 엔지니어를 위한 실습형 연재.

어떤 사실은 평균 낼 시장도, 읽을 공개 피드도 없습니다. 모델의 리스크 스코어, proof-of-reserves 증명, 오프체인 연산의 출력 같은 것들. 이럴 땐 분산 시스템의 가장 오래된 수법으로 돌아갑니다. 서로 독립적인 여럿이 보증하게 하고, 임계치의 동의를 요구하는 것. 이것이 서명 쿼럼(signature quorum)이고, AI 출력이 온체인에 앵커되는 방식으로 점점 자리 잡고 있습니다.

무엇을 증명하는지 정직해야 합니다. 쿼럼은 이 인가된 서명자들이, 이 모델이 이 입력으로 낸 이 값에 보증했다는 것을 증명합니다. 모델이 옳았다는 것은 증명하지 않습니다. 가장 싼 검증 계층 — 신원과 동의 — 이고, 연산 자체를 증명하려면 그 위에 TEE 어테스테이션이나 ZK-inference 증명을 얹습니다.

리포트와 EIP-712 타입

맨 해시가 아니라 구조화된 데이터에 서명하세요. EIP-712는 서명자(그리고 하드웨어 지갑)에게 읽히는 페이로드를 주고, 서명을 여러분의 컨트랙트와 체인에 묶습니다. 리포트는 소비자가 출처를 따질 때 필요한 모든 걸 담습니다.

struct AIReport {
    bytes32 modelId;    // 어떤 모델이 값을 냈는가
    bytes32 inputHash;  // 그 모델에 넣은 정확한 입력의 해시
    int256  value;      // 출력 (리스크 스코어, 가격, 분류...)
    uint64  confidence; // 0..1e6 스케일
    uint64  timestamp;  // 오프체인에서 추론이 돈 시각
    uint64  roundId;    // 엄격 증가; 리플레이 방어
}

bytes32 private constant REPORT_TYPEHASH = keccak256(
    "AIReport(bytes32 modelId,bytes32 inputHash,int256 value,uint64 confidence,uint64 timestamp,uint64 roundId)"
);

typehash 문자열은 struct와 바이트 단위로 일치해야 합니다. 안 그러면 오프체인·온체인 digest가 갈라지고, 모든 서명이 기대한 서명자로 복구되지 못한 채 조용히 실패합니다. 여기서 가장 흔한 연동 버그라, 컨트랙트의 _hashTypedDataV4가 ethers의 TypedDataEncoder.hash와 일치하는지 확인하는 전용 테스트를 둘 가치가 있습니다.

검증 루프, 그리고 공짜 점심

각 서명을 digest에 대해 복구하고 유효·인가된 서명자를 셉니다. 우아한 부분은 한 서명자가 두 번 세어지거나 호출자가 중복으로 채우는 걸 막는 방식입니다.

bytes32 digest = _hashTypedDataV4(_hashReport(report));

address last = address(0);
uint256 valid = 0;
for (uint256 i = 0; i < signatures.length; i++) {
    address signer = ECDSA.recover(digest, signatures[i]);
    if (signer <= last) revert SignaturesUnsortedOrDuplicate();
    if (!isSigner[signer]) revert UnauthorizedSigner(signer);
    last = signer;
    unchecked { valid++; }
}
if (valid < threshold) revert QuorumNotMet();

복구된 주소의 엄격 오름차순을 요구하면 한 번의 값싼 순회로 두 일을 합니다. 중복을 거부하고(반복된 서명자는 직전보다 클 수 없음) 동시에 스토리지나 메모리에 본-적-있는-주소 집합을 둘 필요를 없앱니다. 비용은 오프체인으로 밀립니다. 릴레이어가 제출 전에 복구 서명자 기준으로 서명을 정렬해야 하죠. 비용이 있어야 할 정확히 옳은 자리입니다.

오프체인 측, ethers로:

const signed = await Promise.all(publishers.map((p) => signOne(p, report)));
signed.sort((a, b) => (a.signer.toLowerCase() < b.signer.toLowerCase() ? -1 : 1));
return signed.map((s) => s.signature);

쿼럼을 둘러싼 가드

낡거나 리플레이된 리포트 위의 유효한 쿼럼도 여전히 익스플로잇입니다. 완전한 submitReport는 루프를 다른 오라클과 같은 fail-closed 규율로 감쌉니다.

  • 리플레이: roundId는 마지막으로 받아들인 라운드를 엄격히 초과해야 합니다. 오래됐지만 완벽히 서명된 리포트를 재제출할 수 없습니다.
  • 신선도: 미래 timestamp를 거부하고, maxStaleness보다 오래된 것도 거부합니다.
  • 서킷 브레이커: 기준선이 생기면 업데이트당 편차를 제한(maxDeviationBps)해, 나쁜 쿼럼 하나가 값을 임의로 못 움직이게 합니다.
  • 서명자 거버넌스: owner 게이트 addSigner/removeSigner, 그리고 임계치가 서명자 수를 넘지 못하도록 가드된 setThreshold.

전체 컨트랙트를 로컬 EVM에서 검증했습니다. 유효한 2-of-3 쿼럼은 커밋되고, 임계치 미만·미인가·중복 서명자 제출은 revert하며, 비증가 라운드·미래/stale 타임스탬프는 revert하고, +400% 점프는 브레이커를 트립시키는 반면 +15% 이동은 통과합니다.

왜 이것이 AI 출력의 자연스러운 자리인가

정직한 프레이밍으로 돌아갑시다. 컨트랙트가 AI 오라클 값을 읽을 때, 그것은 여러 갈래의 보안 질문을 던집니다. 누가 이것을 냈는가, 어떤 입력인가, 어떤 모델인가, 연산이 실제로 돌았는가, 틀리면 피해가 얼마인가? 서명 쿼럼은 앞의 셋 — 신원, 입력, 모델 — 을 깔끔히 답하고, 편차 브레이커로 마지막을 제한합니다. “연산이 실제로 옳게 돌았는가”는 답하지 못하고, 아무리 서명해도 답하지 못합니다. 바로 그 경계에서 다음 계층으로 손을 뻗습니다.

  • TEE 어테스테이션은 인가된, 변조되지 않은 코드가 봉인된 환경에서 돌았음을 증명합니다.
  • ZK-inference 증명은 특정 모델이 이 입력에 이 출력을 냈음을 증명합니다. 지금은 비싸지만 가장 강한 보증이자 이 분야가 향하는 방향입니다.

의도적으로 계층을 쌓으세요. 서명은 단일 서버 손상을 막는, 싸고 읽히는 신원·동의 바닥을 줍니다. TEE/ZK는 판돈이 연산 자체의 증명을 정당화할 때 그 위에 앉습니다. 대부분의 프로토콜은 서명 계층에서 시작해, 쿼럼과 임계치를 보수적으로 튜닝하고, 위험에 노출된 가치가 요구하는 곳에만 더 무거운 증명을 추가해야 합니다.

이것으로 시리즈를 닫습니다. 세 소싱 패턴 — 집계 푸시 피드, 온체인 TWAP, 서명 쿼럼 — 어느 것도 신뢰를 없애지 않지만, 모두 신뢰를 충분히 많은 독립 지점으로 부숴서 시스템 배신이 이득이 되지 못하게 합니다. 공학은 여러분 데이터에 맞는 패턴을 고르는 데, 그리고 모든 값을 마치 적이 쓴 것처럼 검증하는 데 있습니다. 실제로 가끔은 적이 쓴 것이니까요.


Tags: Solidity, AI, Cryptography, SmartContracts, Web3

Similar Posts

답글 남기기

이메일 주소는 공개되지 않습니다. 필수 필드는 *로 표시됩니다