Ruka hadi kwenye maudhui makuu

Andika Njozi maalum ya programu ambayo inahifadhi faragha

zero-knowledge
seva
offchain
faragha
Ya hali ya juu
Ori Pomerantz
15 Oktoba 2025
30 soma ndani ya dakika

Utangulizi

Tofauti na unda-mpya, njozi hutumia Mtandao Mkuu wa Ethereum kwa uadilifu, lakini si upatikanaji. Katika makala haya, tunaandika programu inayofanya kazi kama njozi, huku Ethereum ikihakikisha uadilifu (hakuna mabadiliko yasiyoidhinishwa) lakini si upatikanaji (sehemu ya kati inaweza kushuka na kuzima mfumo mzima).

Programu tunayoandika hapa ni benki inayohifadhi faragha. Anwani tofauti zina akaunti zenye salio, na zinaweza kutuma pesa (ETH) kwa akaunti zingine. Benki hutuma hashi za hali (akaunti na salio zake) na miamala, lakini huweka salio halisi nje ya mnyororo ambapo zinaweza kubaki za faragha.

Ubunifu

Huu si mfumo ulio tayari kwa uzalishaji, bali ni zana ya kufundishia. Kwa hivyo, imeandikwa kwa mawazo kadhaa ya kurahisisha.

  • Dimbwi la akaunti zisizobadilika. Kuna idadi maalum ya akaunti, na kila akaunti ni ya anwani iliyoamuliwa mapema. Hii hufanya mfumo kuwa rahisi zaidi kwa sababu ni vigumu kushughulikia miundo ya data ya ukubwa tofauti katika ithibati za zero-knowledge. Kwa mfumo ulio tayari kwa uzalishaji, tunaweza kutumia mzizi wa Merkle kama hashi ya hali na kutoa ithibati za Merkle kwa salio zinazohitajika.

  • Hifadhi ya kumbukumbu. Kwenye mfumo wa uzalishaji, tunahitaji kuandika salio zote za akaunti kwenye diski ili kuzihifadhi iwapo kutakuwa na uanzishaji upya. Hapa, ni sawa ikiwa habari itapotea tu.

  • Uhamisho pekee. Mfumo wa uzalishaji utahitaji njia ya kuweka mali kwenye benki na kuzitoa. Lakini madhumuni hapa ni kuonyesha tu dhana, kwa hivyo benki hii imepunguzwa kwa uhamisho.

Ithibati za zero-knowledge

Katika kiwango cha msingi, uthibitisho wa zero-knowledge unaonyesha kwamba mthibitishaji anajua data fulani, Dataprivate kiasi kwamba kuna uhusiano Relationship kati ya data fulani ya umma, Datapublic, na Dataprivate. Mthibitishaji anajua Uhusiano na Dataumma.

Ili kuhifadhi faragha, tunahitaji hali na miamala iwe ya faragha. Lakini ili kuhakikisha uadilifu, tunahitaji hashi ya kriptografia (opens in a new tab) ya hali iwe ya umma. Ili kuthibitisha kwa watu wanaowasilisha miamala kwamba miamala hiyo ilifanyika kweli, tunahitaji pia kuchapisha hashi za miamala.

Katika hali nyingi, Dataprivate ni ingizo kwa programu ya uthibitisho wa zero-knowledge, na Datapublic ni tokeo.

Sehemu hizi katika Dataprivate:

  • Halin, hali ya zamani
  • Halin+1, hali mpya
  • Muamala, muamala unaobadilika kutoka hali ya zamani hadi hali mpya. Muamala huu unahitaji kujumuisha sehemu hizi:
    • Anwani lengwa inayopokea uhamisho
    • Kiasi kinachohamishwa
    • Nonce ili kuhakikisha kila muamala unaweza kuchakatwa mara moja tu. Anwani ya chanzo haihitaji kuwa katika muamala, kwa sababu inaweza kupatikana kutoka kwa saini.
  • Sahihi, sahihi iliyoidhinishwa kutekeleza muamala. Katika kesi yetu, anwani pekee iliyoidhinishwa kutekeleza muamala ni anwani ya chanzo. Kwa sababu mfumo wetu wa zero-knowledge hufanya kazi jinsi unavyofanya, tunahitaji pia ufunguo wa umma wa akaunti, pamoja na saini ya Ethereum.

Hizi ni sehemu katika Datapublic:

  • Hashi(Halin) hashi ya hali ya zamani
  • Hashi(Halin+1) hashi ya hali mpya
  • Hashi(Muamala) hashi ya muamala unaobadilisha hali kutoka Halin hadi Halin+1.

Uhusiano huangalia hali kadhaa:

  • Hashi za umma ni hashi sahihi za sehemu za faragha.
  • Muamala, unapotumika kwa hali ya zamani, husababisha hali mpya.
  • Sahihi hutoka kwa anwani chanzo ya muamala.

Kwa sababu ya sifa za vipengele vya hashi ya kriptografia, kuthibitisha masharti haya kunatosha kuhakikisha uadilifu.

Miundo ya data

Muundo mkuu wa data ni hali inayoshikiliwa na seva. Kwa kila akaunti, seva hufuatilia salio la akaunti na nonce (opens in a new tab), inayotumika kuzuia mashambulizi ya kurudia (opens in a new tab).

Vipengele

Mfumo huu unahitaji vipengele viwili:

  • Seva inayopokea miamala, kuichakata, na kuchapisha hashi kwenye mnyororo pamoja na ithibati za zero-knowledge.
  • Mkataba-erevu unaohifadhi hashi na kuthibitisha ithibati za zero-knowledge ili kuhakikisha mabadiliko ya hali ni halali.

Mtiririko wa data na udhibiti

Hizi ni njia ambazo vipengele mbalimbali huwasiliana ili kuhamisha kutoka akaunti moja hadi nyingine.

  1. Kivinjari cha wavuti huwasilisha muamala uliosainiwa unaoomba uhamisho kutoka kwa akaunti ya mtia sahihi hadi akaunti tofauti.

  2. Seva inathibitisha kuwa muamala ni halali:

    • Mtia sahihi ana akaunti katika benki yenye salio la kutosha.
    • Mpokeaji ana akaunti katika benki.
  3. Seva hukokotoa hali mpya kwa kutoa kiasi kilichohamishwa kutoka kwa salio la mtia sahihi na kukiongeza kwenye salio la mpokeaji.

  4. Seva hukokotoa uthibitisho wa zero-knowledge kwamba mabadiliko ya hali ni halali.

  5. Seva huwasilisha muamala kwa Ethereum unaojumuisha:

    • Hashi mpya ya hali
    • Hashi ya muamala (ili mtumaji wa muamala aweze kujua kuwa imechakatwa)
    • Uthibitisho wa zero-knowledge unaothibitisha mpito kwa hali mpya ni halali
  6. Mkataba-erevu huthibitisha uthibitisho wa zero-knowledge.

  7. Ikiwa uthibitisho wa zero-knowledge umekubaliwa, mkataba-erevu hufanya vitendo hivi:

    • Sasisha hashi ya hali ya sasa hadi hashi mpya ya hali
    • Toa ingizo la kumbukumbu na hashi mpya ya hali na hashi ya muamala

Zana

Kwa msimbo wa upande wa mteja, tutatumia Vite (opens in a new tab), React (opens in a new tab), Viem (opens in a new tab), na Wagmi (opens in a new tab). Hizi ni zana za kawaida za tasnia; ikiwa huzifahamu, unaweza kutumia mafunzo haya.

Sehemu kubwa ya seva imeandikwa kwa JavaScript kwa kutumia Nodi (opens in a new tab). Sehemu ya zero-knowledge imeandikwa katika Noir (opens in a new tab). Tunahitaji toleo la 1.0.0-beta.10, kwa hivyo baada ya kusakinisha Noir kama ilivyoelekezwa (opens in a new tab), endesha:

noirup -v 1.0.0-beta.10

Mnyororo wa bloku tunaotumia ni anvil, mnyororo wa bloku wa majaribio wa ndani ambao ni sehemu ya Foundry (opens in a new tab).

Utekelezaji

Kwa sababu huu ni mfumo changamano, tutautekeleza kwa hatua.

Hatua ya 1 - Zero knowledge ya mikono

Kwa hatua ya kwanza, tutasaini muamala katika kivinjari na kisha kutoa habari kwa mikono kwa uthibitisho wa zero-knowledge. Msimbo wa zero-knowledge unatarajia kupata taarifa hiyo katika server/noir/Prover.toml (imeandikwa hapa (opens in a new tab)).

Ili kuiona ikifanya kazi:

  1. Hakikisha umesakinisha Nodi (opens in a new tab) na Noir (opens in a new tab). Ikiwezekana, zisakishe kwenye mfumo wa UNIX kama vile macOS, Linux, au WSL (opens in a new tab).

  2. Pakua msimbo wa hatua ya 1 na uanze seva ya wavuti ili kuhudumia msimbo wa mteja.

    git clone https://github.com/qbzzt/250911-zk-bank.git -b 01-manual-zk
    cd 250911-zk-bank
    cd client
    npm install
    npm run dev
    

    Sababu unahitaji seva ya wavuti hapa ni kwamba, ili kuzuia aina fulani za ulaghai, mikoba mingi (kama vile MetaMask) haikubali faili zinazotolewa moja kwa moja kutoka kwenye diski

  3. Fungua kivinjari na mkoba.

  4. Katika mkoba, weka nenosiri jipya. Kumbuka kuwa hii itafuta nenosiri lako lililopo, kwa hivyo hakikisha una nakala rudufu.

    Nenosiri ni test test test test test test test test test test test junk, nenosiri chaguo-msingi la majaribio la anvil.

  5. Vinjari msimbo wa upande wa mteja (opens in a new tab).

  6. Unganisha kwenye mkoba na uchague akaunti yako lengwa na kiasi.

  7. Bofya Saini na utie sahihi kwenye muamala.

  8. Chini ya kichwa cha Prover.toml, utapata maandishi. Badilisha server/noir/Prover.toml na maandishi hayo.

  9. Tekeleza uthibitisho wa zero-knowledge.

    cd ../server/noir
    nargo execute
    

    Tokeo linapaswa kuwa sawa na

    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. Linganisha thamani mbili za mwisho na hashi unayoona kwenye kivinjari cha wavuti ili kuona ikiwa ujumbe umehashiwa ipasavyo.

server/noir/Prover.toml

Faili hii (opens in a new tab) inaonyesha umbizo la maelezo linalotarajiwa na Noir.

message="send 0x70997970C51812dc3A010C7d01b50e0d17dc79C8 500 finney (milliEth) 0                             "

Ujumbe uko katika umbizo la maandishi, ambalo hurahisisha mtumiaji kuelewa (jambo ambalo ni muhimu wakati wa kusaini) na kwa msimbo wa Noir kuchanganua. Kiasi hicho kimetajwa katika finneys ili kuwezesha uhamisho wa sehemu kwa upande mmoja, na kusomeka kwa urahisi kwa upande mwingine. Nambari ya mwisho ni nonce (opens in a new tab).

Kamba ina urefu wa herufi 100. Ithibati za zero-knowledge hazishughulikii vizuri data ya ukubwa unaobadilika, kwa hivyo mara nyingi ni muhimu kuongeza data.

pubKeyX=["0x83",...,"0x75"]
pubKeyY=["0x35",...,"0xa5"]
signature=["0xb1",...,"0x0d"]

Vigezo hivi vitatu ni safu za baiti za ukubwa usiobadilika.

Hii ndiyo njia ya kubainisha safu ya miundo. Kwa kila ingizo, tunabainisha anwani, salio (katika milliETH a.k.a. finney (opens in a new tab)), na thamani inayofuata ya nonce.

client/src/Transfer.tsx

Faili hii (opens in a new tab) hutekeleza uchakataji wa upande wa mteja na kutoa faili ya server/noir/Prover.toml (ile inayojumuisha vigezo vya zero-knowledge).

Hapa kuna maelezo ya sehemu za kuvutia zaidi.

export default attrs =>  {

Kazi hii huunda kijenzi cha React cha Transfer, ambacho faili zingine zinaweza kuagiza.

  const accounts = [
    "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266",
    "0x70997970C51812dc3A010C7d01b50e0d17dc79C8",
    "0x3C44CdDdB6a900fa2b585dd299e03d12FA4293BC",
    "0x90F79bf6EB2c4f870365E785982E1f101E93b906",
    "0x15d34AAf54267DB7D7c367839AAf71A00a2C6A65",
  ]

Hizi ni anwani za akaunti, anwani zilizoundwa na test ... test junk passphrase. Ikiwa unataka kutumia anwani zako mwenyewe, rekebisha tu ufafanuzi huu.

  const account = useAccount()
  const wallet = createWalletClient({
    transport: custom(window.ethereum!)
  })

Kulabu hizi za Wagmi (opens in a new tab) hutuwezesha kufikia maktaba ya viem (opens in a new tab) na mkoba.

  const message = `send ${toAccount} ${ethAmount*1000} finney (milliEth) ${nonce}`.padEnd(100, " ")

Huu ni ujumbe, uliowekwa nafasi. Kila wakati moja ya vigezo vya useState (opens in a new tab) inapobadilika, kijenzi huchorwa upya na message husasishwa.

  const sign = async () => {

Kazi hii inaitwa mtumiaji anapobofya kitufe cha Saini. Ujumbe husasishwa kiotomatiki, lakini saini inahitaji idhini ya mtumiaji katika mkoba, na hatutaki kuiomba isipokuwa inahitajika.

    const signature = await wallet.signMessage({
        account: fromAccount,
        message,
    })

Uliza mkoba kusaini ujumbe (opens in a new tab).

    const hash = hashMessage(message)

Pata hashi ya ujumbe. Inasaidia kumpa mtumiaji kwa ajili ya utatuzi (wa msimbo wa Noir).

    const pubKey = await recoverPublicKey({
        hash,
        signature
    })

Pata ufunguo wa umma (opens in a new tab). Hii inahitajika kwa kazi ya Noir ecrecover (opens in a new tab).

    setSignature(signature)
    setHash(hash)
    setPubKey(pubKey)

Weka vigezo vya hali. Kufanya hivi huchora upya kijenzi (baada ya kazi ya sign kuondoka) na huonyesha mtumiaji thamani zilizosasishwa.

    let proverToml = `

Maandishi ya Prover.toml.

message="${message}"

pubKeyX=${hexToArray(pubKey.slice(4,4+2*32))}
pubKeyY=${hexToArray(pubKey.slice(4+2*32))}

Viem inatupatia ufunguo wa umma kama kamba ya heksadesimali ya baiti 65. Baiti ya kwanza ni 0x04, alama ya toleo. Hii inafuatiwa na baiti 32 za x ya ufunguo wa umma na kisha baiti 32 za y ya ufunguo wa umma.

Hata hivyo, Noir inatarajia kupata taarifa hii kama safu mbili za baiti, moja kwa x na moja kwa y. Ni rahisi zaidi kuichanganua hapa kwa mteja kuliko kama sehemu ya uthibitisho wa zero-knowledge.

Kumbuka kuwa hii ni mazoezi mazuri katika zero-knowledge kwa ujumla. Msimbo ndani ya uthibitisho wa zero-knowledge ni ghali, kwa hivyo uchakataji wowote unaoweza kufanywa nje ya uthibitisho wa zero-knowledge unapaswa kufanywa nje ya uthibitisho wa zero-knowledge.

signature=${hexToArray(signature.slice(2,-2))}

Sahihi pia hutolewa kama kamba ya heksadesimali ya baiti 65. Hata hivyo, baiti ya mwisho ni muhimu tu ili kupata ufunguo wa umma. Kwa kuwa ufunguo wa umma utakuwa tayari umetolewa kwa msimbo wa Noir, hatuitaji ili kuthibitisha saini, na msimbo wa Noir hauihitaji.

${accounts.map(accountInProverToml).reduce((a,b) => a+b, "")}
`

Toa akaunti.

    setProverToml(proverToml)
  }

  return (
    <>
        <h2>Uhamisho</h2>

Hii ni umbizo la HTML (kwa usahihi zaidi, JSX (opens in a new tab)) la kijenzi.

server/noir/src/main.nr

Faili hii (opens in a new tab) ni msimbo halisi wa zero-knowledge.

use std::hash::pedersen_hash;

Hashi ya Pedersen (opens in a new tab) hutolewa na maktaba ya kawaida ya Noir (opens in a new tab). Ithibati za zero-knowledge mara nyingi hutumia kipengele hiki cha hashi. Ni rahisi zaidi kukokotoa ndani ya mizunguko ya hesabu (opens in a new tab) ikilinganishwa na vipengele vya kawaida vya hashi.

use keccak256::keccak256;
use dep::ecrecover;

Kazi hizi mbili ni maktaba za nje, zilizofafanuliwa katika Nargo.toml (opens in a new tab). Ni hasa kile wanachoitwa, kazi ambayo inakokotoa hashi ya keccak256 (opens in a new tab) na kazi ambayo inathibitisha saini za Ethereum na kurejesha anwani ya Ethereum ya mtia saini.

global ACCOUNT_NUMBER : u32 = 5;

Noir imechochewa na Rust (opens in a new tab). Vigezo, kwa chaguo-msingi, ni vidumu. Hivi ndivyo tunavyofafanua vidumu vya usanidi wa kimataifa. Hasa, ACCOUNT_NUMBER ni idadi ya akaunti tunazohifadhi.

Aina za data zilizoitwa u<number> ni idadi hiyo ya biti, zisizo na saini. Aina pekee zinazotumika ni u8, u16, u32, u64, na u128.

global FLAT_ACCOUNT_FIELDS : u32 = 2;

Kigezo hiki kinatumika kwa hashi ya Pedersen ya akaunti, kama ilivyoelezwa hapo chini.

global MESSAGE_LENGTH : u32 = 100;

Kama ilivyoelezwa hapo juu, urefu wa ujumbe umewekwa. Imebainishwa hapa.

global ASCII_MESSAGE_LENGTH : [u8; 3] = [0x31, 0x30, 0x30];
global HASH_BUFFER_SIZE : u32 = 26+3+MESSAGE_LENGTH;

Sahihi za EIP-191 (opens in a new tab) zinahitaji bafa yenye kiambishi awali cha baiti 26, ikifuatiwa na urefu wa ujumbe katika ASCII, na hatimaye ujumbe wenyewe.

struct Account {
    balance: u128,
    address: Field,
    nonce: u32,
}

Taarifa tunayohifadhi kuhusu akaunti. Field (opens in a new tab) ni nambari, kwa kawaida hadi biti 253, ambayo inaweza kutumika moja kwa moja katika mzunguko wa hesabu (opens in a new tab) unaotekeleza uthibitisho wa zero-knowledge. Hapa tunatumia Field kuhifadhi anwani ya Ethereum ya biti 160.

struct TransferTxn {
    from: Field,
    to: Field,
    amount: u128,
    nonce: u32
}

Taarifa tunayohifadhi kwa muamala wa uhamisho.

fn flatten_account(account: Account) -> [Field; FLAT_ACCOUNT_FIELDS] {

Ufafanuzi wa kazi. Kigezo ni habari ya Account. Matokeo ni safu ya vigezo vya Field, ambavyo urefu wake ni FLAT_ACCOUNT_FIELDS

    let flat = [
        account.address,
        ((account.balance << 32) + account.nonce.into()).into(),
    ];

Thamani ya kwanza katika safu ni anwani ya akaunti. Ya pili inajumuisha salio na nonce. Miito ya .into() hubadilisha nambari kuwa aina ya data inayohitajika. account.nonce ni thamani ya u32, lakini ili kuiongeza kwa account.balance << 32, thamani ya u128, inahitaji kuwa u128. Hiyo ndiyo .into() ya kwanza. Ya pili inabadilisha matokeo ya u128 kuwa Sehemu ili itoshee kwenye safu.

    flat
}

Katika Noir, kazi zinaweza kurudisha thamani mwishoni tu (hakuna kurudi mapema). Ili kubainisha thamani ya kurudi, unaitathmini kabla tu ya mabano ya kufunga ya kazi.

fn flatten_accounts(accounts: [Account; ACCOUNT_NUMBER]) -> [Field; FLAT_ACCOUNT_FIELDS*ACCOUNT_NUMBER] {

Kazi hii hugeuza safu ya akaunti kuwa safu ya Sehemu, ambayo inaweza kutumika kama ingizo kwa Hashi ya Petersen.

    let mut flat: [Field; FLAT_ACCOUNT_FIELDS*ACCOUNT_NUMBER] = [0; FLAT_ACCOUNT_FIELDS*ACCOUNT_NUMBER];

Hivi ndivyo unavyobainisha kigezo kinachoweza kubadilishwa, yaani, sio kidumu. Vigezo katika Noir lazima viwe na thamani kila wakati, kwa hivyo tunaanzisha kigezo hiki kwa sifuri zote.

    for i in 0..ACCOUNT_NUMBER {

Hii ni kitanzi cha for. Kumbuka kuwa mipaka ni vidumu. Vitanzi vya Noir lazima mipaka yao ijulikane wakati wa kukusanya. Sababu ni kwamba mizunguko ya hesabu haitumii udhibiti wa mtiririko. Wakati wa kuchakata kitanzi cha for, mkusanyaji huweka tu msimbo ndani yake mara nyingi, moja kwa kila mzunguko.

Mwishowe, tumefika kwenye kazi ambayo inahashi safu ya akaunti.

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;
        }
    }

Kazi hii hupata akaunti yenye anwani maalum. Kazi hii haingekuwa na ufanisi katika msimbo wa kawaida kwa sababu inarudia akaunti zote, hata baada ya kupata anwani.

Hata hivyo, katika ithibati za zero-knowledge, hakuna udhibiti wa mtiririko. Ikiwa tutahitaji kuangalia hali, lazima tuiangalie kila wakati.

Jambo kama hilo hufanyika kwa taarifa za if. Taarifa ya if katika kitanzi hapo juu inatafsiriwa kuwa taarifa hizi za hisabati.

conditionresult = accounts[i].address == address // moja ikiwa ni sawa, sifuri vinginevyo

accountnew = conditionresult*i + (1-conditionresult)*accountold

    assert (account < ACCOUNT_NUMBER, f"{address} does not have an account");

    account
}

Kazi ya assert (opens in a new tab) husababisha uthibitisho wa zero-knowledge kuharibika ikiwa madai ni ya uongo. Katika kesi hii, ikiwa hatuwezi kupata akaunti yenye anwani husika. Ili kuripoti anwani, tunatumia kamba ya umbizo (opens in a new tab).

fn apply_transfer_txn(accounts: [Account; ACCOUNT_NUMBER], txn: TransferTxn) -> [Account; ACCOUNT_NUMBER] {

Kazi hii hutumia muamala wa uhamisho na kurudisha safu mpya ya akaunti.

    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);

Hatuwezi kufikia vipengele vya muundo ndani ya kamba ya umbizo katika Noir, kwa hivyo tunaunda nakala inayoweza kutumika.

    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}");

Hizi ni hali mbili ambazo zinaweza kufanya muamala kuwa batili.

    let mut newAccounts = accounts;

    newAccounts[from].balance -= txn.amount;
    newAccounts[from].nonce += 1;
    newAccounts[to].balance += txn.amount;

    newAccounts
}

Unda safu mpya ya akaunti na kisha uirudishe.

fn readAddress(messageBytes: [u8; MESSAGE_LENGTH]) -> Field

Kazi hii inasoma anwani kutoka kwa ujumbe.

{
    let mut result : Field = 0;

    for i in 7..47 {

Anwani daima ni baiti 20 (a.k.a. Tarakimu 40 za heksadesimali) kwa urefu, na huanza kwa herufi #7.

Soma kiasi na nonce kutoka kwa ujumbe.

{
    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;

Katika ujumbe, nambari ya kwanza baada ya anwani ni kiasi cha finney (a.k.a. elfu ya ETH) ya kuhamisha. Nambari ya pili ni nonce. Maandishi yoyote kati yao yanapuuzwa.

Kurudisha tuple (opens in a new tab) ni njia ya Noir ya kurudisha thamani nyingi kutoka kwa kazi.

Kazi hii hubadilisha ujumbe kuwa baiti, kisha hubadilisha kiasi kuwa TransferTxn.

// The equivalent to Viem's hashMessage
// https://viem.sh/docs/utilities/hashMessage#hashmessage
fn hashMessage(message: str<MESSAGE_LENGTH>) -> [u8;32] {

Tuliweza kutumia Hashi ya Pedersen kwa akaunti kwa sababu zimehashiwa tu ndani ya uthibitisho wa zero-knowledge. Hata hivyo, katika msimbo huu tunahitaji kuangalia saini ya ujumbe, ambayo hutolewa na kivinjari. Kwa hilo, tunahitaji kufuata umbizo la kusaini la Ethereum katika EIP 191 (opens in a new tab). Hii ina maana tunahitaji kuunda bafa iliyounganishwa yenye kiambishi awali cha kawaida, urefu wa ujumbe katika ASCII, na ujumbe wenyewe, na kutumia keccak256 ya kawaida ya Ethereum kuihashi.

Ili kuepuka hali ambapo programu inamwomba mtumiaji kusaini ujumbe ambao unaweza kutumika kama muamala au kwa madhumuni mengine, EIP 191 inabainisha kuwa ujumbe wote uliosainiwa huanza na herufi 0x19 (sio herufi halali ya ASCII) ikifuatiwa na Ethereum Signed Message: na mstari mpya.

Shughulikia urefu wa ujumbe hadi 999 na ushindwe ikiwa ni mkubwa zaidi. Niliongeza msimbo huu, ingawa urefu wa ujumbe ni wa kudumu, kwa sababu inafanya iwe rahisi kuubadilisha. Kwenye mfumo wa uzalishaji, labda ungechukulia tu MESSAGE_LENGTH haibadiliki kwa ajili ya utendaji bora.

    keccak256::keccak256(buffer, HASH_BUFFER_SIZE)
}

Tumia kazi ya kawaida ya Ethereum keccak256.

fn signatureToAddressAndHash(
        message: str<MESSAGE_LENGTH>, 
        pubKeyX: [u8; 32],
        pubKeyY: [u8; 32],
        signature: [u8; 64]
    ) -> (Field, Field, Field)   // address, first 16 bytes of hash, last 16 bytes of hash        
{

Kazi hii inathibitisha saini, ambayo inahitaji hashi ya ujumbe. Kisha inatupatia anwani iliyoisaini na hashi ya ujumbe. Hashi ya ujumbe hutolewa katika thamani mbili za Sehemu kwa sababu hizo ni rahisi kutumia katika programu iliyobaki kuliko safu ya baiti.

Tunahitaji kutumia thamani mbili za Sehemu kwa sababu hesabu za sehemu hufanywa modulo (opens in a new tab) nambari kubwa, lakini nambari hiyo kwa kawaida ni chini ya biti 256 (vinginevyo ingekuwa vigumu kufanya hesabu hizo katika 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();
    }

Bainisha hash1 na hash2 kama vigezo vinavyoweza kubadilishwa, na andika hashi ndani yao baiti kwa baiti.

    (
        ecrecover::ecrecover(pubKeyX, pubKeyY, signature, hash), 

Hii ni sawa na ecrecover ya Solidity (opens in a new tab), na tofauti mbili muhimu:

  • Ikiwa saini si halali, simu inashindwa kudai na programu inasitishwa.
  • Ingawa ufunguo wa umma unaweza kupatikana kutoka kwa saini na hashi, huu ni uchakataji unaoweza kufanywa nje na, kwa hivyo, haifai kufanya ndani ya uthibitisho wa zero-knowledge. Mtu akijaribu kutudanganya hapa, uthibitishaji wa saini utashindwa.

Mwishowe, tunafikia kazi ya main. Tunahitaji kuthibitisha kwamba tuna muamala unaobadilisha kihalali hashi ya akaunti kutoka thamani ya zamani hadi mpya. Tunahitaji pia kuthibitisha kwamba ina hashi hii maalum ya muamala ili mtu aliyeituma ajue muamala wake umeshachakatwa.

{
    let mut txn = readTransferTxn(message);

Tunahitaji txn iweze kubadilika kwa sababu hatusomi anwani ya kutoka kwenye ujumbe, tunaisoma kutoka kwenye saini.

Hatua ya 2 - Kuongeza seva

Katika hatua ya pili, tunaongeza seva inayopokea na kutekeleza miamala ya uhamisho kutoka kwa kivinjari.

Ili kuiona ikifanya kazi:

  1. Simamisha Vite ikiwa inaendeshwa.

  2. Pakua tawi linalojumuisha seva na uhakikishe una moduli zote muhimu.

    git checkout 02-add-server
    cd client
    npm install
    cd ../server
    npm install
    

    Hakuna haja ya kukusanya msimbo wa Noir, ni msimbo uleule uliotumia kwa hatua ya 1.

  3. Anzisha seva.

    npm run start
    
  4. Katika dirisha tofauti la mstari wa amri, endesha Vite ili kuhudumia msimbo wa kivinjari.

    cd client
    npm run dev
    
  5. Vinjari msimbo wa mteja kwenye http://localhost:5173 (opens in a new tab)

  6. Kabla ya kutoa muamala, unahitaji kujua nonce, pamoja na kiasi unachoweza kutuma. Ili kupata habari hii, bofya Sasisha data ya akaunti na utie sahihi kwenye ujumbe.

    Tuna mtanziko hapa. Kwa upande mmoja, hatutaki kusaini ujumbe unaoweza kutumiwa tena (shambulio la kurudia (opens in a new tab)), ndiyo sababu tunataka nonce kwanza. Hata hivyo, bado hatuna nonce. Suluhisho ni kuchagua nonce ambayo inaweza kutumika mara moja tu na ambayo tayari tunayo pande zote mbili, kama vile wakati wa sasa.

    Tatizo na suluhisho hili ni kwamba wakati huenda usisawazishwe kikamilifu. Kwa hivyo badala yake, tunasaini thamani inayobadilika kila dakika. Hii inamaanisha kuwa dirisha letu la hatari kwa mashambulizi ya kurudia ni dakika moja tu. Kwa kuzingatia kwamba katika uzalishaji ombi lililosainiwa litalindwa na TLS, na kwamba upande mwingine wa handaki---seva---inaweza tayari kufichua salio na nonce (inabidi iwajue ili kufanya kazi), hii ni hatari inayokubalika.

  7. Mara kivinjari kinapopata salio na nonce, kinaonyesha fomu ya uhamisho. Chagua anwani lengwa na kiasi na ubofye Hamisha. Saini ombi hili.

  8. Ili kuona uhamisho, ama Sasisha data ya akaunti au angalia kwenye dirisha ambapo unaendesha seva. Seva huweka kumbukumbu ya hali kila inapobadilika.

server/index.mjs

Faili hii (opens in a new tab) ina mchakato wa seva, na inaingiliana na msimbo wa Noir kwenye main.nr (opens in a new tab). Hapa kuna maelezo ya sehemu za kuvutia.

import { Noir } from '@noir-lang/noir_js'

Maktaba ya noir.js (opens in a new tab) inaunganisha kati ya msimbo wa JavaScript na msimbo wa Noir.

const circuit = JSON.parse(await fs.readFile("./noir/target/zkBank.json"))
const noir = new Noir(circuit)

Pakia mzunguko wa hesabu---programu ya Noir iliyokusanywa tuliyoiumba katika hatua ya awali---na andaa kuitekeleza.

// We only provide account information in return to a signed request
const accountInformation = async signature => {
    const fromAddress = await recoverAddress({
        hash: hashMessage("Get account data " + Math.floor((new Date().getTime())/60000)),
        signature
    })

Ili kutoa maelezo ya akaunti, tunahitaji tu saini. Sababu ni kwamba tayari tunajua ujumbe utakuwa nini, na kwa hiyo hashi ya ujumbe.

const processMessage = async (message, signature) => {

Chakata ujumbe na utekeleze muamala unaouweka.

    // Get the public key
    const pubKey = await recoverPublicKey({
        hash,
        signature
    })

Sasa kwa kuwa tunaendesha JavaScript kwenye seva, tunaweza kupata ufunguo wa umma hapo badala ya kwa mteja.

noir.execute huendesha programu ya Noir. Vigezo ni sawa na vile vilivyotolewa katika Prover.toml (opens in a new tab). Kumbuka kuwa thamani ndefu hutolewa kama safu ya kamba za heksadesimali (["0x60", "0xA7"]), sio kama thamani moja ya heksadesimali (0x60A7), jinsi Viem inavyofanya.

    } catch (err) {
        console.log(`Noir error: ${err}`)
        throw Error("Invalid transaction, not processed")
    }

Ikiwa kuna hitilafu, ikamate na kisha upeleke toleo lililorahisishwa kwa mteja.

    Accounts[fromAccountNumber].nonce++
    Accounts[fromAccountNumber].balance -= amount
    Accounts[toAccountNumber].balance += amount

Tekeleza muamala. Tayari tulifanya hivyo katika msimbo wa Noir, lakini ni rahisi kuifanya tena hapa badala ya kutoa matokeo kutoka hapo.

let Accounts = [
    {
        address: "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266",
        balance: 5000,
        nonce: 0,
    },

Muundo wa awali wa Akaunti.

Hatua ya 3 - Mikataba-erevu ya Ethereum

  1. Simamisha michakato ya seva na mteja.

  2. Pakua tawi lenye mikataba-erevu na uhakikishe una moduli zote muhimu.

    git checkout 03-smart-contracts
    cd client
    npm install
    cd ../server
    npm install
    
  3. Endesha anvil katika dirisha tofauti la mstari wa amri.

  4. Tengeneza ufunguo wa uthibitishaji na kithibitishaji cha solidity, kisha nakili msimbo wa kithibitishaji kwenye mradi wa 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
    
  5. Nenda kwenye mikataba-erevu na weka vigezo vya mazingira ili kutumia mnyororo wa bloku wa anvil.

    cd ../../smart-contracts
    export ETH_RPC_URL=http://localhost:8545
    ETH_PRIVATE_KEY=ac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80
    
  6. Pakia Verifier.sol na uhifadhi anwani katika kigezo cha mazingira.

    VERIFIER_ADDRESS=`forge create src/Verifier.sol:HonkVerifier --private-key $ETH_PRIVATE_KEY --optimize --broadcast | awk '/Deployed to:/ {print $3}'`
    echo $VERIFIER_ADDRESS
    
  7. Pakia mkataba wa ZkBank.

    ZKBANK_ADDRESS=`forge create ZkBank --private-key $ETH_PRIVATE_KEY --broadcast --constructor-args $VERIFIER_ADDRESS 0x199aa62af8c1d562a6ec96e66347bf3240ab2afb5d022c895e6bf6a5e617167b | awk '/Deployed to:/ {print $3}'`
    echo $ZKBANK_ADDRESS
    

    Thamani ya 0x199..67b ni hashi ya Pederson ya hali ya awali ya Accounts. Ukirekebisha hali hii ya awali katika server/index.mjs, unaweza kuendesha muamala ili kuona hashi ya awali iliyoripotiwa na uthibitisho wa zero-knowledge.

  8. Endesha seva.

    cd ../server
    npm run start
    
  9. Endesha mteja katika dirisha tofauti la mstari wa amri.

    cd client
    npm run dev
    
  10. Endesha miamala kadhaa.

  11. Ili kuthibitisha kuwa hali imebadilika kwenye mnyororo, anzisha upya mchakato wa seva. Angalia kuwa ZkBank haikubali tena miamala, kwa sababu thamani ya asili ya hashi katika miamala inatofautiana na thamani ya hashi iliyohifadhiwa kwenye mnyororo.

    Hili ni aina ya hitilafu inayotarajiwa.

server/index.mjs

Mabadiliko katika faili hili yanahusiana zaidi na kuunda uthibitisho halisi na kuiwasilisha kwenye mnyororo.

import { exec } from 'child_process'
import util from 'util'

const execPromise = util.promisify(exec)

Tunahitaji kutumia kifurushi cha Barretenberg (opens in a new tab) ili kuunda uthibitisho halisi wa kutuma kwenye mnyororo. Tunaweza kutumia kifurushi hiki ama kwa kuendesha kiolesura cha mstari wa amri (bb) au kwa kutumia maktaba ya JavaScript, bb.js (opens in a new tab). Maktaba ya JavaScript ni polepole zaidi kuliko kuendesha msimbo asilia, kwa hivyo tunatumia exec (opens in a new tab) hapa kutumia mstari wa amri.

Kumbuka kwamba ukiamua kutumia bb.js, unahitaji kutumia toleo linaloendana na toleo la Noir unalotumia. Wakati wa kuandika, toleo la sasa la Noir (1.0.0-beta.11) linatumia toleo la bb.js 0.87.

const zkBankAddress = process.env.ZKBANK_ADDRESS || "0xe7f1725E7734CE288F8367e1Bb143E90bb3F0512"

Anwani hapa ni ile unayopata unapoanza na anvil safi na kufuata maelekezo hapo juu.

const walletClient = createWalletClient({ 
    chain: anvil, 
    transport: http(), 
    account: privateKeyToAccount("0x2a871d0798f97d79848a013d4936a73bf4cc922c825d33c1cf7073dff6d409c6")
})

Ufunguo huu wa faragha ni mojawapo ya akaunti chaguo-msingi zilizofadhiliwa awali katika anvil.

const generateProof = async (witness, fileID) => {

Tengeneza uthibitisho kwa kutumia bb inayoweza kutekelezwa.

    const fname = `witness-${fileID}.gz`    
    await fs.writeFile(fname, witness)

Andika shahidi kwenye faili.

    await execPromise(`bb prove -b ./noir/target/zkBank.json -w ${fname} -o ${fileID} --oracle_hash keccak --output_format fields`)

Unda uthibitisho halisi. Hatua hii pia huunda faili na vigezo vya umma, lakini hatuhitaji hiyo. Tayari tulipata vigezo hivyo kutoka kwa noir.execute.

    const proof = "0x" + JSON.parse(await fs.readFile(`./${fileID}/proof_fields.json`)).reduce((a,b) => a+b, "").replace(/0x/g, "")

Uthibitisho ni safu ya JSON ya thamani za Sehemu, kila moja ikiwakilishwa na thamani ya heksadesimali. Hata hivyo, tunahitaji kuituma katika muamala kama thamani moja ya baiti, ambayo Viem inaiwakilisha kwa kamba kubwa ya heksadesimali. Hapa tunabadilisha umbizo kwa kuunganisha thamani zote, kuondoa 0x zote, na kisha kuongeza moja mwishoni.

    await execPromise(`rm -r ${fname} ${fileID}`)

    return proof
}

Safisha na urudishe uthibitisho.

const processMessage = async (message, signature) => {
    .
    .
    .

    const publicFields = noirResult.returnValue.map(x=>'0x' + x.slice(2).padStart(64, "0"))

Sehemu za umma zinahitaji kuwa safu ya thamani za baiti 32. Hata hivyo, kwa kuwa tulihitaji kugawanya hashi ya muamala kati ya thamani mbili za Sehemu, inaonekana kama thamani ya baiti 16. Hapa tunaongeza sifuri ili Viem ielewe kuwa ni baiti 32 kweli.

    const proof = await generateProof(noirResult.witness, `${fromAddress}-${nonce}`)

Kila anwani hutumia kila nonce mara moja tu ili tuweze kutumia mchanganyiko wa fromAddress na nonce kama kitambulisho cha kipekee kwa faili ya shahidi na saraka ya tokeo.

Tuma muamala kwenye mnyororo.

smart-contracts/src/ZkBank.sol

Huu ni msimbo wa kwenye mnyororo unaopokea muamala.

Msimbo wa kwenye mnyororo unahitaji kufuatilia vigezo viwili: kithibitishaji (mkataba tofauti ulioundwa na nargo) na hashi ya hali ya sasa.

    event TransactionProcessed(
        bytes32 indexed transactionHash,
        bytes32 oldStateHash,
        bytes32 newStateHash
    );

Kila hali inapobadilika, tunatoa tukio la TransactionProcessed.

    function processTransaction(
        bytes calldata _proof,
        bytes32[] calldata _publicFields
    ) public {

Kazi hii inachakata miamala. Inapata uthibitisho (kama baiti) na pembejeo za umma (kama safu ya bytes32), katika umbizo ambalo kithibitishaji kinahitaji (ili kupunguza uchakataji wa kwenye mnyororo na hivyo gharama za gesi).

        require(_publicInputs[0] == currentStateHash,
            "Wrong old state hash");

Uthibitisho wa zero-knowledge unahitaji kuwa muamala unabadilika kutoka kwa hashi yetu ya sasa hadi mpya.

        myVerifier.verify(_proof, _publicFields);

Ita mkataba wa kithibitishaji ili kuthibitisha uthibitisho wa zero-knowledge. Hatua hii inarudisha nyuma muamala ikiwa uthibitisho wa zero-knowledge si sahihi.

Ikiwa kila kitu kiko sawa, sasisha hashi ya hali kwa thamani mpya na utoe tukio la TransactionProcessed.

Matumizi mabaya na sehemu ya kati

Usalama wa habari una sifa tatu:

  • Usiri, watumiaji hawawezi kusoma habari wasizoruhusiwa kusoma.
  • Uadilifu, habari haiwezi kubadilishwa isipokuwa na watumiaji walioidhinishwa kwa njia iliyoidhinishwa.
  • Upatikanaji, watumiaji walioidhinishwa wanaweza kutumia mfumo.

Kwenye mfumo huu, uadilifu hutolewa kupitia ithibati za zero-knowledge. Upatikanaji ni vigumu zaidi kuhakikisha, na usiri hauwezekani, kwa sababu benki inapaswa kujua salio la kila akaunti na miamala yote. Hakuna njia ya kuzuia chombo ambacho kina habari kushiriki habari hiyo.

Inaweza kuwezekana kuunda benki ya siri kweli kwa kutumia anwani za siri (opens in a new tab), lakini hilo liko nje ya upeo wa makala haya.

Taarifa za uongo

Njia moja ambayo seva inaweza kukiuka uadilifu ni kutoa habari za uongo wakati data inapoombwa (opens in a new tab).

Ili kutatua hili, tunaweza kuandika programu ya pili ya Noir inayopokea akaunti kama pembejeo la faragha na anwani ambayo habari inaombwa kama pembejeo la umma. Tokeo ni salio na nonce ya anwani hiyo, na hashi ya akaunti.

Bila shaka, uthibitisho huu hauwezi kuthibitishwa kwenye mnyororo, kwa sababu hatutaki kuchapisha nonces na salio kwenye mnyororo. Hata hivyo, inaweza kuthibitishwa na msimbo wa mteja unaoendeshwa kwenye kivinjari.

Miamala ya kulazimishwa

Utaratibu wa kawaida wa kuhakikisha upatikanaji na kuzuia udhibiti kwenye L2 ni miamala ya kulazimishwa (opens in a new tab). Lakini miamala ya kulazimishwa haichanganyiki na ithibati za zero-knowledge. Seva ndicho chombo pekee kinachoweza kuthibitisha miamala.

Tunaweza kurekebisha smart-contracts/src/ZkBank.sol ili kukubali miamala ya kulazimishwa na kuzuia seva kubadilisha hali hadi itakapochakatwa. Hata hivyo, hii inatuweka wazi kwa shambulio rahisi la kunyimwa huduma. Je, ikiwa muamala wa kulazimishwa si halali na kwa hivyo hauwezekani kuchakatwa?

Suluhisho ni kuwa na uthibitisho wa zero-knowledge kwamba muamala wa kulazimishwa si halali. Hii inatoa seva chaguzi tatu:

  • Chakata muamala wa kulazimishwa, ukitoa uthibitisho wa zero-knowledge kwamba umeshachakatwa na hashi mpya ya hali.
  • Kataa muamala wa kulazimishwa, na utoe uthibitisho wa zero-knowledge kwa mkataba kwamba muamala si halali (anwani isiyojulikana, nonce mbaya, au salio lisilotosha).
  • Puuza muamala wa kulazimishwa. Hakuna njia ya kulazimisha seva kuchakata muamala, lakini inamaanisha mfumo mzima haupatikani.

Dhamana za upatikanaji

Katika utekelezaji wa maisha halisi, labda kungekuwa na aina fulani ya motisha ya faida kwa kuweka seva ikiendeshwa. Tunaweza kuimarisha motisha huu kwa kuwa na seva inayoweka dhamana ya upatikanaji ambayo mtu yeyote anaweza kuichoma ikiwa muamala wa kulazimishwa hauchakatwi ndani ya kipindi fulani.

Msimbo mbaya wa Noir

Kwa kawaida, ili kuwafanya watu waamini mkataba-erevu tunapakia msimbo chanzo kwenye kichunguzi cha bloku (opens in a new tab). Hata hivyo, katika kesi ya ithibati za zero-knowledge, hiyo haitoshi.

Verifier.sol ina ufunguo wa uthibitishaji, ambao ni kazi ya programu ya Noir. Hata hivyo, ufunguo huo hautuambii programu ya Noir ilikuwa nini. Ili kuwa na suluhisho la kuaminika, unahitaji kupakia programu ya Noir (na toleo lililoiumba). Vinginevyo, ithibati za zero-knowledge zinaweza kuakisi programu tofauti, moja yenye mlango wa nyuma.

Hadi wachunguzi wa bloku waanze kuturuhusu kupakia na kuthibitisha programu za Noir, unapaswa kufanya hivyo mwenyewe (ikiwezekana kwa IPFS). Kisha watumiaji wa hali ya juu wataweza kupakua msimbo chanzo, kuukusanya wenyewe, kuunda Verifier.sol, na kuthibitisha kuwa inafanana na ile iliyo kwenye mnyororo.

Hitimisho

Programu za aina ya Njozi zinahitaji sehemu ya kati kama hifadhi ya habari. Hii inafungua udhaifu unaowezekana lakini, kwa kurudi, inaturuhusu kuhifadhi faragha kwa njia zisizopatikana kwenye mnyororo wa bloku wenyewe. Kwa ithibati za zero-knowledge tunaweza kuhakikisha uadilifu na ikiwezekana kuifanya iwe na faida kiuchumi kwa yeyote anayeendesha sehemu ya kati ili kudumisha upatikanaji.

Tazama hapa kwa kazi zangu zaidi (opens in a new tab).

Shukrani

  • Josh Crites alisoma rasimu ya makala haya na kunisaidia na suala gumu la Noir.

Makosa yoyote yaliyobaki ni jukumu langu.

Ukurasa ulisasishwa mwisho: 3 Machi 2026

Je, mafunzo haya yalikuwa ya msaada?