Ruka kwenda 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:

1noirup -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.

    1git clone https://github.com/qbzzt/250911-zk-bank.git -b 01-manual-zk
    2cd 250911-zk-bank
    3cd client
    4npm install
    5npm 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.

    1cd ../server/noir
    2nargo execute

    Tokeo linapaswa kuwa sawa na

    1ori@CryptoDocGuy:~/noir/250911-zk-bank/server/noir$ nargo execute
    2
    3[zkBank] Circuit witness successfully solved
    4[zkBank] Witness saved to target/zkBank.gz
    5[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.

1message="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.

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

Vigezo hivi vitatu ni safu za baiti za ukubwa usiobadilika.

1[[accounts]]
2address="0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266"
3balance=100_000
4nonce=0
5
6[[accounts]]
7address="0x70997970C51812dc3A010C7d01b50e0d17dc79C8"
8balance=100_000
9nonce=0
Onyesha yote

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.

1export default attrs => {

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

1 const accounts = [
2 "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266",
3 "0x70997970C51812dc3A010C7d01b50e0d17dc79C8",
4 "0x3C44CdDdB6a900fa2b585dd299e03d12FA4293BC",
5 "0x90F79bf6EB2c4f870365E785982E1f101E93b906",
6 "0x15d34AAf54267DB7D7c367839AAf71A00a2C6A65",
7 ]

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

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

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

1 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.

1 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.

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

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

1 const hash = hashMessage(message)

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

1 const pubKey = await recoverPublicKey({
2 hash,
3 signature
4 })

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

1 setSignature(signature)
2 setHash(hash)
3 setPubKey(pubKey)

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

1 let proverToml = `

Maandishi ya Prover.toml.

1message="${message}"
2
3pubKeyX=${hexToArray(pubKey.slice(4,4+2*32))}
4pubKeyY=${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.

1signature=${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.

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

Toa akaunti.

1 setProverToml(proverToml)
2 }
3
4 return (
5 <>
6 <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.

1use 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.

1use keccak256::keccak256;
2use 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.

1global 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.

1global FLAT_ACCOUNT_FIELDS : u32 = 2;

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

1global MESSAGE_LENGTH : u32 = 100;

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

1global ASCII_MESSAGE_LENGTH : [u8; 3] = [0x31, 0x30, 0x30];
2global 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.

1struct Account {
2 balance: u128,
3 address: Field,
4 nonce: u32,
5}

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.

1struct TransferTxn {
2 from: Field,
3 to: Field,
4 amount: u128,
5 nonce: u32
6}

Taarifa tunayohifadhi kwa muamala wa uhamisho.

1fn 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

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

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.

1 flat
2}

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.

1fn 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.

1 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.

1 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.

1 let fields = flatten_account(accounts[i]);
2 for j in 0..FLAT_ACCOUNT_FIELDS {
3 flat[i*FLAT_ACCOUNT_FIELDS + j] = fields[j];
4 }
5 }
6
7 flat
8}
9
10fn hash_accounts(accounts: [Account; ACCOUNT_NUMBER]) -> Field {
11 pedersen_hash(flatten_accounts(accounts))
12}
Onyesha yote

Mwishowe, tumefika kwenye kazi ambayo inahashi safu ya akaunti.

1fn find_account(accounts: [Account; ACCOUNT_NUMBER], address: Field) -> u32 {
2 let mut account : u32 = ACCOUNT_NUMBER;
3
4 for i in 0..ACCOUNT_NUMBER {
5 if accounts[i].address == address {
6 account = i;
7 }
8 }

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

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

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

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

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

1 let from = find_account(accounts, txn.from);
2 let to = find_account(accounts, txn.to);
3
4 let (txnFrom, txnAmount, txnNonce, accountNonce) =
5 (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.

1 assert (accounts[from].balance >= txn.amount,
2 f"{txnFrom} does not have {txnAmount} finney");
3
4 assert (accounts[from].nonce == txn.nonce,
5 f"Transaction has nonce {txnNonce}, but the account is expected to use {accountNonce}");

Hizi ni hali mbili ambazo zinaweza kufanya muamala kuwa batili.

1 let mut newAccounts = accounts;
2
3 newAccounts[from].balance -= txn.amount;
4 newAccounts[from].nonce += 1;
5 newAccounts[to].balance += txn.amount;
6
7 newAccounts
8}

Unda safu mpya ya akaunti na kisha uirudishe.

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

Kazi hii inasoma anwani kutoka kwa ujumbe.

1{
2 let mut result : Field = 0;
3
4 for i in 7..47 {

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

1 result *= 0x10;
2 if messageBytes[i] >= 48 & messageBytes[i] <= 57 { // 0-9
3 result += (messageBytes[i]-48).into();
4 }
5 if messageBytes[i] >= 65 & messageBytes[i] <= 70 { // A-F
6 result += (messageBytes[i]-65+10).into()
7 }
8 if messageBytes[i] >= 97 & messageBytes[i] <= 102 { // a-f
9 result += (messageBytes[i]-97+10).into()
10 }
11 }
12
13 result
14}
15
16fn readAmountAndNonce(messageBytes: [u8; MESSAGE_LENGTH]) -> (u128, u32)
Onyesha yote

Soma kiasi na nonce kutoka kwa ujumbe.

1{
2 let mut amount : u128 = 0;
3 let mut nonce: u32 = 0;
4 let mut stillReadingAmount: bool = true;
5 let mut lookingForNonce: bool = false;
6 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.

1 for i in 48..MESSAGE_LENGTH {
2 if messageBytes[i] >= 48 & messageBytes[i] <= 57 { // 0-9
3 let digit = (messageBytes[i]-48);
4
5 if stillReadingAmount {
6 amount = amount*10 + digit.into();
7 }
8
9 if lookingForNonce { // We just found it
10 stillReadingNonce = true;
11 lookingForNonce = false;
12 }
13
14 if stillReadingNonce {
15 nonce = nonce*10 + digit.into();
16 }
17 } else {
18 if stillReadingAmount {
19 stillReadingAmount = false;
20 lookingForNonce = true;
21 }
22 if stillReadingNonce {
23 stillReadingNonce = false;
24 }
25 }
26 }
27
28 (amount, nonce)
29}
Onyesha yote

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

1fn readTransferTxn(message: str<MESSAGE_LENGTH>) -> TransferTxn
2{
3 let mut txn: TransferTxn = TransferTxn { from: 0, to: 0, amount:0, nonce:0 };
4 let messageBytes = message.as_bytes();
5
6 txn.to = readAddress(messageBytes);
7 let (amount, nonce) = readAmountAndNonce(messageBytes);
8 txn.amount = amount;
9 txn.nonce = nonce;
10
11 txn
12}
Onyesha yote

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

1// The equivalent to Viem's hashMessage
2// https://viem.sh/docs/utilities/hashMessage#hashmessage
3fn 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.

1 // ASCII prefix
2 let prefix_bytes = [
3 0x19, // \x19
4 0x45, // 'E'
5 0x74, // 't'
6 0x68, // 'h'
7 0x65, // 'e'
8 0x72, // 'r'
9 0x65, // 'e'
10 0x75, // 'u'
11 0x6D, // 'm'
12 0x20, // ' '
13 0x53, // 'S'
14 0x69, // 'i'
15 0x67, // 'g'
16 0x6E, // 'n'
17 0x65, // 'e'
18 0x64, // 'd'
19 0x20, // ' '
20 0x4D, // 'M'
21 0x65, // 'e'
22 0x73, // 's'
23 0x73, // 's'
24 0x61, // 'a'
25 0x67, // 'g'
26 0x65, // 'e'
27 0x3A, // ':'
28 0x0A // '\n'
29 ];
Onyesha yote

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.

1 let mut buffer: [u8; HASH_BUFFER_SIZE] = [0u8; HASH_BUFFER_SIZE];
2 for i in 0..26 {
3 buffer[i] = prefix_bytes[i];
4 }
5
6 let messageBytes : [u8; MESSAGE_LENGTH] = message.as_bytes();
7
8 if MESSAGE_LENGTH <= 9 {
9 for i in 0..1 {
10 buffer[i+26] = ASCII_MESSAGE_LENGTH[i];
11 }
12
13 for i in 0..MESSAGE_LENGTH {
14 buffer[i+26+1] = messageBytes[i];
15 }
16 }
17
18 if MESSAGE_LENGTH >= 10 & MESSAGE_LENGTH <= 99 {
19 for i in 0..2 {
20 buffer[i+26] = ASCII_MESSAGE_LENGTH[i];
21 }
22
23 for i in 0..MESSAGE_LENGTH {
24 buffer[i+26+2] = messageBytes[i];
25 }
26 }
27
28 if MESSAGE_LENGTH >= 100 {
29 for i in 0..3 {
30 buffer[i+26] = ASCII_MESSAGE_LENGTH[i];
31 }
32
33 for i in 0..MESSAGE_LENGTH {
34 buffer[i+26+3] = messageBytes[i];
35 }
36 }
37
38 assert(MESSAGE_LENGTH < 1000, "Messages whose length is over three digits are not supported");
Onyesha yote

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.

1 keccak256::keccak256(buffer, HASH_BUFFER_SIZE)
2}

Tumia kazi ya kawaida ya Ethereum keccak256.

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

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

1 let hash = hashMessage(message);
2
3 let mut (hash1, hash2) = (0,0);
4
5 for i in 0..16 {
6 hash1 = hash1*256 + hash[31-i].into();
7 hash2 = hash2*256 + hash[15-i].into();
8 }

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

1 (
2 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.
1 hash1,
2 hash2
3 )
4}
5
6fn main(
7 accounts: [Account; ACCOUNT_NUMBER],
8 message: str<MESSAGE_LENGTH>,
9 pubKeyX: [u8; 32],
10 pubKeyY: [u8; 32],
11 signature: [u8; 64],
12 ) -> pub (
13 Field, // Hash of old accounts array
14 Field, // Hash of new accounts array
15 Field, // First 16 bytes of message hash
16 Field, // Last 16 bytes of message hash
17 )
Onyesha yote

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.

1{
2 let mut txn = readTransferTxn(message);

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

1 let (fromAddress, txnHash1, txnHash2) = signatureToAddressAndHash(
2 message,
3 pubKeyX,
4 pubKeyY,
5 signature);
6
7 txn.from = fromAddress;
8
9 let newAccounts = apply_transfer_txn(accounts, txn);
10
11 (
12 hash_accounts(accounts),
13 hash_accounts(newAccounts),
14 txnHash1,
15 txnHash2
16 )
17}
Onyesha yote

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.

    1git checkout 02-add-server
    2cd client
    3npm install
    4cd ../server
    5npm install

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

  3. Anzisha seva.

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

    1cd client
    2npm 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.

    1ori@CryptoDocGuy:~/x/250911-zk-bank/server$ npm run start
    2
    3> server@1.0.0 start
    4> node --experimental-json-modules index.mjs
    5
    6Listening on port 3000
    7Txn send 0x90F79bf6EB2c4f870365E785982E1f101E93b906 36000 finney (milliEth) 0 processed
    8New state:
    90xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266 has 64000 (1)
    100x70997970C51812dc3A010C7d01b50e0d17dc79C8 has 100000 (0)
    110x3C44CdDdB6a900fa2b585dd299e03d12FA4293BC has 100000 (0)
    120x90F79bf6EB2c4f870365E785982E1f101E93b906 has 136000 (0)
    130x15d34AAf54267DB7D7c367839AAf71A00a2C6A65 has 100000 (0)
    14Txn send 0x70997970C51812dc3A010C7d01b50e0d17dc79C8 7200 finney (milliEth) 1 processed
    15New state:
    160xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266 has 56800 (2)
    170x70997970C51812dc3A010C7d01b50e0d17dc79C8 has 107200 (0)
    180x3C44CdDdB6a900fa2b585dd299e03d12FA4293BC has 100000 (0)
    190x90F79bf6EB2c4f870365E785982E1f101E93b906 has 136000 (0)
    200x15d34AAf54267DB7D7c367839AAf71A00a2C6A65 has 100000 (0)
    21Txn send 0x90F79bf6EB2c4f870365E785982E1f101E93b906 3000 finney (milliEth) 2 processed
    22New state:
    230xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266 has 53800 (3)
    240x70997970C51812dc3A010C7d01b50e0d17dc79C8 has 107200 (0)
    250x3C44CdDdB6a900fa2b585dd299e03d12FA4293BC has 100000 (0)
    260x90F79bf6EB2c4f870365E785982E1f101E93b906 has 139000 (0)
    270x15d34AAf54267DB7D7c367839AAf71A00a2C6A65 has 100000 (0)
    Onyesha yote

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.

1import { 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.

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

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

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

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

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

Chakata ujumbe na utekeleze muamala unaouweka.

1 // Get the public key
2 const pubKey = await recoverPublicKey({
3 hash,
4 signature
5 })

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

1 let noirResult
2 try {
3 noirResult = await noir.execute({
4 message,
5 signature: signature.slice(2,-2).match(/.{2}/g).map(x => `0x${x}`),
6 pubKeyX,
7 pubKeyY,
8 accounts: Accounts
9 })
Onyesha yote

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.

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

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

1 Accounts[fromAccountNumber].nonce++
2 Accounts[fromAccountNumber].balance -= amount
3 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.

1let Accounts = [
2 {
3 address: "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266",
4 balance: 5000,
5 nonce: 0,
6 },

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.

    1git checkout 03-smart-contracts
    2cd client
    3npm install
    4cd ../server
    5npm 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.

    1cd noir
    2bb write_vk -b ./target/zkBank.json -o ./target --oracle_hash keccak
    3bb write_solidity_verifier -k ./target/vk -o ./target/Verifier.sol
    4cp target/Verifier.sol ../../smart-contracts/src
  5. Nenda kwenye mikataba-erevu na weka vigezo vya mazingira ili kutumia mnyororo wa bloku wa anvil.

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

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

    1ZKBANK_ADDRESS=`forge create ZkBank --private-key $ETH_PRIVATE_KEY --broadcast --constructor-args $VERIFIER_ADDRESS 0x199aa62af8c1d562a6ec96e66347bf3240ab2afb5d022c895e6bf6a5e617167b | awk '/Deployed to:/ {print $3}'`
    2echo $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.

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

    1cd client
    2npm 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.

    1ori@CryptoDocGuy:~/x/250911-zk-bank/server$ npm run start
    2
    3> server@1.0.0 start
    4> node --experimental-json-modules index.mjs
    5
    6Listening on port 3000
    7Verification error: ContractFunctionExecutionError: The contract function "processTransaction" reverted with the following reason:
    8Wrong old state hash
    9
    10Contract Call:
    11 address: 0xe7f1725E7734CE288F8367e1Bb143E90bb3F0512
    12 function: processTransaction(bytes _proof, bytes32[] _publicInputs)
    13 args: (0x0000000000000000000000000000000000000000000000042ab5d6d1986846cf00000000000000000000000000000000000000000000000b75c020998797da7800000000000000000000000000000000000000000000000
    Onyesha yote

server/index.mjs

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

1import { exec } from 'child_process'
2import util from 'util'
3
4const 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.

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

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

1const walletClient = createWalletClient({
2 chain: anvil,
3 transport: http(),
4 account: privateKeyToAccount("0x2a871d0798f97d79848a013d4936a73bf4cc922c825d33c1cf7073dff6d409c6")
5})

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

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

Tengeneza uthibitisho kwa kutumia bb inayoweza kutekelezwa.

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

Andika shahidi kwenye faili.

1 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.

1 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.

1 await execPromise(`rm -r ${fname} ${fileID}`)
2
3 return proof
4}

Safisha na urudishe uthibitisho.

1const processMessage = async (message, signature) => {
2 .
3 .
4 .
5
6 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.

1 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.

1 try {
2 await zkBank.write.processTransaction([
3 proof, publicFields])
4 } catch (err) {
5 console.log(`Verification error: ${err}`)
6 throw Error("Can't verify the transaction onchain")
7 }
8 .
9 .
10 .
11}
Onyesha yote

Tuma muamala kwenye mnyororo.

smart-contracts/src/ZkBank.sol

Huu ni msimbo wa kwenye mnyororo unaopokea muamala.

1// SPDX-License-Identifier: MIT
2
3pragma solidity >=0.8.21;
4
5import {HonkVerifier} from "./Verifier.sol";
6
7contract ZkBank {
8 HonkVerifier immutable myVerifier;
9 bytes32 currentStateHash;
10
11 constructor(address _verifierAddress, bytes32 _initialStateHash) {
12 currentStateHash = _initialStateHash;
13 myVerifier = HonkVerifier(_verifierAddress);
14 }
Onyesha yote

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

1 event TransactionProcessed(
2 bytes32 indexed transactionHash,
3 bytes32 oldStateHash,
4 bytes32 newStateHash
5 );

Kila hali inapobadilika, tunatoa tukio la TransactionProcessed.

1 function processTransaction(
2 bytes calldata _proof,
3 bytes32[] calldata _publicFields
4 ) 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).

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

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

1 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.

1 currentStateHash = _publicFields[1];
2
3 emit TransactionProcessed(
4 _publicFields[2]<<128 | _publicFields[3],
5 _publicFields[0],
6 _publicFields[1]
7 );
8 }
9}
Onyesha yote

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 ulihaririwa mwisho: 28 Oktoba 2025

Umesaidika na mafunzo haya?