Smart Contracts in der Programmiersprache Rust

Seite 3: Speicherverwaltung von Rust

Inhaltsverzeichnis

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.

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.