인프라 설계

CCTP 브릿지 안정성 — 서버 크래시에도 USDC를 잃지 않는 설계

Circle CCTP로 USDC를 체인 간 이동하는 서비스를 만들 때, 서버가 갑자기 죽으면 어떻게 될까? broadcast 직전 단 한 줄의 코드 순서 변경으로 자금 손실 위험을 없애는 방법.

2026년 4월 30일·20분 읽기
CCTP Bridge Reliability

1. Circle — 인터넷의 달러를 만들려는 회사

Circle은 2013년 설립된 금융 인프라 회사다. 비트코인 결제 앱으로 시작했지만 지금은 완전히 다른 정체성을 갖는다. Circle의 핵심 제품은 USDC — 달러와 1:1로 페깅된 스테이블코인이다. 발행량 550억 달러 이상, 매일 수백억 달러가 결제·DeFi·기업 재무에 쓰이는 실질적인 달러 인프라다.

USDC핵심 제품

달러 담보 스테이블코인. 매월 독립 회계법인 감사, NYDFS 규제 준수. Coinbase와 공동 발행.

Circle Mint기관용

기관 전용 USDC 발행·소각 창구. 달러 입금 → USDC 발행, USDC 소각 → 달러 반환.

CCTP인프라

크로스체인 USDC 전송 프로토콜. 제3자 브릿지 없이 체인 간 네이티브 USDC 이동.

Programmable Wallets개발자 API

개발자용 MPC 지갑 API. 키 관리·서명·온체인 실행을 API 한 줄로.

Circle의 전략은 명확하다. B2B 금융 인프라 회사로서 "USDC가 달러처럼 어디서나 쓰이도록" 만드는 것이다. Visa·BlackRock·Coinbase와 파트너십을 맺고, 미국 IPO를 추진하며, 규제 당국과 적극적으로 협력한다. CCTP는 그 전략의 핵심 레고 블록이다 — USDC가 체인에 갇히지 않도록.

2. 왜 CCTP를 만들었나 — 브릿지 해킹과 래핑 USDC 문제

2021~2022년, 멀티체인 수요가 폭발하면서 크로스체인 브릿지 시장이 급성장했다. 그리고 연달아 대형 해킹이 터졌다.

2022년 브릿지 해킹 연표
2022.02Wormhole$320M서명 검증 취약점
2022.03Ronin (Axie)$625M검증자 개인 키 탈취
2022.08Nomad Bridge$190M루트 해시 초기화 버그

기존 브릿지들은 대부분 Lock-and-Mint 방식이었다. Ethereum에 USDC를 잠가두고 다른 체인에 "wrapped USDC"를 발행하는 구조다. 잠겨 있는 유동성 풀 자체가 해커의 타깃이 됐다. 그리고 두 번째 문제가 생겼다 — "어떤 USDC가 진짜인가?"

방식대표 브릿지원리리스크
Lock-and-MintWormhole, LayerZero소스 체인에 원본 잠금 + 목적지에 래핑 발행잠긴 유동성 풀이 해킹 타깃. 래핑 토큰 파편화
유동성 네트워크Hop, AcrossLP가 목적지에서 즉시 유동성 제공, 나중에 정산유동성 부족 시 슬리피지. LP 리스크 전가
Burn-and-Mint (CCTP)Circle CCTP소스 체인에서 소각, Circle 증명 후 목적지에서 네이티브 발행발행사(Circle) 신뢰 필요. 그게 전부
CCTP가 가능한 이유 — Circle이 발행사이기 때문

Burn-and-Mint 방식은 발행사만 할 수 있다. Wormhole이나 LayerZero는 USDC 발행사가 아니라서 이 방식을 쓸 수 없다. Circle은 USDC 발행사이기 때문에 어느 체인에서든 USDC를 소각하고 다른 체인에서 같은 양을 새로 발행할 권한을 갖는다. 제3자 브릿지가 필요 없고, 유동성 풀이 없고, 래핑 토큰이 없다. 어느 체인에서 발행됐든 USDC는 항상 Circle이 발행한 동일한 USDC다.

3. Circle CCTP 동작 원리

CCTP(Cross-Chain Transfer Protocol)는 Circle이 운영하는 공식 USDC 크로스체인 전송 프로토콜이다. Ethereum ↔ Base, Ethereum ↔ Arbitrum 등 지원 체인 간에 USDC를 네이티브로 이동시킨다. 핵심 메커니즘은 "소각 후 발행(Burn & Mint)"이다 — 소스 체인에서 USDC를 영구 소각하고, Circle의 증명을 받아 목적지 체인에서 같은 양을 새로 발행한다.

브릿지 흐름 — 4단계
1
APPROVING
approveTokenMessenger 컨트랙트에 USDC 사용 권한 부여
2
BURNING
depositForBurn소스 체인에서 USDC 영구 소각 ← 위험 구간
3
ATTESTING
attestation 대기Circle IRIS API가 burn 확인 후 서명 발급 (수 초~수 분)
4
MINTING
receiveMessage목적지 체인에 attestation 제출 → USDC 발행
핵심 성질

burn이 성공하면 USDC는 소스 체인에서 영구 소각된다. mint는 나중에 언제든 할 수 있지만, 반드시 해야 한다. burn은 되었는데 mint를 못 하면 그 USDC는 영원히 사라진다.

4. 자금 손실 시나리오 — 어느 구간이 위험한가?

4단계 중 대부분의 구간에서 서버 크래시가 발생해도 USDC는 안전하다. 문제는 단 하나의 구간에서 발생한다.

안전PENDING / APPROVING

burn 트랜잭션이 아직 전송되지 않음. USDC는 원래 지갑에 그대로.

위험BURNING ★

USDC가 소각됐는데 DB에 burnTxHash가 없으면 재시작 후 어디서 복구할지 모른다.

안전ATTESTING

DB에 burnTxHash가 있으면 Circle API 폴링을 재개할 수 있다.

안전MINTING

burnTxHash로 언제든 receiveMessage를 재시도할 수 있다.

// 크래시 시나리오 — 기존 코드
서버: 트랜잭션 서명
서버: broadcast → 체인에 전송
체인: burn 컨펌 (USDC 영구 소각)
서버: ← 여기서 크래시!!
서버: DB에 burnTxHash 저장  ← 실행 안 됨

재시작 후:
DB: burnTxHash = null
서버: "이 job이 어디까지 됐지? 알 수 없음."
결과: USDC는 소각됐지만 mint 불가 → 자금 손실

5. 해결책 — broadcast 전에 txHash를 먼저 저장하라

단 세 줄의 순서 변경

이더리움 트랜잭션의 핵심 성질

이더리움 트랜잭션은 서명이 완료되는 순간 txHash가 확정된다. 네트워크에 전송(broadcast)하기 전에도 계산할 수 있다.

txHash = keccak256(RLP-encoded signed transaction)
변경 전 (위험)
// broadcast 이후 저장
const burnTxHash =
  await this.evm.broadcastTx(
    signedTx, fromChain
  );

await updateJob({ burnTxHash });
// ↑ broadcast 후 저장
// 크래시 시 저장 안 됨
변경 후 (안전)
// broadcast 전에 계산 & 저장
const burnTxHash =
  ethers.keccak256(signedTx);
  // ↑ 서명만 되면 확정됨

await this.prisma.bridgeJob.update({
  where: { id: jobId },
  data: { burnTxHash },
});
// ↑ broadcast 전에 먼저 저장

await this.evm.broadcastTx(
  signedTx, fromChain
);

이 패턴은 데이터베이스 설계에서 WAL(Write-Ahead Logging)과 동일한 사상이다. 실제 작업을 수행하기 전에 무엇을 할 것인지를 먼저 기록하고, 크래시 후 재시작 시 그 기록을 보고 복구한다.

⚠ 중요한 함정 — burnTxHash가 있다고 burn이 성공한 게 아니다

txHash는 네트워크와 무관한 로컬 계산값이다. keccak256(서명된 tx 바이트)를 계산하는 순간 결정되므로, broadcast 전 크래시나 tx revert가 발생해도 DB에는 burnTxHash가 남아 있다.

같은 burnTxHash, 세 가지 실제 상태
sign 완료
    │
    ▼
burnTxHash = keccak256(signedTx)  ← DB 저장 (여기까지는 동일)
    │
    ├─ [케이스 A] broadcast 전 크래시
    │       체인에 tx 자체가 없음
    │       receipt 조회 → null
    │       USDC 안전 ✓
    │
    ├─ [케이스 B] broadcast 했지만 revert
    │       잔액 부족 / allowance 부족 / 컨트랙트 에러
    │       receipt.status = 0
    │       USDC 소각 안 됨, 안전 ✓
    │
    └─ [케이스 C] broadcast + 성공
            receipt.status = 1
            USDC 실제로 소각됨 → attestation 재개 필수

따라서 재시작 복구 시 burnTxHash만 믿어선 안 된다. 반드시 온체인 receipt로 실제 성공 여부를 검증해야 한다.

const receipt = await provider.getTransactionReceipt(burnTxHash);

if (!receipt) {
  // 케이스 A: broadcast 안 됨 → USDC 안전 → FAILED
  return markFailed(jobId);
}

if (receipt.status === 0) {
  // 케이스 B: tx revert → USDC 안전 → FAILED
  return markFailed(jobId);
}

// 케이스 C: receipt.status === 1 → 진짜 소각됨
await resumeFromAttestation(jobId, burnTxHash);

6. 서버 재시작 시 자동 복구 패턴

txHash를 미리 저장해두면 재시작 시 자동으로 복구할 수 있다. NestJS의 onModuleInit 훅에서 미완료 job을 찾아 재개한다.

서버 재시작 시 복구 흐름
onModuleInit() 실행
DB 조회: status IN [BURNING, ATTESTING, MINTING] AND burnTxHash IS NOT NULL
status = BURNING →
온체인 receipt 조회
receipt 없음 → FAILED (broadcast 안 됨, USDC 안전)
receipt revert → FAILED (USDC 안전)
receipt 성공 → resumeFromAttestation()
status = ATTESTING/MINTING → 바로 resumeFromAttestation()
resumeFromAttestation(): Circle 폴링 → receiveMessage → DONE
상태별 복구 전략
상태burnTxHash처리 방법
BURNING있음온체인 tx 확인 → 성공이면 attestation 재개
BURNING없음burn 안 된 것 → 복구 불필요 (USDC 안전)
ATTESTING있음바로 Circle 폴링 → mint 재개
MINTING있음receiveMessage 재시도 (멱등성 보장됨)
Circle 공식 릴레이어 — 네가 안 해도 해준다

Circle은 CCTP 생태계 확산을 위해 공식 릴레이어 서비스를 직접 운영한다. burn이 컨펌되고 attestation이 발급되면, Circle 릴레이어가 목적지 체인에서 receiveMessage를 자동으로 호출해준다. 즉, 개발자가 mint 트랜잭션을 직접 전송하지 않아도 USDC가 도착하는 경우가 있다.

릴레이어 처리 흐름
burn tx 컨펌
→ Circle IRIS API가 감지 + attestation 서명 발급
→ Circle 릴레이어가 목적지 체인 감시 중
→ receiveMessage(message, attestation) 자동 호출
→ USDC 발행 완료 (개발자 개입 없이)
⚠ 릴레이어가 먼저 처리했을 때 — 이미 성공이다

내 서버도 receiveMessage를 시도하고, Circle 릴레이어도 시도하면 둘 중 하나가 먼저 처리된다. 나중에 시도하는 쪽은 컨트랙트에서 에러가 발생한다. 이 에러가 "Nonce already used"다.

// MessageTransmitter 컨트랙트 내부
// 각 burn에 대해 nonce는 단 한 번만 사용 가능
mapping(bytes32 => bool) public usedNonces;

// 두 번째 receiveMessage 호출 시:
require(!usedNonces[nonceHash], "Nonce already used");
// → revert 발생

"Nonce already used"는 실패가 아니다 — 이미 USDC가 발행됐다는 증거다. 코드에서 이 에러를 반드시 성공으로 간주하고 job을 DONE 처리해야 한다.

서버 재시작은 트리거 중 하나일 뿐

onModuleInit은 서버 시작 시 자동으로 호출되는 편의 장치다. 실제 복구를 담당하는 resumeFromAttestation()은 일반 메서드이기 때문에, 누가 언제 호출하든 동일하게 동작한다. 서버가 재시작될 때까지 기다릴 필요가 없다.

onModuleInit()
서버 시작 시 자동
🖥
어드민 수동 resume
jobId 입력 → 즉시 재개
🔁
일괄 재개 스케줄러
cron으로 주기적 체크
👆
retryJob() API
유저 직접 재시도 버튼

7. DB가 날아갔을 때 — 수동 복구 방법

DB가 완전히 유실된 최악의 상황에서도 burn 트랜잭션 해시만 알고 있다면 복구할 수 있다. CCTP의 mint는 언제든 재시도 가능하기 때문이다.

1Circle IRIS API에서 attestation 조회
GET https://iris-api.circle.com/v2/{sourceDomain}
  ?transactionHash={burnTxHash}

// Response
{
  "status": "complete",
  "message": "0x...",
  "attestation": "0x..."
}
2목적지 체인 MessageTransmitter에 직접 호출
// ethers.js
const transmitter = new ethers.Contract(
  MESSAGE_TRANSMITTER_ADDRESS,
  ['function receiveMessage(bytes message, bytes attestation)'],
  signer
);

await transmitter.receiveMessage(
  irisResponse.message,
  irisResponse.attestation
);

또는 Circle 공식 브릿지 UI(bridge.circle.com)에서 burn tx hash를 입력하면 GUI로 복구할 수 있다. 기술적으로 동일한 과정이다.

8. 전체 안전성 매트릭스

시나리오자금 손실 가능성비고
CCTP approve 중 크래시없음USDC 이동 없음
CCTP burn 중 크래시 (개선 전)높음burnTxHash DB 저장 안 됨 → 복구 불가
CCTP burn 중 크래시 (개선 후)매우 낮음broadcast 전 DB 저장 → 재시작 시 자동 복구
CCTP attestation 중 크래시없음burnTxHash로 언제든 재시도
CCTP mint 중 크래시없음receiveMessage 멱등성 보장 (Nonce 중복 처리)
DB 완전 유실 + burn 성공낮음burn tx hash로 수동 복구 가능
DB 완전 유실 + burn tx hash 분실높음DB 정기 백업으로 방지해야 함

9. 남은 한계와 권장 대응

경미approve 후 burn 전 크래시

USDC는 안전하다. approve만 된 상태로 재시작하면 다음 브릿지 시도 시 allowance가 남아있어 approve를 건너뛸 수 있다. 기능적 문제는 없다.

치명DB 완전 유실 + burn tx hash 분실

이 경우만 수동 복구도 불가능하다. 방어책은 DB 정기 백업(최소 1시간 단위), 그리고 가능하면 burn tx hash를 별도 내구성 스토리지(S3 등)에도 기록하는 것이다.

일시적Circle IRIS API 장애

IRIS API가 응답하지 않으면 attestation을 받을 수 없다. USDC는 소각된 상태지만 복구 경로(burnTxHash)는 확보되어 있다. IRIS API가 복구되는 즉시 재시도하면 된다. 최대 폴링 시간을 넉넉히 설정(30분 이상)해야 한다.

10. 설계 원칙 — 블록체인 서비스의 내결함성

CCTP 브릿지 안정성 설계에서 얻을 수 있는 일반적인 원칙들이다. 온체인 자금을 다루는 모든 서비스에 적용된다.

📝
Write-Ahead Logging

무엇을 할 것인지를 DB에 먼저 기록하고, 그 다음 실제 작업을 수행한다. broadcast 전 txHash 저장이 이 패턴이다.

🔄
멱등성(Idempotency) 설계

같은 작업을 두 번 실행해도 결과가 같아야 한다. receiveMessage의 Nonce 체계가 멱등성을 보장한다.

🧩
상태 머신 + DB 지속성

각 단계를 명시적 상태(PENDING/APPROVING/BURNING/...)로 나누고 DB에 저장한다. 재시작 시 상태를 보고 어디서 재개할지 결정한다.

🔑
복구 키 보존

burnTxHash는 USDC를 복구하는 유일한 열쇠다. 최소한 이 값만큼은 여러 곳에 저장해야 한다. DB + 별도 로그/스토리지.

walits — 안전하게 설계된 스테이블코인 지갑

USDC 브릿지, 온체인 예치, AI 에이전트 자동 실행. 모든 트랜잭션은 실패해도 복구 경로가 설계되어 있습니다.

서비스 알아보기 →