For the complete documentation index, see llms.txt
Proving transactions locally
Midnight은 영지식(ZK) 암호를 사용해 shielded 트랜잭션과 데이터 보호를 실현합니다. 이 아키텍처의 핵심 요소는 Midnight proof server가 제공하는 ZK 기능으로, 로컬에서 proof를 생성하면 이를 온체인에서 검증합니다.
DApp이 proof server에 보내는 정보에는 토큰 소유권의 세부 정보나 DApp의 private state처럼 private 데이터가 포함됩니다. 데이터를 보호하려면 로컬 proof server, 또는 여러분이 통제하는 원격 머신의 proof server에만 암호화된 채널로 접근해야 합니다.
이 가이드는 왜 proving이 여러분의 머신에서 일어나는지 설명하고, proof server를 실행하고, 검증하고, DApp을 연결하고, 네트워크를 고르고, tDUST를 생성하는 과정을 안내합니다. Docker Desktop에서 컨테이너를 시작하는 지갑 사용자용 안내는 Run the proof server를 참고하세요.
Why Midnight needs Docker, the proof server and proof generation
Midnight에서 무언가를 만들어 봤다면, 처음 10분 안에 아마 이 오류를 만났을 것입니다:
Error: connect ECONNREFUSED 127.0.0.1:6300
설정 가이드 어딘가에 docker run 명령이 있었습니다. 그걸 건너뛰었거나 Docker가 실행 중이 아니었고, 이제 아무것도 동작하지 않습니다.
그 시점에 드는 합리적인 질문은 이렇습니다. 블록체인 SDK가 대체 왜 Docker를 필요로 하는가?
이 가이드는 실제 추론의 흐름을 따라가며 그 질문에 답합니다. Midnight에서 스마트 컨트랙트를 호출할 때 무슨 일이 일어나는지에서 시작해, 왜 그것이 별도의 로컬 서비스를 요구하는지, 그리고 왜 그 서비스가 컨테이너로 배포되는지까지 짚습니다. 그런 다음 실행과 검증을 마치게 하고, 이어서 가장 흔히 문제가 되는 두 가지, 네트워크 선택과 tDUST 생성을 넘어서게 합니다.
Prerequisites
- Docker Desktop 설치 및 실행
- Node.js v22 이상
- Compact toolchain 설치. install the Midnight toolchain를 참고하세요.
Step 1: Your smart contract runs locally
대부분의 체인에서는 트랜잭션을 제출하면 밸리데이터가 여러분의 스마트 컨트랙트를 실행합니다. 모두가 그 입력을 봅니다.
Midnight에서는 실행이 여러분의 기기에서 일어납니다. circuit, 즉 스마트 컨트랙트 함수를 가리키는 Midnight의 용어를 호출하면, circuit이 로컬에서 실행되며 두 가지를 만들어 냅니다:
- public transcript: circuit이 읽고 쓴 온체인 값과 따른 규칙입니다.
- private transcript: 여러분의 witness 데이터, 즉 여러분의 머신을 절대 떠나지 않는 비밀 입력입니다.
네트워크에 도달하는 것은 public transcript와, 그 transcript가 올바르다는 ZK proof입니다. 즉 여러분의 숨겨진 값이 circuit의 모든 제약을 만족한다는 proof입니다.
네트워크는 규칙이 지켜졌다는 사실을 알게 됩니다. 여러분의 입력은 결코 알지 못합니다.
이것이 Midnight 프라이버시 모델의 핵심이며, 다른 체인에는 없는 컴포넌트가 여러분의 로컬 환경에 있는 이유이기도 합니다.
Step 2: Proving is expensive, verifying is cheap
여기서는 하나의 비대칭성이 모든 것을 좌우합니다.
proof를 검증하는 것은 빠릅니다. Midnight에 적용된 비용 모델에서 proof 검증은 약 3.3밀리초의 상수에, 크기에 비례하는 작은 항이 더해진 비용입니다. 이것이 온체인 검증을 감당할 만하게 유지해 줍니다.
proof를 만드는 것이 비싼 쪽입니다. Midnight의 proving 시스템은 BLS12-381 곡선 위의 KZG commitment를 쓰는 PLONK 계열입니다. 실제로 이는 proving에 여러분 자신의 circuit용 proving key뿐 아니라, structured reference string이라 불리는 커다란 공유 public 파라미터 집합이 필요하다는 뜻입니다.
그 작업은 브라우저 탭에 어울리지 않으며, JavaScript로 다시 구현하는 것도 현실적이지 않습니다. 그래서 prover는 네이티브 서비스인 proof server로 배포됩니다.
그 역할은 간단합니다:
- proof되지 않은 트랜잭션과, 관련 circuit들의 키 자료(proving key, verifier key, ZKIR)를 받습니다.
- 무거운 연산을 수행합니다.
- proof를 반환합니다.
proof server는 여러분의 지갑 키를 보유하지 않습니다. 트랜잭션에 서명할 수 없고 자금을 쓸 수도 없습니다. proof를 만드는 것, 그것이 전부입니다.
proof server는 여러분의 witness 데이터를 봅니다. 바로 그 데이터에 관해 증명하기 때문입니다. 이 점이 곧바로 다음 논점으로 이어집니다.
Step 3: Why it runs on your machine
proof server가 여러분의 private 입력을 받으므로, 다른 사람의 proof server를 쓴다는 것은 낯선 이에게 여러분의 비밀을 넘기는 것과 같습니다.
로컬에서 실행하거나, 기껏해야 여러분이 통제하는 원격 머신에서 암호화된 채널로 실행하세요.
로컬에서 실행하면 데이터 경로가 짧습니다. 여러분의 private 데이터는 지갑에서 같은 머신의 프로세스로 갔다가 거기서 멈춥니다.
proof server는 시작 시 딱 한 번 바깥으로 나갑니다. https://srs.midnight.network/ 에서 public proving 파라미터를 내려받고, shielded 토큰과 DUST 연산을 위한 내장 키 자료를 준비하기 위해서입니다. 그 데이터는 public이며 모든 사용자에게 동일합니다. 여러분의 witness 데이터는 결코 그 일부가 아닙니다.
시작 시 다운로드를 건너뛰려면 --no-fetch-params를 전달하고, MIDNIGHT_PARAM_SOURCE가 여러분 자신의 mirror를 가리키도록 설정하세요.
이는 첫 시작이 이후의 어떤 실행보다도 훨씬 느린 이유이기도 합니다. 이미지 자체는 약 100 MB입니다. 첫 실행에서 받아오는 파라미터가 느린 부분입니다.
Step 4: Why Docker specifically
이는 개발자가 실제로 던지는 질문이므로, 대안들도 솔직한 답을 받을 자격이 있습니다.
| 방식 | 기본값이 아닌 이유 |
|---|---|
| npm install | prover는 네이티브 암호 의존성을 가진 컴파일된 Rust입니다. npm으로 배포하려면 모든 운영체제와 아키텍처에 대한 사전 빌드 바이너리가 필요하거나, 모든 개발자에게 Rust toolchain 설치를 요구해야 합니다. |
| 네이티브 바이너리 다운로드 | 가능은 하지만, 플랫폼별 링킹·권한·PATH 문제를 떠안게 됩니다. 또한 node, indexer, proof server가 호환을 유지해야 할 때 중요한 버전 고정도 잃게 됩니다. |
| 브라우저 내 WASM | 점점 현실이 되고 있습니다. 일부 지갑은 이제 prover를 WASM으로 컴파일해 탭 안에서 proving합니다. 다만 도구와 튜토리얼이 전제하는 경로는 아니며, 콜드 스타트 시 키 로딩은 그 나름의 트레이드오프입니다. |
| 호스팅형 proof server | 목적을 무너뜨립니다. proof server는 여러분의 witness 데이터를 봅니다. |
| Docker | 한 번의 명령으로, macOS·Linux·WSL에서 동일하게 동작합니다. 버전을 고정할 수 있어 node·indexer와 보조를 맞춥니다. 격리되어 있어 여러분의 시스템을 건드리지 않습니다. 일회용이라 상태가 망가지면 컨테이너를 재시작해 고칩니다. |
Docker는 취향의 문제가 아닙니다. 세 운영체제에 걸쳐 개발자에게 네이티브 암호 서비스를 배포하면서 버전을 맞춰 두는 가장 마찰 적은 방법입니다.
Run the proof server
docker run -p 6300:6300 midnightntwrk/proof-server:8.1.0 midnight-proof-server -v
또는 Docker Compose로 실행하세요. 다음을 proof-server.yml로 저장합니다:
services:
proof-server:
image: 'midnightntwrk/proof-server:8.1.0'
command: ['midnight-proof-server', '-v']
ports:
- '127.0.0.1:6300:6300'
environment:
RUST_BACKTRACE: 'full'
healthcheck:
test: ['CMD-SHELL', 'echo > /dev/tcp/127.0.0.1/6300']
interval: 10s
timeout: 5s
retries: 20
start_period: 10s
다음으로 시작합니다:
docker compose -f proof-server.yml up -d
다음과 비슷한 출력이 보일 것입니다:
starting service: "actix-web-service-0.0.0.0:6300", workers: 12, listening on: 0.0.0.0:6300
계속 실행되도록 두세요. proving하는 모든 트랜잭션이 이곳을 거칩니다.
이 글을 쓰는 시점에 8.1.0이 현재 stable 태그입니다. latest 태그도 존재하지만 뒤처져 있습니다. 마지막으로 다시 배포된 것이 2026년 5월인데, 8.1.0과 9.0.0 릴리스 후보는 그 뒤에 나왔습니다. 버전을 고정하면 proof server가 node·indexer 버전과도 맞춰집니다. 대상 네트워크에 맞춰 테스트된 버전은 compatibility matrix에서 확인하세요.
A note on port 6300
proof server에는 --port 플래그와 MIDNIGHT_PROOF_SERVER_PORT 환경 변수가 있어 포트를 설정할 수 있습니다.
그래도 6300에 두세요. Lace는 Undeployed 네트워크에 대해 localhost:6300을 하드코딩하고, 여러분이 복사해 쓸 예제들도 이를 전제합니다. 호스트에서 6300이 실제로 점유되어 있다면, 호스트 쪽만 다시 매핑하고 DApp 설정을 그에 맞춰 갱신하세요:
docker run -p 6301:6300 midnightntwrk/proof-server:8.1.0 midnight-proof-server -v
Verify it is working
컨테이너가 실행 중이라고 해서 proof server가 동작한다는 보장은 없습니다. 다음 세 가지를 확인하세요:
curl http://localhost:6300/health
# {"status":"ok","timestamp":"..."}
curl http://localhost:6300/version
# 8.1.0
curl http://localhost:6300/ready
# {"status":"ok","jobsProcessing":0,"jobsPending":0,"jobCapacity":0,"timestamp":"..."}
개발 중에는 /ready가 가장 유용합니다. jobsProcessing은 그대로인데 jobsPending이 늘어난다면 proving 작업이 대기열에 쌓이는 중입니다. 서버는 작은 proving worker 풀을 유지하며, 기본값은 2개이고 --num-workers로 조정할 수 있습니다.
시작 로그의 workers: 12 줄은 HTTP worker 수로, CPU 개수에 맞춰 정해집니다. proving worker 수가 아닙니다.
Connect your DApp
proof server는 provider를 통해 접근합니다.
import { httpClientProofProvider } from '@midnight-ntwrk/midnight-js-http-client-proof-provider';
import { NodeZkConfigProvider } from '@midnight-ntwrk/midnight-js-node-zk-config-provider';
const zkConfigProvider = new NodeZkConfigProvider<'myCircuit'>(
'/path/to/contract/build',
);
const proofProvider = httpClientProofProvider(
'http://localhost:6300',
zkConfigProvider,
);
두 provider가 각각 한 가지 일을 합니다. zkConfigProvider는 컴파일러 아티팩트, 즉 각 circuit의 proving key, verifier key, ZKIR를 제공합니다. proofProvider는 이를 트랜잭션과 함께 서버로 전달합니다. 파일 시스템에 있는 아티팩트에는 NodeZkConfigProvider를, HTTP로 호스팅되는 아티팩트에는 FetchZkConfigProvider를 사용하세요.
둘 다 provider 객체에 들어갑니다:
const providers = {
privateStateProvider,
publicDataProvider,
zkConfigProvider,
proofProvider,
walletProvider,
midnightProvider,
};
빌더가 아니라 지갑 사용자라면, Lace가 같은 곳을 가리킵니다. Settings → Midnight → Local로 가서 http://localhost:6300을 선택하세요. 현재 Lace가 지원하는 유일한 proving 옵션입니다.
Where proving sits in a transaction
- circuit을 로컬에서 실행해 proof되지 않은 트랜잭션을 만듭니다.
proofProvider를 통해 ZK proof를 생성합니다. 이것이 proof server의 단계입니다.walletProvider를 통해 트랜잭션의 잔액을 맞춥니다.midnightProvider를 통해 네트워크에 제출합니다.publicDataProvider를 통해 최종성을 기다립니다.
proof server가 관여하는 단계는 2번뿐입니다. DApp이 멈춘다면 그곳을 먼저 살펴보세요.
prover는 proof를 반환하기 전에 자신의 proof를 검증합니다. 키와 circuit이 맞지 않으면, 제출 시점에 조용히 실패하는 대신 키가 맞는지 확인하라는 오류를 냅니다. 실제로 이는 거의 항상 오래된 빌드 아티팩트를 뜻합니다.
해결: 스마트 컨트랙트를 다시 컴파일하세요.
Choose a network
네트워크 선택은 두 번째로 흔한 걸림돌이며, 답은 보기보다 간단합니다.
| Network | Network ID | 무엇인가 | 언제 쓰는가 |
|---|---|---|---|
| Undeployed | undeployed | 직접 실행하는 로컬 스택: node, indexer, proof server | 개발하고 반복할 때. 가장 빠른 루프, faucet 없음, 기다림 없음. |
| Preview | preview | 라이브 테스트 네트워크로, 코어 엔지니어링이 운영하는 주요 개발 환경 | 최신 기능을 갖춘 실제 네트워크를 대상으로 테스트해야 할 때. |
| Preprod | preprod | Mainnet 전 최종 테스트를 위한 라이브 pre-production 네트워크 | 프로덕션에 가까운 동작을 검증할 때. |
| Mainnet | mainnet | 프로덕션 | 출시할 때. |
Undeployed에서 시작하세요. tDUST가 몇 시간이 아니라 몇 분이면 생성되고, faucet 대기열이 없으며, 컨테이너를 재시작해 체인 전체를 초기화할 수 있습니다. 통제할 수 없는 인프라를 대상으로 테스트해야 할 때 Preview나 Preprod로 옮기세요.
어느 쪽을 고르든, 명시적으로 설정하세요.
Undeployed:
import { setNetworkId } from '@midnight-ntwrk/midnight-js-network-id';
setNetworkId('undeployed');
export const CONFIG = {
indexer: 'http://localhost:8088/api/v4/graphql',
indexerWS: 'ws://localhost:8088/api/v4/graphql/ws',
node: 'ws://localhost:9944',
proofServer: 'http://localhost:6300',
};
Preprod:
import { setNetworkId } from '@midnight-ntwrk/midnight-js-network-id';
setNetworkId('preprod');
export const CONFIG = {
indexer: 'https://indexer.preprod.midnight.network/api/v4/graphql',
indexerWS: 'wss://indexer.preprod.midnight.network/api/v4/graphql/ws',
node: 'https://rpc.preprod.midnight.network',
proofServer: 'http://127.0.0.1:6300',
};
Preview:
import { setNetworkId } from '@midnight-ntwrk/midnight-js-network-id';
setNetworkId('preview');
export const CONFIG = {
indexer: 'https://indexer.preview.midnight.network/api/v4/graphql',
indexerWS: 'wss://indexer.preview.midnight.network/api/v4/graphql/ws',
node: 'https://rpc.preview.midnight.network',
proofServer: 'http://127.0.0.1:6300',
};
proofServer 값은 모든 경우에 로컬 주소입니다. 어떤 네트워크를 대상으로 하든 proving은 여러분 머신의 몫입니다.
로컬 스택은 고정된 포트를 씁니다. node는 9944, indexer는 8088, proof server는 6300입니다. 이는 Lace가 Undeployed에 대해 하드코딩하는 기본값과 정확히 같으므로, Lace에서 Undeployed를 선택하면 별도 설정 없이 연결됩니다.
Generate tDUST
proof server가 실행되면 다음 벽은 수수료입니다. Midnight 트랜잭션은 DUST로 지불되고, 테스트 네트워크에서는 tDUST를 씁니다. DUST는 여러분이 써 본 어떤 가스 토큰과도 다르게 동작합니다.
DUST는 전송할 수도 없고 구매할 수도 없습니다. 보유한 NIGHT이 시간이 지나면서 생성하는 리소스입니다.
메커니즘을 간단히 정리하면:
- NIGHT과 DUST는 서로 다른 키를 쓰므로, 등록 테이블이 NIGHT public key를 DUST 주소에 연결합니다. 이 단계를 designation이라고 합니다.
- NIGHT UTXO는 보유한 NIGHT에 비례하는 캡을 향해 DUST를 생성합니다. 초기 파라미터에서는 NIGHT당 5 DUST이며, 약 일주일이면 전체 캡에 도달합니다.
- 생성은 0에서부터 선형이므로, 캡에 도달하기 훨씬 전부터 쓸 수 있는 DUST가 생깁니다. 라이브 네트워크의 새 지갑에서는 대략 12시간, 로컬 네트워크에서는 약 5분을 예상하세요.
- DUST를 쓰면 다시 생성됩니다. 뒷받침하는 NIGHT을 쓰면 DUST는 0으로 감쇠합니다.
Why tDUST looks stuck at zero
새로 만든 지갑에서 가장 흔히 나오는 보고인데, 대개는 멈춘 게 아닙니다. 애초에 시작되지 않은 것입니다.
DUST UTXO는 NIGHT UTXO가 생성되고 또한 그 키에 이미 등록 테이블 항목이 있을 때만 만들어집니다. designation은 소급 적용되지 않으므로, DUST 주소를 designate하기 전에 도착한 tNIGHT은 아무것도 생성하지 않습니다.
순서가 중요합니다:
- 지갑을 만듭니다.
- faucet에서 tNIGHT을 요청합니다.
- DUST 주소를 designate합니다.
지갑에 먼저 자금을 넣고 지금 0 잔액을 보고 있다면, 새 지갑이 필요하지는 않습니다. DUST 주소를 designate한 뒤, 자신에게 tNIGHT을 보내 새 tNIGHT UTXO를 만드세요. 새 UTXO는 정상적으로 생성합니다.
순서 외에도 다음을 확인하세요:
- 생각보다 짧게 기다리고 있습니다. 라이브 네트워크에서는 몇 분이 아니라 몇 시간을 잡으세요.
- 다른 네트워크의 지갑을 보고 있습니다. Preview와 Preprod는 faucet도 잔액도 별개인 별도의 체인입니다.
- 트랜잭션이 너무 오래 방치되었습니다. DUST 지출에는 타임스탬프와 약 3시간의 유예 기간이 붙습니다. 만들어 두고 밤새 방치한 트랜잭션은 거부됩니다.
What to remember
proof server는 부수적인 도구가 아닙니다. Midnight의 핵심 트레이드오프가 물리적으로 자리하는 곳입니다. 무겁고 프라이버시를 보존하는 작업은 여러분의 머신에서, 밀리초 단위의 public 검사는 온체인에서 이루어집니다.
- proof server는 여러분의 witness 데이터를 보므로 여러분의 머신에서 실행됩니다.
- 여러분의 지갑 키는 결코 건드리지 않으며, 서명하거나 지출할 수 없습니다.
- 세 운영체제에 걸쳐 네이티브 암호 서비스를 버전 고정하며 배포하는 가장 깔끔한 방법이므로 Docker로 배포됩니다.
- 태그를 고정하고, 계속 실행해 두고, 모든 것을
localhost:6300으로 향하게 하세요.
Undeployed에서 시작하고, 지갑에 자금을 넣기 전에 DUST 주소를 designate하세요. 그러면 처음 10분이 더는 어려운 부분이 아니게 됩니다.
Additional resources
- Run the proof server: Docker Desktop에서 컨테이너를 시작하는 지갑 사용자용 안내.
- Install the Midnight toolchain: Compact 컴파일러와 개발자 도구.
- Quickstart: 하나의 compose 파일로 node, indexer, proof server를 갖춘 로컬 devnet.
- Networks and environments: 모든 환경의 endpoint와 network ID.
- Funding a wallet: faucet, Lace 등록, wallet SDK 경로.
- DUST architecture: NIGHT과 DUST 모델 심화.
- Support matrix: 어떤 proof server 버전이 어떤 네트워크 컴포넌트와 짝을 이루는지.