DEV Community

Adam Opeyemi
Adam Opeyemi

Posted on

o1js 소개

o1js는 다음을 위한 TypeScript 라이브러리입니다:

  • 일반적인 용도의 영지식(zero knowledge, zk) 프로그램 작성
  • Mina를 위한 zk 스마트 컨트랙트 작성 다음은 o1js를 사용할 때 작성할 수 있는 TypeScript 코드입니다:
import { Field, Poseidon } from 'o1js';

function knowsPreimage(preimage: Field) {
 let hash = Poseidon.hash([preimage]);
 hash.assertEquals(expectedHash);
}

const expectedHash =
 0x1d444102d9e8da6d566467defcc446e8c1c3a3616d059facadbfd674afbc37ecn;

Enter fullscreen mode Exit fullscreen mode

zkApp에서 이 코드는 비밀값을 드러내지 않고 해당 값의 해시가 공개적으로 알려져 있음을 증명하는 데 사용할 수 있습니다. 코드는 일반 TypeScript로 작성되며, 일반 TypeScript처럼 실행됩니다. o1js는 임베디드 도메인 특화 언어(DSL)로 간주될 수 있습니다.

o1js는 증명이 가능한 데이터 타입과 메서드를 제공합니다. 이를 통해 실행 과정을 증명할 수 있습니다.

예제 코드에서 Poseidon.hash()와 Field.assertEquals()는 증명이 가능한 메서드의 예입니다. 증명은 영지식(zero knowledge) 방식으로 이루어지며, 입력값과 실행 과정을 알리지 않고도 검증할 수 있습니다. 애플리케이션에 필요하다면 증명의 일부를 공개할 수도 있습니다.

o1js는 zk 증명을 생성할 수 있는 도구를 제공하는 범용 zk 프레임워크입니다. 기본적인 산술 연산, 해싱, 서명, 논리 연산, 비교 등 풍부한 내장 증명 가능 연산 세트를 활용하여 임의의 zk 프로그램을 작성할 수 있습니다. o1js 프레임워크를 사용하여 Mina에서 zkApp을 작성할 수 있으며, 클라이언트 측에서 실행되고 비공개 입력을 사용하는 스마트 컨트랙트를 구현할 수 있습니다.

o1js 프레임워크는 주요 웹 브라우저와 Node.js에서 사용할 수 있는 단일 TypeScript 라이브러리로 제공됩니다. o1js를 시작하는 가장 좋은 방법은 zkApp CLI를 사용하는 것입니다. 또한 npm에서 npm i o1js 명령어로 o1js를 설치할 수 있습니다.

o1js의 기본적인 zk 프로그래밍 개념을 배우며 여정을 시작하세요.

o1js 감사 (Audits of o1js)
**

  • Veridise 외부 감사(Q3 2024) Veridise라는 보안 감사 회사와 협력하여 o1js 버전 1에 대한 전체 감사를 진행했습니다. Veridise는 39인 주간을 투자하여 o1js를 심층적으로 검토했으며, 중간 이상의 심각도를 가진 모든 문제를 수정했습니다.

내부 감사(Q1 2024)
2024년 3월, o1js 팀은 약 2인 주간을 투자하여 o1js 코드베이스의 일부를 내부적으로 감사했습니다. 이 감사는 주요 증명 가능 코드에 대한 검토에 중점을 두었으며, 여러 문제를 발견하고 수정했습니다
**
o1js 기본 개념
**
o1js는 일반적인 영지식(zero knowledge, zk) 프로그램과 Mina용 zk 스마트 컨트랙트를 작성하기 위한 TypeScript(TS) 라이브러리입니다.

Field​

필드 요소(Field elements)는 영지식 증명 프로그래밍에서 기본 데이터 단위입니다. 각 필드 요소는 거의 256비트 크기의 숫자를 저장할 수 있습니다. 필드 요소는 Solidity의 uint256과 비슷하다고 생각할 수 있습니다.

note
For the cryptography inclined, the exact max value that a field can store is: 28,948,022,309,329,048,855,892,746,252,171,976,963,363,056,481,941,560,715,954,676,764,349,967,630,336.
Enter fullscreen mode Exit fullscreen mode

예를 들어, 일반적인 프로그래밍에서는 다음과 같이 작성할 수 있습니다:const sum = 1 + 3.

o1js에서는 이를 다음과 같이 작성합니다:
const sum = new Field(1).add(new Field(3))

이 코드는 더 간단하게 다음과 같이 표현할 수 있습니다:const sum = new Field(1).add(3)

여기서 3은 자동으로 필드 타입으로 승격(auto-promoted)되어 코드를 더 간결하게 만듭니다.

내장 데이터 타입 (Built-in Data Types)

일반적으로 사용할 수 있는 데이터 타입은 다음과 같습니다:

new Bool(x);   // accepts true or false
new Field(x);  // accepts an integer, or a numeric string if you want to represent a number greater than JavaScript can represent but within the max value that a field can store.
new UInt64(x); // accepts a Field - useful for constraining numbers to 64 bits
new UInt32(x); // accepts a Field - useful for constraining numbers to 32 bits

PrivateKey, PublicKey, Signature; // useful for accounts and signing
new Group(x, y); // a point on our elliptic curve, accepts two Fields/numbers/strings
Scalar; // the corresponding scalar field (different than Field)

CircuitString.from('some string'); // string of max length 128

Enter fullscreen mode Exit fullscreen mode

Field와 Bool의 경우, new 키워드 없이 생성자를 호출할 수도 있습니다.

let x = Field(10);
let b = Bool(true);

Enter fullscreen mode Exit fullscreen mode

조건문 (Conditionals)
o1js에서는 전통적인 조건문을 지원하지 않습니다.

// this will NOT work
if (foo) {
 x.assertEquals(y);
}

Enter fullscreen mode Exit fullscreen mode

대신, 삼항 연산자로 작동하는 o1js의 내장 메서드인 Circuit.if()를 사용해야 합니다.

const x = Circuit.if(new Bool(foo), a, b); // behaves like `foo ? a : b`

Enter fullscreen mode Exit fullscreen mode

함수 (Functions)

함수는 TypeScript에서 기대하는 방식대로 작동합니다. 예를 들어:

function addOneAndDouble(x: Field): Field {
 return x.add(1).mul(2);
}

Enter fullscreen mode Exit fullscreen mode

일반적인 메서드 (Common Methods)
자주 사용되는 일반적인 메서드는 다음과 같습니다:

let x = new Field(4); // x = 4
x = x.add(3); // x = 7
x = x.sub(1); // x = 6
x = x.mul(3); // x = 18
x = x.div(2); // x = 9
x = x.square(); // x = 81
x = x.sqrt(); // x = -9

let b = x.equals(8); // b = Bool(false)
b = x.greaterThan(8); // b = Bool(true)
b = b.not().or(b).and(b); // b = Bool(true)
b.toBoolean(); // true

let hash = Poseidon.hash([x]); // takes array of Fields, returns Field

let privKey = PrivateKey.random(); // create a private key
let pubKey = PublicKey.fromPrivateKey(privKey); // derive public key
let msg = [hash];
let sig = Signature.create(privKey, msg); // sign a message
sig.verify(pubKey, msg); // Bool(true)

Enter fullscreen mode Exit fullscreen mode

재귀 (Recursion)
o1js의 기반이 되는 맞춤 증명 시스템인 Kimchi는 Pickles 재귀 시스템과의 통합을 통해 임의의 무한 재귀 회로 증명 구성을 지원합니다. Mina Protocol은 무한 재귀를 제공하는 유일한 블록체인입니다.

재귀는 다양한 용도를 가진 강력한 원시 기능입니다. 예를 들어:

Mina는 선형 재귀 증명을 사용하여 무한히 성장하는 블록체인을 일정한 크기로 압축합니다.
Mina는 "롤업(Rollup)" 방식의 트리 기반 재귀 증명을 사용하여 블록 내의 거래를 병렬로 일정한 크기로 압축합니다.
Mastermind 게임과 같은 앱 전용 롤업은 선형 재귀 증명을 사용하여 애플리케이션의 상태 머신을 동기화 없이 진행시킬 수 있습니다.
앱 전용 롤업은 서로 통신할 수 있습니다. 예를 들어, Inter-Blockchain Communication(IBC, Cosmos) 프로토콜이나 Cross-Chain Virtual Machine(XVM)을 사용하여 메시지를 주고받는 파라체인(Parachains)처럼 작동합니다.
보다 일반적으로, 재귀를 사용하여 zkApp의 일부로 모든 영지식 프로그램을 검증할 수 있습니다.

**

ZkProgram 개요 (ZkProgram Overview)

**

!note
zkProgram is available as a top-level import. Experimental.ZkProgram is deprecated. If you are experiencing issues with zkProgram, be sure to update o1js to the latest version.

Enter fullscreen mode Exit fullscreen mode

o1js에서는 ZkProgram()을 사용하여 재귀 프로그램의 단계를 정의할 수 있습니다. SmartContract() 메서드와 마찬가지로, ZkProgram() 메서드는 오프체인에서 실행됩니다.

원하는 재귀 단계를 수행한 후, ZkProgram을 SmartContract 메서드에 포함시켜 Mina 블록체인에서 상호작용을 정산할 수 있습니다. 이 과정에서 실행 증명을 검증하고, 메서드 내에서 사용할 수 있는 출력(예: 앱 상태에 저장)을 추출할 수 있습니다.

SmartContract 클래스 내의 메서드와 유사하게, ZkProgram의 입력값은 기본적으로 비공개이며 Mina 네트워크에 노출되지 않습니다. 그러나 SmartContract 메서드와는 달리, zkApp 개발자는 ZkProgram의 모든 메서드에 대한 공개 입력의 형태를 직접 정의해야 합니다.

예제: zkApp에서 간단한 프로그램을 재귀적으로 검증하기

이 간단한 예제는 받은 공개 입력값이 0임을 증명하는 단일 메서드만 포함합니다.

import { Field, ZkProgram } from 'o1js';

const SimpleProgram = ZkProgram({
 name: 'simple-program-example',
 publicInput: Field,

 methods: {
   run: {
     privateInputs: [],

     async method(publicInput: Field) {
       publicInput.assertEquals(Field(0));
     },
   },
 },
});
Enter fullscreen mode Exit fullscreen mode

이 프로그램을 컴파일하려면:

const { verificationKey } = await SimpleProgram.compile();

Enter fullscreen mode Exit fullscreen mode

이제 이 프로그램을 사용해 증명을 생성할 수 있습니다:

const { proof } = await SimpleProgram.run(Field(0));

Enter fullscreen mode Exit fullscreen mode

To verify this proof from within any method of your SmartContract class:

@method async foo(proof: SimpleProgram.Proof) {
 proof.verify().assertTrue();
 const output: Field = proof.value;
 // ...the rest of our method.
 // For example, storing the output of the execution of the program we've
 // proven as on-chain state, if desired.
}

Enter fullscreen mode Exit fullscreen mode

SmartContract 클래스의 메서드 내에서 이 증명을 검증하려면:
이 예제에서 foo는 SimpleProgram 증명을 메서드의 비공개 인수로 받아, 실행이 유효했음을 검증한 후 출력을 활용합니다.

예제: zkApp에서 선형 재귀 프로그램을 재귀적으로 검증하기
이 예제는 재귀적인 ZkProgram을 설명하며, 이를 통해 재귀적인 영지식 증명을 생성할 수 있습니다. 다른 증명 시스템에서는 이를 구성하기가 매우 어렵거나 불가능할 수 있습니다. 그러나 o1js에서는 간단한 재귀 함수로 재귀적인 ZkProgram을 표현할 수 있습니다.

이 프로그램은 숫자에 1을 반복적으로 더하는 재귀 연산을 설명합니다:

import { SelfProof, Field, ZkProgram, verify } from 'o1js';

const AddOne = ZkProgram({
 name: 'add-one-example',
 publicInput: Field,

 methods: {
   baseCase: {
     privateInputs: [],

     async method(publicInput: Field) {
       publicInput.assertEquals(Field(0));
     },
   },

   step: {
     privateInputs: [SelfProof],

     async method(publicInput: Field, earlierProof: SelfProof<Field, void>) {
       earlierProof.verify();
       earlierProof.publicInput.add(1).assertEquals(publicInput);
     },
   },
 },
});

Enter fullscreen mode Exit fullscreen mode

이 예제에서는 이전 증명을 메서드의 비공개 인수로 사용하는 점에 주의하세요.

먼저 이 프로그램을 컴파일하고, 기본 증명을 생성하세요:

const { verificationKey } = await AddOne.compile();

const { proof } = await AddOne.baseCase(Field(0));

Enter fullscreen mode Exit fullscreen mode

이번에는 이 증명을 입력으로 사용하여 1을 다시 재귀적으로 더합니다:

const { proof: proof1 } = await AddOne.step(Field(1), proof);

Enter fullscreen mode Exit fullscreen mode

이 과정을 원하는 만큼 반복할 수 있습니다:

const { proof: proof2 } = await AddOne.step(Field(2), proof1);

Enter fullscreen mode Exit fullscreen mode

앞서 설명한 예제와 마찬가지로, SmartContract 내에서 증명을 검증할 수 있습니다.

@method async foo(proof: Proof) {
 proof.verify().assertTrue();
 /* ... the rest of our method
  * For example using the total value as the fee for some other transaction. */
}
Enter fullscreen mode Exit fullscreen mode

**

예제: zkApp에서 트리 기반 재귀 프로그램 재귀적으로 검증하기
**
트리 재귀는 다른 증명 시스템과 zk 툴킷에서 드물게 사용됩니다. 트리 재귀는 Mina 내부에서 롤업의 탈중앙화 증명 및 시퀀싱 메커니즘의 일부로 사용되며, Kimchi에서 강력하게 지원됩니다.

이 예제 프로그램은 숫자를 더하는 매우 간단한 롤업을 설명합니다.

import { SelfProof, Field, ZkProgram, verify } from 'o1js';

let RollupAdd = ZkProgram({
 name: 'rollup-add-example',
 publicInput: Field,

 methods: {
   baseCase: {
     privateInputs: [],

     async method(publicInput: Field) {},
   },

   step: {
     privateInputs: [SelfProof, SelfProof],

     async method(
       publicInput: Field,
       left: SelfProof<Field, void>,
       right: SelfProof<Field, void>
     ) {
       left.verify();
       right.verify();
       // assert that the left and right equal this input
       left.publicInput.add(right.publicInput).assertEquals(publicInput);
     },
   },
 },
});

Enter fullscreen mode Exit fullscreen mode

보너스: zkApp 외부에서 ZkProgram 사용하기
**
ZkProgram은 zkApp 외부에서도 직접 사용하여 임의의 영지식 프로그램(또는 회로)을 증명하고 검증할 수 있습니다.

const { verificationKey } = await MyProgram.compile();

const { proof } = await MyProgram.base(Field(0));

Enter fullscreen mode Exit fullscreen mode

이제 JSON으로 인코딩된 증명을 직접 검증하여 유효성을 나타내는 불리언 값을 반환할 수 있습니다:

import { verify } from 'o1js';

const ok = await verify(proof.toJSON(), verificationKey);

Enter fullscreen mode Exit fullscreen mode

**Gadgets

**
Gadgets는 새로운 암호화 원시값을 생성하는 과정을 간소화하는 작고 재사용 가능한 저수준 구성 요소입니다. 대부분의 Gadgets는 사용자 정의 게이트를 기반으로 구축되며, 증명 시스템 내에서 저수준 가속기로 작동합니다.

o1js에서는 Gadgets 네임스페이스에서 이러한 증명 가능 및 헬퍼 메서드를 가져올 수 있습니다:

-비트 연산
외부 필드 산술(Foreign Field Arithmetic)

비트 연산
비트 연산은 숫자의 이진 표현 내 개별 비트를 조작합니다. 이는 때로는 불리언 연산과 유사해 보일 수 있지만, 불리언 대신 비트의 시퀀스에 적용됩니다. 비트 연산은 TypeScript를 포함한 대부분의 프로그래밍 언어에서 일반적으로 사용 가능합니다. o1js는 필드(Field) 요소에서 작동하는 비트 연산 버전과 해당 계산에 대한 영지식 증명을 생성하기 위한 회로 제약을 제공합니다. 이는 특히 SHA256과 같은 해시 알고리즘을 구현할 때 유용합니다.

o1js에서 비트 연산과 관련 헬퍼 함수는 Gadgets로 구현됩니다.

비트 연산:

  • and()
  • not()
  • xor()
  • leftShift32()
  • leftShift64()
  • rightShift64()
  • rotate32()
  • rotate64()

헬퍼 함수:

  • addMod32()
  • divMod32()
  • rangeCheck32()
  • rangeCheck64()
  • multiRangeCheck()
  • compactMultiRangeCheck()

and()​

and(a: Field, b: Field, length: number) => Field

Enter fullscreen mode Exit fullscreen mode

and() Gadget는 JavaScript의 비트 AND (&) 연산자에 대한 증명 가능한 동등한 연산입니다. 두 개의 Field 요소를 받아 각 요소의 이진 표현에서 각각의 비트 쌍을 비교합니다. 두 비트 모두 1일 경우에만 1을 반환하며, 그 외에는 0을 반환합니다. 결과는 새로운 이진 숫자로 변환되어 Field 요소로 반환됩니다.

길이(length) 매개변수:

  • 비교할 비트의 수를 지정합니다.
  • 더 큰 숫자에 대해 추가 제약을 추가합니다 .

Example:

let a = Field(3);    // ... 000011
let b = Field(5);    // ... 000101
let c = Gadgets.and(a, b, 2);    // ... 000001
c.assertEquals(1);

Enter fullscreen mode Exit fullscreen mode

not()​

not(a: Field, length: number, checked: boolean) => Field

Enter fullscreen mode Exit fullscreen mode

not() Gadget는 JavaScript의 비트 NOT(~) 연산자에 대한 증명 가능한 동등한 연산입니다. Field 요소를 입력받아 이진 표현의 각 비트를 반전시킵니다. 즉, 1을 0으로, 0을 1로 바꾸어 Field 요소의 모든 비트를 뒤집습니다. 결과적으로 새로운 이진 숫자가 생성되며, 이는 Field 요소로 반환됩니다.

구현은 입력 길이를 확인하는지 여부에 따라 달라집니다. 입력 길이를 확인하지 않으면 더 효율적입니다. 입력 값은 모든 비트가 1로 설정된 비트마스크(all-ones bitmask)에서 빼줍니다. 단, 입력 길이를 미리 알고 있는 경우에만 안전합니다. 이는 입력 Field가 이미 길이가 알려진 증명된 연산의 결과일 때 가능합니다.
입력 길이를 확인하는 경우, xor() Gadget를 재사용합니다. 입력 Field와 동일한 길이의 비트마스크를 두 번째 인수로 제공하며, 이를 통해 입력 길이를 증명하면서 동일한 연산을 수행합니다. 이 경우 더 많은 제약 조건이 추가됩니다.

입력 Field는 최대 254비트여야 합니다.
length 매개변수

  • 반전할 비트 수를 지정합니다. 더 큰 숫자에 대해 더 많은 제약을 추가합니다.

checked 매개변수:

-입력 길이를 확인할지 여부를 지정합니다.
기본값은 false입니다.
자세한 구현은 Mina Book의 NOT 섹션을 참조하세요.

예제:

let a = Field(0b0101);
let b = Gadgets.not(a,4,true);

b.assertEquals(0b1010);

Enter fullscreen mode Exit fullscreen mode

xor()​

xor(a: Field, b: Field, length: number) => Field

Enter fullscreen mode Exit fullscreen mode

xor() Gadget는 JavaScript의 비트 XOR(^) 연산자에 대한 증명 가능한 동등한 연산입니다. 두 개의 Field 요소를 입력받아 각 요소의 이진 표현에서 각 비트 쌍을 비교합니다. 두 비트가 다르면 1을 반환하고, 같으면 0을 반환합니다. 결과적으로 새로운 이진 숫자가 생성되며, 이는 Field 요소로 반환됩니다.

length 매개변수:
비교할 비트 수를 지정합니다.
더 큰 숫자에 대해 더 많은 제약을 추가합니다.

자세한 구현은 Mina Book의 XOR 섹션을 참조하세요.

Example:

let a = Field(0b0101);
let b = Field(0b0011);

let c = Gadgets.xor(a, b, 4); // xor-ing 4 bits
c.assertEquals(0b0110);

Enter fullscreen mode Exit fullscreen mode

leftShift32()​

leftShift32(field: Field, bits: number) => Field

Enter fullscreen mode Exit fullscreen mode

leftShift32() Gadget는 JavaScript의 왼쪽 시프트(<<) 연산자에 대한 증명 가능한 동등한 연산입니다. 이진 숫자의 비트를 지정된 비트 수만큼 왼쪽으로 이동시킵니다. 왼쪽에서 벗어난 비트는 버려지며, 오른쪽에서 0이 채워집니다. 결과는 32비트 범위가 확인된 새로운 Field 요소로 반환됩니다.
제약 조건:
입력 Field는 32비트를 초과할 수 없습니다.
rangeCheck32를 사용하여 이를 확인할 수 있습니다.

const x = Provable.witness(Field, () => Field(0b001100)); // 12 in binary
const y = Gadgets.leftShift32(x, 2); // left shift by 2 bits
y.assertEquals(0b110000); // 48 in binary

Enter fullscreen mode Exit fullscreen mode

leftShift64()​

leftShift64(field: Field, bits: number) => Field

Enter fullscreen mode Exit fullscreen mode

leftShift64() Gadget는 JavaScript의 왼쪽 시프트(<<) 연산자에 대한 증명 가능한 동등한 연산입니다. 비트 이동 방식은 leftShift32()와 동일하지만, 결과는 64비트 범위가 확인된 새로운 Field 요소로 반환됩니다.

제약 조건:
입력 Field는 64비트를 초과할 수 없습니다.
rangeCheck64를 사용하여 이를 확인할 수 있습니다.

const x = Provable.witness(Field, () => Field(0b001100)); // 12 in binary
const y = Gadgets.leftShift64(x, 2); // left shift by 2 bits
y.assertEquals(0b110000); // 48 in binary

const xLarge = Provable.witness(Field, () => Field(12345678901234567890123456789012345678n));
Gadgets.leftShift64(xLarge, 32); // throws an error since input exceeds 64 bits

Enter fullscreen mode Exit fullscreen mode

rightShift64()​

rightShift64(field: Field, bits: number) => Field

Enter fullscreen mode Exit fullscreen mode

rightShift64() Gadget는 JavaScript의 오른쪽 시프트(>>) 연산자에 대한 증명 가능한 동등한 연산입니다. 이진 숫자의 비트를 지정된 비트 수만큼 오른쪽으로 이동시킵니다. 오른쪽에서 벗어난 비트는 버려지며, 왼쪽에서 0이 채워집니다. 결과는 새로운 Field 요소로 반환됩니다.

제약 조건:
입력 Field는 64비트를 초과할 수 없습니다.
rangeCheck64를 사용하여 이를 확인할 수 있습니다.

const x = Provable.witness(Field, () => Field(0b001100)); // 12 in binary
const y = Gadgets.rightShift64(x, 2); // right shift by 2 bits
y.assertEquals(0b000011); // 3 in binary

const xLarge = Provable.witness(Field, () => Field(12345678901234567890123456789012345678n));
Gadgets.rightShift64(xLarge, 32); // throws an error since input exceeds 64 bits

Enter fullscreen mode Exit fullscreen mode

rotate32()​

rotate32(field: Field, bits: number, direction: 'left' | 'right' = 'left') {
 return rotate32(field, bits, direction);
},

Enter fullscreen mode Exit fullscreen mode

rotate32() Gadget는 32비트 숫자에 대해 증명 가능한 비트 회전을 수행합니다. 왼쪽 시프트 및 오른쪽 시프트와 비슷하지만, 끝에서 벗어난 비트가 버려지는 대신 반대쪽 끝에 다시 나타납니다. 이 Gadget는 Field 요소, 회전할 비트 수, 그리고 왼쪽 또는 오른쪽 방향을 입력으로 받습니다. 기본 방향은 왼쪽입니다.

제약 조건:
입력 Field는 32비트를 초과할 수 없습니다.
rangeCheck32를 사용하여 이를 확인할 수 있습니다.
자세한 구현은 Mina Book의 ROTATION 섹션을 참조하세요.

예제:

const x = Provable.witness(Field, () => Field(0b001100));
const y = Gadgets.rotate32(x, 2, 'left'); // left rotation by 2 bits
const z = Gadgets.rotate32(x, 2, 'right'); // right rotation by 2 bits
y.assertEquals(0b110000);
z.assertEquals(0b000011);

const xLarge = Provable.witness(Field, () => Field(12345678901234567890123456789012345678n));
Gadgets.rotate32(xLarge, 32, "left"); // throws an error since input exceeds 32 bits

Enter fullscreen mode Exit fullscreen mode

rotate64()​

rotate64(field: Field, bits: number, direction: 'left' | 'right' = 'left') {
 return rotate64(field, bits, direction);
},

Enter fullscreen mode Exit fullscreen mode

rotate64() Gadget는 64비트 숫자에 대해 증명 가능한 비트 회전을 수행합니다. 동작 방식은 rotate32()와 동일하지만, 입력 크기와 범위가 다릅니다.

제약 조건:
입력 Field는 64비트를 초과할 수 없습니다.
rangeCheck64를 사용하여 이를 확인할 수 있습니다.
자세한 구현은 Mina Book의 ROTATION 섹션을 참조하세요.

const x = Provable.witness(Field, () => Field(0b001100));
const y = Gadgets.rotate64(x, 2, 'left'); // left rotation by 2 bits
const z = Gadgets.rotate64(x, 2, 'right'); // right rotation by 2 bits
y.assertEquals(0b110000);
z.assertEquals(0b000011);

const xLarge = Provable.witness(Field, () => Field(12345678901234567890123456789012345678n));
Gadgets.rotate64(xLarge, 32, "left"); // throws an error since input exceeds 64 bits

Enter fullscreen mode Exit fullscreen mode

addMod32()​

addMod32(a: Field, b: Field) => Field

Enter fullscreen mode Exit fullscreen mode

addMod32() Helper는 32비트 숫자에 대한 오버플로우를 포함한 덧셈을 수행합니다. 이는 int32 타입과 유사하게 동작하며, 결과는 2³²로 모듈러 연산된 새로운 Field 요소로 반환됩니다.

제약 조건:
입력 Field는 32비트를 초과할 수 없습니다.
rangeCheck32를 사용하여 이를 확인할 수 있습니다.
예제:

let a = Field(8n);
let b = Field(1n << 32n);

Gadgets.addMod32(a, b).assertEquals(Field(8n));

Enter fullscreen mode Exit fullscreen mode

divMod32()​

divMod32(field: Field) => { remainder: Field, quotient: Field }

Enter fullscreen mode Exit fullscreen mode

divMod32() Helper는 2³²로 나눗셈을 수행하며, Field 요소를 나머지와 몫의 두 32비트 부분으로 분해합니다. 이 Helper는 두 개의 Field 요소를 튜플 형태로 반환합니다.

제약 조건:
입력 값은 64비트를 초과할 수 없습니다.
출력 값은 각각 32비트를 초과하지 않아야 합니다.
추가적인 범위 검사는 필요 없습니다.
예제:

let n = Field((1n << 32n) + 8n)
let { remainder, quotient } = Gadgets.divMod32(n);
// remainder = 8, quotient = 1

n.assertEquals(quotient.mul(1n << 32n).add(remainder));

Enter fullscreen mode Exit fullscreen mode

**rangeCheck32()​

**

rangeCheck32(x: Field) => void

Enter fullscreen mode Exit fullscreen mode

rangeCheck32() Helper는 입력 Field가 32비트를 초과하지 않도록 보장합니다. 음수의 작은 입력 값은 필드 크기에 가까운 큰 정수로 해석되어 32비트 검사를 통과하지 못합니다. 값이 int32 범위 [-2³¹, 2³¹) 내에 있음을 증명하려면 rangeCheck32(x.add(1n << 31n))을 사용할 수 있습니다.

예제:

const x = Provable.witness(Field, () => Field(12345678n));
Gadgets.rangeCheck32(x); // successfully proves 32-bit range

const xLarge = Provable.witness(Field, () => Field(12345678901234567890123456789012345678n));
Gadgets.rangeCheck32(xLarge); // throws an error since input exceeds 32 bits
Enter fullscreen mode Exit fullscreen mode

rangeCheck64()​

rangeCheck64(x: Field) => void

Enter fullscreen mode Exit fullscreen mode

rangeCheck64() Helper는 입력 Field가 64비트를 초과하지 않도록 보장합니다. 음수의 작은 입력 값은 필드 크기에 가까운 큰 정수로 해석되어 64비트 검사를 통과하지 못합니다. 값이 int64 범위 [-2⁶³, 2⁶³) 내에 있음을 증명하려면 rangeCheck64(x.add(1n << 63n))을 사용할 수 있습니다.

예제:

const x = Provable.witness(Field, () => Field(12345678n));
Gadgets.rangeCheck64(x); // successfully proves 64-bit range

const xLarge = Provable.witness(Field, () => Field(12345678901234567890123456789012345678n));
Gadgets.rangeCheck64(xLarge); // throws an error since input exceeds 64 bits
Enter fullscreen mode Exit fullscreen mode

multiRangeCheck()​

multiRangeCheck([x, y, z]: [Field, Field, Field]) => void

Enter fullscreen mode Exit fullscreen mode

multiRangeCheck() Helper는 세 개의 입력 Field가 각각 88비트를 초과하지 않음을 효율적으로 보장합니다. 독립적인 범위 검사보다 더 효율적으로 수행됩니다. 3x88 비트 범위 검사는 최대 264비트의 BigInt를 지원하며, 이는 2²⁵⁹ 이하의 모듈러 연산이 필요한 외부 필드 곱셈에 충분합니다.

Example:
c

onst x = Provable.witness(Field, () => Field(12345678n));
const y = Provable.witness(Field, () => Field(12345678n));
const z = Provable.witness(Field, () => Field(12345678n));
const xLarge = Provable.witness(Field, () => Field(12345678901234567890123456789012345678n));

Gadgets.multiRangeCheck([x, y, z]); // succeeds
Gadgets.multiRangeCheck([xLarge, y, z]); // fails
Enter fullscreen mode Exit fullscreen mode

compactMultiRangeCheck()


compactMultiRangeCheck(xy: Field, z: Field) => [Field, Field, Field];

Enter fullscreen mode Exit fullscreen mode

compactMultiRangeCheck() Helper는 multiRangeCheck의 변형으로, 첫 번째와 두 번째 입력 x와 y가 결합된 형태 xy = x + 2^88*y로 전달됩니다. 이를 분리하여 범위 검사를 수행한 뒤 x, y, 및 z를 별도로 반환합니다.

Example:

let [x, y, z] = Gadgets.compactMultiRangeCheck([xy, z]);

Enter fullscreen mode Exit fullscreen mode

**

왜 외부 필드인가?

**
외부 필드는 증명 시스템의 네이티브 필드와 다른 유한 필드입니다. o1js는 2²⁵⁹보다 작은 크기의 유한 필드에서 작동하는 모듈러 덧셈 및 곱셈과 같은 작업을 제공합니다.

외부 필드는 증명 가능한 코드에서 암호화 알고리즘을 구현하는 데 유용합니다. 예를 들어, Ethereum 호환 ECDSA 서명을 검증할 때 사용됩니다.

왜 외부 필드인가?

o1js의 핵심 데이터 타입인 Field는 증명 시스템의 네이티브 필드를 나타내며, 이는 덧셈과 곱셈과 같은 기본 연산의 기반입니다. o1js는 효율적인 암호화 알고리즘을 지원하기 위해 네이티브 필드에서 작동하는 여러 클래스와 모듈을 제공합니다. 예: Poseidon, PublicKey, PrivateKey, Signature, Encryption.

그러나 이러한 클래스와 모듈은 전 세계적으로 사용되는 암호화와는 호환되지 않습니다. 예를 들어, Signature.verify()는 서명된 JWT나 이메일을 검증할 수 없으며, Encryption.decrypt()는 WhatsApp 메시지를 해독하지 못합니다. 이는 네이티브 필드가 zk 증명을 효율적으로 수행하기 위해 선택된 반면, 외부 암호화는 다른 유한 필드를 사용하기 때문입니다.

외부 필드는 zkApp을 전 세계 암호화와 연결할 수 있는 알고리즘을 수행할 수 있게 합니다. 네이티브 필드에 비해 효율성은 떨어지지만, 정교하게 설계된 외부 필드는 흥미로운 여러 사용 사례를 지원하기에 충분히 효율적입니다.

기본 사용법
외부 필드를 사용하는 진입점은 createForeignField() 함수입니다. 사용 방법에 대한 자세한 내용은 API 참조 또는 각 메서드의 주석을 참조하십시오.

import { createForeignField } from 'o1js';

class Field17 extends createForeignField(17n) {}
Enter fullscreen mode Exit fullscreen mode

createForeignField() 함수가 받는 유일한 매개변수는 필드의 모듈러스(또는 크기)입니다. 이 코드 예제는 17n을 전달하여 Field17이 모듈로 17 연산을 수행할 수 있도록 합니다.

let x = Field17.from(16);
x.assertEquals(-1); // 16 = -1 (mod 17)
x.mul(x).assertEquals(1); // 16 * 16 = 15 * 17 + 1 = 1 (mod 17)
Enter fullscreen mode Exit fullscreen mode

모듈러스는 최대 259비트까지 지원됩니다. 이는 ForeignField가 종종 256비트 이하인 타원 곡선 알고리즘에 사용될 수 있지만, 일반적으로 2048비트 크기를 가지는 RSA에는 사용할 수 없음을 의미합니다.
특히, 모듈러스는 소수일 필요가 없습니다. 예를 들어, 모듈러스를 2^256으로 설정하여 UInt256 클래스를 생성할 수 있습니다.

class UInt256 extends createForeignField(1n << 256n) {}

// and now you can do arithmetic modulo 2^256!
let a = UInt256.from(1n << 255n);
let b = UInt256.from((1n << 255n) + 7n);
a.add(b).assertEquals(7);
Enter fullscreen mode Exit fullscreen mode

createForeignField()로 생성된 클래스의 공통 기본 타입은 ForeignField입니다.

import { ForeignField } from 'o1js';

// ...

let zero: ForeignField = Field17.from(0);
let alsoZero: ForeignField = UInt256.from(0);
Enter fullscreen mode Exit fullscreen mode

ForeignField supports the basic arithmetic operations:

x.add(x); // addition
x.sub(2); // subtraction
x.neg(); // negation
x.mul(3); // multiplication
x.div(x); // division
x.inv(); // inverse
Enter fullscreen mode Exit fullscreen mode

이 연산은 필드 크기에 대한 모듈로 수행된다는 점에 유의하세요. 예를 들어, Field17.from(1).div(2)는 9를 반환합니다. 이는 2 * 9 = 18 = 1 (mod 17)이기 때문입니다.

x.assertEquals(y); // assert x == y
x.assertLessThan(2); // assert x < 2

let bits = x.toBits(); // convert to a `Bool` array of size log2(modulus);
Field17.fromBits(bits); // convert back
Enter fullscreen mode Exit fullscreen mode

And there are non-provable methods for converting to and from JS values:

let y = SmallField.from(5n); // convert from bigint or number
y.toBigInt() === 5n; // convert to bigint
Enter fullscreen mode Exit fullscreen mode

각 메서드에 대한 자세한 정보는 API 참조 문서에서 확인할 수 있습니다.

세 가지 종류의 ForeignField

기본적인 사용 예제가 간단해 보인다면, 이제 조금 복잡해질 수 있는 부분을 살펴보겠습니다.

createForeignField()로 생성된 각 ForeignField 클래스에는 실제로 세 가지 다른 변형(variants)이 있습니다: 비축소(unreduced), 거의 축소(almost reduced), 그리고 표준(canonical)입니다.
이 변형들은 클래스의 정적 속성(static properties)으로 제공되며, 자체적으로도 클래스입니다.

let x = new Field17.Unreduced(0);
let y = new Field17.AlmostReduced(0);
let z = new Field17.Canonical(0);
Enter fullscreen mode Exit fullscreen mode

비축소 필드 요소는 ForeignField 타입만 가집니다. 다른 두 변형의 경우, 각 변형에 공통적인 더 좁은 기본 타입이 있습니다.

import { AlmostReducedField, CanonicalField } from 'o1js';

y satisfies AlmostReducedField;
z satisfies CanonicalField;
Enter fullscreen mode Exit fullscreen mode

다음 섹션에서는 언제 다른 변형을 사용하고, 변환하는 방법을 배울 수 있습니다. 하지만 모든 것을 기억할 필요는 없습니다. 타입 시스템이 각 상황에 적합한 변형을 사용할 수 있도록 안내합니다.

비축소 필드

대부분의 산술 연산은 비축소 필드를 반환합니다.

import assert from 'assert';

let z = x.add(x);
assert(z instanceof Field17.Unreduced);
Enter fullscreen mode Exit fullscreen mode

간단히 말해, 비축소는 값이 모듈러스보다 클 수 있음을 의미합니다.
예를 들어, x의 값이 16이라면, x.add(x)는 32라는 값을 가질 수 있습니다. 이 덧셈은 17로 나눈 나머지(modulo 17) 연산으로는 올바르지만, 결과가 17보다 작다는 보장은 없습니다.

note
Unreduced doesn't usually mean that the underlying witness is larger than the modulus. It just means that it is not proved to be smaller. A malicious prover could make it larger by slightly modifying their local version of o1js and creating a proof with that version.
Enter fullscreen mode Exit fullscreen mode

비축소 필드는 더하기 및 빼기 연산은 가능하지만, 곱하기나 나누기 연산은 지원하지 않습니다.

z.add(1).sub(x); // works

assert((z as any).mul === undefined); // z.mul() is not defined
assert((z as any).inv === undefined);
assert((z as any).div === undefined);
Enter fullscreen mode Exit fullscreen mode

거의 축소된 필드 (Almost Reduced Fields)​

곱셈을 수행하려면 거의 축소된 필드가 필요합니다. .assertAlmostReduced()를 사용하여 이를 변환할 수 있습니다:

let zAlmost = z.assertAlmostReduced();
assert(zAlmost instanceof SmallField.AlmostReduced);
Enter fullscreen mode Exit fullscreen mode

이제 곱셈 및 나눗셈이 가능합니다.

let zz = zAlmost.mul(zAlmost); // zAlmost.mul() is defined

// but .mul() returns an unreduced field again:
assert(zz instanceof SmallField.Unreduced);

// zAlmost.inv() is defined, and returns an almost reduced field:
assert(zAlmost.inv() instanceof SmallField.AlmostReduced);
Enter fullscreen mode Exit fullscreen mode

스마트 계약의 입력값으로 거의 축소된 필드를 요구하는 것이 편리할 수 있습니다. 이를 위해 타입으로도 작동할 수 있는 클래스를 생성하고, 상태 데코레이터에 값을 전달할 때 .provable 속성을 사용합니다:

class AlmostField17 extends Field17.AlmostReduced {}

class MyContract extends SmartContract {
 @state(AlmostField17.provable) x = State<AlmostField17>();

 @method async myMethod(y: AlmostField17) {
   let x = y.mul(2);
   this.x.set(x.assertAlmostReduced());
 }
}
Enter fullscreen mode Exit fullscreen mode
What does almost reduced mean?​

Enter fullscreen mode Exit fullscreen mode

거의 축소된 정의는 다소 기술적입니다. 주요 목적은 모듈러 곱셈을 증명하는 방식이 신뢰할 수 있도록 보장하는 것입니다. 이는 필드 요소가 2^259보다 작을 경우 확실히 참입니다. (모듈러스가 2^259 미만이어야 한다는 점을 기억하세요.)
하지만 실제로는 더 강력한 조건을 증명합니다. 이는 몇몇 경우에 제약을 줄이는 데 도움이 됩니다:

z가 f 모듈로 거의 축소되었다는 것은 z >> 176이 f >> 176보다 작거나 같은 경우를 의미합니다. (>>는 오른쪽 시프트를 뜻합니다.)

note
Example: Assume x is a UInt256 holding the value 2^130. After computing z = x.mul(x), it is valid for z to be 2^260.
However, by calling z.assertAlmostReduced(), you prove that z is smaller than 2^259 and safe to use in another multiplication. According to the stronger definition, you even have z < 2^256.
Enter fullscreen mode Exit fullscreen mode

거의 축소된 필드가 별도의 타입으로 노출되는 이유는, 곱셈에 필요한 조건을 항상 증명하도록 만들면 추가 제약이 발생하기 때문입니다.

ForeignField는 타입 시스템에 의해 안전하게 안내되면서 최소한의 제약을 사용할 수 있도록 설계되었습니다. 더 자세한 내용은 "제약 최소화"를 참조하세요.

표준 필드 (Canonical Fields)​

표준 필드는 가장 엄격한 변형입니다. 모듈러스보다 작은 것이 보장됩니다.

상수에서 필드를 생성할 때, 필드는 항상 완전히 축소됩니다. ForeignField.from()의 타입 서명은 이를 반영하며 표준 필드를 반환합니다.

let constant = Field17.from(16);
assert(constant instanceof Field17.Canonical);

// these also work, because `from()` takes the input mod 17:
Field17.from(100000000n) satisfies CanonicalForeignField;
Field17.from(-1) satisfies CanonicalForeignField;
Enter fullscreen mode Exit fullscreen mode

모든 필드를 표준으로 변환하려면 .assertCanonical()을 호출하세요.

let zCanonical = z.assertCanonical();
assert(zCanonical instanceof Field17.Canonical);
Enter fullscreen mode Exit fullscreen mode

표준 필드는 타입 수준에서 거의 축소된 필드의 특수한 경우입니다:

constant satisfies AlmostForeignField;
constant.mul(constant); // works
Enter fullscreen mode Exit fullscreen mode

기존 필드 요소가 표준임을 증명하는 가장 저렴한 방법은 그것이 상수와 같음을 보여주는 것입니다.

let zCanonical = z.assertEquals(3);
assert(zCanonical instanceof Field17.Canonical);
Enter fullscreen mode Exit fullscreen mode

표준 필드에서만 가능한 연산은 불리언 동등성 검사입니다:

let xCanonical = x.assertCanonical();
let yCanonical = y.assertCanonical();
let isEqual = xCanonical.equals(yCanonical);
Enter fullscreen mode Exit fullscreen mode

equals() 메서드의 입력은 표준이어야 합니다. 이 연산은 필드 크기 모듈로 동등성이 아니라 엄격한 동등성을 검사하기 때문입니다.
엄격히 동등하지 않다고 해서 필드 요소로서도 동등하지 않다는 것을 의미하지 않으므로, 비표준 필드에서 equals()를 사용하는 것은 오류를 유발할 수 있습니다.

제약 최소화 (Minimizing Constraints)​

제약을 최소화하려면 다음 전략을 따르세요.

assertAlmostReduced()​
여러 필드 요소를 "거의 축소"해야 할 때 제약을 줄이는 요령은 항상 3개씩 배치로 축소하는 것입니다.
예를 들어, 연속으로 많은 곱셈을 수행할 때 이를 활용하세요:

let z1 = x.mul(7);
let z2 = x.add(11);
let z3 = x.sub(13);

let [z1r, z2r, z3r] = Field17.assertAlmostReduced(z1, z2, z3);

z1r.mul(z2r);
z2r.div(z3r);
Enter fullscreen mode Exit fullscreen mode

assertAlmostReduced()는 입력값을 여러 개 받을 수 있지만, 3의 배수로 사용하는 것이 가장 효율적입니다. 예를 들어:

  • 1개의 입력: 4.5 제약
  • 2개의 입력: 5 제약
  • 3개의 입력: 5.5 제약

sum()​

또 다른 제약을 절약할 기회는 많은 덧셈이나 뺄셈을 연속으로 수행할 때 발생합니다. x.add(y).sub(z)와 같은 방식 대신, ForeignField.sum()을 사용하세요:

// u = x + y - z
let u = Field17.sum([x, y, z], [1, -1]);
Enter fullscreen mode Exit fullscreen mode

두 번째 인자는 부호 목록으로, 각 값에 대해 더하거나 빼려는지에 따라 1 또는 -1을 입력합니다. 예제에서 1은 "x와 y를 더한다"는 의미이고, -1은 "z를 뺀다"는 의미입니다.

몇 가지 추가 예제는 다음과 같습니다:

// u = x - y - z
let u = Field17.sum([x, y, z], [-1, -1]);

// u = 2*x + y
let u = Field17.sum([x, x, y], [1, 1]);

// u = -3*z
let u = Field17.sum([0, z, z, z], [-1, -1, -1]);

Enter fullscreen mode Exit fullscreen mode

작은 곱셈(예: -3*z)을 이렇게 수행하는 것이 mul()을 사용하는 것보다 더 효율적입니다. sum()은 처음 두 항에 대해 6 제약을 사용하지만, 추가 항목당 1 제약만 사용합니다.

머클 트리 (Merkle Tree)

zkApp 계정은 체인에 저장할 수 있는 데이터 양이 제한되어 Mina의 체인이 간결함을 유지하고 과도하게 비대해지지 않도록 설계되었습니다. 하지만 일부 zkApp은 zkApp 계정에 온체인으로 저장할 수 있는 양을 초과하는 데이터를 액세스해야 할 수도 있습니다.

오프체인 데이터 참조​

이를 어떻게 달성할 수 있을까요? 정답은 머클 트리입니다!
머클 트리(또는 Verkle 트리와 같은 유사 구조)는 온체인에 단일 해시만 저장하여 오프체인 데이터를 참조할 수 있도록 합니다.

어떻게 작동하나요?​

머클 트리는 특수한 이진 트리로, 모든 리프 노드(트리 맨 아래의 노드)는 기본 데이터의 암호화 해시로 표시되며, 내부 노드는 자식 노드의 레이블(해시)을 연결하여 암호화 해시로 표시됩니다.

이 알고리즘을 트리의 맨 위까지 따라가면, 트리의 루트 해시를 저장하는 하나의 단일 노드(루트 노드)가 생성됩니다. 루트 해시는 트리의 리프에 포함된 모든 데이터를 참조하므로, 작은 해시 하나로 대량의 데이터를 참조할 수 있습니다.

머클 트리의 또 다른 이점은 증인(Witness)입니다. 이는 머클 증명(Merkle Proof) 또는 머클 경로(Merkle Path)로도 알려져 있습니다. 증인은 특정 리프 노드에서 트리의 맨 위(루트)까지의 경로입니다. 머클 증명은 특정 데이터(예: 원장 계정 또는 리더보드의 점수)가 트리 전체에 포함되어 있음을 증명합니다.

zkApp에서 머클 트리의 활용​

머클 트리를 사용하면 대량의 오프체인 데이터를 참조하고, 작은 해시(루트)와 증인만으로 특정 데이터의 포함 여부를 증명할 수 있습니다.

Mina의 zkApp에서 머클 트리를 사용하고 오프체인 데이터를 참조하려면 트리의 루트를 온체인에 저장하세요. 그러면 이제 오프체인 데이터에 접근할 수 있습니다.

예를 들어, 게임과 리더보드를 관리하는 zkApp을 상상해 보세요. 이 zkApp에는 플레이어가 숫자를 맞히면 점수를 업데이트하는 메서드가 있습니다. 플레이어가 특정 점수에 도달하면, 다른 메서드를 호출하여 보상을 받을 수 있습니다.
많은 플레이어가 게임에 참여하도록 하려면 온체인에 저장할 수 있는 데이터 양이 크게 제한됩니다. 8명 이상의 참가자가 있으면 온체인 공간이 빠르게 소진될 것입니다.

이 문제를 해결하는 한 가지 가능한 방법은 머클 트리(Merkle Tree)의 강력한 기능을 사용하는 것입니다. 각 플레이어의 공개 키와 해당 점수를 오프체인에 저장하고, 스마트 컨트랙트에서 이 키를 참조하는 것입니다.

먼저 데이터 구조를 살펴봅니다. 예를 들어, 플레이어의 ID를 점수와 매핑하려면 다음과 같은 방식으로 처리합니다:

0: 5 points
1: 3 points
2: 0 points
3: 8 points
... : ...
7: 2 points
Enter fullscreen mode Exit fullscreen mode

스마트 컨트랙트 구현​

이제 리더보드 zkApp이 어떻게 작동할지 살펴볼 차례입니다. 오프체인 머클 트리를 가리키는 온체인 상태를 유지하려면 이 변수를 root라고 부릅니다.

info
Sometimes the variable root is called commitment, because it commits to something.
Enter fullscreen mode Exit fullscreen mode

또한, 플레이어가 추측해야 할 값을 해시한 결과 z를 저장하는 변수를 추가합니다:H(guess) = z

info
Guessing a simple hash like this example can easily be brute forced, especially if the preimage is simple (like a 5-letter word or a small number with only a few digits).
Ensure that your zkApps are always secure, especially when dealing with funds.
Enter fullscreen mode Exit fullscreen mode

첫 번째 메서드는 플레이어가 추측을 제출할 수 있도록 합니다. 추측이 맞으면 플레이어는 한 점을 얻게 됩니다. 이 메서드는 플레이어의 추측 값을 해시한 다음, 해시 값 H(guess)가 온체인 상태 z와 일치하는지 확인합니다. 일치할 경우, 플레이어는 리더보드에서 한 점을 얻습니다.

두 번째 메서드는 보상을 처리합니다. 이 메서드는 플레이어의 점수가 특정 임계값을 초과했는지 확인하고, 조건을 충족하면 보상을 지급합니다. 또한, 이 메서드는 머클 증인(Merkle Witness)을 검증하고, 온체인에 저장된 머클 루트와 일치하는지 확인해야 합니다.

note
The examples folder in the o1js repository includes a working Merkle tree example with all of the required boilerplate code.
Enter fullscreen mode Exit fullscreen mode
class Leaderboard extends SmartContract {
 // the root is the root hash of our off-chain Merkle tree
 @state(Field) root = State<Field>();

 // z is the hashed number we want to guess!
 @state(Field) z = State<Field>();

 init() {
   super.init();

   // this is our hash we want to guess! its the hash of the preimage "22", but keep it a secret!
   this.z.set(
     Field(
       '17057234437185175411792943285768571642343179330449434169483610110583519635705'
     )
   );
 }

 @method async guessPreimage(guess: Field, account: Account, path: MerkleWitness) {
   // we fetch z from the chain
   const z = this.z.get();
   this.z.requireEquals(z);

   // if our guess preimage hashes to our target, we won a point!
   Poseidon.hash([guess]).assertEquals(z);

   // we fetch the on-chain commitment/root
   const root = this.root.get();
   this.root.requireEquals(root);

   // we check that the account is within the committed Merkle Tree
   path.calculateRoot(account.hash()).assertEquals(root);

   // we update the account and grant one point!
   let newAccount = account.addPoints(1);

   // we calculate the new Merkle Root, based on the account changes
   const newRoot = path.calculateRoot(newAccount.hash());

   this.root.set(newRoot);
 }

 @method async claimReward(account: Account, path: MerkleWitness) {
   // we fetch the on-chain commitment
   const root = this.root.get();
   this.root.requireEquals(root);

   // we check that the account is within the committed Merkle Tree
   path.calculateRoot(account.hash()).assertEquals(root);

   // we check that the account has at least 10 score points in order to claim the reward
   account.score.assertGte(UInt32.from(10));

   // finally, we send the player a reward
   this.send({
     to: account.address,
     amount: 100_000_000,
   });
 }
}

Enter fullscreen mode Exit fullscreen mode

머클 트리를 사용하면 몇 줄의 코드만 추가하여 오프체인 데이터를 쉽게 참조할 수 있습니다. 하지만 zkApp의 개발자로서, 온체인에서 참조하는 머클 트리가 실제 오프체인 데이터 구조와 항상 동기화되도록 보장해야 합니다.
머클 트리의 활용 방법을 더 잘 이해하려면 o1js 저장소에 있는 머클 트리 예제를 참조하세요.

info
Merkle trees are great for referencing off-chain state, but you must also store this off-chain state somewhere.
Where and how to store the data off-chain storage is left up to you, the developer. Tell us how you are using Merkle trees in the #zkapps-developers channel in Mina Protocol Discord.
Enter fullscreen mode Exit fullscreen mode

Merkle Tree - API 참조​

const treeHeight = 8;

// creates a tree of height 8
const Tree = new MerkleTree(treeHeight);

// creates the corresponding MerkleWitness class that is circuit-compatible
class MyMerkleWitness extends MerkleWitness(treeHeight) {}

// sets a value at position 0n
Tree.setLeaf(0n, Field(123));

// gets the current root of the tree
const root = Tree.getRoot();

// gets a plain witness for leaf at index 0n
const witness = Tree.getWitness(0n);

// creates a circuit-compatible witness
const circuitWitness = new MyMerkleWitness(witness);

// calculates the root of the witness
const calculatedRoot = circuitWitness.calculateRoot(Field(123));

calculatedRoot.assertEquals(root);

Enter fullscreen mode Exit fullscreen mode

Keccak (SHA-3)

Keccak은 기존의 SHA 해시 알고리즘보다 더 높은 보안을 제공하는 유연한 암호화 해시 함수입니다.

Keccak이란?​

Keccak은 벨기에의 암호학자 팀이 개발한 함수로, NIST SHA-3 경쟁에서 우승하여 표준화된 SHA-3으로 채택되었습니다. Keccak은 다양한 응용 프로그램에서 사용되며, 특히 Ethereum 생태계에서 중요한 역할을 합니다.

Ethereum에서는 주소, 트랜잭션, 블록을 해싱하는 데 Keccak이 사용됩니다. 또한, Ethereum 블록체인의 상태를 저장하는 데이터 구조인 상태 트리(state trie)를 해싱하는 데도 사용됩니다.
Ethereum에서 Keccak이 널리 사용되기 때문에, 이는 o1js에서 Ethereum 트랜잭션과 블록을 검증하는 핵심 구성 요소입니다.

Keccak과 Poseidon​

o1js 개발자로서, Poseidon이라는 영지식(zero knowledge) 네이티브 해시 함수에 익숙할 가능성이 높습니다. Poseidon은 Pallas 기본 필드에서 작동하며, Mina에 특화된 파라미터를 사용하여 o1js에서 가장 효율적인 해시 함수로 자리 잡았습니다.

반면, Keccak은 이진 산술을 요구하는 해시 함수입니다. 이진 데이터를 처리하며, 대부분의 영지식 증명에는 네이티브가 아닙니다. 이로 인해 Keccak은 Poseidon만큼 효율적이지는 않지만, 여전히 Ethereum 트랜잭션 및 블록을 검증하는 데 매우 유용합니다.
어떤 해시 함수를 사용할지 결정할 때는 사용 사례와 해싱해야 할 데이터가 중요한 고려 사항입니다.

기본 사용법​

o1js의 Hash 네임스페이스에는 다음과 같은 Keccak 구성이 포함되어 있습니다:

  • Hash.SHA3_256: 출력 크기가 256비트인 NIST SHA3 해시 함수.
  • Hash.SHA3_384: 출력 크기가 384비트인 NIST SHA3 해시 함수.
  • Hash.SHA3_512: 출력 크기가 512비트인 NIST SHA3 해시 함수.
  • Hash.Keccak256: 출력 크기가 256비트인 NIST 이전 Keccak 해시 함수. Ethereum에서 블록, 트랜잭션 등을 해싱하는 데 사용됩니다.
  • Hash.Keccak384: 출력 크기가 384비트인 NIST 이전 Keccak 해시 함수.
  • Hash.Keccak512: 출력 크기가 512비트인 NIST 이전 Keccak 해시 함수. Keccak은 Poseidon처럼 네이티브 필드 요소가 아니라 이진 데이터 위에서 작동하므로, o1js에서는 Bytes 타입을 사용합니다. Bytes는 고정 길이의 바이트 배열로, 이진 데이터를 표현하는 데 사용됩니다. 내부적으로 Bytes는 UInt8 요소의 배열로 나타납니다.

Bytes를 사용하려면 Bytes 클래스를 확장하고 바이트 길이를 지정해야 합니다.

// This creates a Bytes class that represents 16 bytes
class Bytes16 extends Bytes(16) {}
Enter fullscreen mode Exit fullscreen mode

To initiate your Bytes16 class with a value, you can use from, fromHex, or fromString.

// `.from` accepts an  array of `number`, `bigint`, `UInt8` elements or a `Uint8Array` or `Bytes`
let bytes = Bytes16.from(new Array(16).fill(0));

// converts a hex string to bytes
bytes = Bytes16.fromHex('646f67');

// converts a string to bytes
bytes = Bytes16.fromString('dog');

// print the contents of `bytes` to the console
bytes.bytes.forEach((b) => console.log(b.toBigInt()));
// [100n, 111n, 103n, 0n, 0n, 0n, 0n, 0n, 0n, 0n, 0n, 0n, 0n, 0n, 0n, 0n]

Enter fullscreen mode Exit fullscreen mode
note
If the input array is smaller than the length of the Bytes class, it will be padded with zeros.
Enter fullscreen mode Exit fullscreen mode

Bytes 객체를 다시 16진수 문자열로 변환하려면 toHex메서드를 사용할 수 있습니다

class Bytes3 extends Bytes(3) {}

let bytes = Bytes3.fromHex('646f67');
let hex = bytes.toHex();
console.log('hex', hex);
// 646f67

Enter fullscreen mode Exit fullscreen mode

Bytes를 입력값으로 사용하는 방법을 이해했으니, 이를 활용해 Keccak으로 데이터를 해싱할 수 있습니다.

// define a preimage
let preimage = 'The quick brown fox jumps over the lazy dog';

// create a Bytes class that represents 43 bytes
class Bytes43 extends Bytes(43) {}

// convert the preimage to bytes
let preimageBytes = Bytes43.fromString(preimage);

// hash the preimage
let hash = Hash.SHA3_256.hash(preimageBytes);

console.log(hash.toHex());
//69070dda01975c8c120c3aada1b282394e7f032fa9cf32f4cb2259a0897dfc04
Enter fullscreen mode Exit fullscreen mode

SHA-256 및 Keccak을 사용하는 해싱 예제는 o1js 리포지토리에서 확인할 수 있습니다.

Bytes - API reference​

// creates a Bytes class that represents n bytes (n is 32 in this example)
let n = 32;
class BytesN extends Bytes(n) {}

// initiate an instance from a hex string
let bytes = BytesN.fromHex('646f67');

// initiate an instance from a string
bytes = BytesN.fromString('dog');

// initiate an instance from an array of numbers
bytes = BytesN.from([100, 111, 103]);

// initiate an instance from an array of bigints
bytes = BytesN.from([100n, 111n, 103n]);

// initiate an instance from an array of UInt8 elements
bytes = BytesN.from([UInt8(100), UInt8(111), UInt8(103)]);

// initiate an instance from a Uint8Array
bytes = BytesN.from(new Uint8Array([100, 111, 103]));

// initiate an instance from another Bytes instance
bytes = BytesN.from(BytesN.fromHex('646f67'));

// convert the bytes to a hex string
const hex = bytes.toHex();
Enter fullscreen mode Exit fullscreen mode

Keccak - API reference​

// https://keccak.team/keccak.html hash function, pre-NIST specification
// hash bytes using Keccak256 with output size of 256 bits, mainly used in Ethereum
Hash.Keccak256.hash(bytes);

// hash bytes using Keccak384 with output size of 384 bits
Hash.Keccak384.hash(bytes);

// hash bytes using Keccak512 with output size of 512 bits
Keccak.Keccak512.hash(bytes);

// https://csrc.nist.gov/pubs/fips/202/final hash function, official NIST specification
// hash bytes using SHA3_256 with output size of 256 bits
Hash.SHA3_256.hash(bytes);

// hash bytes using SHA3_384 with output size of 384 bits
Hash.SHA3_384.hash(bytes);

// hash bytes using SHA3_512 with output size of 512 bits
Hash.SHA3_512.hash(bytes);

Enter fullscreen mode Exit fullscreen mode

ECDSA (타원곡선 디지털 서명 알고리즘)

ECDSA는 메시지를 서명하고 검증하는 데 사용되는 암호화 알고리즘입니다. Ethereum을 포함한 여러 블록체인에서 트랜잭션 서명에 사용되며, 다양한 타원곡선과 함께 작동합니다. Bitcoin과 Ethereum은 모두 secp256k1 곡선을 사용합니다.

ECDSA의 필요성​

o1js는 다른 블록체인과 상호 작용하거나 외부 세계의 데이터를 검증하려면 서명을 검증할 수 있어야 합니다. ECDSA는 널리 사용되는 알고리즘으로, 많은 라이브러리와 도구에서 지원됩니다.
예를 들어, Ethereum 트랜잭션은 secp256k1 곡선을 사용하는 ECDSA로 서명됩니다. zkApp 개발자로서 Ethereum 트랜잭션을 검증하고 이에 대한 진술을 하려면, 트랜잭션 서명을 검증할 수 있어야 합니다. 이는 zkApp에서 ECDSA가 중요한 이유입니다.

기본 사용법​

ECDSA 가젯은 ECDSA 서명을 검증하는 데 사용됩니다. 이 가젯은 메시지, 서명, 서명자의 공개 키를 입력으로 받고, 서명이 유효한지 여부를 나타내는 Bool 값을 출력합니다.

서명을 검증하기 전에 곡선 구성을 통해 가젯을 초기화해야 합니다.

// create a secp256k1 curve
class Secp256k1 extends createForeignCurve(Crypto.CurveParams.Secp256k1) {}

Enter fullscreen mode Exit fullscreen mode

기본적으로 o1js는 사전 정의된 곡선 세트를 내보냅니다. createForeignCurve 함수로 CurveParams 객체에서 곡선을 생성할 수 있습니다. CurveParams 객체에는 다음과 같은 곡선의 매개변수가 포함됩니다:

모듈러스
생성기
곡선 방정식 𝑦2=𝑥3+𝑎𝑥+𝑏y 2=x 3 +ax+b의 매개변수 a와 b
Crypto.CurveParams 네임스페이스는 Pallas, Vesta, Secp256k1과 같은 사전 정의된 곡선을 제공합니다.

// predefined curve parameters
CurveParams: {
 Secp256k1: CurveParams;
 Pallas: CurveParams;
 Vesta: CurveParams;
}

Enter fullscreen mode Exit fullscreen mode

Ethereum에서 사용되는 secp256k1 곡선을 사용하는 예제를 살펴보겠습니다. 곡선을 생성한 후, ECDSA 가젯 인스턴스를 생성할 수 있습니다.

// create an instance of ECDSA over secp256k1, previously specified
class Ecdsa extends createEcdsa(Secp256k1) {}
Enter fullscreen mode Exit fullscreen mode

서명을 검증하기 전에 메시지를 서명해야 합니다. 메시지는 Bytes 타입이어야 하며, 자세한 내용은 Bytes - API Reference를 참조하세요. 메시지를 서명하려면 Ecdsa 클래스의 sign 함수를 사용하세요. 단, 서명은 증명 가능한(provable) 연산이 아니며, 검증만 가능합니다.

// a private key is a random scalar of secp256k1
let privateKey = Secp256k1.Scalar.random();
let publicKey = Secp256k1.generator.scale(privateKey);

// create a message, for a detailed explanation of `Bytes` take a look at the Keccak overview
let message = Bytes32.fromString('cat');

// sign a message - this is not a provable method!
let signature = Ecdsa.sign(message.toBytes(), privateKey.toBigInt());
Enter fullscreen mode Exit fullscreen mode

마지막으로 verify 메서드를 사용해 서명을 검증할 수 있습니다.

// verify the signature, returns a Bool indicating whether the signature is valid or not
let isValid: Bool = signature.verify(message, publicKey);
Enter fullscreen mode Exit fullscreen mode

ECDSA 사용 예제는 o1js 리포지토리에서 확인하세요.

ECDSA - API reference​

// create a secp256k1 curve from a set of predefined parameters
class Secp256k1 extends createForeignCurve(Crypto.CurveParams.Secp256k1) {}

// create an instance of ECDSA over secp256k1
class Ecdsa extends createEcdsa(Secp256k1) {}

// a private key is a random scalar of secp256k1 - not provable!
let privateKey = Secp256k1.Scalar.random();

// a public key is a point on the curve
let publicKey = Secp256k1.generator.scale(privateKey);

// sign an array of bytes - not provable!
let signature = Ecdsa.sign(bytes, privateKey.toBigInt());

// sign a hash of a message - not provable!
let signature = Ecdsa.signHash(hash, privateKey.toBigInt());

// verify a signature
let isValid: Bool = signature.verify(message, publicKey);

// verify a hash of a message
let isValid: Bool = signature.verifyHash(hash, publicKey);

// create a signature from a hex string
let signature = Ecdsa.fromHex('6f6d6e69627573206f6e206120636174...');

// create a signature from s and r, which can be of type `AlmostForeignField`, `Field3`, `bigint` or `number`
let signature = Ecdsa.fromScalars({ r, s });

// convert a signature into a r and s of type bigint
let { r, s } = signature.toBigInt();
Enter fullscreen mode Exit fullscreen mode

SHA-256

SHA-2는 미국 국가안보국(NSA)이 설계한 암호학적 해시 함수 세트로, SHA-1의 개선된 버전입니다. 보안이 강화되었으며, SHA-256 및 SHA-512와 같이 해시 크기에 따라 다양한 변형으로 제공됩니다. Keccak(SHA-3)의 전신으로도 알려져 있습니다. SHA-2 패밀리는 256비트(SHA-256) 또는 512비트(SHA-512)와 같은 다양한 출력 길이로 제공됩니다. o1js는 256비트 출력 길이의 SHA-2만 지원합니다.

What is SHA-2 and SHA-256?​

SHA-256은 SHA-2 패밀리의 일부로, 256비트(32바이트) 해시 출력을 생성하는 암호학적 해시 함수입니다. 이는 전통적인 Web2 응용 프로그램 및 프로토콜뿐만 아니라 블록체인 기술에서도 널리 사용됩니다. 예를 들어, Bitcoin의 블록 헤더는 SHA-256을 사용해 두 번 해싱됩니다.

SHA-256 and Poseidon​

o1js 개발자로서, Poseidon이라는 영지식(zero-knowledge) 네이티브 해시 함수에 익숙할 것입니다. Poseidon은 Mina를 위해 특별히 생성된 매개변수를 사용하며, 네이티브 Pallas 기본 필드 위에서 작동합니다. 이로 인해 Poseidon은 o1js에서 사용할 수 있는 가장 효율적인 해시 함수로 평가됩니다.

반면에, SHA-2는 이진 산술(binary arithmetic)을 요구하는 해시 함수입니다. 이는 이진 데이터 위에서 작동하며 대부분의 영지식 증명에 네이티브하지 않습니다. 이러한 이유로, SHA-256은 Poseidon만큼 효율적이지 않지만, 여전히 Ethereum 트랜잭션 및 블록을 검증하는 데 매우 유용합니다. 어떤 해시 함수를 사용할지 선택할 때는 사용 사례와 해싱해야 할 데이터 유형이 중요한 고려 사항입니다.

기본 사용법​

SHA-256은 o1js의 Hash 네임스페이스에서 다음과 같은 구성으로 제공됩니다:

Hash.SHA2_256: 출력 크기가 256비트인 SHA2-256 해시 함수.
SHA-256은 Poseidon처럼 네이티브 필드 요소가 아닌 이진 데이터 위에서 작동하므로 o1js는 Bytes 타입을 사용합니다. Bytes는 고정 길이의 바이트 배열로, 이진 데이터를 나타내는 데 사용됩니다. 내부적으로 Bytes는 UInt8 요소의 배열로 표현됩니다.

Bytes를 사용하려면 Bytes 클래스를 확장하고 바이트 길이를 지정해야 합니다. 자세한 설명은 Keccak이 Bytes를 활용하는 방법에서 확인하세요.

// define a preimage
let preimage = 'The quick brown fox jumps over the lazy dog';

// create a Bytes class that represents 43 bytes
class Bytes43 extends Bytes(43) {}

// convert the preimage to bytes
let preimageBytes = Bytes43.fromString(preimage);

// hash the preimage
let hash = Hash.SHA2_256.hash(preimageBytes);

console.log(hash.toHex());
//d7a8fbb307d7809469ca9abcb0082e4f8d5651e46d3cdb762d02d0bf37c9e592

Enter fullscreen mode Exit fullscreen mode

SHA-256 및 Keccak을 사용하는 해싱 예제는 o1js 리포지토리에서 확인할 수 있습니다.

SHA-256 - API reference​

// hash bytes using SHA256 with output size of 256 bits
Hash.SHA2_256.hash(bytes);

Enter fullscreen mode Exit fullscreen mode

Top comments (0)