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:
-
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.)
-
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.
-
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.
-
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.
-
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ő.) -
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 azA
címről aB
címre 1 ethert, akkor minden node aktualizálja az adott címek egyenlegét. -
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.
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.