Skip to content
Go back

Web3 신뢰 인프라 #2 — 앵커링, Solidity 스마트 컨트랙트부터 Etherscan까지

1편에서 만든 지갑 SDK 위에 “앵커링” 기능을 쌓았다. 앵커링이란 데이터의 해시1를 블록체인에 기록하여 **“이 데이터가 이 시점에 존재했다”**는 증거를 남기는 것이다. 공증사무소의 디지털 버전이라고 보면 된다.

핵심은 원본 데이터는 저장하지 않고 해시만 기록한다는 것. 개인정보 노출 없이 무결성을 증명할 수 있다.

프로젝트 구조

packages/@core/anchor/
├── contracts/
│   └── AnchorRegistry.sol    ← Solidity 스마트 컨트랙트
├── src/
│   ├── hash/                 ← SHA-256 해싱
│   ├── registry/             ← anchor(), ABI, 주소 관리
│   └── verify/               ← verify(), verifyData()
├── test/
│   └── AnchorRegistry.test.ts
└── hardhat.config.ts

스마트 컨트랙트 — AnchorRegistry

최대한 단순하게 설계했다. 함수 2개, 이벤트 1개.

contract AnchorRegistry {
    struct AnchorData {
        address registrant;    // 누가 등록했나
        uint256 blockNumber;   // 몇 번째 블록에
        uint256 timestamp;     // 언제
    }

    mapping(bytes32 => AnchorData) public anchors;

    event Anchored(bytes32 indexed hash, address indexed registrant);

    function anchor(bytes32 hash) external {
        require(anchors[hash].timestamp == 0, "Already anchored");
        anchors[hash] = AnchorData(msg.sender, block.number, block.timestamp);
        emit Anchored(hash, msg.sender);
    }

    function verify(bytes32 hash) external view returns (
        bool exists, address registrant,
        uint256 blockNumber, uint256 timestamp
    ) {
        AnchorData memory data = anchors[hash];
        return (data.timestamp != 0, data.registrant,
                data.blockNumber, data.timestamp);
    }
}

anchor()는 쓰기 — 가스비 필요, 블록에 포함될 때까지 대기. verify()는 읽기 — 무료, 즉시 응답. indexed2 키워드로 이벤트 필터링을 가능하게 했다 — 나중에 “내가 앵커링한 이력”을 조회할 때 쓴다.

Sepolia 배포 + Etherscan Verify

npx hardhat run scripts/deploy.ts --network sepolia
npx hardhat verify --network sepolia 0xA595Ac6353Ff7B78AB22bC89bE011b00c7F730eE

Etherscan에서 Verify하면 소스코드가 공개되고, 함수 호출 시 파라미터가 파싱되어 보인다.

Etherscan TX 상세:
  Function: anchor(bytes32 hash)
  MethodID: 0xeecdf927
  [0]: a180dfeef50883e700485950f519a4180e27e1cf9efcee183bb32fc16a47b1ea

Verify 안 했으면 0xeecdf927a180dfee...라는 hex 덩어리로만 보인다.

SDK — @core/anchor

해싱 — Web Crypto API

export async function hashData(data: string | Uint8Array): Promise<string> {
  const bytes = typeof data === 'string'
    ? new TextEncoder().encode(data) : data;
  const hashBuffer = await crypto.subtle.digest('SHA-256',
    new Uint8Array(bytes).buffer);
  return '0x' + Array.from(new Uint8Array(hashBuffer))
    .map(b => b.toString(16).padStart(2, '0')).join('');
}

crypto.subtle은 브라우저와 Node.js 양쪽에서 동작한다. 1편의 Buffer.from 교훈을 반영해서 처음부터 Web API만 사용했다.

이력 조회 — 이중 전략

앵커링 이력 조회는 두 가지 방법이 있다.

방법대상장점단점
Etherscan API퍼블릭 체인블록 범위 무제한, 정렬 지원API 키 필요
eth_getLogs프라이빗 체인API 키 불필요블록 범위 제한 (보통 10,000)
export async function fetchHistory(address, chainId, provider, apiKey?) {
  if (apiKey && ETHERSCAN_CHAINS.has(chainId)) {
    return fetchFromEtherscan(chainId, contractAddress, address);
  }
  return fetchFromRpc(provider, contractAddress, address);
}

퍼블릭 체인 + API 키가 있으면 Etherscan, 그 외에는 eth_getLogs로 폴백.

트러블 슈팅

(a) Hardhat 2 vs 3 충돌

Hardhat 3가 나온 시점이라 최신을 설치했는데, @nomicfoundation/hardhat-toolbox 최신 버전이 Hardhat 2와만 호환됐다. 결국 Hardhat 2로 고정하고 package.json에서 "type": "module"을 제거했다 — Hardhat 2는 CommonJS 기반이라 ESM과 충돌한다.

{
  "hardhat": "^2.22.0",
  "@nomicfoundation/hardhat-toolbox": "^5.0.0"
}

Hardhat 3가 안정화되면 마이그레이션할 예정이지만, 현 시점에서는 2가 안전하다.

(b) eth_getLogs 퍼블릭 RPC 제한

처음에 publicnode.com RPC로 eth_getLogs를 호출했는데 403 Forbidden이 반환됐다. 공개 RPC 중 상당수가 getLogs를 차단한다. sepolia.drpc.org로 바꿨는데 이번에는 10,000블록 제한이 있었다.

결국 앵커링 이력은 Etherscan API의 getLogs 엔드포인트를 사용하고, eth_getLogs는 프라이빗 체인 전용 폴백으로 남겼다.

(c) Etherscan V1 → V2 마이그레이션

Etherscan V1 API를 호출하니 deprecation 에러가 반환됐다.

"You are using a deprecated V1 endpoint, switch to Etherscan API V2"

V2는 체인별 URL 분기 대신 단일 엔드포인트(https://api.etherscan.io/v2/api)에 chainid 파라미터를 추가하는 방식이다. 기존의 api-sepolia.etherscan.io/api 호출을 전부 변경했다.

(d) 이벤트 topic 인덱스 실수

Anchored(bytes32 indexed hash, address indexed registrant) 이벤트에서 hash는 topic1, registrant는 topic2다. 처음에 topic1으로 registrant를 필터링해서 “내 앵커링 이력”이 안 나왔다. topic 인덱스를 topic2로 변경하고 topic0_2_opr: 'and'를 추가해서 해결.

const params = {
  topic0: ANCHORED_TOPIC,        // event signature
  topic2: paddedAddress,         // registrant (내 주소)
  topic0_2_opr: 'and',           // topic0 AND topic2
};

(e) 앵커링 이력 정렬

Etherscan API에 sort: 'desc'를 보냈는데 getLogs 엔드포인트에서는 무시됐다. 결과가 오래된 순으로 나와서 클라이언트에서 reverse()를 추가했다.

Extension 앵커링 탭

Extension에 앵커링/검증/이력 3개 탭을 추가했다.

기능
앵커링데이터 입력 → SHA-256 해싱 → 블록체인 기록 (+ 테스트 데이터 생성)
검증해시 입력 → 등록 여부 + 등록자 + 블록 번호
이력내 앵커링 이력 목록 (Etherscan TX 링크 포함)

테스트 데이터 생성 버튼은 BAT-2024-XXX 형식의 랜덤 배터리 데이터를 JSON으로 만들어준다. 실제 데이터 없이도 앵커링 흐름을 테스트할 수 있다.

Etherscan에서 확인

앵커링 TX를 Etherscan에서 열면 이렇게 보인다.

From: 0xf1e003... (발급자 지갑)
To:   0xA595Ac... (AnchorRegistry 컨트랙트)
Value: 0 ETH (돈 안 보냄)

Input Data:
  Function: anchor(bytes32 hash)
  [0]: a180dfeef50883e700485950f519a4180e27e1cf9efcee183bb32fc16a47b1ea

Value가 0 ETH인 게 핵심이다. 돈을 보내는 게 아니라 함수를 호출하는 것이다. 가스비만 내고, 컨트랙트가 해시를 영구 기록한다.

회고

해시만 저장하고 원본은 저장하지 않는다 — 이 한 문장이 앵커링의 핵심이다. 블록체인에 개인정보를 올리면 전 세계에 공개되지만, 해시만 올리면 원본 복원 불가 + 무결성 검증 가능.

세 가지로 정리하면.

  1. Hardhat은 아직 2가 안전 — 3는 빠르지만 toolbox 호환이 불안정하다. CommonJS vs ESM 충돌을 피하려면 anchor 패키지에서 "type": "module"을 빼야 한다.
  2. 퍼블릭 RPC의 getLogs는 믿을 수 없다 — 차단하거나 블록 제한이 있다. Etherscan API가 훨씬 안정적이다.
  3. Etherscan Verify는 필수 — Verify 안 하면 TX의 Input Data가 hex 덩어리로 보여서 뭘 했는지 알 수 없다. 투명성이 블록체인의 핵심 가치인데 소스를 숨기면 의미가 반감된다.

다음 편: DID/VC/VP — 탈중앙 신원 증명

Footnotes

  1. SHA-256 해시 — 임의 길이 데이터를 256비트 고정 길이로 변환하는 일방향 함수. 원본 → 해시는 가능하지만 해시 → 원본은 불가능.

  2. indexed event parameter — Solidity 이벤트의 파라미터에 indexed를 붙이면 해당 값으로 이벤트를 필터링(검색)할 수 있다. 최대 3개까지.


Share this post on:

Comments


Previous Post
Web3 신뢰 인프라 #3 — DID/VC/VP, 탈중앙 신원 증명을 만들어보다
Next Post
Web3 신뢰 인프라 #1 — 지갑 SDK와 Chrome Extension