Napište Plasmu specifickou pro aplikaci, která zachovává soukromí
Úvod
Na rozdíl od rollupů využívají Plasmy Ethereum Mainnet pro integritu, ale ne pro dostupnost. V tomto článku napíšeme aplikaci, která se chová jako Plasma, přičemž Ethereum zaručuje integritu (žádné neoprávněné změny), ale ne dostupnost (centralizovaná komponenta může selhat a vyřadit z provozu celý systém).
Aplikace, kterou zde napíšeme, je banka zachovávající soukromí. Různé adresy mají účty se zůstatky a mohou posílat peníze (ETH) na jiné účty. Banka zveřejňuje hashe stavu (účtů a jejich zůstatků) a transakcí, ale skutečné zůstatky udržuje offchain, kde mohou zůstat v soukromí.
Návrh
Toto není systém připravený pro produkci, ale výukový nástroj. Jako takový je napsán s několika zjednodušujícími předpoklady.
-
Pevná množina účtů. Existuje specifický počet účtů a každý účet patří k předem určené adrese. To vytváří mnohem jednodušší systém, protože v důkazech s nulovou znalostí je obtížné pracovat s datovými strukturami proměnné velikosti. Pro systém připravený na produkci můžeme použít Merkleho kořen jako hash stavu a poskytovat Merkleho důkazy pro požadované zůstatky.
-
Ukládání do paměti. V produkčním systému musíme zapisovat všechny zůstatky na účtech na disk, abychom je zachovali v případě restartu. Zde nevadí, pokud se informace jednoduše ztratí.
-
Pouze převody. Produkční systém by vyžadoval způsob, jak do banky vložit aktiva a jak je vybrat. Účelem zde je však pouze ilustrovat koncept, takže tato banka je omezena na převody.
Důkazy s nulovou znalostí
Na základní úrovni důkaz s nulovou znalostí ukazuje, že dokazovatel zná nějaká data, Dataprivate, taková, že existuje vztah Relationship mezi nějakými veřejnými daty, Datapublic, a Dataprivate. Ověřovatel zná Relationship a Datapublic.
Pro zachování soukromí potřebujeme, aby stavy a transakce byly soukromé. Ale pro zajištění integrity potřebujeme, aby kryptografický hash (opens in a new tab) stavů byl veřejný. Abychom lidem, kteří odesílají transakce, dokázali, že se tyto transakce skutečně staly, musíme také zveřejňovat hashe transakcí.
Ve většině případů jsou Dataprivate vstupem do programu pro důkaz s nulovou znalostí a Datapublic jsou výstupem.
Tato pole v Dataprivate:
- Staten, starý stav
- Staten+1, nový stav
- Transaction, transakce, která mění starý stav na nový. Tato transakce musí obsahovat následující pole:
- Cílová adresa, která přijímá převod
- Částka, která se převádí
- Nonce, aby se zajistilo, že každá transakce může být zpracována pouze jednou. Zdrojová adresa nemusí být v transakci, protože ji lze obnovit z podpisu.
- Signature, podpis, který je oprávněn provést transakci. V našem případě je jedinou adresou oprávněnou provést transakci zdrojová adresa. Protože náš systém s nulovým vědomím funguje tak, jak funguje, potřebujeme kromě podpisu Etherea také veřejný klíč účtu.
Toto jsou pole v Datapublic:
- Hash(Staten) hash starého stavu
- Hash(Staten+1) hash nového stavu
- Hash(Transaction) hash transakce, která mění stav ze Staten na Staten+1.
Vztah kontroluje několik podmínek:
- Veřejné hashe jsou skutečně správné hashe pro soukromá pole.
- Transakce po aplikaci na starý stav vede k novému stavu.
- Podpis pochází ze zdrojové adresy transakce.
Vzhledem k vlastnostem kryptografických hashovacích funkcí stačí prokázání těchto podmínek k zajištění integrity.
Datové struktury
Primární datovou strukturou je stav udržovaný serverem. Pro každý účet server sleduje zůstatek na účtu a nonce (opens in a new tab), které se používá k zabránění útokům typu replay (opens in a new tab).
Komponenty
Tento systém vyžaduje dvě komponenty:
- Server, který přijímá transakce, zpracovává je a odesílá hashe do řetězce spolu s důkazy s nulovou znalostí.
- Chytrý kontrakt, který ukládá hashe a ověřuje důkazy s nulovou znalostí, aby zajistil, že přechody stavů jsou legitimní.
Tok dat a řízení
Toto jsou způsoby, jakými různé komponenty komunikují při převodu z jednoho účtu na druhý.
-
Webový prohlížeč odešle podepsanou transakci s žádostí o převod z účtu podepisujícího na jiný účet.
-
Server ověří, že je transakce platná:
- Podepisující má v bance účet s dostatečným zůstatkem.
- Příjemce má v bance účet.
-
Server vypočítá nový stav odečtením převedené částky ze zůstatku podepisujícího a jejím přičtením k zůstatku příjemce.
-
Server vypočítá důkaz s nulovou znalostí, že změna stavu je platná.
-
Server odešle do Etherea transakci, která obsahuje:
- Hash nového stavu
- Hash transakce (aby odesílatel transakce věděl, že byla zpracována)
- Důkaz s nulovou znalostí, který dokazuje, že přechod do nového stavu je platný
-
Chytrý kontrakt ověří důkaz s nulovou znalostí.
-
Pokud je důkaz s nulovou znalostí v pořádku, chytrý kontrakt provede tyto akce:
- Aktualizuje hash aktuálního stavu na hash nového stavu
- Vygeneruje log s hashem nového stavu a hashem transakce
Nástroje
Pro kód na straně klienta použijeme Vite (opens in a new tab), React (opens in a new tab), Viem (opens in a new tab) a Wagmi (opens in a new tab). Jedná se o standardní oborové nástroje; pokud s nimi nejste obeznámeni, můžete použít tento tutoriál.
Většina serveru je napsána v JavaScriptu pomocí Node (opens in a new tab). Část s nulovým vědomím je napsána v jazyce Noir (opens in a new tab). Potřebujeme verzi 1.0.0-beta.10, takže poté, co nainstalujete Noir podle pokynů (opens in a new tab), spusťte:
noirup -v 1.0.0-beta.10
Blockchain, který používáme, je anvil, lokální testovací blockchain, který je součástí Foundry (opens in a new tab).
Implementace
Protože se jedná o složitý systém, budeme jej implementovat po fázích.
Fáze 1 - Manuální přístup s nulovým vědomím
V první fázi podepíšeme transakci v prohlížeči a poté manuálně poskytneme informace pro důkaz s nulovou znalostí. Kód s nulovým vědomím očekává, že tyto informace získá v server/noir/Prover.toml (dokumentováno zde (opens in a new tab)).
Chcete-li to vidět v akci:
-
Ujistěte se, že máte nainstalovaný Node (opens in a new tab) a Noir (opens in a new tab). Nejlépe je nainstalujte na UNIXový systém, jako je macOS, Linux nebo WSL (opens in a new tab).
-
Stáhněte si kód pro fázi 1 a spusťte webový server, který bude obsluhovat klientský kód.
git clone https://github.com/qbzzt/250911-zk-bank.git -b 01-manual-zk cd 250911-zk-bank cd client npm install npm run devDůvodem, proč zde potřebujete webový server, je to, že aby se zabránilo určitým typům podvodů, mnoho peněženek (jako je MetaMask) nepřijímá soubory obsluhované přímo z disku.
-
Otevřete prohlížeč s peněženkou.
-
V peněžence zadejte novou přístupovou frázi. Upozorňujeme, že tímto smažete svou stávající přístupovou frázi, takže se ujistěte, že máte zálohu.
Přístupová fráze je
test test test test test test test test test test test junk, což je výchozí testovací přístupová fráze pro anvil. -
Přejděte na kód na straně klienta (opens in a new tab).
-
Připojte se k peněžence a vyberte cílový účet a částku.
-
Klikněte na Sign (Podepsat) a podepište transakci.
-
Pod nadpisem Prover.toml najdete text. Nahraďte
server/noir/Prover.tomltímto textem. -
Spusťte důkaz s nulovou znalostí.
cd ../server/noir nargo executeVýstup by měl být podobný tomuto:
ori@CryptoDocGuy:~/noir/250911-zk-bank/server/noir$ nargo execute
[zkBank] Circuit witness successfully solved [zkBank] Witness saved to target/zkBank.gz [zkBank] Circuit output: (0x199aa62af8c1d562a6ec96e66347bf3240ab2afb5d022c895e6bf6a5e617167b, 0x0cfc0a67cb7308e4e9b254026b54204e34f6c8b041be207e64c5db77d95dd82d, 0x450cf9da6e180d6159290554ae3d8787, 0x6d8bc5a15b9037e52fb59b6b98722a85)
10. Porovnejte poslední dvě hodnoty s hashem, který vidíte ve webovém prohlížeči, abyste zjistili, zda je zpráva správně hashována.
#### `server/noir/Prover.toml` \{#server-noir-prover-toml\}
[Tento soubor](https://github.com/qbzzt/250911-zk-bank/blob/01-manual-zk/server/noir/Prover.toml) ukazuje formát informací, který Noir očekává.
```toml
message="send 0x70997970C51812dc3A010C7d01b50e0d17dc79C8 500 finney (milliEth) 0 "
Zpráva je v textovém formátu, což uživateli usnadňuje její pochopení (což je při podepisování nezbytné) a kódu Noir její parsování. Částka je uvedena ve finney, aby na jedné straně umožnila zlomkové převody a na druhé straně byla snadno čitelná. Poslední číslo je nonce (opens in a new tab).
Řetězec je dlouhý 100 znaků. Důkazy s nulovou znalostí nezvládají dobře data s proměnnou velikostí, takže je často nutné data doplnit (tzv. padding).
pubKeyX=["0x83",...,"0x75"]
pubKeyY=["0x35",...,"0xa5"]
signature=["0xb1",...,"0x0d"]
Tyto tři parametry jsou pole bajtů s pevnou velikostí.
[[accounts]]
address="0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266"
balance=100_000
nonce=0
[[accounts]]
address="0x70997970C51812dc3A010C7d01b50e0d17dc79C8"
balance=100_000
nonce=0
Toto je způsob, jak specifikovat pole struktur. Pro každý záznam specifikujeme adresu, zůstatek (v milliETH neboli finney (opens in a new tab)) a další hodnotu nonce.
client/src/Transfer.tsx
Tento soubor (opens in a new tab) implementuje zpracování na straně klienta a generuje soubor server/noir/Prover.toml (ten, který obsahuje parametry pro důkaz s nulovým vědomím).
Zde je vysvětlení těch zajímavějších částí.
export default attrs => {
Tato funkce vytváří React komponentu Transfer, kterou mohou importovat další soubory.
const accounts = [
"0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266",
"0x70997970C51812dc3A010C7d01b50e0d17dc79C8",
"0x3C44CdDdB6a900fa2b585dd299e03d12FA4293BC",
"0x90F79bf6EB2c4f870365E785982E1f101E93b906",
"0x15d34AAf54267DB7D7c367839AAf71A00a2C6A65",
]
Toto jsou adresy účtů, adresy vytvořené přístupovou frází test ... test junk. Pokud chcete použít vlastní adresy, stačí tuto definici upravit.
const account = useAccount()
const wallet = createWalletClient({
transport: custom(window.ethereum!)
})
Tyto Wagmi hooky (opens in a new tab) nám umožňují přístup ke knihovně Viem (opens in a new tab) a k peněžence.
const message = `send ${toAccount} ${ethAmount*1000} finney (milliEth) ${nonce}`.padEnd(100, " ")
Toto je zpráva doplněná mezerami. Pokaždé, když se změní jedna z proměnných useState (opens in a new tab), komponenta se překreslí a message se aktualizuje.
const sign = async () => {
Tato funkce se volá, když uživatel klikne na tlačítko Sign (Podepsat). Zpráva se automaticky aktualizuje, ale podpis vyžaduje schválení uživatelem v peněžence a my o něj nechceme žádat, pokud to není nutné.
const signature = await wallet.signMessage({
account: fromAccount,
message,
})
Požádejte peněženku o podepsání zprávy (opens in a new tab).
const hash = hashMessage(message)
Získejte hash zprávy. Je užitečné jej poskytnout uživateli pro účely ladění (kódu Noir).
const pubKey = await recoverPublicKey({
hash,
signature
})
Získejte veřejný klíč (opens in a new tab). Ten je vyžadován pro funkci Noir ecrecover (opens in a new tab).
setSignature(signature)
setHash(hash)
setPubKey(pubKey)
Nastavte stavové proměnné. Tím se komponenta překreslí (po ukončení funkce sign) a uživateli se zobrazí aktualizované hodnoty.
let proverToml = `
Text pro Prover.toml.
message="${message}"
pubKeyX=${hexToArray(pubKey.slice(4,4+2*32))}
pubKeyY=${hexToArray(pubKey.slice(4+2*32))}
Viem nám poskytuje veřejný klíč jako 65bajtový hexadecimální řetězec. První bajt je 0x04, což je značka verze. Následuje 32 bajtů pro x veřejného klíče a poté 32 bajtů pro y veřejného klíče.
Noir však očekává, že tyto informace získá jako dvě pole bajtů, jedno pro x a jedno pro y. Je snazší to parsovat zde na klientovi než jako součást důkazu s nulovou znalostí.
Všimněte si, že toto je obecně dobrá praxe v oblasti důkazů s nulovým vědomím. Kód uvnitř důkazu s nulovou znalostí je drahý, takže jakékoli zpracování, které lze provést mimo důkaz s nulovou znalostí, by mělo být provedeno mimo něj.
signature=${hexToArray(signature.slice(2,-2))}
Podpis je také poskytován jako 65bajtový hexadecimální řetězec. Poslední bajt je však nutný pouze k obnovení veřejného klíče. Vzhledem k tomu, že veřejný klíč již bude poskytnut kódu Noir, nepotřebujeme jej k ověření podpisu a kód Noir jej nevyžaduje.
${accounts.map(accountInProverToml).reduce((a,b) => a+b, "")}
`
Poskytněte účty.
setProverToml(proverToml)
}
return (
<>
<h2>Transfer</h2>
Toto je HTML (přesněji JSX (opens in a new tab)) formát komponenty.
server/noir/src/main.nr
Tento soubor (opens in a new tab) je samotný kód s nulovým vědomím.
use std::hash::pedersen_hash;
Pedersenův hash (opens in a new tab) je poskytován se standardní knihovnou Noir (opens in a new tab). Důkazy s nulovou znalostí tuto hashovací funkci běžně používají. Je mnohem snazší ji vypočítat uvnitř aritmetických obvodů (opens in a new tab) ve srovnání se standardními hashovacími funkcemi.
use keccak256::keccak256;
use dep::ecrecover;
Tyto dvě funkce jsou externí knihovny definované v Nargo.toml (opens in a new tab). Dělají přesně to, jak se jmenují: funkce, která počítá hash keccak256 (opens in a new tab), a funkce, která ověřuje podpisy Etherea a obnovuje Ethereum adresu podepisujícího.
global ACCOUNT_NUMBER : u32 = 5;
Noir je inspirován jazykem Rust (opens in a new tab). Proměnné jsou ve výchozím nastavení konstanty. Takto definujeme globální konfigurační konstanty. Konkrétně ACCOUNT_NUMBER je počet účtů, které ukládáme.
Datové typy pojmenované u<number> představují daný počet bitů bez znaménka. Jediné podporované typy jsou u8, u16, u32, u64 a u128.
global FLAT_ACCOUNT_FIELDS : u32 = 2;
Tato proměnná se používá pro Pedersenův hash účtů, jak je vysvětleno níže.
global MESSAGE_LENGTH : u32 = 100;
Jak bylo vysvětleno výše, délka zprávy je pevná. Je specifikována zde.
global ASCII_MESSAGE_LENGTH : [u8; 3] = [0x31, 0x30, 0x30];
global HASH_BUFFER_SIZE : u32 = 26+3+MESSAGE_LENGTH;
Podpisy EIP-191 (opens in a new tab) vyžadují buffer s 26bajtovou předponou, následovanou délkou zprávy v ASCII a nakonec samotnou zprávou.
struct Account {
balance: u128,
address: Field,
nonce: u32,
}
Informace, které ukládáme o účtu. Field (opens in a new tab) je číslo, obvykle až 253 bitů, které lze použít přímo v aritmetickém obvodu (opens in a new tab), který implementuje důkaz s nulovou znalostí. Zde používáme Field k uložení 160bitové adresy Etherea.
struct TransferTxn {
from: Field,
to: Field,
amount: u128,
nonce: u32
}
Informace, které ukládáme pro transakci převodu.
fn flatten_account(account: Account) -> [Field; FLAT_ACCOUNT_FIELDS] {
Definice funkce. Parametrem jsou informace o Account. Výsledkem je pole proměnných Field, jehož délka je FLAT_ACCOUNT_FIELDS
let flat = [
account.address,
((account.balance << 32) + account.nonce.into()).into(),
];
První hodnota v poli je adresa účtu. Druhá zahrnuje jak zůstatek, tak nonce. Volání .into() mění číslo na datový typ, kterým potřebuje být. account.nonce je hodnota u32, ale aby ji bylo možné přidat k account.balance << 32, což je hodnota u128, musí to být u128. To je první .into(). Druhé převede výsledek u128 na Field, aby se vešel do pole.
flat
}
V Noir mohou funkce vracet hodnotu pouze na konci (neexistuje žádný předčasný návrat). Chcete-li specifikovat návratovou hodnotu, vyhodnotíte ji těsně před uzavírací závorkou funkce.
fn flatten_accounts(accounts: [Account; ACCOUNT_NUMBER]) -> [Field; FLAT_ACCOUNT_FIELDS*ACCOUNT_NUMBER] {
Tato funkce převede pole účtů na pole Field, které lze použít jako vstup pro Petersenův hash.
let mut flat: [Field; FLAT_ACCOUNT_FIELDS*ACCOUNT_NUMBER] = [0; FLAT_ACCOUNT_FIELDS*ACCOUNT_NUMBER];
Takto specifikujete měnitelnou (mutable) proměnnou, tedy ne konstantu. Proměnné v Noir musí mít vždy hodnotu, takže tuto proměnnou inicializujeme samými nulami.
for i in 0..ACCOUNT_NUMBER {
Toto je smyčka for. Všimněte si, že hranice jsou konstanty. Smyčky v Noir musí mít své hranice známé v době kompilace. Důvodem je, že aritmetické obvody nepodporují řízení toku. Při zpracování smyčky for kompilátor jednoduše vloží kód uvnitř ní vícekrát, jednou pro každou iteraci.
let fields = flatten_account(accounts[i]);
for j in 0..FLAT_ACCOUNT_FIELDS {
flat[i*FLAT_ACCOUNT_FIELDS + j] = fields[j];
}
}
flat
}
fn hash_accounts(accounts: [Account; ACCOUNT_NUMBER]) -> Field {
pedersen_hash(flatten_accounts(accounts))
}
Konečně jsme se dostali k funkci, která hashuje pole účtů.
fn find_account(accounts: [Account; ACCOUNT_NUMBER], address: Field) -> u32 {
let mut account : u32 = ACCOUNT_NUMBER;
for i in 0..ACCOUNT_NUMBER {
if accounts[i].address == address {
account = i;
}
}
Tato funkce najde účet se specifickou adresou. Tato funkce by byla ve standardním kódu strašně neefektivní, protože iteruje přes všechny účty, a to i poté, co adresu najde.
V důkazech s nulovou znalostí však neexistuje žádné řízení toku. Pokud někdy potřebujeme zkontrolovat podmínku, musíme ji zkontrolovat pokaždé.
Podobná věc se děje s příkazy if. Příkaz if ve smyčce výše je přeložen do těchto matematických příkazů.
conditionresult = accounts[i].address == address // jedna, pokud jsou si rovny, jinak nula
accountnew = conditionresult*i + (1-conditionresult)*accountold
assert (account < ACCOUNT_NUMBER, f"{address} does not have an account");
account
}
Funkce assert (opens in a new tab) způsobí pád důkazu s nulovou znalostí, pokud je tvrzení nepravdivé. V tomto případě, pokud nemůžeme najít účet s příslušnou adresou. K nahlášení adresy používáme formátovací řetězec (opens in a new tab).
fn apply_transfer_txn(accounts: [Account; ACCOUNT_NUMBER], txn: TransferTxn) -> [Account; ACCOUNT_NUMBER] {
Tato funkce aplikuje transakci převodu a vrací nové pole účtů.
let from = find_account(accounts, txn.from);
let to = find_account(accounts, txn.to);
let (txnFrom, txnAmount, txnNonce, accountNonce) =
(txn.from, txn.amount, txn.nonce, accounts[from].nonce);
V Noir nemůžeme přistupovat k prvkům struktury uvnitř formátovacího řetězce, takže vytvoříme použitelnou kopii.
assert (accounts[from].balance >= txn.amount,
f"{txnFrom} does not have {txnAmount} finney");
assert (accounts[from].nonce == txn.nonce,
f"Transaction has nonce {txnNonce}, but the account is expected to use {accountNonce}");
Toto jsou dvě podmínky, které by mohly učinit transakci neplatnou.
let mut newAccounts = accounts;
newAccounts[from].balance -= txn.amount;
newAccounts[from].nonce += 1;
newAccounts[to].balance += txn.amount;
newAccounts
}
Vytvořte nové pole účtů a poté jej vraťte.
fn readAddress(messageBytes: [u8; MESSAGE_LENGTH]) -> Field
Tato funkce čte adresu ze zprávy.
{
let mut result : Field = 0;
for i in 7..47 {
Adresa je vždy dlouhá 20 bajtů (neboli 40 hexadecimálních číslic) a začíná na znaku č. 7.
result *= 0x10;
if messageBytes[i] >= 48 & messageBytes[i] <= 57 { // 0-9
result += (messageBytes[i]-48).into();
}
if messageBytes[i] >= 65 & messageBytes[i] <= 70 { // A-F
result += (messageBytes[i]-65+10).into()
}
if messageBytes[i] >= 97 & messageBytes[i] <= 102 { // a-f
result += (messageBytes[i]-97+10).into()
}
}
result
}
fn readAmountAndNonce(messageBytes: [u8; MESSAGE_LENGTH]) -> (u128, u32)
Přečtěte částku a nonce ze zprávy.
{
let mut amount : u128 = 0;
let mut nonce: u32 = 0;
let mut stillReadingAmount: bool = true;
let mut lookingForNonce: bool = false;
let mut stillReadingNonce: bool = false;
Ve zprávě je prvním číslem po adrese částka ve finney (neboli tisícinách ETH) k převodu. Druhým číslem je nonce. Jakýkoli text mezi nimi je ignorován.
for i in 48..MESSAGE_LENGTH {
if messageBytes[i] >= 48 & messageBytes[i] <= 57 { // 0-9
let digit = (messageBytes[i]-48);
if stillReadingAmount {
amount = amount*10 + digit.into();
}
if lookingForNonce { // Právě jsme to našli
stillReadingNonce = true;
lookingForNonce = false;
}
if stillReadingNonce {
nonce = nonce*10 + digit.into();
}
} else {
if stillReadingAmount {
stillReadingAmount = false;
lookingForNonce = true;
}
if stillReadingNonce {
stillReadingNonce = false;
}
}
}
(amount, nonce)
}
Vrácení n-tice (tuple) (opens in a new tab) je způsob, jakým Noir vrací více hodnot z funkce.
fn readTransferTxn(message: str<MESSAGE_LENGTH>) -> TransferTxn
{
let mut txn: TransferTxn = TransferTxn { from: 0, to: 0, amount:0, nonce:0 };
let messageBytes = message.as_bytes();
txn.to = readAddress(messageBytes);
let (amount, nonce) = readAmountAndNonce(messageBytes);
txn.amount = amount;
txn.nonce = nonce;
txn
}
Tato funkce převede zprávu na bajty a poté převede částky na TransferTxn.
// Ekvivalent k hashMessage z Viem
// https://viem.sh/docs/utilities/hashMessage#hashmessage
fn hashMessage(message: str<MESSAGE_LENGTH>) -> [u8;32] {
Pro účty jsme mohli použít Pedersenův hash, protože jsou hashovány pouze uvnitř důkazu s nulovou znalostí. V tomto kódu však potřebujeme zkontrolovat podpis zprávy, který je generován prohlížečem. K tomu musíme dodržet formát podepisování Etherea v EIP-191 (opens in a new tab). To znamená, že musíme vytvořit kombinovaný buffer se standardní předponou, délkou zprávy v ASCII a samotnou zprávou a k jeho hashování použít standardní Ethereum keccak256.
// ASCII prefix
let prefix_bytes = [
0x19, // \x19
0x45, // 'E'
0x74, // 't'
0x68, // 'h'
0x65, // 'e'
0x72, // 'r'
0x65, // 'e'
0x75, // 'u'
0x6D, // 'm'
0x20, // ' '
0x53, // 'S'
0x69, // 'i'
0x67, // 'g'
0x6E, // 'n'
0x65, // 'e'
0x64, // 'd'
0x20, // ' '
0x4D, // 'M'
0x65, // 'e'
0x73, // 's'
0x73, // 's'
0x61, // 'a'
0x67, // 'g'
0x65, // 'e'
0x3A, // ':'
0x0A // '\n'
];
Aby se předešlo případům, kdy aplikace požádá uživatele o podepsání zprávy, kterou lze použít jako transakci nebo k jinému účelu, EIP-191 specifikuje, že všechny podepsané zprávy začínají znakem 0x19 (což není platný znak ASCII), po kterém následuje Ethereum Signed Message: a nový řádek.
let mut buffer: [u8; HASH_BUFFER_SIZE] = [0u8; HASH_BUFFER_SIZE];
for i in 0..26 {
buffer[i] = prefix_bytes[i];
}
let messageBytes : [u8; MESSAGE_LENGTH] = message.as_bytes();
if MESSAGE_LENGTH <= 9 {
for i in 0..1 {
buffer[i+26] = ASCII_MESSAGE_LENGTH[i];
}
for i in 0..MESSAGE_LENGTH {
buffer[i+26+1] = messageBytes[i];
}
}
if MESSAGE_LENGTH >= 10 & MESSAGE_LENGTH <= 99 {
for i in 0..2 {
buffer[i+26] = ASCII_MESSAGE_LENGTH[i];
}
for i in 0..MESSAGE_LENGTH {
buffer[i+26+2] = messageBytes[i];
}
}
if MESSAGE_LENGTH >= 100 {
for i in 0..3 {
buffer[i+26] = ASCII_MESSAGE_LENGTH[i];
}
for i in 0..MESSAGE_LENGTH {
buffer[i+26+3] = messageBytes[i];
}
}
assert(MESSAGE_LENGTH < 1000, "Messages whose length is over three digits are not supported");
Zpracujte délky zpráv až do 999 a selžete, pokud je větší. Tento kód jsem přidal, i když je délka zprávy konstanta, protože to usnadňuje její změnu. V produkčním systému byste pravděpodobně jen předpokládali, že se MESSAGE_LENGTH nemění, a to v zájmu lepšího výkonu.
keccak256::keccak256(buffer, HASH_BUFFER_SIZE)
}
Použijte standardní funkci Etherea keccak256.
fn signatureToAddressAndHash(
message: str<MESSAGE_LENGTH>,
pubKeyX: [u8; 32],
pubKeyY: [u8; 32],
signature: [u8; 64]
) -> (Field, Field, Field) // adresa, prvních 16 bajtů hashe, posledních 16 bajtů hashe
{
Tato funkce ověřuje podpis, což vyžaduje hash zprávy. Poté nám poskytne adresu, která jej podepsala, a hash zprávy. Hash zprávy je dodáván ve dvou hodnotách Field, protože ty se ve zbytku programu používají snáze než pole bajtů.
Musíme použít dvě hodnoty Field, protože výpočty v poli se provádějí modulo (opens in a new tab) velké číslo, ale toto číslo je obvykle menší než 256 bitů (jinak by bylo obtížné tyto výpočty provádět v EVM).
let hash = hashMessage(message);
let mut (hash1, hash2) = (0,0);
for i in 0..16 {
hash1 = hash1*256 + hash[31-i].into();
hash2 = hash2*256 + hash[15-i].into();
}
Specifikujte hash1 a hash2 jako měnitelné proměnné a zapište do nich hash bajt po bajtu.
(
ecrecover::ecrecover(pubKeyX, pubKeyY, signature, hash),
To je podobné jako ecrecover v Solidity (opens in a new tab), se dvěma důležitými rozdíly:
- Pokud podpis není platný, volání selže na
asserta program je přerušen. - Ačkoli lze veřejný klíč obnovit z podpisu a hashe, jedná se o zpracování, které lze provést externě, a proto se nevyplatí jej provádět uvnitř důkazu s nulovou znalostí. Pokud se nás zde někdo pokusí podvést, ověření podpisu selže.
hash1,
hash2
)
}
fn main(
accounts: [Account; ACCOUNT_NUMBER],
message: str<MESSAGE_LENGTH>,
pubKeyX: [u8; 32],
pubKeyY: [u8; 32],
signature: [u8; 64],
) -> pub (
Field, // Hash pole starých účtů
Field, // Hash pole nových účtů
Field, // Prvních 16 bajtů hashe zprávy
Field, // Posledních 16 bajtů hashe zprávy
)
Konečně se dostáváme k funkci main. Musíme dokázat, že máme transakci, která platně mění hash účtů ze staré hodnoty na novou. Musíme také dokázat, že má tento specifický hash transakce, aby osoba, která ji odeslala, věděla, že její transakce byla zpracována.
{
let mut txn = readTransferTxn(message);
Potřebujeme, aby txn byla měnitelná, protože nečteme adresu odesílatele ze zprávy, ale z podpisu.
let (fromAddress, txnHash1, txnHash2) = signatureToAddressAndHash(
message,
pubKeyX,
pubKeyY,
signature);
txn.from = fromAddress;
let newAccounts = apply_transfer_txn(accounts, txn);
(
hash_accounts(accounts),
hash_accounts(newAccounts),
txnHash1,
txnHash2
)
}
Fáze 2 - Přidání serveru
Ve druhé fázi přidáme server, který přijímá a implementuje transakce převodu z prohlížeče.
Chcete-li to vidět v akci:
-
Zastavte Vite, pokud běží.
-
Stáhněte si větev, která obsahuje server, a ujistěte se, že máte všechny potřebné moduly.
git checkout 02-add-server cd client npm install cd ../server npm installNení třeba kompilovat kód Noir, je to stejný kód, jaký jste použili pro fázi 1.
-
Spusťte server.
npm run start -
V samostatném okně příkazového řádku spusťte Vite, aby obsluhoval kód prohlížeče.
cd client npm run dev -
Přejděte na klientský kód na adrese http://localhost:5173 (opens in a new tab)
-
Než budete moci vydat transakci, musíte znát nonce a také částku, kterou můžete odeslat. Chcete-li tyto informace získat, klikněte na Update account data (Aktualizovat data účtu) a podepište zprávu.
Zde máme dilema. Na jedné straně nechceme podepsat zprávu, kterou lze znovu použít (replay útok (opens in a new tab)), což je důvod, proč vůbec chceme nonce. Zatím však žádnou nonce nemáme. Řešením je zvolit nonce, kterou lze použít pouze jednou a kterou již máme na obou stranách, jako je například aktuální čas.
Problém s tímto řešením je, že čas nemusí být dokonale synchronizován. Místo toho tedy podepíšeme hodnotu, která se mění každou minutu. To znamená, že naše okno zranitelnosti vůči replay útokům je maximálně jedna minuta. Vzhledem k tomu, že v produkci bude podepsaný požadavek chráněn pomocí TLS a že druhá strana tunelu – server – již může odhalit zůstatek a nonce (musí je znát, aby fungoval), je to přijatelné riziko.
-
Jakmile prohlížeč získá zpět zůstatek a nonce, zobrazí formulář pro převod. Vyberte cílovou adresu a částku a klikněte na Transfer (Převést). Podepište tento požadavek.
-
Chcete-li vidět převod, buď klikněte na Update account data (Aktualizovat data účtu), nebo se podívejte do okna, kde běží server. Server loguje stav při každé jeho změně.
ori@CryptoDocGuy:~/x/250911-zk-bank/server$ npm run start
server@1.0.0 start node --experimental-json-modules index.mjs
Listening on port 3000 Txn send 0x90F79bf6EB2c4f870365E785982E1f101E93b906 36000 finney (milliEth) 0 processed New state: 0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266 has 64000 (1) 0x70997970C51812dc3A010C7d01b50e0d17dc79C8 has 100000 (0) 0x3C44CdDdB6a900fa2b585dd299e03d12FA4293BC has 100000 (0) 0x90F79bf6EB2c4f870365E785982E1f101E93b906 has 136000 (0) 0x15d34AAf54267DB7D7c367839AAf71A00a2C6A65 has 100000 (0) Txn send 0x70997970C51812dc3A010C7d01b50e0d17dc79C8 7200 finney (milliEth) 1 processed New state: 0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266 has 56800 (2) 0x70997970C51812dc3A010C7d01b50e0d17dc79C8 has 107200 (0) 0x3C44CdDdB6a900fa2b585dd299e03d12FA4293BC has 100000 (0) 0x90F79bf6EB2c4f870365E785982E1f101E93b906 has 136000 (0) 0x15d34AAf54267DB7D7c367839AAf71A00a2C6A65 has 100000 (0) Txn send 0x90F79bf6EB2c4f870365E785982E1f101E93b906 3000 finney (milliEth) 2 processed New state: 0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266 has 53800 (3) 0x70997970C51812dc3A010C7d01b50e0d17dc79C8 has 107200 (0) 0x3C44CdDdB6a900fa2b585dd299e03d12FA4293BC has 100000 (0) 0x90F79bf6EB2c4f870365E785982E1f101E93b906 has 139000 (0) 0x15d34AAf54267DB7D7c367839AAf71A00a2C6A65 has 100000 (0)
#### `server/index.mjs` \{#server-index-mjs-1\}
[Tento soubor](https://github.com/qbzzt/250911-zk-bank/blob/02-add-server/server/index.mjs) obsahuje proces serveru a interaguje s kódem Noir v [`main.nr`](https://github.com/qbzzt/250911-zk-bank/blob/02-add-server/server/noir/src/main.nr). Zde je vysvětlení zajímavých částí.
```js
import { Noir } from '@noir-lang/noir_js'
Knihovna noir.js (opens in a new tab) tvoří rozhraní mezi kódem JavaScript a kódem Noir.
const circuit = JSON.parse(await fs.readFile("./noir/target/zkBank.json"))
const noir = new Noir(circuit)
Načtěte aritmetický obvod – zkompilovaný program Noir, který jsme vytvořili v předchozí fázi – a připravte se na jeho spuštění.
// Informace o účtu poskytujeme pouze jako odpověď na podepsaný požadavek
const accountInformation = async signature => {
const fromAddress = await recoverAddress({
hash: hashMessage("Get account data " + Math.floor((new Date().getTime())/60000)),
signature
})
K poskytnutí informací o účtu potřebujeme pouze podpis. Důvodem je, že již víme, jaká zpráva bude, a tedy i hash zprávy.
const processMessage = async (message, signature) => {
Zpracujte zprávu a proveďte transakci, kterou kóduje.
// Získat veřejný klíč
const pubKey = await recoverPublicKey({
hash,
signature
})
Nyní, když spouštíme JavaScript na serveru, můžeme veřejný klíč získat tam, a ne na klientovi.
let noirResult
try {
noirResult = await noir.execute({
message,
signature: signature.slice(2,-2).match(/.{2}/g).map(x => `0x${x}`),
pubKeyX,
pubKeyY,
accounts: Accounts
})
noir.execute spouští program Noir. Parametry jsou ekvivalentní těm, které jsou uvedeny v Prover.toml (opens in a new tab). Všimněte si, že dlouhé hodnoty jsou poskytovány jako pole hexadecimálních řetězců (["0x60", "0xA7"]), nikoli jako jedna hexadecimální hodnota (0x60A7), jak to dělá Viem.
} catch (err) {
console.log(`Noir error: ${err}`)
throw Error("Invalid transaction, not processed")
}
Pokud dojde k chybě, zachyťte ji a poté předejte zjednodušenou verzi klientovi.
Accounts[fromAccountNumber].nonce++
Accounts[fromAccountNumber].balance -= amount
Accounts[toAccountNumber].balance += amount
Aplikujte transakci. Už jsme to udělali v kódu Noir, ale je snazší to udělat znovu zde, než odtud extrahovat výsledek.
let Accounts = [
{
address: "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266",
balance: 5000,
nonce: 0,
},
Počáteční struktura Accounts.
Fáze 3 - Chytré kontrakty Etherea
-
Zastavte procesy serveru a klienta.
-
Stáhněte si větev s chytrými kontrakty a ujistěte se, že máte všechny potřebné moduly.
git checkout 03-smart-contracts cd client npm install cd ../server npm install -
Spusťte
anvilv samostatném okně příkazového řádku. -
Vygenerujte ověřovací klíč a ověřovatele v Solidity, poté zkopírujte kód ověřovatele do projektu Solidity.
cd noir bb write_vk -b ./target/zkBank.json -o ./target --oracle_hash keccak bb write_solidity_verifier -k ./target/vk -o ./target/Verifier.sol cp target/Verifier.sol ../../smart-contracts/src -
Přejděte do chytrých kontraktů a nastavte proměnné prostředí tak, aby používaly blockchain
anvil.cd ../../smart-contracts export ETH_RPC_URL=http://localhost:8545 ETH_PRIVATE_KEY=ac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80 -
Nasaďte
Verifier.sola uložte adresu do proměnné prostředí.VERIFIER_ADDRESS=`forge create src/Verifier.sol:HonkVerifier --private-key $ETH_PRIVATE_KEY --optimize --broadcast | awk '/Deployed to:/ {print $3}'` echo $VERIFIER_ADDRESS -
Nasaďte kontrakt
ZkBank.ZKBANK_ADDRESS=`forge create ZkBank --private-key $ETH_PRIVATE_KEY --broadcast --constructor-args $VERIFIER_ADDRESS 0x199aa62af8c1d562a6ec96e66347bf3240ab2afb5d022c895e6bf6a5e617167b | awk '/Deployed to:/ {print $3}'` echo $ZKBANK_ADDRESSHodnota
0x199..67bje Pedersonův hash počátečního stavuAccounts. Pokud upravíte tento počáteční stav vserver/index.mjs, můžete spustit transakci a zobrazit počáteční hash nahlášený důkazem s nulovou znalostí. -
Spusťte server.
cd ../server npm run start -
Spusťte klienta v jiném okně příkazového řádku.
cd client npm run dev -
Spusťte několik transakcí.
-
Chcete-li ověřit, že se stav změnil onchain, restartujte proces serveru. Uvidíte, že
ZkBankjiž nepřijímá transakce, protože původní hodnota hashe v transakcích se liší od hodnoty hashe uložené onchain.Toto je očekávaný typ chyby.
ori@CryptoDocGuy:~/x/250911-zk-bank/server$ npm run start
server@1.0.0 start node --experimental-json-modules index.mjs
Listening on port 3000 Verification error: ContractFunctionExecutionError: The contract function "processTransaction" reverted with the following reason: Wrong old state hash
Contract Call: address: 0xe7f1725E7734CE288F8367e1Bb143E90bb3F0512 function: processTransaction(bytes _proof, bytes32[] _publicInputs) args: (0x0000000000000000000000000000000000000000000000042ab5d6d1986846cf00000000000000000000000000000000000000000000000b75c020998797da7800000000000000000000000000000000000000000000000
#### `server/index.mjs` \{#server-index-mjs-2\}
Změny v tomto souboru se týkají především vytvoření samotného důkazu a jeho odeslání onchain.
```js
import { exec } from 'child_process'
import util from 'util'
const execPromise = util.promisify(exec)
K vytvoření samotného důkazu k odeslání onchain musíme použít balíček Barretenberg (opens in a new tab). Tento balíček můžeme použít buď spuštěním rozhraní příkazového řádku (bb), nebo pomocí knihovny JavaScript, bb.js (opens in a new tab). Knihovna JavaScript je mnohem pomalejší než nativní spouštění kódu, takže zde používáme exec (opens in a new tab) pro použití příkazového řádku.
Všimněte si, že pokud se rozhodnete použít bb.js, musíte použít verzi, která je kompatibilní s verzí Noir, kterou používáte. V době psaní tohoto článku používá aktuální verze Noir (1.0.0-beta.11) bb.js verze 0.87.
const zkBankAddress = process.env.ZKBANK_ADDRESS || "0xe7f1725E7734CE288F8367e1Bb143E90bb3F0512"
Zdejší adresa je ta, kterou získáte, když začnete s čistým anvil a budete postupovat podle výše uvedených pokynů.
const walletClient = createWalletClient({
chain: anvil,
transport: http(),
account: privateKeyToAccount("0x2a871d0798f97d79848a013d4936a73bf4cc922c825d33c1cf7073dff6d409c6")
})
Tento soukromý klíč je jedním z výchozích předem financovaných účtů v anvil.
const generateProof = async (witness, fileID) => {
Vygenerujte důkaz pomocí spustitelného souboru bb.
const fname = `witness-${fileID}.gz`
await fs.writeFile(fname, witness)
Zapište svědka do souboru.
await execPromise(`bb prove -b ./noir/target/zkBank.json -w ${fname} -o ${fileID} --oracle_hash keccak --output_format fields`)
Skutečně vytvořte důkaz. Tento krok také vytvoří soubor s veřejnými proměnnými, ale ten nepotřebujeme. Tyto proměnné jsme již získali z noir.execute.
const proof = "0x" + JSON.parse(await fs.readFile(`./${fileID}/proof_fields.json`)).reduce((a,b) => a+b, "").replace(/0x/g, "")
Důkaz je JSON pole hodnot Field, z nichž každá je reprezentována jako hexadecimální hodnota. Musíme jej však odeslat v transakci jako jedinou hodnotu bytes, kterou Viem reprezentuje velkým hexadecimálním řetězcem. Zde změníme formát zřetězením všech hodnot, odstraněním všech 0x a následným přidáním jednoho na konec.
await execPromise(`rm -r ${fname} ${fileID}`)
return proof
}
Proveďte úklid a vraťte důkaz.
const processMessage = async (message, signature) => {
.
.
.
const publicFields = noirResult.returnValue.map(x=>'0x' + x.slice(2).padStart(64, "0"))
Veřejná pole musí být pole 32bajtových hodnot. Vzhledem k tomu, že jsme však potřebovali rozdělit hash transakce mezi dvě hodnoty Field, jeví se jako 16bajtová hodnota. Zde přidáme nuly, aby Viem pochopil, že se ve skutečnosti jedná o 32 bajtů.
const proof = await generateProof(noirResult.witness, `${fromAddress}-${nonce}`)
Každá adresa používá každou nonce pouze jednou, takže můžeme použít kombinaci fromAddress a nonce jako jedinečný identifikátor pro soubor svědka a výstupní adresář.
try {
await zkBank.write.processTransaction([
proof, publicFields])
} catch (err) {
console.log(`Verification error: ${err}`)
throw Error("Can't verify the transaction onchain")
}
.
.
.
}
Odešlete transakci do řetězce.
smart-contracts/src/ZkBank.sol
Toto je onchain kód, který přijímá transakci.
// SPDX-License-Identifier: MIT
pragma solidity >=0.8.21;
import {HonkVerifier} from "./Verifier.sol";
contract ZkBank {
HonkVerifier immutable myVerifier;
bytes32 currentStateHash;
constructor(address _verifierAddress, bytes32 _initialStateHash) {
currentStateHash = _initialStateHash;
myVerifier = HonkVerifier(_verifierAddress);
}
Onchain kód musí sledovat dvě proměnné: ověřovatele (samostatný kontrakt, který je vytvořen pomocí nargo) a hash aktuálního stavu.
event TransactionProcessed(
bytes32 indexed transactionHash,
bytes32 oldStateHash,
bytes32 newStateHash
);
Pokaždé, když se stav změní, emitujeme událost TransactionProcessed.
function processTransaction(
bytes calldata _proof,
bytes32[] calldata _publicFields
) public {
Tato funkce zpracovává transakce. Získá důkaz (jako bytes) a veřejné vstupy (jako pole bytes32) ve formátu, který ověřovatel vyžaduje (aby se minimalizovalo onchain zpracování a tím i náklady na gas).
require(_publicInputs[0] == currentStateHash,
"Wrong old state hash");
Důkaz s nulovou znalostí musí spočívat v tom, že se transakce změní z našeho aktuálního hashe na nový.
myVerifier.verify(_proof, _publicFields);
Zavolejte kontrakt ověřovatele k ověření důkazu s nulovou znalostí. Tento krok vrátí transakci zpět (revert), pokud je důkaz s nulovou znalostí chybný.
currentStateHash = _publicFields[1];
emit TransactionProcessed(
_publicFields[2]<<128 | _publicFields[3],
_publicFields[0],
_publicFields[1]
);
}
}
Pokud je vše v pořádku, aktualizujte hash stavu na novou hodnotu a emitujte událost TransactionProcessed.
Zneužití centralizovanou komponentou
Informační bezpečnost se skládá ze tří atributů:
- Důvěrnost, uživatelé nemohou číst informace, ke kterým nemají oprávnění.
- Integrita, informace nemohou být změněny jinak než oprávněnými uživateli a schváleným způsobem.
- Dostupnost, oprávnění uživatelé mohou systém používat.
V tomto systému je integrita zajištěna prostřednictvím důkazů s nulovou znalostí. Dostupnost je mnohem těžší zaručit a důvěrnost je nemožná, protože banka musí znát zůstatek každého účtu a všechny transakce. Neexistuje způsob, jak zabránit subjektu, který má informace, aby je sdílel.
Možná by bylo možné vytvořit skutečně důvěrnou banku pomocí skrytých adres (opens in a new tab), ale to je nad rámec tohoto článku.
Nepravdivé informace
Jedním ze způsobů, jak může server narušit integritu, je poskytnutí nepravdivých informací, když jsou vyžádána data (opens in a new tab).
Abychom to vyřešili, můžeme napsat druhý program v jazyce Noir, který přijme účty jako soukromý vstup a adresu, pro kterou jsou informace vyžádány, jako veřejný vstup. Výstupem je zůstatek a nonce dané adresy a hash účtů.
Tento důkaz samozřejmě nelze ověřit onchain, protože nechceme zveřejňovat nonce a zůstatky onchain. Může jej však ověřit klientský kód běžící v prohlížeči.
Vynucené transakce
Obvyklým mechanismem pro zajištění dostupnosti a prevenci cenzury na L2 jsou vynucené transakce (opens in a new tab). Vynucené transakce se však nedají kombinovat s důkazy s nulovou znalostí. Server je jediným subjektem, který může ověřovat transakce.
Můžeme upravit smart-contracts/src/ZkBank.sol, aby přijímal vynucené transakce a zabránil serveru ve změně stavu, dokud nebudou zpracovány. To nás však vystavuje jednoduchému útoku typu odepření služby (denial-of-service). Co když je vynucená transakce neplatná, a proto ji nelze zpracovat?
Řešením je mít důkaz s nulovou znalostí o tom, že vynucená transakce je neplatná. To dává serveru tři možnosti:
- Zpracovat vynucenou transakci a poskytnout důkaz s nulovou znalostí o tom, že byla zpracována, a nový hash stavu.
- Odmítnout vynucenou transakci a poskytnout kontraktu důkaz s nulovou znalostí o tom, že transakce je neplatná (neznámá adresa, špatná nonce nebo nedostatečný zůstatek).
- Ignorovat vynucenou transakci. Neexistuje způsob, jak server donutit, aby transakci skutečně zpracoval, ale znamená to, že celý systém je nedostupný.
Kauce za dostupnost
V reálné implementaci by pravděpodobně existoval nějaký ziskový motiv pro udržení serveru v chodu. Tuto motivaci můžeme posílit tím, že server složí kauci za dostupnost, kterou může kdokoli spálit, pokud vynucená transakce není zpracována v určité lhůtě.
Špatný kód v jazyce Noir
Normálně, abychom lidi přesvědčili k důvěře v chytrý kontrakt, nahrajeme zdrojový kód do prohlížeče bloků (opens in a new tab). V případě důkazů s nulovou znalostí to však nestačí.
Verifier.sol obsahuje ověřovací klíč, který je funkcí programu v jazyce Noir. Tento klíč nám však neříká, jaký program v jazyce Noir to byl. Abyste měli skutečně důvěryhodné řešení, musíte nahrát program v jazyce Noir (a verzi, která jej vytvořila). Jinak by důkazy s nulovou znalostí mohly odrážet jiný program, takový, který má zadní vrátka.
Dokud nám prohlížeče bloků nezačnou umožňovat nahrávat a ověřovat programy v jazyce Noir, měli byste to dělat sami (nejlépe na IPFS). Pokročilí uživatelé si pak budou moci stáhnout zdrojový kód, sami jej zkompilovat, vytvořit Verifier.sol a ověřit, že je identický s tím onchain.
Závěr
Aplikace typu Plasma vyžadují centralizovanou komponentu jako úložiště informací. To otevírá potenciální zranitelnosti, ale na oplátku nám to umožňuje zachovat soukromí způsoby, které na samotném blockchainu nejsou dostupné. Pomocí důkazů s nulovou znalostí můžeme zajistit integritu a případně učinit ekonomicky výhodným pro toho, kdo provozuje centralizovanou komponentu, aby udržoval její dostupnost.
Zde najdete více z mé práce (opens in a new tab).
Poděkování
- Josh Crites si přečetl koncept tohoto článku a pomohl mi se záludným problémem v Noir.
Za veškeré zbývající chyby nesu odpovědnost já.