Ausführung des Benutzercodes auf GO

    In der Tat dreht sich alles um intelligente Verträge.


    Wenn Sie sich jedoch nicht genau vorstellen, was ein intelligenter Vertrag ist und in der Regel weit von der Krypta entfernt ist, dann stellen Sie sich vor, was eine in der Datenbank gespeicherte Prozedur ist. Der Benutzer erstellt Code-Teile, die auf unserem Server funktionieren. Es ist bequem für den Benutzer, sie zu schreiben und zu veröffentlichen, und wir können sie sicher ausführen.

    Leider haben wir die Sicherheit noch nicht entwickelt, deshalb werde ich sie jetzt nicht beschreiben, aber ich werde ein paar Hinweise geben.

    Wir schreiben auch auf Go, und die Laufzeit unterliegt sehr spezifischen Einschränkungen. Die wichtigste davon ist, dass wir im Allgemeinen keine Verknüpfung zu einem anderen Projekt erstellen können, das nicht für ihn geschrieben wurde. Dadurch wird unsere Laufzeit jedes Mal angehalten, wenn wir Code von Drittanbietern ausführen. Im Allgemeinen haben wir die Möglichkeit, eine Art Interpreter zu verwenden, da wir recht vernünftige Lua und völlig wahnsinnige WASM fanden. Aber irgendwie möchte ich keine Kunden auf Lua haken, und jetzt gibt es bei WASM mehr Probleme als Vorteile, die er verfassen kann Das wird jeden Monat aktualisiert. Wir werden also warten, bis die Spezifikation abgeschlossen ist. Verwenden Sie es als zweiten Motor.

    Aufgrund langwieriger Kämpfe mit seinem eigenen Gewissen wurde beschlossen, intelligente Verträge auf GO zu schreiben. Tatsache ist, dass Sie, wenn Sie die Ausführungsarchitektur eines kompilierten GO-Codes erstellen, diese Ausführung in einem separaten Prozess ausführen müssen, da Sie sich daran erinnern, dass wir der Sicherheit dienen. Die Ausführung in einem separaten Prozess ist ein Leistungsverlust auf IPC, obwohl wir später den Umfang der ausführbaren Datei verstanden haben Es war sogar angenehm, dass wir uns für diese Lösung entschieden haben. Die Sache ist, dass es skalierbar ist, obwohl es eine Verzögerung für jeden einzelnen Anruf hinzufügt. Wir können viele Remote-Ausführungsumgebungen abrufen.

    Ein wenig mehr über die Entscheidungen, um es klar zu machen. Jeder intelligente Vertrag besteht aus zwei Teilen, einem Teil ist der Klassencode und der zweite sind Objektdaten. Wenn Sie den Code im selben Code veröffentlichen, können Sie mehrere Verträge erstellen, die sich auf dieselbe Weise verhalten, jedoch mit unterschiedlichen Einstellungen. und mit einem anderen Zustand. Wenn Sie weiter reden - dann geht es um den Block und nicht um das Thema dieser Geschichte.

    Und so spielen wir GO


    Wir haben uns für den Plugin-Mechanismus entschieden, der nicht so bereit und gut ist. Es tut Folgendes: Wir kompilieren ein Plugin auf besondere Weise in der gemeinsam genutzten Bibliothek, laden es dann, suchen die darin enthaltenen Zeichen und übergeben dort die Ausführung. Der Haken daran ist jedoch, dass GO eine Laufzeit hat, und das ist fast ein Megabyte Code, und standardmäßig wird auch dieser Runt in diese Bibliothek aufgenommen, und wir haben überall Rantyp-Laufzeit. Aber jetzt haben wir uns entschlossen, darauf zu setzen und darauf zu vertrauen, dass wir es in Zukunft schlagen können.

    Alles wird einfach beim Erstellen Ihrer Bibliothek erledigt, Sie sammeln sie mit dem key - buildmode = plugin und erhalten die .so - Datei, die Sie dann öffnen.

    p, err := plugin.Open(path)

    Suchen Sie nach dem Symbol, das Sie interessiert:

    symbol, err := p.Lookup(Method)

    Je nachdem, ob es sich um eine Variable oder eine Funktion handelt, wird sie entweder aufgerufen oder als Variable verwendet.

    Unter der Haube verfügt dieser Mechanismus über ein einfaches dlopen (3). Wir laden die Bibliothek, prüfen, ob es sich um ein Plugin handelt, und überlagern den Wrapper. Beim Erstellen des Wrappers werden alle exportierten Zeichen in die Schnittstelle {} eingebunden und gespeichert. Wenn es sich um eine Funktion handelt, sollte sie auf den richtigen Funktionstyp gebracht und einfach aufgerufen werden: Wenn eine Variable vorhanden ist, funktioniert sie als Variable.

    Die Hauptsache ist, dass ein Symbol eine Variable ist, die für den gesamten Prozess global ist und nicht gedankenlos verwendet werden kann.

    Wenn ein Typ im Plugin deklariert wurde, ist es sinnvoll, ihn in einem separaten Paket abzulegen, damit der Hauptprozess damit arbeiten kann, z. B. Argumente an die Funktionen des Plugins übergeben. Dies ist optional, Sie können nicht baden und Spiegelungen verwenden.

    Unsere Verträge sind Objekte der entsprechenden "Klasse", und zu Beginn wurde die Instanz dieses Objekts in unserer exportierten Variablen gespeichert, sodass wir eine weitere Variable erstellen können:

    export, err := p.Lookup("EXPORT")
    obj := reflect.New(reflect.ValueOf(export).Elem().Type()).Interface()

    Und deserialisieren Sie bereits innerhalb dieser lokalen Variablen des korrekten Typs den Zustand des Objekts. Nachdem das Objekt wiederhergestellt wurde, können wir Methoden dafür aufrufen. Danach wird das Objekt serialisiert und zurück zum Repository hinzugefügt. Wir haben die Methode im Vertrag aufgerufen.

    Wenn Sie daran interessiert sind, wie, aber zu faul, um die Dokumentation zu lesen, dann:

    method := reflect.ValueOf(obj).MethodByName(Method)
    res:= method.Call(in)

    In der Mitte müssen Sie auch das Array mit leeren, leeren Schnittstellen füllen, die den korrekten Argumenttyp enthalten. Wenn Sie interessiert sind, sehen Sie selbst, wie der Quellcode geöffnet wurde. Allerdings ist es schwierig , diesen Platz in der Historie zu finden.

    Im Allgemeinen hat alles für uns funktioniert, Sie können Code mit so etwas wie einer Klasse schreiben, ihn in die Blockchain setzen, einen Vertrag dieser Klasse erneut in der Blockchain erstellen, einen Methodenaufruf ausführen und den neuen Status des Vertrages in die Blockchain zurückschreiben. Herrlich! Wie erstelle ich einen neuen Vertrag mit dem Code? Sehr einfach haben wir Funktionskonstruktoren, die ein neu erstelltes Objekt zurückgeben, bei dem es sich um einen neuen Vertrag handelt. Bisher funktioniert alles durch Nachdenken und der Benutzer muss schreiben:

    var EXPORT ContractType

    Damit wir wissen, welches Symbol die Vertretung darstellt, wurde es tatsächlich als Vorlage verwendet.

    Das mögen wir nicht wirklich. Und wir stoßen in den Wind.

    Parsing


    Erstens sollte der Benutzer nichts überflüssiges schreiben, und zweitens haben wir die Idee, dass die Interaktion eines Vertrags mit einem Vertrag einfach sein sollte und getestet werden sollte, ohne die Blockchain zu erhöhen, die Blockchain ist langsam und schwierig.

    Daher haben wir uns entschieden, den Vertrag in einen Wrapper einzuwickeln, der auf Basis des Vertrags und des Wrapper-Patterns grundsätzlich eine klare Entscheidung darstellt. Erstens erstellt der Wrapper ein Exportobjekt für uns und zweitens ersetzt er die Bibliothek, mit der der Vertrag abgerufen wird, wenn der Benutzer einen Vertrag schreibt, die Bibliothek (Stiftung) zum Testen mit Mocks in verwendet wird, und wenn der Vertrag veröffentlicht wird, wird er durch den Kampf ersetzt, der mit der Blockchain selbst arbeitet .

    Zu Beginn sollte der Code zerlegt werden und verstehen, was wir alle haben, um eine Struktur zu finden, die von BaseContract geerbt wird, um einen Wrapper um ihn herum zu generieren.

    Dies geschieht ganz einfach: Wir lesen die Datei mit dem Code in [] Byte, obwohl der Parser selbst die Dateien lesen kann. Es ist gut, den Text zu haben, auf den sich alle AST-Elemente beziehen, sie beziehen sich auf die Byte-Nummer in der Datei und wir möchten weiter erhalten Der Code der Strukturen, so wie er ist, nehmen wir nur etwas vom Typ.

    func(pf *ParsedFile)codeOfNode(n ast.Node)string {
    	returnstring(pf.code[n.Pos()-1 : n.End()-1])
    }

    Die Datei parsimieren wir tatsächlich und erhalten den obersten Knoten AST, aus dem wir eine Durchforstungsdatei erzeugen werden.

    fileSet = token.NewFileSet()
    node, err := parser.ParseFile(fileSet, name, code, parser.ParseComments)

    Als Nächstes gehen wir den Code vom oberen Knoten aus durch und sammeln alles Interessante in einer separaten Struktur.

    for _, decl := range node.Decls {
    	switch d := decl.(type) {
    	case *ast.GenDecl:
    		…
    	case *ast.FuncDecl:
    		…
    	}
    }

    Decls, dies ist bereits eine Liste von allem, was in der Datei definiert ist, die bereits in ein Array geparst wurde. Dies ist jedoch ein Array von Decl-Interfaces, die nicht beschreiben, was sich darin befindet. Daher sollte jedes Element auf einen bestimmten Typ reduziert werden. Die go / ast-Schnittstelle ist eher eine Basisklasse.

    Wir interessieren uns für die Knoten der Typen GenDecl und FuncDecl. GenDecl ist eine Definition einer Variablen oder eines Typs. Sie müssen überprüfen, was sich in einem Typ befindet, und erneut zu einem TypeDecl-Typ führen, mit dem Sie bereits arbeiten können. FuncDecl ist einfacher - dies ist eine Funktion, und wenn das Feld Recv ausgefüllt ist, ist dies die Methode der entsprechenden Struktur. Alle diese Sachen sammeln wir in einem praktischen Speicher, denn dann verwenden wir den Text / die Vorlage, aber er hat keine große Ausdruckskraft.

    Das Einzige, was wir gesondert berücksichtigen müssen, ist der Name des Datentyps, der von BaseContract geerbt wird. Es ist darum herum, dass wir tanzen werden.

    Codegenerierung


    Daher kennen wir alle Typen und Funktionen, die in unserem Vertrag enthalten sind, und müssen aus dem eingehenden Methodennamen und dem serialisierten Array von Argumenten einen Methodenaufruf für das Objekt ausführen können. Zum Zeitpunkt der Codegenerierung kennen wir schließlich das gesamte Vertragsgerät. Daher fügen wir neben unserer Vertragsdatei eine weitere Datei mit demselben Paketnamen hinzu, in die wir alle erforderlichen Importe zwingen, die Typen sind bereits in der Hauptdatei definiert und unnötig.

    Und vor allem die Wrapper über die Funktionen. Der Name des Wrappers wird durch ein Präfix ergänzt. Nun kann der Wrapper leicht gesucht werden.

    symbol, err := p.Lookup("INSMETHOD_" + Method)
    wrapper, ok := symbol.(func(ph proxyctx.ProxyHelper, object []byte,
    	data []byte)(object []byte, result []byte, err error))
    

    Jeder Wrapper hat dieselbe Signatur, sodass wir beim Aufruf aus dem Hauptprogramm keine unnötigen Überlegungen benötigen. Funktions-Wrapper unterscheiden sich von Methoden-Wrappern nur dadurch, dass sie den Status des Objekts nicht abrufen oder zurückgeben.

    Was haben wir im Wrapper?

    Wir erstellen ein Array mit leeren Variablen, die den Funktionsargumenten entsprechen, fügen es in eine Variable vom Typ Array mit Schnittstellen ein und deserialisieren die Argumente. Wenn wir eine Methode sind, müssen wir auch den Status des Objekts serialisieren, im Allgemeinen wie folgt:

    {{ range $method := .Methods }}
    funcINSMETHOD_{{ $method.Name }}(ph proxyctx.ProxyHelper, object []byte, data []byte) ([]byte, []byte, error) {
        self := new({{ $.ContractType }})
        err := ph.Deserialize(object, self)
        if err != nil {
            returnnil, nil, err
        }
        {{ $method.ArgumentsZeroList }}
        err = ph.Deserialize(data, &args)
        if err != nil {
            returnnil, nil, err
        }
    {{ if $method.Results }}
        {{ $method.Results }} := self.{{ $method.Name }}( {{ $method.Arguments }} )
    {{ else }}
        self.{{ $method.Name }}( {{ $method.Arguments }} )
    {{ end }}
        state := []byte{}
        err = ph.Serialize(self, &state)
        if err != nil {
            returnnil, nil, err
        }
    {{ range $i := $method.ErrorInterfaceInRes }}
        ret{{ $i }} = ph.MakeErrorSerializable(ret{{ $i }})
    {{ end }}
        ret := []byte{}
        err = ph.Serialize([]interface{} { {{ $method.Results }} }, &ret)
        return state, ret, err
    }
    {{ end }}
    

    Der aufmerksame Leser fragt, und was ist der Proxyhelper? - Dies ist ein Kombinationsobjekt, das wir noch benötigen, aber jetzt nutzen wir seine Fähigkeit zur Serialisierung und Deserialisierung.

    Nun, jeder liest, fragt: "Aber das sind Ihre Argumente, woher kommen sie?" Hier ist auch eine klare Antwort, aber die Text- / Vorlagensterne vom Himmel reichen nicht aus. Deshalb berechnen wir diese Zeilen im Code und nicht in der Vorlage.

    method.ArgumentsZeroList enthält etwas vom Typ

    var arg0 int = 0
    Var arg1 string = “”
    Var arg2 ackwardType = ackwardType{}
    Args  := []interface{}{&arg0, &arg1, &arg2}

    Argumente enthalten jeweils "arg0, arg1, arg2".

    Auf diese Weise können wir mit jeder Signatur alles abrufen, was wir wollen.

    Es kann jedoch keine Antwort serialisiert werden, Tatsache ist, dass Serialisierer mit Reflektion arbeiten, aber sie geben keinen Zugriff auf die nicht exportierten Felder von Strukturen. Deshalb haben wir eine spezielle Proxy-Methode, die ein Fehleroberflächenobjekt verwendet und daraus ein Basisobjekt erstellt. Fehler, der sich von dem üblichen unterscheidet, da der Fehlertext im exportierten Feld enthalten ist, und wir können ihn serialisieren, wenn auch mit einem gewissen Verlust.

    Wenn wir jedoch einen Code erzeugenden Sterilisator verwenden, werden wir ihn auch nicht brauchen. Wir werden in derselben Verpackung zusammengestellt und haben Zugriff auf nicht exportierte Felder.

    Was ist, wenn wir einen Vertrag aus einem Vertrag abrufen möchten?


    Sie verstehen nicht die ganze Tiefe des Problems, wenn Sie der Meinung sind, dass es leicht ist, einen Vertrag aus einem Vertrag herauszurufen. Tatsache ist, dass die Richtigkeit eines anderen Vertrags den Konsens bestätigen sollte und die Tatsache dieser Aufforderung in der Blockchain unterschrieben werden sollte. Im Allgemeinen sollte der Vertrag einfach mit einem anderen Vertrag kompiliert und nach seiner Methode aufgerufen werden - es wird nicht funktionieren, obwohl ich das wirklich will. Aber wir sind Freunde von Programmierern, deshalb sollten wir ihnen die Möglichkeit geben, alles auf der Linie zu tun und alle Tricks unter der Haube des Systems zu verbergen. So wird die Entwicklung eines Vertrages wie bei direkten Aufrufen durchgeführt, und Verträge ziehen sich transparent voneinander ab. Wenn wir jedoch einen Vertrag zur Veröffentlichung abrufen, senden wir anstelle eines anderen Vertrages einen Vertreter, der nur die Adresse und die Anrufunterschriften über den Vertrag kennt.

    Wie wäre das alles zu organisieren? - Wir müssen andere Verträge in einem speziellen Verzeichnis speichern, das unser Generator erkennen und für jeden importierten Vertrag einen Proxy erstellen kann.

    Dh wenn wir uns trafen:

    import  “ContractsDir/ContractAddress"

    Wir schreiben es in die Liste der importierten Verträge.

    Übrigens, dazu müssen Sie nicht mehr den Quellcode des Vertrages kennen, Sie müssen nur die Beschreibung kennen, die wir bereits gesammelt haben. Wenn wir also eine solche Beschreibung irgendwo veröffentlichen und alle Anrufe durch das Hauptsystem gehen, ist es uns egal, was dabei ist Ein anderer Vertrag ist in der Sprache geschrieben. Wenn wir Methoden darauf aufrufen können, können wir auf Go einen Stub dafür schreiben, der wie ein Paket mit einem Vertrag aussieht, der direkt aufgerufen werden kann. Napoleonische Pläne, kommen wir zur Umsetzung.

    Im Prinzip haben wir bereits eine Proxyhelper-Methode mit dieser Signatur:

    RouteCall(ref Address, method string, args []byte) ([]byte, error)

    Diese Methode kann direkt aus einem Vertrag aufgerufen werden. Sie ruft einen Fernvertrag auf und gibt eine serialisierte Antwort zurück, die wir analysieren und zu unserem Vertrag zurückschicken müssen.

    Der Benutzer muss jedoch folgendermaßen aussehen:

    ret := contractPackage.GetObject(Address).Method(arg1,arg2, …)

    Lassen Sie uns beginnen. Zuerst müssen Sie im Proxy alle Typen auflisten, die in den Signaturen der Vertragsmethoden verwendet werden. Wenn wir uns jedoch erinnern, können wir für jeden AST-Knoten eine Textdarstellung verwenden, sodass die Zeit für diesen Mechanismus gekommen ist.

    Als nächstes müssen wir eine Art von Vertrag erstellen, im Prinzip kennt er seine Klasse bereits, nur die Adresse wird benötigt.

    type {{ .ContractType }} struct {
        Reference Address
    }

    Außerdem müssen wir die GetObject-Funktion irgendwie implementieren, die einen Instanz-Proxy an die Adresse in der Blockchain zurückgibt, die mit diesem Vertrag funktionieren kann. Für den Benutzer sieht es wie die tatsächliche Instanz des Vertrags aus.

    funcGetObject(ref Address)(r *{{ .ContractType }}) {
        return &{{ .ContractType }}{Reference: ref}
    }

    Interessanterweise ist die GetObject-Methode im Benutzer-Debugging-Modus direkt eine BaseContract-Strukturmethode. Sie ist jedoch nicht vorhanden. Nichts hindert uns daran, die SLA zu tun, was für uns bequem ist. Jetzt können wir einen Vertretervertrag erstellen, dessen Methoden wir kontrollieren. Es bleibt die eigentliche Erstellung von Methoden.

    {{ range $method := .MethodsProxies }}
    func(r *{{ $.ContractType }}) {{ $method.Name }}( {{ $method.Arguments }} ) ( {{ $method.ResultsTypes }} ) {
        {{ $method.InitArgs }}
        var argsSerialized []byte
        err := proxyctx.Current.Serialize(args, &argsSerialized)
        if err != nil {
            panic(err)
        }
        res, err := proxyctx.Current.RouteCall(r.Reference, "{{ $method.Name }}", argsSerialized)
        if err != nil {
       		panic(err)
        }
        {{ $method.ResultZeroList }}
        err = proxyctx.Current.Deserialize(res, &resList)
        if err != nil {
            panic(err)
        }
        return {{ $method.Results }}
    }
    {{ end }}

    Dasselbe gilt für die Erstellung der Liste von Argumenten, da wir faul sind und genau die ast.Node-Methode speichern. Dann erfordern Berechnungen viele Typkonvertierungen, die die Vorlagen nicht kennen, sodass alles im Voraus vorbereitet wird. Mit Funktionen ist alles sehr viel komplizierter, und dies ist das Thema eines anderen Artikels.

    Unsere Funktionen sind Objektkonstruktoren und es wird großer Wert darauf gelegt, wie Objekte in unserem System tatsächlich erstellt werden. Die Tatsache der Erstellung wird auf einem Remote-Executor registriert, das Objekt wird an einen anderen Executor übertragen, es wird geprüft und tatsächlich gespeichert, und es gibt viele Möglichkeiten, es zu speichern. Dieser Wissensbereich wird als Krypta bezeichnet. Und die Idee ist im Prinzip einfach: ein Wrapper, in dem nur die Adresse gespeichert ist, und die Methoden, die den Anruf serialisieren und unseren Singleton-Harvester ziehen, der den Rest erledigt. Wir können den übergebenen Proxyhelper nicht verwenden, da der Benutzer ihn nicht an uns weitergegeben hat, also mussten wir ihn zu einem Einzelspieler machen.

    Ein weiterer Trick - in der Tat verwenden wir immer noch den Kontext des Aufrufs. Dies ist ein Objekt, in dem Informationen darüber gespeichert werden, wer wann, warum, warum unser intelligenter Vertrag aufgerufen wurde. Auf der Grundlage dieser Informationen entscheidet der Benutzer, ob der Anruf überhaupt ausgeführt werden soll und ob Sie dies können wie

    Zuvor haben wir den Kontext einfach übergeben, es war ein nicht abgelaufenes Feld im Typ BaseContract mit einem Setter und einem Getter, und der Setter erlaubte es, das Feld nur einmal einzustellen bzw. den Kontext vor der Ausführung des Vertrags festzulegen und der Benutzer konnte es nur lesen.

    Das Problem ist jedoch, dass der Benutzer nur diesen Kontext liest. Wenn er eine Systemfunktion anruft, zum Beispiel Stellvertreter eines anderen Vertrags, dann erhalten diese Stellvertreter keinen Kontext, da ihn niemand an ihn weitergibt. Und hier kommt der Goroutine-Speicher. Wir haben uns entschieden, kein eigenes zu schreiben, sondern github.com/tylerb/gls zu verwenden.

    Damit können Sie den Kontext für die aktuelle Auswahl festlegen und übernehmen. Wenn also kein Gorutin innerhalb des Vertrags erstellt wurde, setzen wir einfach den Kontext in gls, bevor der Vertrag gestartet wird. Jetzt geben wir dem Benutzer keine Methode, sondern nur eine Funktion.

    funcGetContext() *core.LogicCallContext {
    	return gls.Get("ctx").(*core.LogicCallContext)
    }

    Und er verwendet es gerne, aber wir verwenden es zum Beispiel in RouteCall (), um zu verstehen, welcher Vertrag nun jemanden verursacht.

    Der Benutzer kann im Prinzip ein Gorutin erstellen. Wenn er dies tut, ist der Kontext jedoch verloren, sodass wir etwas tun müssen. Wenn der Benutzer beispielsweise das Schlüsselwort go verwendet, muss der Parser solche Aufrufe in unseren Wrapper packen, den der Kontext erstellt und wird den Kontext darin wiederherstellen, aber dies ist das Thema eines anderen Artikels.

    Alle zusammen


    Wir mögen das Prinzip, wie die GO-Toolchain funktioniert. Tatsächlich handelt es sich um eine Reihe verschiedener Befehle, die eine Funktion ausführen, die beispielsweise zusammen ausgeführt werden, wenn Sie einen Go-Build ausführen. Wir beschlossen, dasselbe zu tun, ein Team legt die Vertragsdatei in das temporäre Verzeichnis, das zweite legt einen Wrapper dafür und ruft den dritten mehrmals auf, wodurch für jeden Vertrag ein Proxy erstellt wird, das vierte erstellt es, das fünfte veröffentlicht es in der Blockchain. Und es gibt einen Befehl, um alle in der richtigen Reihenfolge auszuführen.

    Hooray, wir haben jetzt Toolchain und Runtime, um GO von GO aus auszuführen. Es gibt immer noch eine Menge Probleme, zum Beispiel müssen Sie den ungenutzten Code irgendwie entladen. Sie müssen feststellen, dass er hängen bleibt, und den angehaltenen Prozess erneut starten. All dies sind jedoch Aufgaben, die eindeutig zu lösen sind.

    Ja, natürlich schreibt der von uns geschriebene Code nicht für Bibliothekarismus vor, er kann nicht direkt verwendet werden, aber das Lesen eines Beispiels für die Erzeugung von Arbeitscode ist immer großartig, ich hatte zu meiner Zeit nicht genug davon. Dementsprechend kann ein Teil der Codegenerierung im Compiler und dessen Ausführung im Performer angezeigt werden .

    Jetzt auch beliebt: