Ruka hadi maudhui makuu

Andika Plasma mahususi kwa programu inayohifadhi faragha

sifuri-maarifa
seva
nje ya mnyororo
faragha
Ya juu
Ori Pomerantz
15 Oktoba 2025
30 dakika za kusoma

Utangulizi

Tofauti na mikusanyiko, Plasma hutumia Mtandao Mkuu wa Ethereum kwa uadilifu, lakini si kwa upatikanaji. Katika makala haya, tunaandika programu inayofanya kazi kama Plasma, huku Ethereum ikihakikisha uadilifu (hakuna mabadiliko yasiyoidhinishwa) lakini si upatikanaji (kijenzi kilichowekwa kati kinaweza kuharibika na kulemaza mfumo mzima).

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

Muundo

Huu si mfumo ulio tayari kwa uzalishaji, bali ni zana ya kufundishia. Kwa hivyo, umeandikwa kwa dhana kadhaa zinazorahisisha.

  • Kundi 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 yenye ukubwa unaobadilika katika uthibitisho wa maarifa-sifuri. Kwa mfumo ulio tayari kwa uzalishaji, tunaweza kutumia mzizi wa Merkle kama heshi ya hali na kutoa uthibitisho wa Merkle kwa salio zinazohitajika.

  • Uhifadhi wa kumbukumbu. Kwenye mfumo wa uzalishaji, tunahitaji kuandika salio zote za akaunti kwenye diski ili kuzihifadhi endapo utawashwa upya. Hapa, ni sawa ikiwa taarifa itapotea tu.

  • Uhamisho pekee. Mfumo wa uzalishaji ungehitaji njia ya kuweka rasilimali kwenye benki na kuzitoa. Lakini lengo hapa ni kuelezea tu dhana, kwa hivyo benki hii inakomea kwenye uhamisho pekee.

Uthibitisho wa maarifa-sifuri

Katika kiwango cha msingi, uthibitisho wa maarifa-sifuri unaonyesha kwamba mthibitishaji anajua baadhi ya data, Dataprivate kiasi kwamba kuna uhusiano Relationship kati ya baadhi ya data za umma, Datapublic, na Dataprivate. Mhakiki anajua Relationship na Datapublic.

Ili kuhifadhi faragha, tunahitaji hali na miamala iwe ya faragha. Lakini ili kuhakikisha uadilifu, tunahitaji heshi ya kriptografia (inafunguka katika kichupo kipya) ya hali iwe ya umma. Ili kuwathibitishia watu wanaowasilisha miamala kwamba miamala hiyo ilifanyika kweli, tunahitaji pia kuchapisha heshi za muamala.

Katika hali nyingi, Dataprivate ndiyo ingizo kwa programu ya uthibitisho wa maarifa-sifuri, na Datapublic ndiyo towe.

Sehemu hizi katika Dataprivate:

  • Staten, hali ya zamani
  • Staten+1, hali mpya
  • Transaction, muamala unaobadilika kutoka hali ya zamani hadi mpya. Muamala huu unahitaji kujumuisha sehemu hizi:
    • Anwani ya mwisho inayopokea hamisho
    • Kiasi kinachohamishwa
    • Nonsi ili kuhakikisha kila muamala unaweza kuchakatwa mara moja tu. Anwani ya chanzo haihitaji kuwa kwenye muamala, kwa sababu inaweza kupatikana kutoka kwenye sahihi.
  • Signature, sahihi iliyoidhinishwa kufanya muamala. Kwa upande wetu, anwani pekee iliyoidhinishwa kufanya muamala ni anwani ya chanzo. Kwa sababu mfumo wetu wa sifuri-maarifa unafanya kazi jinsi unavyofanya, tunahitaji pia ufunguo wa umma wa akaunti, pamoja na sahihi ya Ethereum.

Hizi ni sehemu katika Datapublic:

  • Hash(Staten) heshi ya hali ya zamani
  • Hash(Staten+1) heshi ya hali mpya
  • Hash(Transaction) heshi ya muamala unaobadilisha hali kutoka Staten hadi Staten+1.

Uhusiano huangalia masharti kadhaa:

  • Heshi za umma kwa kweli ni heshi sahihi kwa sehemu za faragha.
  • Muamala, unapotumika kwa hali ya zamani, husababisha hali mpya.
  • Sahihi inatoka kwenye anwani ya chanzo cha muamala.

Kwa sababu ya sifa za kazi za heshi za 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 nonsi (inafunguka katika kichupo kipya), inayotumika kuzuia mashambulizi ya kurudiwa (inafunguka katika kichupo kipya).

Vijenzi

Mfumo huu unahitaji vijenzi viwili:

  • Seva inayopokea miamala, kuichakata, na kuchapisha heshi kwenye mnyororo pamoja na uthibitisho wa maarifa-sifuri.
  • Mkataba mahiri unaohifadhi heshi na kuhakiki uthibitisho wa maarifa-sifuri ili kuhakikisha mabadiliko ya hali ni halali.

Mtiririko wa data na udhibiti

Hizi ni njia ambazo vijenzi mbalimbali huwasiliana ili kufanya hamisho kutoka akaunti moja hadi nyingine.

  1. Kivinjari cha wavuti huwasilisha muamala uliosainiwa ukiomba hamisho kutoka kwa akaunti ya mtia saini kwenda kwa akaunti tofauti.

  2. Seva inahakiki kwamba muamala ni halali:

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

  4. Seva inakokotoa uthibitisho wa maarifa-sifuri kwamba mabadiliko ya hali ni halali.

  5. Seva inawasilisha kwa Ethereum muamala unaojumuisha:

    • Heshi ya hali mpya
    • Heshi ya muamala (ili mtumaji wa muamala aweze kujua imechakatwa)
    • Uthibitisho wa maarifa-sifuri unaothibitisha mabadiliko ya hali mpya ni halali
  6. Mkataba mahiri unahakiki uthibitisho wa maarifa-sifuri.

  7. Ikiwa uthibitisho wa maarifa-sifuri ni sahihi, mkataba mahiri hufanya vitendo hivi:

    • Kusasisha heshi ya hali ya sasa kuwa heshi ya hali mpya
    • Kutoa ingizo la logi lenye heshi ya hali mpya na heshi ya muamala

Zana

Kwa msimbo wa upande wa mteja, tutatumia Vite (inafunguka katika kichupo kipya), React (inafunguka katika kichupo kipya), Viem (inafunguka katika kichupo kipya), na Wagmi (inafunguka katika kichupo kipya). Hizi ni zana za kiwango cha tasnia; ikiwa huzifahamu, unaweza kutumia somo hili.

Sehemu kubwa ya seva imeandikwa kwa JavaScript kwa kutumia Node (inafunguka katika kichupo kipya). Sehemu ya sifuri-maarifa imeandikwa kwa Noir (inafunguka katika kichupo kipya). Tunahitaji toleo 1.0.0-beta.10, kwa hivyo baada ya kusakinisha Noir kama ilivyoelekezwa (inafunguka katika kichupo kipya), endesha:

noirup -v 1.0.0-beta.10

Mnyororo wa vitalu tunaotumia ni anvil, mnyororo wa vitalu wa majaribio wa ndani ambao ni sehemu ya Foundry (inafunguka katika kichupo kipya).

Utekelezaji

Kwa sababu huu ni mfumo changamano, tutautekeleza kwa hatua.

Hatua ya 1 - Sifuri-maarifa ya mikono

Kwa hatua ya kwanza, tutasaini muamala kwenye kivinjari na kisha kutoa taarifa hiyo kwa uthibitisho wa maarifa-sifuri kwa mikono. Msimbo wa sifuri-maarifa unatarajia kupata taarifa hiyo katika server/noir/Prover.toml (imeandikwa hapa (inafunguka katika kichupo kipya)).

Ili kuiona ikifanya kazi:

  1. Hakikisha umesakinisha Node (inafunguka katika kichupo kipya) na Noir (inafunguka katika kichupo kipya). Ikiwezekana, isakinishe kwenye mfumo wa UNIX kama vile macOS, Linux, au WSL (inafunguka katika kichupo kipya).

  2. Pakua msimbo wa hatua ya 1 na uwashe 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 inayofanya uhitaji seva ya wavuti hapa ni kwamba, ili kuzuia aina fulani za udanganyifu, mikoba mingi (kama vile MetaMask) haikubali faili zinazohudumiwa moja kwa moja kutoka kwenye diski

  3. Fungua kivinjari kilicho na mkoba.

  4. Kwenye mkoba, weka nenosiri jipya. Kumbuka kwamba 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 chaguomsingi la majaribio la anvil.

  5. Vinjari hadi kwenye msimbo wa upande wa mteja (inafunguka katika kichupo kipya).

  6. Unganisha kwenye mkoba na uchague akaunti yako ya mwisho na kiasi.

  7. Bofya Saini na usaini muamala.

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

  9. Tekeleza uthibitisho wa maarifa-sifuri.

    cd ../server/noir
    nargo execute
    

    Towe linapaswa kufanana 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 heshi unayoiona kwenye kivinjari cha wavuti ili kuona ikiwa ujumbe umeheshishwa kwa usahihi.

server/noir/Prover.toml

Faili hili (inafunguka katika kichupo kipya) linaonyesha muundo wa taarifa unaotarajiwa na Noir.

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

Ujumbe uko katika muundo wa maandishi, jambo ambalo hufanya iwe rahisi kwa mtumiaji kuelewa (ambayo ni muhimu wakati wa kusaini) na kwa msimbo wa Noir kuchanganua. Kiasi kimenukuliwa katika finney ili kuwezesha uhamisho wa sehemu kwa upande mmoja, na kusomeka kwa urahisi kwa upande mwingine. Nambari ya mwisho ni nonsi (inafunguka katika kichupo kipya).

Mfuatano una urefu wa herufi 100. Uthibitisho wa maarifa-sifuri haushughulikii data zenye ukubwa unaobadilika vizuri, kwa hivyo mara nyingi ni muhimu kujaza data.

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

Vigezo hivi vitatu ni safu za baiti zenye ukubwa usiobadilika.

Hii ndiyo njia ya kubainisha safu ya miundo. Kwa kila ingizo, tunabainisha anwani, salio (kwa milliETH a.k.a. finney (inafunguka katika kichupo kipya)), na thamani ya nonsi inayofuata.

client/src/Transfer.tsx

Faili hili (inafunguka katika kichupo kipya) hutekeleza uchakataji wa upande wa mteja na kuzalisha faili la server/noir/Prover.toml (lile linalojumuisha vigezo vya sifuri-maarifa).

Hapa kuna maelezo ya sehemu za kuvutia zaidi.

export default attrs =>  {

Kazi hii inaunda kijenzi cha React cha Transfer, ambacho faili zingine zinaweza kuleta.

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

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

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

Hizi kulabu za Wagmi (inafunguka katika kichupo kipya) huturuhusu kufikia maktaba ya Viem (inafunguka katika kichupo kipya) na mkoba.

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

Huu ni ujumbe, uliojazwa na nafasi. Kila wakati mojawapo ya vigeu vya useState (inafunguka katika kichupo kipya) inapobadilika, kijenzi huchorwa upya na message husasishwa.

  const sign = async () => {

Kazi hii inaitwa wakati mtumiaji anabofya kitufe cha Saini. Ujumbe husasishwa kiotomatiki, lakini sahihi inahitaji idhini ya mtumiaji kwenye mkoba, na hatutaki kuiomba isipokuwa inahitajika.

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

Omba mkoba usaini ujumbe (inafunguka katika kichupo kipya).

    const hash = hashMessage(message)

Pata heshi ya ujumbe. Inasaidia kuipatia mtumiaji kwa ajili ya utatuzi (wa msimbo wa Noir).

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

Pata ufunguo wa umma (inafunguka katika kichupo kipya). Hii inahitajika kwa kazi ya Noir ecrecover (inafunguka katika kichupo kipya).

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

Weka vigeu vya hali. Kufanya hivi huchora upya kijenzi (baada ya kazi ya sign kutoka) na kumuonyesha mtumiaji thamani zilizosasishwa.

    let proverToml = `

Maandishi kwa ajili 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 mfuatano wa heksadesimali wa baiti 65. Baiti ya kwanza ni 0x04, alama ya toleo. Hii inafuatiwa na baiti 32 kwa ajili ya x ya ufunguo wa umma na kisha baiti 32 kwa ajili ya y ya ufunguo wa umma.

Hata hivyo, Noir inatarajia kupata taarifa hii kama safu mbili za baiti, moja kwa ajili ya x na moja kwa ajili ya y. Ni rahisi kuichanganua hapa kwa mteja badala ya kuwa sehemu ya uthibitisho wa maarifa-sifuri.

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

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

Sahihi pia inatolewa kama mfuatano wa heksadesimali wa baiti 65. Hata hivyo, baiti ya mwisho inahitajika tu ili kurejesha ufunguo wa umma. Kwa kuwa ufunguo wa umma utakuwa tayari umetolewa kwa msimbo wa Noir, hatuuhitaji ili kuhakiki sahihi, na msimbo wa Noir hauuhitaji.

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

Toa akaunti.

    setProverToml(proverToml)
  }

  return (
    <>
        <h2>Transfer</h2>

Huu ni muundo wa HTML (kwa usahihi zaidi, JSX (inafunguka katika kichupo kipya)) wa kijenzi.

server/noir/src/main.nr

Faili hili (inafunguka katika kichupo kipya) ndio msimbo halisi wa sifuri-maarifa.

use std::hash::pedersen_hash;

Heshi ya Pedersen (inafunguka katika kichupo kipya) inatolewa pamoja na maktaba ya kawaida ya Noir (inafunguka katika kichupo kipya). Uthibitisho wa maarifa-sifuri kwa kawaida hutumia kazi hii ya heshi. Ni rahisi sana kukokotoa ndani ya saketi za hesabu (inafunguka katika kichupo kipya) ikilinganishwa na kazi za kawaida za heshi.

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

Kazi hizi mbili ni maktaba za nje, zilizofafanuliwa katika Nargo.toml (inafunguka katika kichupo kipya). Ni kwa usahihi kile zilichopewa jina, kazi inayokokotoa heshi ya keccak256 (inafunguka katika kichupo kipya) na kazi inayohakiki sahihi za Ethereum na kurejesha anwani ya mtia saini wa Ethereum.

global ACCOUNT_NUMBER : u32 = 5;

Noir imetiwa hamasa na Rust (inafunguka katika kichupo kipya). Vigeu, kwa chaguo-msingi, ni thabiti. Hivi ndivyo tunavyofafanua thabiti za usanidi wa kimataifa. Hasa, ACCOUNT_NUMBER ni idadi ya akaunti tunazohifadhi.

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

global FLAT_ACCOUNT_FIELDS : u32 = 2;

Kigeu hiki kinatumika kwa heshi ya Pedersen ya akaunti, kama ilivyoelezwa hapa chini.

global MESSAGE_LENGTH : u32 = 100;

Kama ilivyoelezwa hapo juu, urefu wa ujumbe haubadiliki. Umebainishwa hapa.

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

Sahihi za EIP-191 (inafunguka katika kichupo kipya) 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 (inafunguka katika kichupo kipya) ni nambari, kwa kawaida hadi biti 253, inayoweza kutumika moja kwa moja katika saketi ya hesabu (inafunguka katika kichupo kipya) inayotekeleza uthibitisho wa maarifa-sifuri. Hapa tunatumia Field kuhifadhi anwani ya Ethereum ya biti 160.

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

Taarifa tunayohifadhi kwa ajili ya muamala wa hamisho.

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

Ufafanuzi wa kazi. Kigezo ni taarifa ya Account. Matokeo ni safu ya vigeu 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 nonsi. Wito wa .into() hubadilisha nambari kuwa aina ya data inavyohitaji kuwa. account.nonce ni thamani ya u32, lakini ili kuiongeza kwenye account.balance << 32, thamani ya u128, inahitaji kuwa u128. Hiyo ndiyo .into() ya kwanza. Ya pili inabadilisha matokeo ya u128 kuwa Field ili iingie kwenye safu.

    flat
}

Katika Noir, kazi zinaweza kurejesha thamani mwishoni pekee (hakuna urejeshaji wa mapema). Ili kubainisha thamani inayorejeshwa, unaitathmini kabla tu ya mabano ya kufunga ya kazi.

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

Kazi hii hubadilisha safu ya akaunti kuwa safu ya Field, ambayo inaweza kutumika kama ingizo kwa Heshi ya Petersen.

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

Hivi ndivyo unavyobainisha kigeu kinachoweza kubadilika, yaani, sio thabiti. Vigeu katika Noir lazima viwe na thamani kila wakati, kwa hivyo tunaanzisha kigeu hiki kwa sifuri zote.

    for i in 0..ACCOUNT_NUMBER {

Hii ni kitanzi cha for. Kumbuka kwamba mipaka ni thabiti. Vitanzi vya Noir lazima viwe na mipaka inayojulikana wakati wa kukusanya. Sababu ni kwamba saketi za hesabu hazitumii udhibiti wa mtiririko. Wakati wa kuchakata kitanzi cha for, kikusanyaji huweka tu msimbo ndani yake mara nyingi, moja kwa kila mzunguko.

Hatimaye, tumefika kwenye kazi inayoheshisha 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 inatafuta akaunti yenye anwani maalum. Kazi hii ingekuwa isiyofaa sana katika msimbo wa kawaida kwa sababu inazunguka akaunti zote, hata baada ya kupata anwani.

Hata hivyo, katika uthibitisho wa maarifa-sifuri, hakuna udhibiti wa mtiririko. Ikiwa tutawahi kuhitaji kuangalia sharti, lazima tuliangalie kila wakati.

Jambo kama hilo hutokea kwa kauli za if. Kauli ya if katika kitanzi hapo juu inatafsiriwa katika kauli hizi za hisabati.

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

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

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

    account
}

Kazi ya assert (inafunguka katika kichupo kipya) husababisha uthibitisho wa maarifa-sifuri kuacha kufanya kazi ikiwa dai ni la uongo. Katika kesi hii, ikiwa hatuwezi kupata akaunti yenye anwani husika. Ili kuripoti anwani, tunatumia mfuatano wa muundo (inafunguka katika kichupo kipya).

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

Kazi hii inatumia muamala wa hamisho na kurejesha 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 mfuatano wa muundo 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}");

Haya ni masharti mawili yanayoweza kufanya muamala uwe 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 kisha uirejeshe.

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

Kazi hii inasoma anwani kutoka kwenye ujumbe.

{
    let mut result : Field = 0;

    for i in 7..47 {

Anwani daima ina urefu wa baiti 20 (a.k.a. tarakimu 40 za heksadesimali), na huanza kwenye herufi #7.

Soma kiasi na nonsi kutoka kwenye 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 moja ya ETH) cha kuhamisha. Nambari ya pili ni nonsi. Maandishi yoyote kati yao hupuuzwa.

Kurejesha tapo (inafunguka katika kichupo kipya) ni njia ya Noir kurejesha thamani nyingi kutoka kwenye kazi.

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

// Sawa na hashMessage ya Viem
// https://viem.sh/docs/utilities/hashMessage#hashmessage
fn hashMessage(message: str<MESSAGE_LENGTH>) -> [u8;32] {

Tuliweza kutumia Heshi ya Pedersen kwa akaunti kwa sababu zinaheshishwa tu ndani ya uthibitisho wa maarifa-sifuri. Hata hivyo, katika msimbo huu tunahitaji kuangalia sahihi ya ujumbe, ambayo inazalishwa na kivinjari. Kwa hilo, tunahitaji kufuata muundo wa kusaini wa Ethereum katika EIP-191 (inafunguka katika kichupo kipya). Hii ina maana tunahitaji kuunda bafa iliyounganishwa na kiambishi awali cha kawaida, urefu wa ujumbe katika ASCII, na ujumbe wenyewe, na kutumia kiwango cha Ethereum cha keccak256 kukiheshisha.

Ili kuepuka matukio ambapo programu inamuomba mtumiaji kusaini ujumbe unaoweza kutumika kama muamala au kwa madhumuni mengine, EIP-191 inabainisha kwamba jumbe zote zilizosainiwa 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 thabiti, kwa sababu inafanya iwe rahisi kuubadilisha. Kwenye mfumo wa uzalishaji, labda ungedhani tu MESSAGE_LENGTH haibadiliki kwa ajili ya utendaji bora.

    keccak256::keccak256(buffer, HASH_BUFFER_SIZE)
}

Tumia kazi ya kawaida ya Ethereum ya keccak256.

fn signatureToAddressAndHash(
        message: str<MESSAGE_LENGTH>, 
        pubKeyX: [u8; 32],
        pubKeyY: [u8; 32],
        signature: [u8; 64]
    ) -> (Field, Field, Field)   // anwani, baiti 16 za kwanza za heshi, baiti 16 za mwisho za heshi        
{

Kazi hii inahakiki sahihi, ambayo inahitaji heshi ya ujumbe. Kisha inatupatia anwani iliyoisaini na heshi ya ujumbe. Heshi ya ujumbe hutolewa katika thamani mbili za Field kwa sababu hizo ni rahisi kutumia katika sehemu iliyosalia ya programu kuliko safu ya baiti.

Tunahitaji kutumia thamani mbili za Field kwa sababu hesabu za uga hufanywa modulo (inafunguka katika kichupo kipya) idadi 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 vigeu vinavyoweza kubadilika, na uandike heshi ndani yake baiti kwa baiti.

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

Hii inafanana na ecrecover ya Solidity (inafunguka katika kichupo kipya), kukiwa na tofauti mbili muhimu:

  • Ikiwa sahihi sio halali, wito unashindwa assert na programu inakatishwa.
  • Ingawa ufunguo wa umma unaweza kurejeshwa kutoka kwenye sahihi na heshi, huu ni uchakataji unaoweza kufanywa nje na, kwa hivyo, haufai kufanywa ndani ya uthibitisho wa maarifa-sifuri. Ikiwa mtu atajaribu kutudanganya hapa, uhakiki wa sahihi utashindwa.

Hatimaye, tunafika kwenye kazi ya main. Tunahitaji kuthibitisha kwamba tuna muamala unaobadilisha heshi ya akaunti kwa uhalali kutoka thamani ya zamani hadi mpya. Tunahitaji pia kuthibitisha kwamba ina heshi hii maalum ya muamala ili mtu aliyeituma ajue muamala wake umechakatwa.

{
    let mut txn = readTransferTxn(message);

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

Hatua ya 2 - Kuongeza seva

Katika hatua ya pili, tunaongeza seva inayopokea na kutekeleza miamala ya hamisho kutoka kwenye kivinjari.

Ili kuiona ikifanya kazi:

  1. Simamisha Vite ikiwa inafanya kazi.

  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 sawa na msimbo 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 hadi kwenye msimbo wa mteja kwenye http://localhost:5173 (inafunguka katika kichupo kipya)

  6. Kabla ya kutoa muamala, unahitaji kujua nonsi, pamoja na kiasi unachoweza kutuma. Ili kupata taarifa hii, bofya Sasisha data ya akaunti na usaini ujumbe.

    Tuna utata hapa. Kwa upande mmoja, hatutaki kusaini ujumbe unaoweza kutumika tena (shambulio la kurudiwa (inafunguka katika kichupo kipya)), ndiyo maana tunataka nonsi hapo awali. Hata hivyo, hatuna nonsi bado. Suluhisho ni kuchagua nonsi inayoweza kutumika mara moja tu na ambayo tayari tunayo pande zote mbili, kama vile wakati wa sasa.

    Tatizo la suluhisho hili ni kwamba wakati unaweza usiwe uliosawazishwa kikamilifu. Kwa hivyo badala yake, tunasaini thamani inayobadilika kila dakika. Hii ina maana kwamba dirisha letu la kuathiriwa na mashambulizi ya kurudiwa si zaidi ya dakika moja. Kwa kuzingatia kwamba katika uzalishaji ombi lililosainiwa litalindwa na TLS, na kwamba upande mwingine wa handaki---seva---inaweza tayari kufichua salio na nonsi (inabidi izijue ili kufanya kazi), hii ni hatari inayokubalika.

  7. Pindi kivinjari kinaporejesha salio na nonsi, kinaonyesha fomu ya hamisho. Chagua anwani ya mwisho na kiasi kisha ubofye Hamisha. Saini ombi hili.

  8. Ili kuona hamisho, ama Sasisha data ya akaunti au angalia kwenye dirisha ambapo unaendesha seva. Seva inarekodi hali kila wakati inapobadilika.

server/index.mjs

Faili hili (inafunguka katika kichupo kipya) lina mchakato wa seva, na huingiliana na msimbo wa Noir katika main.nr (inafunguka katika kichupo kipya). Hapa kuna maelezo ya sehemu za kuvutia.

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

Maktaba ya noir.js (inafunguka katika kichupo kipya) huunganisha 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 saketi ya hesabu---programu iliyokusanywa ya Noir tuliyounda katika hatua iliyopita---na uwe tayari kuitekeleza.

// Tunatoa tu taarifa za akaunti kama majibu kwa ombi lililotiwa sahihi
const accountInformation = async signature => {
    const fromAddress = await recoverAddress({
        hash: hashMessage("Get account data " + Math.floor((new Date().getTime())/60000)),
        signature
    })

Ili kutoa taarifa za akaunti, tunahitaji tu sahihi. Sababu ni kwamba tayari tunajua ujumbe utakuwa nini, na kwa hivyo heshi ya ujumbe.

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

Chakata ujumbe na utekeleze muamala unaousimba.

    // Pata ufunguo wa umma
    const pubKey = await recoverPublicKey({
        hash,
        signature
    })

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

noir.execute inaendesha programu ya Noir. Vigezo ni sawa na vile vilivyotolewa katika Prover.toml (inafunguka katika kichupo kipya). Kumbuka kwamba thamani ndefu hutolewa kama safu ya mifuatano ya 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 kosa, likamate kisha upitishe toleo lililorahisishwa kwa mteja.

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

Tumia 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 Accounts.

Hatua ya 3 - Mikataba mahiri ya Ethereum

  1. Simamisha michakato ya seva na mteja.

  2. Pakua tawi lenye mikataba mahiri 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. Zalisha ufunguo wa uhakiki na mhakiki wa Solidity, kisha unakili msimbo wa mhakiki 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 mahiri na uweke vigeu vya mazingira ili kutumia mnyororo wa vitalu wa anvil.

    cd ../../smart-contracts
    export ETH_RPC_URL=http://localhost:8545
    ETH_PRIVATE_KEY=ac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80
    
  6. Sambaza Verifier.sol na uhifadhi anwani kwenye kigeu 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. Sambaza 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 heshi ya Pedersen ya hali ya awali ya Accounts. Ikiwa utarekebisha hali hii ya awali katika server/index.mjs, unaweza kuendesha muamala ili kuona heshi ya awali iliyoripotiwa na uthibitisho wa maarifa-sifuri.

  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 baadhi ya miamala.

  11. Ili kuhakiki kwamba hali imebadilika mnyororoni, washa upya mchakato wa seva. Angalia kwamba ZkBank haikubali tena miamala, kwa sababu thamani ya asili ya heshi katika miamala inatofautiana na thamani ya heshi iliyohifadhiwa mnyororoni.

    Hili ndilo aina ya kosa linalotarajiwa.

server/index.mjs

Mabadiliko katika faili hili yanahusiana zaidi na kuunda uthibitisho halisi na kuuwasilisha mnyororoni.

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

const execPromise = util.promisify(exec)

Tunahitaji kutumia kifurushi cha Barretenberg (inafunguka katika kichupo kipya) ili kuunda uthibitisho halisi wa kutuma mnyororoni. Tunaweza kutumia kifurushi hiki ama kwa kuendesha kiolesura cha mstari wa amri (bb) au kwa kutumia maktaba ya JavaScript, bb.js (inafunguka katika kichupo kipya). Maktaba ya JavaScript ni polepole sana kuliko kuendesha msimbo kiasili, kwa hivyo tunatumia exec (inafunguka katika kichupo kipya) hapa ili 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 bb.js toleo la 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 siri ni mojawapo ya akaunti chaguomsingi zilizofadhiliwa mapema katika anvil.

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

Zalisha uthibitisho ukitumia faili inayoweza kutekelezwa ya bb.

    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 inaunda faili lenye vigeu vya umma, lakini hatuhitaji hilo. Tayari tulipata vigeu hivyo kutoka kwenye 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 Field, kila moja ikiwakilishwa kama thamani ya heksadesimali. Hata hivyo, tunahitaji kuituma katika muamala kama thamani moja ya bytes, ambayo Viem inaiwakilisha kwa mfuatano mkubwa wa heksadesimali. Hapa tunabadilisha muundo kwa kuunganisha thamani zote, kuondoa 0x zote, na kisha kuongeza moja mwishoni.

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

    return proof
}

Safisha na urejeshe uthibitisho.

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

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

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

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

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

Tuma muamala kwenye mnyororo.

smart-contracts/src/ZkBank.sol

Huu ni msimbo wa mnyororoni unaopokea muamala.

Msimbo wa mnyororoni unahitaji kufuatilia vigeu viwili: mhakiki (mkataba tofauti unaoundwa na nargo) na heshi ya hali ya sasa.

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

Kila wakati hali inapobadilika, tunatoa tukio la TransactionProcessed.

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

Kazi hii inachakata miamala. Inapata uthibitisho (kama bytes) na maingizo ya umma (kama safu ya bytes32), katika muundo ambao mhakiki anahitaji (ili kupunguza uchakataji wa mnyororoni na kwa hivyo gharama za gesi).

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

Uthibitisho wa maarifa-sifuri unahitaji kuwa kwamba muamala unabadilika kutoka heshi yetu ya sasa hadi mpya.

        myVerifier.verify(_proof, _publicFields);

Ita mkataba wa mhakiki ili kuhakiki uthibitisho wa maarifa-sifuri. Hatua hii hurejesha muamala ikiwa uthibitisho wa maarifa-sifuri ni mbaya.

Ikiwa kila kitu ni sawa, sasisha heshi ya hali kuwa thamani mpya na utoe tukio la TransactionProcessed.

Unyanyasaji na kijenzi kilichowekwa kati

Usalama wa taarifa una sifa tatu:

  • Usiri, watumiaji hawawezi kusoma taarifa ambazo hawajaidhinishwa kuzisoma.
  • Uadilifu, taarifa haziwezi kubadilishwa isipokuwa na watumiaji walioidhinishwa kwa njia iliyoidhinishwa.
  • Upatikanaji, watumiaji walioidhinishwa wanaweza kutumia mfumo.

Kwenye mfumo huu, uadilifu hutolewa kupitia uthibitisho wa maarifa-sifuri. Upatikanaji ni mgumu sana kuhakikisha, na usiri hauwezekani, kwa sababu benki inabidi ijue salio la kila akaunti na miamala yote. Hakuna njia ya kuzuia taasisi iliyo na taarifa kushiriki taarifa hiyo.

Inaweza kuwezekana kuunda benki ya siri kweli kwa kutumia anwani za siri (inafunguka katika kichupo kipya), lakini hilo liko nje ya upeo wa makala haya.

Taarifa za uongo

Njia moja ambayo seva inaweza kukiuka uadilifu ni kutoa taarifa za uongo wakati data inaombwa (inafunguka katika kichupo kipya).

Ili kutatua hili, tunaweza kuandika programu ya pili ya Noir inayopokea akaunti kama ingizo la faragha na anwani ambayo taarifa yake inaombwa kama ingizo la umma. Towe ni salio na nonsi ya anwani hiyo, na heshi ya akaunti.

Bila shaka, uthibitisho huu hauwezi kuhakikiwa mnyororoni, kwa sababu hatutaki kuchapisha nonsi na salio mnyororoni. Hata hivyo, inaweza kuhakikiwa 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 (inafunguka katika kichupo kipya). Lakini miamala ya kulazimishwa haiendani na uthibitisho wa maarifa-sifuri. Seva ndiyo taasisi pekee inayoweza kuhakiki miamala.

Tunaweza kurekebisha smart-contracts/src/ZkBank.sol ili kukubali miamala ya kulazimishwa na kuzuia seva kubadilisha hali hadi ichakatwe. Hata hivyo, hili linatufungua kwa shambulio rahisi la kunyimwa huduma. Je, ikiwa muamala wa kulazimishwa ni batili na kwa hivyo hauwezekani kuchakatwa?

Suluhisho ni kuwa na uthibitisho wa maarifa-sifuri kwamba muamala wa kulazimishwa ni batili. Hii inaipa seva chaguzi tatu:

  • Kuchakata muamala wa kulazimishwa, kutoa uthibitisho wa maarifa-sifuri kwamba umechakatwa na heshi ya hali mpya.
  • Kukataa muamala wa kulazimishwa, na kutoa uthibitisho wa maarifa-sifuri kwa mkataba kwamba muamala ni batili (anwani isiyojulikana, nonsi mbaya, au salio lisilotosha).
  • Kupuuza 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 nia ya faida kwa kuweka seva inafanya kazi. Tunaweza kuimarisha kichocheo hiki kwa kuwa na seva kuweka dhamana ya upatikanaji ambayo mtu yeyote anaweza kuiteketeza ikiwa muamala wa kulazimishwa hautachakatwa ndani ya kipindi fulani.

Msimbo mbaya wa Noir

Kwa kawaida, ili kuwafanya watu waamini mkataba mahiri tunapakia msimbo wa chanzo kwenye kichunguzi cha bloku (inafunguka katika kichupo kipya). Hata hivyo, katika kesi ya uthibitisho wa maarifa-sifuri, hiyo haitoshi.

Verifier.sol ina ufunguo wa uhakiki, ambao ni kazi ya programu ya Noir. Hata hivyo, ufunguo huo hautuambii programu ya Noir ilikuwa nini. Ili kuwa na suluhisho linaloaminika, unahitaji kupakia programu ya Noir (na toleo lililoiunda). Vinginevyo, uthibitisho wa maarifa-sifuri unaweza kuonyesha programu tofauti, yenye mlango wa nyuma.

Hadi vichunguzi vya bloku vianze kuturuhusu kupakia na kuhakiki programu za Noir, unapaswa kufanya hivyo mwenyewe (ikiwezekana kwa IPFS). Kisha watumiaji mahiri wataweza kupakua msimbo wa chanzo, kuukusanya wenyewe, kuunda Verifier.sol, na kuhakiki kwamba inafanana na ile iliyo mnyororoni.

Hitimisho

Programu za aina ya Plasma zinahitaji kijenzi kilichowekwa kati kama hifadhi ya taarifa. Hili hufungua uwezekano wa udhaifu lakini, kwa malipo, huturuhusu kuhifadhi faragha kwa njia ambazo hazipatikani kwenye mnyororo wa vitalu wenyewe. Kwa uthibitisho wa maarifa-sifuri tunaweza kuhakikisha uadilifu na ikiwezekana kuifanya iwe na faida kiuchumi kwa yeyote anayeendesha kijenzi kilichowekwa kati kudumisha upatikanaji.

Tazama hapa kwa kazi zangu zaidi (inafunguka katika kichupo kipya).

Shukrani

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

Makosa yoyote yaliyosalia ni jukumu langu.