Skip to main content

Explicit Disclosure: Midnight "Witness Protection Program"

Introduction

Midnight은 프라이버시를 최대한 보호하면서도 필요에 따라 비공개 정보를 선택적으로 공개할 수 있는 애플리케이션 개발을 지원합니다. 선택적 공개는 모든 것이 공개되는 기존 블록체인이나 모든 것이 비공개인 엄격한 프라이버시 블록체인과 다릅니다. 예를 들어 은행은 Midnight의 선택적 공개를 활용하여 규제에 필요한 데이터만 공개하고 나머지 계좌 정보는 비공개로 유지할 수 있습니다.

비공개 정보(파생 정보 포함)의 공개 여부는 각 DApp이 결정해야 합니다. 공개 요구 사항은 상황마다 다르기 때문입니다. 다만 비공개 정보는 꼭 필요한 경우에만 공개되어야 하므로, Compact 언어는 공개를 명시적으로 선언하도록 요구합니다. 즉, 비공개일 수 있는 데이터를 공개 ledger에 저장하거나 export된 circuit에서 반환하거나 다른 컨트랙트에 전달하려면, 먼저 해당 데이터의 공개 의도를 명시적으로 선언해야 합니다. 이로써 프라이버시가 기본값이 되고 공개는 명시적 예외가 되어 실수로 인한 정보 노출 위험을 줄입니다.

Compact 프로그램에서 생성된 컨트랙트는 공개 ledger 업데이트와 영지식 증명의 결합입니다. 영지식 증명은 witness 또는 _witness 데이터_라 불리는 데이터에 대해 특정 속성이 성립함을 증명하되, 그 속성이 성립한다는 사실 외에는 아무것도 공개하지 않습니다. Compact에서 witness 데이터는 주로 컨트랙트 내에 witness로 선언되어 DApp이 제공하는 외부 콜백 함수를 통해 전달됩니다. export된 circuit 인수나 생성자 인수로도 컨트랙트에 들어올 수 있으며, witness 데이터에서 파생된 모든 값도 witness 데이터로 간주됩니다. witness 데이터는 비공개 정보를 포함할 수 있으므로 일반적으로 영지식 증명 구성에만 사용하고 공개해서는 안 되지만, 때로는 예외가 필요합니다. 이때 공개를 명시적으로 선언해야 합니다.

Declaring disclosure explicitly

Compact에서 witness 데이터를 공개하겠다는 의도를 명시적으로 선언하는 것은 간단합니다. 공개할 witness 데이터를 포함할 수 있는 값을 가진 표현식 주위에 disclose() 래퍼를 추가하기만 하면 됩니다. 다음의 간단한 프로그램이 이를 보여줍니다:

import CompactStandardLibrary;
witness getBalance(): Bytes<32>;
export ledger balance: Bytes<32>;

export circuit recordRalance(): [] {
balance = disclose(getBalance());
}

disclose() 래퍼가 없으면, 컴파일러는 유용한 오류 메시지와 함께 프로그램을 거부합니다. 예를 들어, 다음 Compact 프로그램을 컴파일하려고 하면:

import CompactStandardLibrary;
witness getBalance(): Bytes<32>;
export ledger balance: Bytes<32>;

export circuit recordBalance(): [] {
balance = getBalance(); // disclose() 래퍼 누락
}

컴파일러가 다음 오류 메시지와 함께 중단됩니다:

Exception: /tmp/q3.compact line 6 char 11:
potential witness-value disclosure must be declared but is not:
witness value potentially disclosed:
the return value of witness getBalance at line 2 char 1
nature of the disclosure:
ledger operation might disclose the witness value
via this path through the program:
the right-hand side of = at line 6 char 11

오류 메시지는 이 시점에서 공개되는 모든 witness 데이터의 출처를 나열하므로, 프로그래머는 disclose() 래퍼를 추가하면 모든 출처가 공개 선언된다는 것을 알 수 있습니다.

disclose() 래퍼 자체가 공개를 유발하지는 않습니다. 래핑된 표현식의 값을 공개해도 괜찮다고 컴파일러에 알려줄 뿐입니다. 달리 말하면, 래핑된 표현식의 값이 실제로 witness 데이터를 포함하든 아니든 포함하지 않는 것으로 취급하라고 컴파일러에 지시하는 것입니다.

Tracking indirect witness data assignments

실제로 공개가 이렇게 직접적인 경우만 있는 것은 아니지만, 공개를 명시적으로 선언해야 하는 요구 사항은 항상 적용됩니다. 예를 들어 다음과 같이 공개를 난독화하더라도:

import CompactStandardLibrary;
struct S { x: Field; }
witness getBalance(): Bytes<32>;
export ledger balance: Bytes<32>;

circuit obfuscate(x: Field): Field { // 심각하게 나쁜 난독화
return x + 73;
}

export circuit recordBalance(): [] {
const s = S { x: getBalance() as Field };
const x = obfuscate(s.x);
balance = x as Bytes<32>;
}

여전히 유사한 오류 메시지와 함께 컴파일러가 중단됩니다:

Exception: /tmp/q3.compact line 13 char 11:
potential witness-value disclosure must be declared but is not:
witness value potentially disclosed:
the return value of witness getBalance at line 3 char 1
nature of the disclosure:
ledger operation might disclose the result of an addition involving the witness value
via this path through the program:
the binding of s at line 11 char 3
the argument to obfuscate at line 12 char 13
the computation at line 7 char 10
the binding of x at line 12 char 3
the right-hand side of = at line 13 char 11

이 경우, 의도적인 공개라면 getBalance() 호출 주위에, balance 할당의 우변 주위에, 또는 호출 지점에서 공개 지점까지의 경로상 어디든 disclose() 래퍼를 배치하여 선언할 수 있습니다. 예를 들어, obfuscate circuit의 본문에서 인수 참조 주위에 추가할 수 있습니다.

import CompactStandardLibrary;
struct S { x: Field; }
witness getBalance(): Bytes<32>;
export ledger balance: Bytes<32>;

circuit obfuscate(x: Field): Field { // 심각하게 나쁜 난독화
return disclose(x) + 73;
}

export circuit recordBalance(): [] {
const s = S { x: getBalance() as Field };
const x = obfuscate(s.x);
balance = x as Bytes<32>;
}

대부분의 경우 disclose() 래퍼는 공개 지점에 최대한 가깝게 배치하는 것이 좋습니다. 데이터가 여러 경로를 거치는 경우 의도하지 않은 공개를 방지할 수 있기 때문입니다. 단, 구조화된 값(튜플, 벡터, 구조체 등)에서는 witness 데이터가 포함된 부분에만 disclose() 래퍼를 적용하여 다른 부분이 실수로 공개되지 않도록 해야 합니다. 한편, 항상 비공개가 아닌 데이터를 반환하거나 암호학적으로 충분히 난독화된 데이터를 반환하는 witness의 경우에는 witness 호출 자체에 disclose() 래퍼를 배치하는 것이 합리적입니다.

Indirect disclosure via conditionals

위 예제에서 볼 수 있듯이, witness 데이터에 산술 연산을 적용하거나 타입을 변환하거나 다른 circuit에 전달해도 컴파일러의 공개 감지를 피할 수 없습니다. 컴파일러는 조건식을 통한 간접 공개도 감지합니다. 예를 들어:

import CompactStandardLibrary;
witness getBalance(): Uint<64>;

export circuit balanceExceeds(n: Uint<64>): Boolean {
return getBalance() > n;
}

다음 메시지와 함께 컴파일러가 중단됩니다:

Exception: /tmp/q3.compact line 5 char 3:
potential witness-value disclosure must be declared but is not:
witness value potentially disclosed:
the return value of witness getBalance at line 2 char 1
nature of the disclosure:
the value returned from exported circuit balanceExceeds might disclose the result of a
comparison involving the witness value
via this path through the program:
the comparison at line 5 char 10

이 메시지는 공개가 간접적이라는 점을 명시하여 프로그래머에게 도움을 줍니다. 이 예제는 공개가 ledger에 저장될 때뿐 아니라 export된 circuit에서 반환될 때에도 발생한다는 것을 보여줍니다.

Safe Compact standard library routines

컴파일러는 특정 Compact 표준 라이브러리 루틴이 witness 데이터를 충분히 위장하여 명시적 공개 선언이 필요하지 않음을 인식합니다. 값에 witness 데이터가 포함된 표현식 e에 대해, 컴파일러는 transientCommit(e)를 witness 데이터를 포함하지 않는 것으로 처리하지만, transientHash(e)는 포함하는 것으로 처리합니다.

How explicit disclosure is implemented

선언되지 않은 witness 데이터 공개를 감지하고 보고하는 컴파일러 모듈을 "증인 보호 프로그램"이라고 부릅니다. 이 모듈은 _추상 인터프리터_로 구현되며, 추상 값은 실제 런타임 값이 아닌 해당 값에 포함될 수 있는 witness 데이터 정보를 나타냅니다.

추상 인터프리터는 추상 값을 실제 값처럼 취급하며 프로그램을 평가합니다. 다만 각 연산은 입력에서 출력으로 witness 데이터 정보를 전파하거나 차단하도록 수정되어 있습니다. 평가 중에 witness 데이터를 포함하는 추상 값이 선언 없이 공개되는 지점(예: ledger 저장)을 만나면, 컴파일러가 중단되고 적절한 오류 메시지가 생성됩니다.

Conclusion

Compact의 disclose() 래퍼는 민감한 비공개 witness 데이터 및 파생 데이터를 다룰 때 의도적인 프로그래밍 결정을 강제합니다. 명시적 공개 메커니즘은 비공개일 수 있는 데이터를 공개 ledger에 저장하거나, export된 circuit에서 반환하거나, 다른 컨트랙트에 전달하기 전에 반드시 공개 의도를 선언하도록 요구합니다. 이로써 프라이버시가 기본값이 되고 공개는 명시적 예외가 되어 실수로 인한 정보 노출 위험을 줄입니다.