Chainlink Automation은 크론 잡(Cron Job)이 아닙니다. 그것은 합의 결정(Consensus Decision)입니다.
요약
Chainlink Automation이 단순한 크론 잡이 아닌, OCR3 합의 메커니즘을 기반으로 작동하는 탈중앙화된 오라클 네트워크임을 설명합니다. 트리거 유형별 작동 방식과 보안을 위한 아키텍처 설계의 중요성을 다룹니다.
핵심 포인트
- Chainlink Automation은 단일 봇이 아닌 OCR3 합의를 통해 결정됨
- 시간 기반, 커스텀 로직 등 세 가지 트리거 유형 존재
- 보안을 위해 AutomationCompatibleInterface 구현 및 설계 주의 필요
- Registry를 통한 조정 계층과 노드 운영자의 상호작용 방식 설명
대부분의 개발자가 가진 멘탈 모델(Mental Model)은 틀렸습니다
대부분의 개발자에게 Chainlink Automation이 어떻게 작동하는지 물어보면 다음과 같이 대답할 것입니다: "봇이 정해진 일정에 따라 당신의 컨트랙트를 확인하고, 만약 checkUpkeep이 true를 반환하면 performUpkeep을 호출합니다." 이것이 완전히 틀린 것은 아니지만, 보안에 있어 실제로 중요한 부분을 놓치고 있습니다. 바로 performUpkeep을 호출하기로 하는 결정이 단 하나의 봇에 의해 내려지는 것이 아니라는 점입니다. 이는 OCR3를 통해 합의(Consensus)에 도달하는 탈중앙화된 오라클 네트워크(Decentralized Oracle Network)에 의해 내려지며, 그 결과는 체인에 반영되기 전에 암호학적으로 서명됩니다.
이러한 차이점은 학술적인 논의에 그치지 않습니다. 당신의 자동화된 컨트랙트(Automated Contract)의 보안 특성은 당신이 이를 설계할 때 이 멘탈 모델의 어떤 버전을 사용하느냐에 전적으로 달려 있습니다.
이 글은 28일간 진행되는 Chainlink 아키텍처 시리즈의 8일 차입니다. 오늘은 Automation의 실제 아키텍처, 세 가지 트리거(Trigger) 유형, OCR3 합의 흐름, 그리고 Automation을 단순한 키퍼 봇(Keeper Bot)으로 취급할 때 발생하는 감사(Audit) 관련 실수들에 대해 살펴봅니다.
Upkeep의 실제 정체
Upkeep은 Automation 네트워크에 등록된 작업(Job)입니다. Upkeep을 등록할 때, 당신은 Automation Registry에 다음과 같이 알리는 것입니다: "여기 컨트랙트가 있고, 확인해야 할 조건이 있으며, 해당 조건이 참일 때 호출할 함수가 있고, 이 작업을 지원할 LINK 잔액이 여기 있습니다."
Registry는 조정 계층(Coordination Layer)입니다. 이는 어떤 Upkeep이 존재하는지, 어떤 노드 운영자(Node Operator)가 이를 서비스하기 위해 등록되었는지, 그리고 각 Upkeep이 이미 수행된 작업에 대해 얼마를 지불했는지를 추적합니다. 노드 운영자들은 스스로 체인을 관찰하여 Upkeep을 찾아내지 않습니다. 그들은 무엇을 서비스해야 하는지 알기 위해 Registry에 쿼리(Query)를 보냅니다.
하나가 아닌 세 가지 트리거 유형
이 부분이 대부분의 콘텐츠가 지나치게 단순화하는 지점입니다. Automation은 단순히 "정해진 일정에 따라 실행"되는 것이 아닙니다. 서로 다른 체크 메커니즘을 가진 세 가지 별개의 트리거 유형이 존재합니다:
**시간 기반 트리거(Time-based triggers)**는 크론 잡(cron job)과 가장 유사합니다. CRON 표현식을 지정하면 네트워크가 해당 스케줄에 따라 함수를 호출합니다. 2025년 12월 11일부로 시간 기반 업킵(time-based upkeeps)이 업데이트되어, 이제 특정 Upkeep에 할당된 고유한 Forwarder 컨트랙트만이 대상 함수를 호출할 수 있게 되었습니다. 이는 제3자가 업키프를 직접 트리거하는 것을 막고, 잘못된 가스 매개변수를 가진 부적절하게 게이트된(gated) 호출이 의도된 작업을 성공적으로 실행하지 못하면서 타이머만 전진시키는 유형의 공격을 방지합니다.
**커스텀 로직 트리거(Custom logic triggers)**가 가장 유연합니다. 사용자의 컨트랙트는 AutomationCompatibleInterface를 구현해야 하며, 이 인터페이스는 두 개의 함수(checkUpkeep, performUpkeep)를 요구합니다. 노드들은 오프체인(off-chain)에서 뷰 함수(view function)로 checkUpkeep을 시뮬레이션하여 적격 여부를 결정한 다음, 합의가 시간이 되었다고 판단하면 온체인(on-chain)에서 performUpkeep을 호출합니다. 보안과 관련된 대부분의 설계 결정은 바로 이 부분에 속하며, 아래에서 자세히 다루겠습니다.
**로그 트리거(Log triggers)**는 이벤트 기반입니다. 매 블록마다 특정 조건을 폴링(polling)하는 대신, Upkeep은 특정 컨트랙트에 의해 방출된 특정 이벤트가 발생할 때 작동합니다. Automation 네트워크는 해당 로그를 모니터링하다가 나타날 때 performUpkeep을 트리거합니다. 이를 통해 반응형 자동화(reactive automation)가 가능해집니다: Uniswap 스왑이 이벤트를 발생시키면, 사용자의 Upkeep이 이에 대응할 수 있습니다. 이는 아비트라지 봇(arbitrage bots), 청산 감시자(liquidation watchers) 및 상태를 폴링하는 대신 온체인 이벤트에 반응해야 하는 모든 시스템에 실용적입니다.
OCR3 합의 흐름, 단계별 분석
Upkeep이 적격해지는 순간부터 performUpkeep이 실행되는 순간까지 실제로 어떤 일이 일어나는지 설명합니다. 이는 '크론 잡'이라는 사고방식이 완전히 놓치고 있는 부분입니다.
Automation 노드들은 OCR3를 사용하여 피어-투-피어(peer-to-peer) 네트워크를 형성하며, OCR3는 이 시리즈의 Day 4에서 데이터 피드(Data Feeds) 맥락으로 다루었던 것과 동일한 프로토콜입니다. 각 노드는 자신만의 체인 상태 뷰(view of the chain state)를 기반으로 독립적으로 checkUpkeep 함수를 시뮬레이션합니다. 단 하나의 노드가 아닙니다. 네트워크의 모든 노드가 독립적으로, 동일한 시뮬레이션을 실행하는 것입니다.
한 노드의 시뮬레이션 결과가 upkeepNeeded = true를 반환하더라도, 즉시 트랜잭션을 전송하지는 않습니다. 대신 해당 관찰 결과를 네트워크 내의 다른 노드들에게 브로드캐스트(broadcast)합니다. 이후 OCR3 합의 라운드(consensus round)가 진행됩니다. 노드들은 각자의 관찰 결과를 공유하고, 어떤 Upkeep이 실행 자격이 있는지에 대해 합의에 도달한 뒤 보고서(report)에 서명합니다. 이 서명된 보고서에는 온체인(on-chain)에서 실행될 performData가 포함되어 있습니다.
서명된 보고서는 Registry 컨트랙트로 제출됩니다. Registry는 무엇인가를 실행하기 전에 보고서의 서명들을 검증합니다. 만약 서명들이 설정된 노드 세트의 충분한 정족수(quorum)를 나타내지 않는다면, 아무것도 실행되지 않습니다. 여기서 암호학적 보증은 단순히 "노드들이 확인했다"는 수준이 아니라, "충분한 수의 독립적인 노드들이 합의했으며, Registry가 당신의 컨트랙트에 접근하기 전에 그들의 합의를 검증했다"는 것을 의미합니다.
이것이 바로 Automation이 단순한 자동화된 연산(automated compute)이 아니라, 검증 가능한 연산(verifiable compute)이라고 불리는 이유입니다. 온체인 검증 단계는 Data Feeds 아키텍처에서 어그리게이터(aggregator) 컨트랙트의 서명 확인과 정확히 마찬가지로, 탈중앙화(decentralization) 주장이 실제로 강제되는 지점입니다.
checkUpkeep: 무료 연산, 실제 설계 제약 사항
checkUpkeep은 뷰(view) 함수입니다. 오프체인(off-chain)에서 시뮬레이션될 때는 가스(gas) 비용이 들지 않으며, 바로 이 점 때문에 노드들이 LINK 잔액을 지속적으로 소모하지 않고도 등록된 모든 Upkeep에 대해 매 블록마다 시뮬레이션을 수행할 여력이 생깁니다. checkUpkeep/performUpkeep 분리의 핵심 목적은 비용이 많이 드는 연산을 오프체인으로 무료로 밀어내고, 실제로 필요할 때만 온체인 실행에 대한 비용을 지불하는 것입니다.
이는 실제적인 설계 패턴을 만들어냅니다. 모든 복잡한 로직은 checkUpkeep에서 처리하십시오. 정확히 어떤 계정이 청산(liquidating)되어야 하는지, 어떤 포지션이 리밸런싱(rebalancing)되어야 하는지, 배열의 어떤 인덱스가 임계값(threshold)을 넘었는지 등을 결정합니다. 이 모든 것을 performData로 인코딩하십시오. 그러면 performUpkeep은 해당 사전 계산된(pre-computed) 결과를 전달받아, checkUpkeep이 이미 필요하다고 결정한 사항만을 실행하게 됩니다.
개발자들을 괴롭히는 제약 사항 중 하나는 checkUpkeep에 시뮬레이션(simulation)을 위한 가스 한도(gas limit)가 있다는 점입니다. 만약 조건 확인(condition check) 과정에서 너무 많은 연산을 수행하면, 시뮬레이션이 checkGasLimit을 초과하게 되고 Upkeep은 단순히 실행되지 않습니다. 이때 문제를 드러낼 온체인 리버트(on-chain revert)는 발생하지 않습니다. 컨트랙트 관점에서 이 실패는 소리 없이(silent) 지나갑니다.
Automation 통합을 위한 감사 체크리스트 (Audit Checklist)
1. performUpkeep이 적절하게 게이트(gated)되어 있는가?
performUpkeep은 Registry(또는 시간 기반 Upkeep의 경우 Forwarder)에 의해 호출됩니다. 만약 귀하의 performUpkeep 함수가 어떤 주소에 의해서도 호출될 수 있다면, Automation 네트워크뿐만 아니라 누구나 임의의 performData를 사용하여 이를 트리거할 수 있습니다. msg.sender를 Registry 주소와 대조하여 확인하거나, 각 Upkeep에 고유하고 불변하는 호출자 주소를 할당하는 Chainlink의 Forwarder 패턴을 사용하십시오.
// 취약함: 호출자 확인 없음
function performUpkeep(bytes calldata performData) external override {
// 누구나 이를 호출할 수 있음
...
2. performUpkeep이 멱등성(idempotent)을 갖는가?
어떤 조건에 대해 checkUpkeep이 true를 반환하면, 온체인 상태(on-chain state)가 업데이트되기 전 동일한 적격성 윈도우(eligibility window) 동안 네트워크의 여러 노드가 performUpkeep을 호출하려고 시도할 수 있습니다. 귀하의 performUpkeep은 온체인에서 조건을 다시 확인해야 하며, 작업이 이미 완료되었다면 안전하게 반환(revert 하지 않음)해야 합니다. performUpkeep 내부의 리버트(revert)는 단순히 트랜잭션을 실패시키는 것에 그치지 않고, Automation 네트워크가 재시도 로직(retry logic)을 처리하는 방식에 영향을 줄 수 있습니다.
3. 가스 한도(gas limit)가 충분히 높게 설정되었는가?
performGasLimit은 등록 시점에 설정됩니다. 만약 performUpkeep의 실행이 이 한도를 초과하면, 시뮬레이션 결과 실패로 나타나기 때문에 네트워크는 이를 실행하지 않습니다. 실행 시점에 알게 되는 일반적인 트랜잭션과 달리, Automation은 시뮬레이션이 한도를 초과하면 온체인 실행을 시도조차 하지 않습니다. 이는 블록 익스플로러(block explorer)에서 볼 수 있는 리버트(revert)가 아니라, 소리 없는 미실행(silent non-execution)입니다.
4. LINK 잔액이 모니터링되고 있는가?
4. LINK 잔액이 모니터링되고 있는가?
LINK 잔액이 0이거나 최소 금액 미만인 경우, Upkeep은 단순히 수행되지 않습니다. 온체인(on-chain)에서 되돌림(revert)이나 오류 메시지가 발생하는 것이 아니라, 그저 침묵합니다. 청산 봇(liquidation bot)이나 Automation에 의존하여 중요한 유지보수를 하는 DeFi 프로토콜의 경우, 잘못된 순간에 LINK 잔액이 바닥나는 것은 실제적인 재정적 결과를 초래할 수 있습니다. 따라서 잔액이 최소 임계값에 도달하기 전에 충전 자동화(top-up)를 설정하거나 알림을 설정해야 합니다.
5. checkUpkeep은 상태 변경 없이 읽기 전용(view-only)인가?
checkUpkeep은 읽기 함수(view function)로 오프체인(off-chain)에서 시뮬레이션됩니다. checkUpkeep 내부의 어떤 상태 변경도 영구적으로 저장되지 않습니다 (이는 실제 트랜잭션이 아닌 시뮬레이션입니다). 만약 코드가 checkUpkeep이 스토리지에 기록한다고 가정한다면, 이는 조용히 잘못된 것입니다: 체크는 실행되고, 쓰기는 시뮬레이션 컨텍스트에서 발생한 것처럼 보이다가 시뮬레이션이 끝나면 사라집니다.
구체적인 패턴: 오프체인 계산, 온체인 정밀성
어떤 포지션들이 임계값을 넘어 이탈했을 때 그들을 재조정하는 것과 같이 사소하지 않은 작업을 위해 checkUpkeep/performUpkeep 분할이 올바르게 사용될 때의 모습은 다음과 같습니다.
function checkUpkeep(bytes calldata)
external view override
returns (bool upkeepNeeded, bytes memory performData)
...
checkUpkeep은 잠재적으로 수백 개의 포지션을 반복하며 모든 계산을 가스비 없이 오프체인에서 수행합니다. 이 함수는 어떤 포지션들이 작업을 필요로 하는지를 정확하게 인코딩하고, 이를 performData로 전달합니다. performUpkeep은 이 사전 계산된 목록을 받고 이미 이탈한 것으로 식별된 포지션에만 접촉하며, 온체인에서 불필요한 계산이나 전체 배열을 반복하는 과정이 없습니다.
이러한 패턴이야말로
Chainlink Automation은 단순히 귀하의 함수를 호출하는 봇이 아닙니다. 이는 귀하의 조건이 참인지에 대해 암호학적으로 검증된 합의(Consensus)에 도달하고, 해당 합의를 증명하는 보고서에 서명한 다음, 그제서야 귀하의 컨트랙트(Contract)로 호출을 전달하는 OCR3 기반의 오라클 네트워크(Oracle Network)입니다. checkUpkeep은 비용 없이 오프체인(Off-chain)에서 무거운 작업을 수행하도록 설계하고, performUpkeep은 게이트가 설정되고(Gated), 멱등성(Idempotent)을 가지며, 가스 제한(Gas-bounded)이 되도록 설계하십시오. 그리고 다른 중요한 인프라를 모니터링하는 것처럼 귀하의 LINK 잔액을 모니터링하십시오.
저는 28일 동안 Chainlink의 전체 아키텍처(Architecture)를 통해 글을 쓰고 있는 스마트 컨트랙트(Smart Contract) 보안 연구가입니다. ramprasadgoud.dev 또는 X @0xramprasad에서 계속 지켜봐 주세요.
AI 자동 생성 콘텐츠
본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기