
Elusiv와 Light로 구현하는 Solana 프라이버시: SOL 프라이버시의 모든 것
사전 요구 사항
- Node.js 설치(필수 종속성을 설치하고 스크립트를 실행하며 $SOL을 비공개로 전송하는 데 필요)
- TypeScript에 대한 기본적인 이해
- @solana/web3.js에 대한 기본적인 이해
이 글에서 다루는 내용
프라이버시는 현대 블록체인 기술의 핵심입니다. 공개 원장 시스템에서는 트랜잭션, 계정 잔액, 때로는 프로그램의 소스 코드까지 누구나 볼 수 있도록 공개됩니다. 이처럼 전례 없는 투명성은 블록체인 기술의 대표적인 특징으로 여겨지지만, 많은 애플리케이션과 일상적인 사용 사례에서는 프라이버시가 필수 요건일 수 있습니다.
Elusiv와 Light Protocol은 비공개 트랜잭션, 비공개 프로그램, 탈중앙화 규정 준수 솔루션에 zk-SNARKS를 활용하여 Solana 프라이버시를 위한 최첨단 솔루션을 제공합니다. 이 글에서는 프라이버시의 중요성과 Solana 같은 블록체인에 프라이버시 기능이 필요한 이유를 살펴봅니다. 또한 Elusiv의 TypeScript SDK와 Light의 zk.js 모듈을 사용해 SOL을 비공개로 전송하는 방법을 단계별로 안내합니다.
프라이버시는 중요합니다
프라이버시는 자율성과 인간 존엄성을 지키는 데 필수적인 기본 인권입니다. 표현의 자유와 결사의 자유 같은 다른 권리를 행사하는 기반이므로 자유롭고 개방된 사회에 꼭 필요합니다. 프라이버시는 정치적 삶과 분리된 공간을 만들며, 누구나 제3자의 지속적인 감시 없이 생각하고 말하며 의견을 표현하고 행동할 수 있게 합니다. 에드워드 스노든의 폭로, WikiLeaks가 공개한 정보, Facebook-Cambridge Analytica 데이터 스캔들과 같은 최근 논란은 프라이버시의 중요성과 경계를 둘러싼 치열한 논쟁을 촉발했습니다.
누구나 프라이버시에 관심을 가져야 합니다
투명성을 핵심 원칙으로 내세우는 탈중앙화 기술 위에 구축된 공개 원장에 프라이버시 기능이 필요하다고 주장하는 것은 언뜻 모순처럼 보일 수 있습니다. 확고한 사이퍼펑크든 일반 사용자든 모두가 프라이버시에 관심을 가져야 합니다. 온라인에서 유해하거나 불법적인 일을 하지 않으니 블록체인에서 내 활동을 누가 보든 무슨 상관이냐고 생각할 수 있습니다. 다음 사고 실험을 살펴보세요.
두 인물이 있습니다. Alice는 프라이버시를 중요하게 여기며 감시받지 않고 물건을 구매하고 싶은 일반 사용자입니다. 다른 한 명은 악의적 행위자로, 여기서는 MA라고 줄여 부르겠습니다. 악의적 행위자는 억압적인 정부, 통제적인 고용주, 사이버 범죄자일 수 있습니다. Alice가 정부가 모든 시민을 상시 감시하는 국가에 살거나, 특정한 합법적 구매(예: 정치 후원, 특정 주제의 도서 구매)를 엄격히 금지하는 회사에 다닌다고 가정해 보겠습니다. 어느 날 Alice는 Solana와 같은 전통적인 블록체인이 안전하고 비공개인 결제 수단이라고 믿고 이를 사용해 물건을 구매합니다. 이후 다음과 같은 일이 벌어집니다.
- Alice의 트랜잭션이 공개 원장에 기록됩니다. 트랜잭션에는 Alice의 공개 키, 수신자의 공개 키, 전송 금액이 포함됩니다
- Alice의 행동을 감시하거나 통제하려는 MA가 Alice의 공개 키를 사용해 트랜잭션을 추적하기 시작합니다
- MA는 Alice의 구매 내역을 발견하고 자신들의 규정에 따라 이를 "부적절한" 것으로 판단합니다
- MA는 Alice에게 조치를 취합니다. 상황에 따라 법적 조치, 투옥, 직장 내 제재, 공개적인 망신, 협박 등으로 이어질 수 있습니다
이 사고 실험은 블록체인의 프라이버시 부족이 일반 사용자에게 미칠 수 있는 냉혹한 현실을 보여줍니다. 온체인 추적자와 악의적인 데이터 수집 기술이 늘어나면서 일반 사용자의 트랜잭션을 추적하기가 매우 쉬워졌고, 이는 현실에서 심각한 결과로 이어질 수 있습니다. 디지털화가 심화되는 세상에서 프라이버시가 없다면 개인의 주권은 약화되고, 모든 결정이 감시되며, 존재의 모든 측면이 상품화될 수 있습니다. 프라이버시 기술을 도입하지 않으면 블록체인은 이러한 현상을 가속할 뿐입니다. 바로 이 지점에서 Elusiv와 Light Protocol이 필요합니다.
Elusiv란 무엇인가요?
Elusiv는 스스로를 "사용자와 애플리케이션이 범용 암호화를 이용할 수 있게 하는" "블록체인 기반 영지식 프라이버시 프로토콜"이라고 설명합니다. Elusiv는 사용자와 네트워크에 영지식 암호화와 다자간 연산을 적용하여 보안, 안전성, 탈중앙화를 희생하지 않으면서 프라이버시를 구현하고자 합니다. 범용 암호화라는 목표를 바탕으로 사용자가 자신의 데이터를 직접 제어할 수 있는 도구를 제공합니다. 이제 사용자는 블록체인에서 어떤 정보를 공유하고 어떤 정보를 공유하지 않을지 선택할 수 있습니다.
Elusiv는 어떻게 작동하나요?
Elusiv에서 사용자는 Elusiv 프로그램이 관리하는 공동 풀로 자금을 보내 비공개 잔액을 충전합니다. 사용자는 이 풀에서 SOL 같은 토큰을 주고받거나 자금을 인출할 수 있습니다. 이 공동 풀 모델에서도 Elusiv로의 입금과 Elusiv에서 나가는 결제는 관찰할 수 있다는 점에 유의해야 합니다. 하지만 사용자와 트랜잭션이 늘어날수록 시간이 지나면서 특정 계정의 입금과 결제를 연결하기가 점점 어려워집니다. 10 SOL을 넣고 10 SOL을 빼는 단 한 명의 사용자를 연결하는 것은 쉽지만, 수백만 명 중에서 소액으로 여러 번 입금하고 결제하는 사용자를 연결하기는 훨씬 어렵습니다. 익명성은 참여자가 많을수록 강해집니다.
풀의 자금에는 입금자에 관한 정보가 포함되지 않습니다. 하지만 영지식 증명을 사용하여 비밀 값을 사용자의 자금과 연결하므로 미사용 자금에 대한 권한을 증명할 수 있습니다. Elusiv에서 사용하는 영지식 증명은 zk-SNARKS(Zero-Knowledge Succinct Non-Interactive Argument of Knowledge)라고 합니다. zk-SNARK 증명의 복잡한 원리를 자세히 다루지는 않겠습니다. 친구에게 비밀 암호가 무엇인지 절대 알려주지 않으면서도 자신이 그 암호를 안다는 사실을 수학적으로 증명하는 방법이라고 생각하면 됩니다. 친구는 추가 질문 없이도 여러분이 진실을 말하는지 빠르게 확인할 수 있습니다. 따라서 Elusiv는 풀에 보유한 금액을 Solana 전체에 공개하지 않고도 미사용 자금을 추적할 수 있습니다. 영지식 증명을 더 자세히 알아보고 싶다면 Porter의 영지식 증명 강의를 확인해 보세요.
Light Protocol이란 무엇인가요?
Light Protocol은 스스로를 Solana에서 직접 비공개 프로그램 실행을 지원하는 "Solana의 차세대 zkLayer"라고 설명합니다. Light가 Solana에서 프라이버시를 구현하는 데 필요한 핵심 개념은 세 가지입니다.
- 비공개 상태
- UTXO 모델
- 온체인에서 비공개 상태 전환 검증
Light Protocol은 어떻게 작동하나요?
Light Protocol을 사용하면 온체인 상태를 암호화할 수 있습니다. 사용자는 이 상태를 "소유"하며 독점적인 복호화 권한을 갖습니다. Elusiv의 공동 자금 풀 방식과 달리 Light Protocol은 Solana에 UTXO(Unspent Transaction Outputs) 모델을 도입합니다. UTXO는 실드 키 쌍에 연결되며 두 가지 잔액을 보유할 수 있습니다. 하나는 Light Protocol의 수수료 토큰인 SOL이고, 다른 하나는 별도의 SPL 토큰입니다. 이 방식에서는 각 트랜잭션이 선형 목록에 추가되는 새로운 UTXO를 생성하므로 기존 데이터를 직접 업데이트하지 않습니다. 어떤 상태를 누가 업데이트하는지에 관한 정보가 노출되지 않고 상태가 숨겨집니다.
Solana의 프로그램 로직은 거의 항상 고유한 제약 조건이 있는 에스크로 형태입니다. Light Protocol은 클라이언트 측에서 프로그램 로직을 영지식 회로(zk-SNARKS)로 인코딩해 연산과 증명을 생성하는 방식으로 이 설계를 혁신합니다. 이 증명은 온체인에서 공유되며, 해당 회로에 맞게 제작된 전용 검증 프로그램이 유효성을 확인합니다. 검증에 성공하면 사용자 지정 검증인 Program이 여러 보안 검사를 완료하고 새로운 커밋을 Light의 Merkle 트리 프로그램에 추가하는 또 다른 네이티브 검증인을 호출합니다.
이 글의 범위를 벗어나지만 Light를 사용하면 자체 비공개 Solana Program(PSP)을 처음부터 만들 수 있습니다. 직접 만들기 전에 이 글의 나머지 부분을 읽고 문서를 확인하는 것이 좋습니다. 문서에는 자체 PSP를 만드는 튜토리얼이 있으며 여기에서 확인할 수 있습니다.
흥미로운 내용이지만 이를 Solana 프로그래밍에 직접 적용하려면 어떻게 해야 할까요?
Elusiv를 사용해 SOL을 비공개로 전송하기
전체 코드
import * as ed from "@noble/ed25519";
import { sha512 } from "@noble/hashes/sha512";
ed.etc.sha512Sync = (...m) => sha512(ed.etc.concatBytes(...m));
import {
Connection,
PublicKey,
Keypair,
ConfirmedSignatureInfo,
Cluster,
LAMPORTS_PER_SOL,
clusterApiUrl,
} from "@solana/web3.js";
import { Elusiv, TokenType, SEED_MESSAGE } from "@elusiv/sdk";
const CLUSTER: Cluster = "devnet";
const RPC = "https://rpc-devnet.helius.xyz";
const main = async () => {
const connection: Connection = new Connection(RPC);
const keyPair: Keypair = generateKeypair();
const recipientPublicKey: PublicKey = new PublicKey(generateKeypair().publicKey);
await airdropSol(keyPair);
const seed: Uint8Array = ed.sign(Buffer.from(SEED_MESSAGE, "utf-8"), keyPair.secretKey.slice(0, 32));
const elusiv: Elusiv = await Elusiv.getElusivInstance(seed, keyPair.publicKey, connection, CLUSTER);
const balance: bigint = await elusiv.getLatestPrivateBalance("LAMPORTS");
try {
if (balance > BigInt(0)) {
const signature = await send(elusiv, recipientPublicKey, 1 * LAMPORTS_PER_SOL, "LAMPORTS");
console.log(`Sent with signature ${signature.signature}`);
} else {
console.log("Private balance is empty. Topping up...");
const amount = 1 * LAMPORTS_PER_SOL;
const tokenType = "LAMPORTS";
const topUpTxData = await elusiv.buildTopUpTx(amount, tokenType);
topUpTxData.tx.partialSign(keyPair);
const topUpSig: ConfirmedSignatureInfo = await elusiv.sendElusivTx(topUpTxData);
console.log(`Top-up complete with signature ${topUpSig.signature}`);
const signature: ConfirmedSignatureInfo = await send(elusiv, recipientPublicKey, 1 * LAMPORTS_PER_SOL, "LAMPORTS");
console.log(`Sent with signature ${signature.signature}`);
}
} catch (e) {
console.error(`Error sending SOL via Elusiv: ${e}`);
} finally {
console.log("Exiting program...");
process.exit(0);
}
};
const send = async (
elusiv: Elusiv,
recipient: PublicKey,
amount: number,
tokenType: TokenType
): Promise => {
const txt = await elusiv.buildSendTx(amount, recipient, tokenType);
return elusiv.sendElusivTx(txt);
};
const generateKeypair = (): Keypair => {
let keyPair = Keypair.generate();
console.log(`Public key: ${keyPair.publicKey.toBase58()}`);
console.log(`Private key: ${keyPair.secretKey}`);
return keyPair;
};
const airdropSol = async (wallet: Keypair) => {
try {
const connection = new Connection(clusterApiUrl("devnet"));
const airdropSignature = await connection.requestAirdrop(new PublicKey(wallet.publicKey), 1 * LAMPORTS_PER_SOL);
const latestBlockHash = await connection.getLatestBlockhash();
await connection.confirmTransaction({
blockhash: latestBlockHash.blockhash,
lastValidBlockHeight: latestBlockHash.lastValidBlockHeight,
signature: airdropSignature,
});
console.log(`Airdropped 1 SOL to ${wallet.publicKey.toBase58()}`);
} catch (e) {
console.error(`Error sending SOL via Elusiv: ${e}`);
}
};
main();Elusiv 코드 분석
import * as ed from "@noble/ed25519";
import { sha512 } from "@noble/hashes/sha512";
ed.etc.sha512Sync = (...m) => sha512(ed.etc.concatBytes(...m));먼저 지갑 연결을 설정하지 않았으므로 noble-ed25519 라이브러리를 가져옵니다. 프로덕션 환경에서는 대신 Solana Wallet Adapter를 사용해 트랜잭션에 서명합니다. 여기서는 sign 기능만 필요하지만, import { sign } from "@noble/ed25519";을 사용하면 etc.sha512Sync이 설정되지 않았다는 오류가 발생할 수 있습니다. 이 오류를 방지하기 위해 직접 설정합니다. 이 부분은 크게 신경 쓰지 않아도 됩니다.
import {
Connection,
PublicKey,
Keypair,
ConfirmedSignatureInfo,
Cluster,
LAMPORTS_PER_SOL,
clusterApiUrl,
} from "@solana/web3.js";
import { Elusiv, TokenType, SEED_MESSAGE } from "@elusiv/sdk";그런 다음 필요한 모듈과 함께 Solana의 web3.js와 Elusiv의 TypeScript SDK를 가져옵니다.
const CLUSTER: Cluster = "devnet";
const RPC = "https://rpc-devnet.helius.xyz";여기서는 CLUSTER을 "devnet"으로 정의하고 RPC를 Helius devnet RPC로 설정합니다. 이 스크립트를 mainnet에서 사용하려면 CLUSTER에 "mainnet"을 할당하고 다음 플랜 중 하나에서 Helius API 키를 받으세요 😎
const main = async () => {
const connection: Connection = new Connection(RPC);
const keyPair: Keypair = generateKeypair();
const recipientPublicKey: PublicKey = new PublicKey(generateKeypair().publicKey);
// Rest of the code
}main 함수 블록 안에서 여러 함수를 await하므로 이 함수를 비동기로 지정합니다. 그런 다음 RPC을 사용해 devnet 연결을 설정하고 유틸리티 함수 generateKeypair으로 Keypair 객체를 생성합니다. 임의의 주소에서 실드된 SOL을 받을 수 있도록 앞서 언급한 generateKeypair 함수를 사용해 recipientPublicKey도 생성합니다. 참고로 generateKeypair 함수는 다음과 같이 정의됩니다.
const generateKeypair = (): Keypair => {
let keyPair = Keypair.generate();
console.log(`Public key: ${keyPair.publicKey.toBase58()}`);
console.log(`Private key: ${keyPair.secretKey}`);
return keyPair;
};이 함수는 이 스크립트에서 사용할 테스트용 Solana 키 쌍을 생성합니다. mainnet에서 사용하려면 이 키 쌍을 자신의 지갑 키 쌍으로 교체하세요. 비공개 키는 절대로 일반 텍스트로 저장하지 말고 올바른 파일 경로를 사용해 적절히 가져와야 합니다.
await airdropSol(keyPair);새로 생성한 테스트 키 쌍을 사용하므로 devnet SOL이 필요합니다. aidropSol 유틸리티 함수를 사용해 새 키 쌍으로 SOL을 에어드롭합니다. 이 함수는 다음과 같이 정의됩니다.
const airdropSol = async (wallet: Keypair) => {
try {
const connection = new Connection(clusterApiUrl("devnet"));
const airdropSignature = await connection.requestAirdrop(new PublicKey(wallet.publicKey), 1 * LAMPORTS_PER_SOL);
const latestBlockHash = await connection.getLatestBlockhash();
await connection.confirmTransaction({
blockhash: latestBlockHash.blockhash,
lastValidBlockHeight: latestBlockHash.lastValidBlockHeight,
signature: airdropSignature,
});
console.log(`Airdropped 1 SOL to ${wallet.publicKey.toBase58()}`);
} catch (e) {
console.error(`Error sending SOL via Elusiv: ${e}`);
}
};여기서는 devnet에서 자신에게 1 SOL(1 * LAMPORTS_PER_SOL)을 에어드롭합니다. latestBlockHash을 가져와 블록 해시와 마지막 유효 블록 높이를 connection.confirmTransaction()에 전달합니다. 이를 통해 올바른 주소로 1 SOL을 에어드롭했는지 확인합니다.
const seed: Uint8Array = ed.sign(Buffer.from(SEED_MESSAGE, "utf-8"), keyPair.secretKey.slice(0, 32));Elusiv는 특정 작업의 결정성을 보장하기 위해 입력 시드를 요구합니다. 따라서 SEED_MESSAGE을 가져와 사용합니다. SEED_MESSAGE은 Elusiv 시드를 생성하는 데 사용되므로 반드시 서명해야 하는 메시지입니다. 애플리케이션은 이 시드를 사용해 관련 비밀 값으로 비공개 자산을 복호화하고 자금을 추적할 수 있습니다. Buffer.from()을 사용해 utf-8 인코딩된 SEED_MESSAGE에서 새 버퍼, 즉 원시 바이너리 데이터 구조를 생성합니다. Solana의 키 쌍 타입에서는 처음 32바이트가 비공개 키이고 마지막 32바이트가 공개 키이므로 slice을 사용해야 합니다. 이렇게 비공개 키로 필수 시드 메시지에 서명합니다.
const elusiv: Elusiv = await Elusiv.getElusivInstance(seed, keyPair.publicKey, connection, CLUSTER);여기서는 seed, keyPair.publicKey, connection, CLUSTER을 사용해 Elusiv 인스턴스인 elusiv을 정의합니다. 즉, devnet RPC, 공개 키, 방금 서명한 긴 메시지를 사용해 devnet에 Elusiv 인스턴스를 생성합니다.
const balance: bigint = await elusiv.getLatestPrivateBalance("LAMPORTS");그런 다음 Elusiv 풀에 보유한 Lamports 금액인 비공개 잔액을 가져옵니다. 이 값은 BigInt로 반환됩니다. main 함수의 나머지 부분은 다음과 같이 정의된 try-catch-finally block 안에 포함됩니다.
try {
if (balance > BigInt(0)) {
const signature = await send(elusiv, recipientPublicKey, 1 * LAMPORTS_PER_SOL, "LAMPORTS");
console.log(`Sent with signature ${signature.signature}`);
} else {
// Rest of code
}
}비공개 잔액이 있으면 balance이 0보다 크므로 if-else 블록의 첫 번째 부분이 실행됩니다. if 조건에서 0을 BigInt으로 캐스팅하는 이유는 getLatestPrivateBalance의 반환 타입이 BigInt이기 때문입니다. 그런 다음 send()의 반환 값으로 설정된 변수 signature을 생성합니다. send() 함수는 다음과 같이 정의됩니다.
const send = async (
elusiv: Elusiv,
recipient: PublicKey,
amount: number,
tokenType: TokenType
): Promise => {
const txt = await elusiv.buildSendTx(amount, recipient, tokenType);
return elusiv.sendElusivTx(txt);
};이 함수는 Elusiv를 통해 트랜잭션을 전송하고 ConfirmedSignatureInfo 타입인 Promise, 즉 상태가 포함된 확인된 서명을 반환합니다. 인수로 Elusiv 인스턴스, 수신자의 공개 키, 전송할 금액, 토큰 타입을 받습니다. 토큰 타입은 Lamports가 아니어도 되며 USDC, USDT, mSOL, BONK, SAMO 등 다양한 토큰을 사용할 수 있습니다. 그런 다음 elusiv.buildSendTx()을 사용해 txt을 설정합니다. buildSendTx()은 Elusiv 인스턴스의 메서드로, 비공개 잔액을 사용해 지정된 수신자에게 토큰을 보내는 트랜잭션을 생성합니다. 마지막으로 return elusiv.sendElusivTx(txt) 문에 생성된 트랜잭션 txt을 전달하여 전송하고 반환합니다.
비공개 잔액이 없으면 else 블록이 실행됩니다.
console.log("Private balance is empty. Topping up...");
const amount = 1 * LAMPORTS_PER_SOL;
const tokenType = "LAMPORTS";
const topUpTxData = await elusiv.buildTopUpTx(amount, tokenType);
topUpTxData.tx.partialSign(keyPair);
const topUpSig: ConfirmedSignatureInfo = await elusiv.sendElusivTx(topUpTxData);
console.log(`Top-up complete with signature ${topUpSig.signature}`);
const signature: ConfirmedSignatureInfo = await send(elusiv, recipientPublicKey, 1 * LAMPORTS_PER_SOL, "LAMPORTS");
console.log(`Sent with signature ${signature.signature}`);비공개 잔액이 없어 충전할 예정이라고 사용자에게 로그로 알립니다. 그런 다음 Elusiv 인스턴스의 .buildTopUpTx() 메서드를 사용해 충전 트랜잭션을 생성합니다. SOL을 전송하므로 1 SOL과 Lamports 토큰 타입을 전달합니다. 공개 키에서 충전하므로 트랜잭션 키 쌍의 .partialSign()을 사용해 충전에 서명합니다. 이어서 elusiv.sendElusivTx(topUpTxData)으로 생성된 트랜잭션을 전송하고 트랜잭션 서명을 로그에 기록합니다.
try-catch-finally 블록의 나머지 부분은 다음과 같습니다.
} catch (e) {
console.error(`Error sending SOL via Elusiv: ${e}`);
} finally {
console.log("Exiting program...");
process.exit(0);
}Elusiv를 통해 SOL을 전송하는 중 오류가 발생하면 console.error()을 사용해 콘솔에 기록합니다. 그런 다음 process.exit(0)으로 프로그램을 종료합니다.
전체 프로세스를 시작하려면 파일 맨 아래에서 main() 함수를 호출합니다.
Light Protocol을 사용해 SOL을 비공개로 전송하기
전체 코드
import { BN } from "@coral-xyz/anchor";
import { PublicKey, Keypair, Connection } from "@solana/web3.js";
import { User, Provider as LightProvider, TestRelayer, confirmConfig, airdropSol } from "@lightprotocol/zk.js";
const initializeSolanaWallet = async (): Promise => {
const wallet = Keypair.generate();
console.log("Wallet initialized");
return wallet;
};
const requestAirdrop = async (connection: Connection, publicKey: PublicKey): Promise => {
await airdropSol({
connection,
lamports: 2e9,
recipientPublicKey: publicKey,
});
console.log("Airdrop requested...");
};
const setupTestRelayer = async (solanaWallet: Keypair): Promise => {
const testRelayer = new TestRelayer({
relayerPubkey: solanaWallet.publicKey,
relayerRecipientSol: solanaWallet.publicKey,
relayerFee: new BN(100_000),
payer: solanaWallet,
});
console.log("Test relayer initialized");
return testRelayer;
};
const initializeLightProvider = async (solanaWallet: Keypair, testRelayer: TestRelayer): Promise => {
const lightProvider = await LightProvider.init({
wallet: solanaWallet,
relayer: testRelayer,
confirmConfig,
});
console.log("Light provider initialized");
return lightProvider;
};
const initializeLightUser = async (lightProvider: LightProvider): Promise => {
const user = await User.init({ provider: lightProvider });
console.log("Light user initialized");
return user;
};
const performShieldOperation = async (user: User) => {
await user.shield({
publicAmountSol: "1.1",
token: "SOL",
});
console.log("Performed shield operation");
};
const executePrivateTransfer = async (user: User, testRecipientPublicKey: string) => {
const response = await user.transfer({
amountSol: "1",
token: "SOL",
recipient: testRecipientPublicKey,
});
console.log(`Executed private transfer! Txt hash: ${response.txHash}`);
};
const main = async () => {
try {
const solanaWallet = await initializeSolanaWallet();
const connection = new Connection("http://127.0.0.1:8899");
await requestAirdrop(connection, solanaWallet.publicKey);
const testRelayer = await setupTestRelayer(solanaWallet);
const lightProvider = await initializeLightProvider(solanaWallet, testRelayer);
const user = await initializeLightUser(lightProvider);
await performShieldOperation(user);
const testRecipientKeypair = Keypair.generate();
await requestAirdrop(connection, testRecipientKeypair.publicKey);
const lightProviderRecipient = await initializeLightProvider(testRecipientKeypair, testRelayer);
const testRecipient = await initializeLightUser(lightProviderRecipient);
await executePrivateTransfer(user, testRecipient.account.getPublicKey());
console.log(`Successfully sent 1 $SOL to ${testRecipient.account.getPublicKey()} privately!`);
} catch (e) {
console.error(`Error sending SOL via Light: ${e}`);
} finally {
console.log("Exiting program...");
process.exit(0);
}
};
main();코드 분석
import { BN } from "@coral-xyz/anchor";
import { PublicKey, Keypair, Connection } from "@solana/web3.js";
import { User, Provider as LightProvider, TestRelayer, confirmConfig, airdropSol } from "@lightprotocol/zk.js";먼저 Light Protocol로 Solana와 상호작용하는 데 필요한 모듈을 Anchor, Solana/web3.js, Light에서 가져옵니다. 그런 다음 main 함수로 넘어가기 전에 여러 유틸리티 함수를 정의합니다. Light의 초기화 과정은 Elusiv 구현보다 복잡합니다.
const initializeSolanaWallet = async (): Promise => {
const wallet = Keypair.generate();
console.log("Wallet initialized");
return wallet;
};여기서는 Keypair.generate() 메서드를 사용해 새 Solana 지갑을 생성하는 initializeSolanaWallet 함수를 만듭니다.
const requestAirdrop = async (connection: Connection, publicKey: PublicKey): Promise => {
await airdropSol({
connection,
lamports: 2e9,
recipientPublicKey: publicKey,
});
console.log("Airdrop requested...");
};Elusiv와 달리 Light는 지정된 공개 키로 Lamports를 에어드롭하는 airdropSol 함수를 라이브러리에서 제공합니다. 전달된 publicKey으로 SOL을 보내는 requestAirdrop 유틸리티 함수를 생성합니다.
const setupTestRelayer = async (solanaWallet: Keypair): Promise => {
const testRelayer = new TestRelayer({
relayerPubkey: solanaWallet.publicKey,
relayerRecipientSol: solanaWallet.publicKey,
relayerFee: new BN(100_000),
payer: solanaWallet,
});
console.log("Test relayer initialized");
return testRelayer;
};Light를 통해 실드된 SOL을 보내려면 TestRelayer 모듈을 사용해 테스트 릴레이어를 설정해야 합니다. 릴레이어는 상태 전환을 증명하는 ZK 증명을 클라이언트에서 해당 온체인 프로그램으로 전달하는 웹 서버로서 Light 생태계에서 중요한 역할을 합니다. 증명 검증에 성공하면 수수료를 받는 대신 트랜잭션 수수료도 지불합니다. 수수료는 100k Lamports이며 new BN(100_000)으로 설정합니다.
const initializeLightProvider = async (solanaWallet: Keypair, testRelayer: TestRelayer): Promise => {
const lightProvider = await LightProvider.init({
wallet: solanaWallet,
relayer: testRelayer,
confirmConfig,
});
console.log("Light provider initialized");
return lightProvider;
};그런 다음 적절한 이름의 initializeLightProvider() 함수를 사용해 Light Provider를 초기화합니다. 이 함수는 Provider를 설정하고 Light가 제공한 구성 confirmConfig을 사용해 Solana 지갑 및 테스트 릴레이어와 연결합니다. 이 구성은 ConfirmOptions 타입이며 트랜잭션 검증 단계와 커밋 수준 등을 처리합니다.
const initializeLightUser = async (lightProvider: LightProvider): Promise => {
const user = await User.init({ provider: lightProvider });
console.log("Light user initialized");
return user;
};initializeLightUser 함수는 이름 그대로 전달된 lightProvider을 사용해 Light Protocol 사용자를 초기화합니다.
const performShieldOperation = async (user: User) => {
await user.shield({
publicAmountSol: "1.1",
token: "SOL",
});
console.log("Performed shield operation");
};performShieldOperation 함수를 사용하면 SOL을 실드하여 비공개로 만들 수 있습니다. 여기서는 1개의 실드된 SOL만 보내지만 수수료를 고려해 1 SOL보다 많은 금액을 실드합니다.
const executePrivateTransfer = async (user: User, testRecipientPublicKey: string) => {
const response = await user.transfer({
amountSol: "1",
token: "SOL",
recipient: testRecipientPublicKey,
});
console.log(`Executed private transfer! Txt hash: ${response.txHash}`);
};이 함수는 실제로 비공개 전송을 실행하므로 가장 중요한 유틸리티 함수라고 할 수 있습니다. 여기서는 user에서 지정된 수신자 testRecipientPublicKey에게 1 SOL을 비공개로 전송합니다.
const main = async () => {
try {
const solanaWallet = await initializeSolanaWallet();
const connection = new Connection("http://127.0.0.1:8899");
await requestAirdrop(connection, solanaWallet.publicKey);
const testRelayer = await setupTestRelayer(solanaWallet);
const lightProvider = await initializeLightProvider(solanaWallet, testRelayer);
const user = await initializeLightUser(lightProvider);
await performShieldOperation(user);
const testRecipientKeypair = Keypair.generate();
await requestAirdrop(connection, testRecipientKeypair.publicKey);
const lightProviderRecipient = await initializeLightProvider(testRecipientKeypair, testRelayer);
const testRecipient = await initializeLightUser(lightProviderRecipient);
await executePrivateTransfer(user, testRecipient.account.getPublicKey());
console.log(`Successfully sent 1 $SOL to ${testRecipient.account.getPublicKey()} privately!`);
} catch (e) {
console.error(`Error sending SOL via Light: ${e}`);
} finally {
console.log("Exiting program...");
process.exit(0);
}
};main 함수는 스크립트의 진입점입니다. 범위 안에서 여러 비동기 함수를 호출하므로 비동기로 지정합니다. 지갑 초기화부터 비공개 전송 실행까지 모든 단계를 순서대로 실행합니다. 먼저 Solana 지갑과 연결을 설정합니다. 그런 다음 지갑으로 SOL을 에어드롭하고 릴레이어와 Light Provider를 초기화합니다. 이어서 지갑을 Light User로 초기화하고 SOL을 실드합니다. 테스트 수신자를 생성해 SOL을 에어드롭하고 Light User로 초기화한 다음 1 SOL을 비공개로 보냅니다. Elusiv 코드와 마찬가지로 오류를 포착하고 완료 후 프로그램을 자동으로 종료하기 위해 로직을 try-catch-finally 블록에 넣습니다.
Elusiv에서는 devnet을 사용했지만 여기서는 연결을 "http://127.0.0.1:8899"로 설정했다는 점을 확인할 수 있습니다. 이 글을 작성하는 시점에는 Light Protocol이 devnet/mainnet에 배포되지 않았으므로 Helius 키가 아직 작동하지 않습니다(조만간 가능할지도 모릅니다 👀). localhost에서는 코드를 실행할 수 있도록 필요한 계정과 프로그램이 미리 로드된 test-validator를 실행해야 합니다. 이를 위해 Light Protocol CLI를 설치하고 다음 명령어를 실행하세요.
light test-validator그러면 검증인이 백그라운드에서 시작됩니다. 현재 UTXO 클래스 안에 단위 테스트가 있으므로 chai (테스트를 더 쉽게 만드는 어설션 라이브러리)도 직접 추가해야 합니다. 하지만 이 문제는 다음 릴리스에서 해결될 예정입니다. 이 스크립트의 package.json은 다음과 비슷해야 합니다.
{
"name": "helius-tutorial",
"version": "1.0.0",
"main": "index.js",
"license": "MIT",
"devDependencies": {
"@lightprotocol/cli": "^0.1.1-alpha.21",
"@types/node": "^20.6.0",
"chai": "^4.3.8",
"typescript": "^5.2.2"
},
"dependencies": {
"@coral-xyz/anchor": "^0.28.1-beta.2",
"@lightprotocol/zk.js": "^0.3.2-alpha.14",
"@solana/web3.js": "^1.78.4"
}
}결론
축하합니다! 이 튜토리얼에서는 프라이버시의 세부 원리와 중요성을 살펴보고 Elusiv와 Light Protocol을 사용해 SOL을 비공개로 전송하는 방법을 알아봤습니다. Elusiv를 통해 비공개 잔액을 조회하고 SOL을 비공개로 보내며, 비공개 잔액이 소진되거나 존재하지 않을 때 자동으로 충전하는 방법까지 살펴봤습니다. Light Protocol에서는 복잡하지만 모듈성이 높은 초기화 과정을 설정하고 SOL을 실드한 후 비공개 전송을 실행하는 방법을 알아봤습니다.
그렇다면 무엇을 사용해야 할지 궁금할 수 있습니다. 정답은 상황에 따라 다릅니다. Elusiv 코드에서는 공동 풀에서 비공개 잔액을 충전하고 트랜잭션을 비공개로 보내는 데 중점을 뒀습니다. 이를 위해 스크립트가 Elusiv 풀을 관리하는 사전 정의된 스마트 컨트랙트와 상호작용합니다. Light Protocol에서는 프라이버시를 보존하는 Solana 프로그램과 여러 사용자가 상호작용하는 워크플로를 만들기 위해 더 유연하지만 복잡한 초기화 과정을 구성했습니다. 필요하다면 Light로 자체 비공개 Solana Programs(PSPs)도 만들 수 있습니다. 두 프로토콜은 문서와 구체적인 구현 방식 등에서 각각 장단점이 있지만, 모두 프라이버시와 안전한 트랜잭션을 목표로 합니다. 애플리케이션에서 프라이버시 기능을 더 세밀하게 제어하고 애플리케이션 상태의 특정 부분을 암호화하려면 Light Protocol을 고려해 보세요. 더 단순한 애플리케이션을 개발하면서 devnet/mainnet의 토큰 전송에 프라이버시 계층을 추가하고 싶다면 Elusiv가 더 나은 선택일 수 있습니다.
이 글에서 소개한 기법, 코드, 인사이트는 Solana에서 프라이버시 중심 애플리케이션을 구축하기 위한 탄탄한 출발점입니다. 블록체인 기술에서 프라이버시의 중요성은 아무리 강조해도 지나치지 않습니다. 탈중앙화 시스템에 대한 수요가 커질수록 사용자에게 권한을 부여하고 권리를 보호하며 초기 블록체인 기술의 진정한 목표를 실현할 프라이버시 기능도 더 많이 필요해집니다. 프라이버시는 중요합니다. 이제 Elusiv와 Light Protocol을 사용해 Solana를 더 안전한 블록체인으로 만들 수 있습니다!
익명의 독자 여러분, 여기까지 읽어주셔서 감사합니다!
추가 자료 / 더 읽어보기
- 사이퍼펑크 선언문
- Elusiv
- Elusiv 문서
- Elusiv SDK
- Elusiv 샘플
- Light Protocol
- zk.js
- 비공개 Solana Program 구현 예제
관련 아티클
Helius 구독하기
최신 Solana 개발 소식을 확인하고 새 게시물 알림을 받아보세요


