Skip to main content

Use the Compact JavaScript implementation

이 가이드에서는 개발 워크플로에서 Compact JavaScript 구현을 사용하는 방법을 다룹니다. 구현 import, 컨트랙트 인스턴스 생성, JavaScript/TypeScript 환경에서의 함수 호출 방법을 설명합니다.

Import the implementation

Compact 컨트랙트를 컴파일하면 managed 디렉토리에 다음 파일이 생성됩니다:

  • index.js: JavaScript 구현
  • index.d.ts: TypeScript 타입 정의
  • index.js.map: 디버깅용 소스 맵

일반 ES 모듈처럼 import하면 됩니다:

import { Contract, State, pureCircuits, ledger } from './managed/bboard/contract/index.js';

TypeScript를 사용하면 함께 생성된 index.d.ts가 컨트랙트와 메서드의 타입 힌트를 자동으로 제공합니다.

Implement witnesses

witness 함수가 있는 Compact 컨트랙트는 인스턴스화할 때 witnesses 객체가 필요합니다. 이 객체에 Compact 코드에서 선언한 모든 witness 함수의 구현을 담습니다.

bulletin board 컨트랙트의 경우 contract/srcwitnesses.ts 파일을 생성하세요:

import { Ledger } from "./managed/bboard/contract/index.js";
import { WitnessContext } from "@midnight-ntwrk/compact-runtime";

export type BBoardPrivateState = {
readonly secretKey: Uint8Array;
};

export const createBBoardPrivateState = (secretKey: Uint8Array) => ({
secretKey,
});

export const witnesses = {
localSecretKey: ({
privateState,
}: WitnessContext<Ledger, BBoardPrivateState>): [
BBoardPrivateState,
Uint8Array,
] => [privateState, privateState.secretKey],
};

witnesses 객체는 witness 함수 이름을 구현에 매핑합니다. 각 witness 함수는 ledger 상태, private state, 컨트랙트 주소가 포함된 WitnessContext를 받고, 업데이트된 private state와 witness 값의 튜플을 반환합니다.

Call contract circuits

각 circuit은 contract.circuits 또는 contract.impureCircuits에 JavaScript 함수로 노출됩니다. 이 래퍼가 입력 준비, JavaScript 구현 실행을 처리하고, 출력, 업데이트된 컨텍스트, proof 데이터가 포함된 결과를 반환합니다.

post impure circuit 호출 예시:

const initialContext = {
originalState: {
state: State.VACANT,
message: { is_some: false, value: '' },
sequence: 1n,
owner: new Uint8Array(32)
},
privateState: {
secretKey: new Uint8Array(32)
},
contractAddress: '0x...',
transactionContext: {}
};

const message = "Hello from Compact!";

const { result, context, proofData, gasCost } =
contract.circuits.post(initialContext, message);

반환 객체 구성:

  • result: circuit의 반환 값 (post의 경우 빈 배열)
  • context: 새 ledger 상태가 포함된 업데이트된 circuit 컨텍스트
  • proofData: proof 생성을 위한 입력, 출력, 트랜스크립트 데이터
  • gasCost: 가스 비용 추적 정보

publicKey pure circuit 호출 예시:

const secretKey = new Uint8Array(32);
const sequenceBytes = new Uint8Array(32);

const ownerCommitment = pureCircuits.publicKey(secretKey, sequenceBytes);

Pure circuit은 circuit 컨텍스트 없이 직접 호출 가능하며, 결정론적 계산을 수행하고 즉시 결과를 반환합니다.

Write unit tests

Compact 구현은 표준 ES 모듈이므로 Vitest, Jest, Mocha 등 테스트 프레임워크와 바로 연동할 수 있습니다.

import { describe, it, expect } from 'vitest';
import { Contract, State } from './managed/bboard/contract/index.js';
import { witnesses, createBBoardPrivateState } from './witnesses.js';

describe('Bulletin board contract', () => {
it('accepts a new post on vacant board', () => {
const contract = new Contract(witnesses);

const context = {
originalState: {
state: State.VACANT,
message: { is_some: false, value: '' },
sequence: 1n,
owner: new Uint8Array(32)
},
privateState: createBBoardPrivateState(new Uint8Array(32)),
contractAddress: '0x0000000000000000000000000000000000000000000000000000000000000000',
transactionContext: {}
};

const { result, context: newContext } = contract.circuits.post(context, "Test message");

expect(newContext.originalState.state).toBe(State.OCCUPIED);
expect(newContext.originalState.message.is_some).toBe(true);
expect(newContext.originalState.message.value).toBe("Test message");
});

it('rejects post on occupied board', () => {
const contract = new Contract(witnesses);

const context = {
originalState: {
state: State.OCCUPIED,
message: { is_some: true, value: 'Existing message' },
sequence: 1n,
owner: new Uint8Array(32)
},
privateState: createBBoardPrivateState(new Uint8Array(32)),
contractAddress: '0x0000000000000000000000000000000000000000000000000000000000000000',
transactionContext: {}
};

expect(() => contract.circuits.post(context, "New message"))
.toThrow("Attempted to post to an occupied board");
});
});

Midnight Node나 proof server 없이 입력을 완전히 제어하면서 오프체인에서 컨트랙트 로직을 테스트할 수 있습니다.

Why the Compact JavaScript implementation matters

Compact가 JavaScript 구현을 생성하는 이유와, 이 설계가 프라이버시 보호 스마트 컨트랙트 개발에 중요한 이유를 설명합니다.

A bridge between ZK circuits and everyday code

ZK circuit은 강력하지만 복잡하고 불투명하여, 직접 디버깅하거나 테스트하기가 쉽지 않습니다.

JavaScript 구현은 저수준 proof 시스템과 고수준 컨트랙트 로직 사이의 다리 역할을 합니다. contract.circuits.post(context, "Hello world!")를 호출하면 ZK circuit이 온체인에서 실행하는 것과 동일한 로직이 실행되지만, Node.js에서 단계별로 실행하고 로그를 남기고 검사할 수 있습니다.

따라서 proof를 생성하거나 트랜잭션을 제출하기 전에 로컬에서 컨트랙트 동작을 미리 검증할 수 있습니다.

Type safety and consistency across environments

CompactTypeBoolean, CompactTypeBytes 같은 Compact의 타입 디스크립터를 사용하므로, JavaScript 테스트에서 전달하는 데이터가 온체인과 동일하게 인코딩됩니다. 바이트 순서, 필드 정렬, 인코딩 길이 차이로 인한 미묘한 버그를 원천적으로 방지합니다.

const message = "Hello Midnight!";
const proof = contract.circuits.post(context, message);

ZK circuit이 동일하게 동작한다는 확신 하에 컨트랙트 로직을 테스트하고 검증할 수 있습니다.

Reproducibility and proof transparency

contract circuit 호출 시마다 구조화된 proofData 객체가 반환됩니다. 이 데이터는 circuit 표현과 함께 prover의 입력으로 사용됩니다.

재현 가능한 테스트와 투명한 검증에 핵심적인 역할을 합니다:

{
input: { value: [...], alignment: [...] },
output: { value: [...], alignment: [...] },
publicTranscript: [...],
privateTranscriptOutputs: [...]
}

JavaScript에서 직접 사용할 수 있어, 일반 테스트 플로우에서 circuit 실행을 기록하고 재생하고 검증할 수 있습니다. 별도의 외부 도구가 필요 없습니다.

Developer productivity without compromising privacy

이 설계 덕분에 프라이버시 보호 로직을 다루면서도 TypeScript, Jest, VSCode, Node.js 같은 익숙한 도구를 그대로 사용할 수 있습니다.

전용 proof 환경에 갇히지 않고 다음 작업이 가능합니다:

  • 애플리케이션과 동일한 언어로 통합 테스트 작성
  • 오프체인에서 사용자 플로우 시뮬레이션
  • circuit 재컴파일 전 로직 변경 사항 검증

내부적으로 암호학적 보증을 유지하면서도 개발자 친화적인 워크플로를 제공합니다.

Next steps

Compact JavaScript 구현의 사용법을 살펴보았습니다. 실제 사용 예시는 Bulletin board DApp에서 확인할 수 있습니다.