1. Solidity alapok

Smart contract struktúra

pragma solidity ^0.4.21; // Solidity compiler verzió

contract MyContract { // contract név

    // állapotváltozók
    uint64 public current_time;
    mapping (string => string) nameOf;

    // konstruktor
    function MyContract() public {
        // ...
    }

    // publikus metódus
    function myMethod1(string param) public {
        // ...
    }

    // publikus konstans metódus
    function myMethod2(uint256 param) public view returns (bool) {
        // ...
    }

}

A fenti példában a Solidiy smart contract-ok (SC) általános felépítését láthatjuk. Első ránézésre egy SC hasonlít az objektumorientált programozásból ismert osztályokhoz: Az okosszerződésünknek vannak állapotváltozói, egy konstruktora amely ezeket inicializálja, illetve különböző publikus és privát metódusai.

Típusok

A Solidity egy statikusan típusos nyelv, tehát a változóink és függvényeink típusát deklarálnunk kell a forráskódban. Itt főleg az ismerős típusokkal dolgozhatunk: bool, egész típusok (pl. uint256), string, bytes, illetve definiálhatunk struktúrákat is. További részletek itt.

Két Solidity-re jellemző, gyakran használt típus az address, illetve a mapping. Az address típus egy Ethereum címet reprezentál:

address my_address = 0xdCad3a6d3569DF655070DEd06cb7A1b2Ccd1D3AF;

function foo() {
    address sender_address = msg.sender;
}

Az utóbbi példa megmutatja, hogy minden metódusban elérhető a hívó címe az msg.sender implicit paraméteren keresztül.

A mapping (A => B) típus egy hozzárendelést valósít meg. Ez hasonlít a más nyelvekből ismert map, hashmap, illetve dictionary adatstruktúrákra.

mapping (uint16 => string) customers;

uint16 current_id = 123;
string current_customer_name = "Andrew Smith";
customers[current_id] = current_customer_name;

A Solidity-nek nincs önálló dátum típusa, ehelyett uint Unix timestampekkel kell dolgoznunk. Azonban sok hasznos nyelvi elem segít ebben minket.

uint256 current_time = now;
uint256 tomorrow_this_time = now + 1 days.

További információk itt.

Végrehajtás

Egy Ethereum SC végrehajtás a következő lépések végrehajtásával jár:

  1. Először csatlakoznunk kell egy Ethereum node-hoz, amely egyfajta gateway lesz az Ethereum hálózat felé. Ehhez használhatunk saját node-ot is (pl. geth), de akár mások node-jait is igénybe vehetjük (pl. Metamask, Infura). (Az utóbbi esetben nyilván meg kell bíznunk a használt szolgáltatóban, hiszen ők akár át is verhetnének minket, pl. bizonyos események cenzúrázásával.)

  2. Ezután létre kell hoznunk egy tranzakciót. Ez történhet lokálisan is, illetve saját node esetén magán a node-on is. Egy tranzakció leírja, hogy ki (melyik cím) hívja melyik kontraktot (szintén cím szerint). Maga a hívás kódja (metódus azonosító, paraméterek) és a tranzakció részeként küldött ether is része az üzenetnek.

  3. A tranzakciót a küldő aláírja a saját privát kulcsával, majd beküldi a hálózatba a node-on keresztül.

  4. Az Ethereum hálózat node-jai egy p2p gossip protokoll segítségével informálják egymást az új tranzakciókról, így többé-kevésbé minden node lát minden tranzakciót.

  5. A bányász node-ok időnként összeválogatnak valamennyi tranzakciót, majd megpróbálják ezeket a PoW algoritmus során egy blokkba foglalni. (Emlékeztető: a PoW során a cél általában egy nonce integer érték kitalálása úgy, hogy az így eredményül kapott blokk bizonyos tulajdonságokkal bírjon. A probléma jellegéből adódóan ez gyakorlatilag csak véletlenszerű próbálkozással teljesíthető, így az átlagos blokk-idő is jól becsülhető.)

  6. Ha egy bányász node sikeresen előállít egy blokkot a mi tranzakciónkkal, akkor ezt a korábbiakhoz hasonlóan közli a hálózat többi node-jával. A többi node is validálja a blokkot (helyesek-e a tranakciók, helyes-e a PoW nonce), majd a blokk alapján aktualizálják a lokálisan számon tartott blockchain állapotot. Pl. ha a blokk egyik tranakciója elutalt az A címről a B címre 1 ethert, akkor minden node aktualizálja az adott címek egyenlegét.

  7. A tranzakciónk akkor tekinthető sikeresnek, ha egyrészt bekerült egy blokkba, másrészt ez a blokk már elég mélyen van a blockchain-ben, tehát már sok másik blokk épült rá.

A fenti leírás a tranzakciókra vonatkozik, melyek alapvető célja a blockchain állapotának megváltoztatása (pl. utalás, SC végrehajtás). Azonban bizonyos SC metódusok egyfajta getter funkciót töltenek be: nem változtatják az állapotot, csak olvassák azt. Az ilyen metódusokat a Solidity esetében a view (vagy const) kulcsszóval kell ellátnunk, és ezek meghívása a call (vs transaction).

contract MyContract {

    uint64 public current_time;

    function i_am_changing_the_state() public {
        current_time = now;
    }

    function i_am_reading_the_state() public view returns (uint256) {
        uint64 time = current_time;
        return time;
    }

}

Láthatóság

Az objektumorientált programozáshoz hasonlóan a Solidity esetében is beszélhetünk public és private láthatóságról (ld. még: internal, external). Azonban ezen kulcsszavak jelentése félrevezető.

Mint tudjuk, a blokklánc egy bárki által olvasható, publikus, append-only (csak hozzáfűzést engedélyező) főkönyv. Tehát attól hogy egy változót private-ként deklarálunk, még ugyanúgy olvasható bárki által. Emiatt jelszavakat és más nem publikus információkat semmiképp ne tároljunk a blockchain-en!

Solidity esetében a láthatósági kulcsszavak pusztán a SC publikus interfészét határozzák meg. Publikus metódusokat hívhatunk a contract-on kívülről (transaction illetve call segítségével). Ezzel szemben a private függvények csak a szerződésen belülről hívhatóak. Változók esetén pedig a public csupán annyit jelent, hogy a fordító generál egy gettert az adott változóhoz, tehát egy önálló metódust, melynek segítségével a SC-on kívülről is kényelmesen olvasható a változó állapota.

Az alapértelmezett érték változók esetén private, metódusok esetén pedig public (!). A félreértések elkerülése végett érdemes explicit módon feltüntetni a metódus láthatósági kulcsszavakat.

Megjegyzés: A házi feladat megoldása során ne térjetek el a kiadott skeleton szerkezetétől, mert a contract publikus interfészét is ellenőrizni fogjuk.

Ether

Sok SC pénzt is kezel ether formájában. Minden ethert fogadni képes metódust el kell látnunk a payable kucsszóval. A metódus meghívását megtestesító tranzakció ether mennyiségét az implicit msg.value értéken keresztül érhetjük el:

uint256 initialBalance;

function initialize() public payable {
    initialBalance = msg.value;
}

A SC-ok rendelkezhetnek egy ún. fallback függénnyel is, amely akkor hívódik meg, ha a tranzakció egy egyszerű utalás, nem pedig függvény hívás.

// fallback function
function () public payable {
    // do something...
}

Ethert küldeni az adott cím send illetve transfer függvényeivel lehet:

function get_all_money() public {
    uint256 amount = address(this).balance;
    msg.sender.send(amount);
}

Az egyenlegek alapvetően wei-ben tárolódnak, azonban itt is segítségünkre lehet pár hasznos nyelvi elem:

function get_one_ether() public {
    uint256 amount = 1 ether;
    msg.sender.send(amount);
}

Hibakezelés

Az Ethereum-os tranzakciók alapvetően vagy teljesen végrehajtódnak (i.e. lefut a meghívott metódus), vagy meghiúsulnak, amely esetben nem történik állapotváltozás. Az utóbbi esetnek két oka lehet: a végrehajtás túl erőforrás igényes (insufficient gas), vagy maga a kód triggerelte a tranzakció meghiúsulását.

Solidity metódusoknál gyakran lehet szükségünk bizonyos előfeltételek ellenőrzésére. Ebben segít a require kulcsszó. A require egy egyszerű igaz-hamis kifejezést vár, és csak akkor engedi továbbhaladni a végrehajtást, ha a kifejezés igaz, egyébként a tranzakció meghiúsul.

address owner; // initialize in constructor

function get_all_money() public {
    require (msg.sender == owner);
    uint256 amount = address(this).balance;
    msg.sender.send(amount);
}

A fenti metódust csak az owner váltózóban tárolt címről lehet meghívni.

Egyéb

A Solidity az előzőekben említett elemek mellett még rendelkezik az ismert vezérlő struktúrákkal: if-else, while, for. Ciklusok használata nem javasolt, hiszen könnyen túlléphetjük a megengedett végrehajtási limitet.

A főbb nem tárgyalt nyelvi elemek: modifiers, events, inheritance, libraries.

Érdemes fejben tartani, hogy az Ethereum blockchain fölötti kód végrehajtás pénzbe kerül. Az blockchain írása pedig kifejezetten drága, így igyekezzünk olyan kódot írni, mely minimalizálja a blockchain-re történő írások számát.

Továbbá érdemes olvasni a gyakori hibákról, pl. itt.

SC példák itt és itt.

2. Smart contract fejlesztés és tesztelés

Példa kód

pragma solidity ^0.4.21;

contract Counter {

    address owner;
    uint64 public counter;

    function Counter() public {
        owner = msg.sender;
        counter = 0;
    }

    function increment() public {
        require (msg.sender == owner);
        counter++;
    }

}

Remix lokális tesztelés

A legelterjedtebben használt Solidity IDE a gyakorlaton már megismert Remix, mely a böngészőben fut.

Miután beírtuk a kódunkat a böngészőben futó szerkesztőbe, a Start to compile gombra kattintva fordíthatjuk le azt. Ilyenkor a fordító értesít minket a hibákról és az esetleges javaslatairól (warnings).

Az egyszerű lokális teszteléshez válasszuk a Run fül alatti Environment mező JavaScript VM értékét. Ez a funkció nem csatlakozik az Ethereum hálózathoz, hanem lokálisan futtat egy EVM (Ethereum Virtual Machine) példányt, így az ezen való tesztelés gyors és ingyenes.

Az Environment alatti Account mező a rendelkezésre álló címeket mutatja, melyeket a Remix 100 etheres kezdő egyenlegre inicializált. Az aktuális címet a mező melletti gombra kattinva másolhatjuk ki. Az aktuálisan kiválasztott cím lesz a tranzakció küldője.

A Value mező a tranzakció során elküldendő ether/wei mennyiséget határozza meg.

A következő rész a SC létrehozásában segít. A Create gomb melletti mezőben vesszővel elválasztva sorolhatjuk fel a konstruktor argumentumokat, majd a Create-re kattintva létrehozhatunk egy új példányt. Példa az argumentumok megadására:

"0xca35b7d915458ef540ade6068dfe2f44e8fa733c", 145

Ezesetben az első konstruktor paraméter egy cím (figyelem! ezeket a Remix felületén stringként kell megadni), a második pedig egy egész szám.

A SC létrehozása után a jobb alsó szegmens mutatja a contract publikus interfészét, a meghívható metódusokat. A kék gombok a read-only (view) metódusokat jelölik, míg a piros gombok azokat a függvényeket reprezentálják, melyeket csak tranzakciókkal lehet meghívni.

Láthatjuk, hogy a Counter contract-nak két publikus metódusa van: az általunk definiált increment metódus, illetve a fordító által automatikusan generált counter getter (emlékeztető: a publikus tagváltozókhoz automatikusan kapunk ilyen getter függvényeket).

A counter gombra kattintva rögtön visszakapjuk a kezdeti 0 értéket. Ha ezután meghívjuk az increment függvényt, majd újra a counter-t, ezúttal már 1-et kapunk.

A kód alatti konzol ablakban láthatjuk az elküldött tranzakciókat (transact) és read-only hívásokat (call). Ezeknek megvizsgálhatjuk a részleteit is (Details), illetve itt érhető el a Debugger is (Debug).

Próbáljuk meg egy másik címről meghívni az increment metódust! Ehhez állítsuk át a jobb felső szegmensben az Address mező értékét, majd kattintsunk újra az increment gombra. Ha ezután megvizsgáljuk a konzol ablakban a küldött tranzakciót, a következőt látjuk:

status: 0x0 Transaction mined but execution failed

Tehát jól működik a contract-unk, más címről küldve meghiúsul az increment hívás.

Ropsten testnet

Mint tudjuk, az Ethereum hálózaton végzett minden művelet (tranzakció, contract példányosítás, stb.) etherbe kerül. Azonban a hálózaton való tesztelésre van több alternatív Ethereum hálózat, melyeken ingyenesek ezek a műveletek. Ezek közül mi a Ropsten hálózatot fogjuk használni.

Ahhoz, hogy csatlakozhassunk egy hálózathoz, szükségünk lesz egy Ethereum node-ra. Ehhez töltsük le a Metamask böngésző plugint, amely egy egyszerűen használható interfészen keresztül teszi elérhetővé az Ethereum hálózatokat. (Ezesetben a Metamask által biztosított node-okat használjuk.)

A Metamask megnyitása után először válasszuk ki a Ropsten teszthálózatot …

… majd hozzunk létre egy új account-ot …

… majd kattintsunk a Buy gombra és itt a Ropsten test facet gombra:

A request 1 ether from faucet gombra kattintva az oldal alján meg fog jelenni egy tranzakció, amely átutal az account-unkra 1 teszt ethert. (A honlap nem mindig működik, ez esetben érdemes többször próbálkozni. Alternatív faucet itt.)

A contract teszthálózaton való kipróbálásához állítsuk át a Remix Environment mezőjét Injected Web3-ra. (Ez gyakorlatilag úgy működik, hogy a Metamask beinjektálja a szükséget JavaScript objektumokat az oldalba, a Remix pedig ezeken keresztül tud vele kommunikálni.) Ezek után az Account mező már az előző lépésben létrehozott címünket tartalmazza.

Ha ekkor rákattintunk a Create gombra, először a Metamaskon jóvá kell hagynunk a küldendő tranzakciót:

Mivel teszthálózaton vagyunk (“ingyen van a pénz”), érdemesebb magasabbra venni a Gas Price-t, hiszen így gyorsabban lefut a tranzakciónk. A tranzakció jóváhagyása után az account-unk oldalán láthatjuk a tranzakciónk állapotát…

… illetve erre rákattintva megvizsgálhatjuk a tranzakció részleteit az Etherscan oldalon.

A tranzakció maximum maximum 1-2 perc alatt bekerül egy blokkba, ezután megjelenik a Remix oldalán is, innen ki tudjuk olvasni az új contract címét is.

További tranzakciók (pl. increment) küldése ugyanígy történik Metamaskon keresztül, meg a sima hívások (pl. counter) tranzakció nélkül azonnal lefutnak, hiszen ezek csak olvassák a blockchain állapotát így nincs szükség konszenzusra.