Smart Contracts in der Programmiersprache Rust
Seite 3: Speicherverwaltung von Rust
Rust hingegen hat ganz andere Ansprüche an Programmierer*innen. Code soll sich ohne "Schnickschnack" möglichst direkt auf Speicherlayout abbilden lassen. Diese Einstellung kommt aus den C- und C++-Welten, in denen manuelle Speicherverwaltung Usus ist. Das bisher noch experimentelle Ethereum-SDK für Rust lässt einen mit der Registerallokation weitgehend allein, sodass man die Adressen selbst ausrechnen muss. Das ist der Grund, warum der struct zum Contract leer geblieben ist: Es gibt keine Notwendigkeit dafür, irgendwelche Werte im Stack abzulegen. Stattdessen deklariert man sich zwei globale Konstanten:
lazy_static! {
static ref OWNER_KEY: H256 =
H256::from([2,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]);
static ref BALANCE_KEY: H256 =
H256::from([3,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0])
}
Würde man das automatisieren wollen, müsste man sich ein Makro schreiben, das ähnlich wie eth_abi
die Struktur des Vertrags analysiert und Register passend alloziert. Der große Vorteil von Rust-Makros gegenüber dem C-Präprozessor ist, dass man damit ASTs manipulieren kann, statt grobkörnig Text zu ersetzen. Der dritte Schritt ist die Implementierung der tatsächlichen Logik des Vertrags:
impl WalletInterface for WalletContract {
fn constructor(&mut self) {
let sender: [u8; 32] = H256::from(pwasm_ethereum::sender()).into();
pwasm_ethereum::write(&OWNER_KEY, &sender)
}
fn owner(&mut self) -> Address {
H256::from(pwasm_ethereum::read(&OWNER_KEY)).into()
}
fn balance(&mut self) -> U256 {
pwasm_ethereum::read(&BALANCE_KEY).into()
}
fn addfund(&mut self) -> bool {
let sender = pwasm_ethereum::sender();
if sender != self.owner() {
false
}
else {
let new_balance: [u8; 32] = (self.balance() + pwasm_ethereum::value()).into();
pwasm_ethereum::write(&BALANCE_KEY, &new_balance);
true
}
}
fn withdraw(&mut self) -> bool {
let sender = pwasm_ethereum::sender();
if sender != self.owner() {
false
}
else {
pwasm_ethereum::suicide(&sender)
}
}
}
Die Abläufe sind ähnlich zum Solidity-Pendant, aber es ist deutlich zu sehen, dass das SDK weniger Hilfestellung leistet. Insbesondere muss man häufig zwischen verschiedenen Zahlentypen konvertieren (Hashes, Unsigned Integer, Byte-Arrays). Der Registerzugriff läuft hier über spezielle Funktionen, die die pwasm_ethereum-Bibliothek bereitstellt. Intern handelt es sich dabei um dünne Wrapper über EWASM-Primitive, die also in der VM implementiert sind.
Call & Deploy
Als letzten Schritt definieren wir noch die Einstiegspunkte für den Vertrag für die beiden Fälle des initialen Deployments und normaler Aufrufe:
#[no_mangle]
pub fn call() {
let mut endpoint = WalletEndpoint::new(WalletContract{});
pwasm_ethereum::ret(&endpoint.dispatch(&pwasm_ethereum::input()));
}
#[no_mangle]
pub fn deploy() {
let mut endpoint = WalletEndpoint::new(WalletContract{});
endpoint.dispatch_ctor(&pwasm_ethereum::input());
}
Die beiden Funktionen sind bei den meisten Smart Contracts identisch. Die einzige Aufgabe besteht darin, die eingehenden Argumente zu verarbeiten und an die automatisch (per eth_abi
) generierten dispatch
- und dispatch_ctor
-Methoden weiterzuleiten. Sie kümmern sich um die Auswahl der korrekten Smart-Contract-Methode.