Il s'agit en fait de contrats intelligents.
Mais si vous n'imaginez pas tout à fait ce qu'est un contrat intelligent et que vous êtes en général loin de la cryptographie, alors qu'est-ce qu'une procédure stockée dans une base de données, vous pouvez parfaitement l'imaginer. L'utilisateur crée des morceaux de code qui fonctionnent ensuite sur notre serveur. Il est pratique pour l'utilisateur de les écrire et de les publier, et il est sûr pour nous de les exécuter.
Malheureusement, nous n'avons pas encore développé la sécurité, donc maintenant je ne vais pas la décrire, mais je vais donner quelques conseils.
Nous écrivons également sur Go, et son runtime impose des restrictions très spécifiques, dont la principale est, dans l'ensemble, nous ne pouvons pas créer de lien vers un autre projet écrit pas sur le pouce, cela arrêtera notre runtime chaque fois que nous exécuterons du code tiers. En général, nous avons la possibilité d'utiliser une sorte d'interprète, pour lequel nous avons trouvé un Lua complètement sain et un WASM complètement sain, mais d'une manière ou d'une autre je ne veux pas ajouter de clients à Lua, mais avec WASM maintenant, il y a plus de problèmes que d'avantages, c'est dans un projet d'état , qui est mis à jour tous les mois, nous attendrons donc la fin des spécifications. Nous l'utilisons comme deuxième moteur.
À la suite de longues batailles avec sa propre conscience, il a été décidé d'écrire des contrats intelligents sur GO. Le fait est que si vous construisez l'architecture pour exécuter le code GO compilé, vous devrez transférer cette exécution vers un processus distinct, comme vous vous en souvenez, pour des raisons de sécurité, et le transfert vers un processus distinct est une perte de performances sur IPC, bien qu'à l'avenir, lorsque nous aurons compris le volume d'exécutable code, il était même en quelque sorte agréable que nous avons choisi cette solution. Le fait est qu'il est évolutif, bien qu'il ajoute un retard à chaque appel individuel. Nous pouvons générer de nombreux runtimes distants.
Un peu plus sur les décisions prises pour que ce soit clair. Chaque contrat intelligent se compose de deux parties, une partie est le code de classe et la seconde est les données d'objet, donc sur le même code, nous pouvons, une fois que nous avons publié le code, créer de nombreux contrats qui se comporteront essentiellement de la même manière, mais avec des paramètres différents et avec un état différent. Si nous parlons plus loin, cela concerne déjà la blockchain et non le sujet de cette histoire.
Et donc, nous exécutons GO
Nous avons décidé d'utiliser le mécanisme de plugin, qui n'est pas seulement prêt et bon. Il fait ce qui suit, nous compilons ce qui sera un plugin d'une manière spéciale dans une bibliothèque partagée, puis le chargeons, y trouvons les symboles et passons l'exécution là -bas. Mais le hic, c'est que GO a un runtime, et c'est presque un mégaoctet de code, et par défaut, ce runtime va également à cette bibliothèque, et nous avons un runtime raznipipenny partout. Mais maintenant, nous avons décidé d'y aller, étant sûrs que nous pourrons le vaincre à l'avenir.
Tout est simple lorsque vous construisez votre bibliothèque, vous la construisez avec le plugin key - buildmode = et obtenez le fichier .so, que vous ouvrez ensuite.
p, err := plugin.Open(path)
Vous recherchez le personnage qui vous intéresse:
symbol, err := p.Lookup(Method)
Et maintenant, selon que la variable est une fonction ou une fonction, vous l'appelez ou vous l'utilisez comme variable.
Sous le capot de ce mécanisme se trouve un simple dlopen (3), nous chargeons la bibliothèque, vérifions qu'il s'agit d'un plugin et donnons le wrapper dessus, lors de la création du wrapper, tous les caractères exportés sont encapsulés dans l'interface {} et stockés. S'il s'agit d'une fonction, elle doit être réduite au type de fonction correct et simplement appelée, si la variable - fonctionne alors comme une variable.
La principale chose à retenir est que si un symbole est une variable, il est global tout au long du processus et vous ne pouvez pas l'utiliser sans réfléchir.
Si un type a été déclaré dans le plugin, alors il est logique de mettre ce type dans un package séparé afin que le processus principal puisse travailler avec lui, par exemple, en passant comme arguments aux fonctions du plugin. Ceci est facultatif, vous ne pouvez pas cuire à la vapeur et utiliser la réflexion.
Nos contrats sont des objets de la «classe» correspondante, et au début l'instance de cet objet était stockée dans notre variable exportée, donc nous pourrions créer une autre même variable:
export, err := p.Lookup("EXPORT") obj := reflect.New(reflect.ValueOf(export).Elem().Type()).Interface()
Et déjà à l'intérieur de cette variable locale du type correct, désérialisez l'état de l'objet. Une fois l'objet restauré, nous pouvons appeler des méthodes dessus. Après quoi l'objet est sérialisé et ajouté de nouveau au magasin, applaudissements nous avons appelé la méthode sur le contrat.
Si vous êtes intéressé par la façon, mais trop paresseux pour lire la documentation, alors:
method := reflect.ValueOf(obj).MethodByName(Method) res:= method.Call(in)
Au milieu, vous devez également remplir le tableau in avec des interfaces vides contenant le bon type d'argument, si vous êtes intéressé, voyez par vous-même comment cela a été fait, les sources sont ouvertes, bien qu'il sera difficile de trouver cet endroit dans l'
histoire .
En général, tout a fonctionné pour nous, vous pouvez écrire du code avec quelque chose comme une classe, le mettre sur la blockchain, créer à nouveau un contrat de cette classe sur la blockchain, faire un appel de méthode et le nouvel état du contrat est réécrit dans la blockchain. Super! Comment créer un nouveau contrat avec le code en main? Très simple, nous avons des fonctions constructeurs qui retournent un objet fraîchement créé, qui est le nouveau contrat. Jusqu'à présent, tout fonctionne par réflexion et l'utilisateur doit écrire:
var EXPORT ContractType
Afin que nous sachions quel symbole est une représentation du contrat, et que nous l'utilisons réellement comme modèle.
Nous ne l’aimons pas vraiment. Et nous avons frappé fort.
Analyse
Premièrement, l'utilisateur ne doit rien écrire de superflu, et deuxièmement, nous avons l'idée que l'interaction du contrat avec le contrat doit être simple et testée sans augmenter la blockchain, la blockchain est lente et difficile.
Par conséquent, nous avons décidé d'envelopper le contrat dans un wrapper, qui est généré sur la base du contrat et du modèle de wrapper, en principe, une solution compréhensible. Premièrement, le wrapper crée un objet d'exportation pour nous, et deuxièmement, il remplace la bibliothèque avec laquelle le contrat est collecté lorsque l'utilisateur écrit le contrat, la bibliothèque de base est utilisée avec les mokas à l'intérieur, et lorsque le contrat est publié, il est remplacé par un combat qui fonctionne avec la blockchain elle-même .
Pour commencer, vous devez analyser le code et comprendre ce que nous avons généralement, trouver la structure héritée de BaseContract afin de générer un wrapper autour de lui.
Cela se fait tout simplement, nous lisons le fichier avec le code en [] octets, bien que l'analyseur lui-même puisse lire les fichiers, il est bon d'avoir le texte quelque part auquel tous les éléments AST se réfèrent, ils se réfèrent au numéro d'octet dans le fichier, et à l'avenir, nous voulons recevoir le code de structure tel qu'il est, nous prenons simplement quelque chose comme.
func (pf *ParsedFile) codeOfNode(n ast.Node) string { return string(pf.code[n.Pos()-1 : n.End()-1]) }
En fait, nous analysons le fichier et obtenons le nœud AST le plus haut à partir duquel nous allons analyser le fichier.
fileSet = token.NewFileSet() node, err := parser.ParseFile(fileSet, name, code, parser.ParseComments)
Ensuite, nous parcourons le code à partir du nœud supérieur et collectons tout ce qui est intéressant dans une structure distincte.
for _, decl := range node.Decls { switch d := decl.(type) { case *ast.GenDecl: … case *ast.FuncDecl: … } }
Decls, il est déjà analysé dans un tableau, une liste de tout ce qui est défini dans le fichier, mais c'est un tableau d'interfaces Decl qui ne décrit pas ce qu'il y a à l'intérieur, donc chaque élément doit être converti en un type spécifique, ici les auteurs du langage se sont éloignés de leur idée d'utiliser des interfaces, l'interface dans go / ast est plutôt une classe de base.
Nous nous intéressons aux nœuds de type GenDecl et FuncDecl. GenDecl est la définition d'une variable ou d'un type, et vous devez vérifier quel est exactement le type à l'intérieur, et encore une fois le convertir en le type TypeDecl avec lequel vous pouvez déjà travailler. FuncDecl est plus simple - c'est une fonction, et si le champ Recv est rempli, alors c'est une méthode de la structure correspondante. Nous collectons tout cela dans un stockage pratique, car nous utilisons ensuite du texte / modèle, et il n'a pas beaucoup de pouvoir expressif.
La seule chose dont nous devons nous souvenir séparément est le nom du type de données hérité de BaseContract, et nous allons danser autour de lui.
Génération de code
Et donc, nous connaissons tous les types et fonctions qui sont dans notre contrat et nous devons être en mesure de faire un appel de méthode sur un objet à partir du nom de méthode entrant et du tableau d'arguments sérialisés. Mais après tout, au moment de la génération du code, nous connaissons tout le dispositif du contrat, donc nous mettons à côté de notre fichier de contrat à côté d'un autre fichier, avec le même nom de package, dans lequel nous mettons toutes les importations nécessaires, les types sont déjà définis dans le fichier principal et ne sont pas nécessaires.
Et voici l'essentiel, les wrappers sur les fonctions. Le nom du wrapper est complété par une sorte de préfixe et maintenant le wrapper est facile à trouver.
symbol, err := p.Lookup("INSMETHOD_" + Method) wrapper, ok := symbol.(func(ph proxyctx.ProxyHelper, object []byte, data []byte) (object []byte, result []byte, err error))
Chaque wrapper a la même signature, donc lorsque nous l'appelons depuis le programme principal, nous n'avons pas besoin de réflexions supplémentaires, la seule chose est que les wrappers de fonction sont différents des wrappers de méthode, ils ne reçoivent pas et ne renvoient pas l'état de l'objet.
Qu'avons-nous à l'intérieur de l'emballage?
Nous créons un tableau de variables vides correspondant aux arguments de la fonction, le mettons dans une variable de type un tableau d'interfaces et y désérialisons les arguments, si nous sommes une méthode, nous devons également sérialiser l'état de l'objet, généralement quelque chose comme ceci:
{{ range $method := .Methods }} func INSMETHOD_{{ $method.Name }}(ph proxyctx.ProxyHelper, object []byte, data []byte) ([]byte, []byte, error) { self := new({{ $.ContractType }}) err := ph.Deserialize(object, self) if err != nil { return nil, nil, err } {{ $method.ArgumentsZeroList }} err = ph.Deserialize(data, &args) if err != nil { return nil, 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 { return nil, 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 }}
Un lecteur attentif sera intéressé par ce qu'est un assistant proxy? - c'est un tel objet de combinaison dont nous avons encore besoin, mais pour l'instant nous utilisons sa capacité à sérialiser et désérialiser.
Eh bien, quiconque lit demandera: "Mais ce sont vos arguments, d'où viennent-ils?" Voici également une réponse compréhensible, oui texte / modèle il n'y a pas assez d'étoiles du ciel, c'est pourquoi nous calculons ces lignes dans le code, et non dans le modèle.
method.ArgumentsZeroList contient quelque chose comme
var arg0 int = 0 Var arg1 string = “” Var arg2 ackwardType = ackwardType{} Args := []interface{}{&arg0, &arg1, &arg2}
Et Arguments contient donc "arg0, arg1, arg2".
Ainsi, nous pouvons appeler tout ce que nous voulons, avec n'importe quelle signature.
Mais nous ne pouvons sérialiser aucune réponse, le fait est que les sérialiseurs fonctionnent avec la réflexion et ne donnent pas accès à des champs de structures non exportés, c'est pourquoi nous avons une méthode d'assistance proxy spéciale qui prend un objet d'interface d'erreur et crée un objet de type fondation à partir de celui-ci. Erreur, qui diffère de l'habituel en ce que le texte de l'erreur s'y trouve dans le champ exporté, et nous pouvons le sérialiser, mais avec une certaine perte.
Mais si nous utilisons un stérilisateur générateur de code, nous n'en avons même pas besoin, nous sommes compilés dans le même package, nous avons accès à des champs non exportés.
Mais que faire si nous voulons appeler un contrat Ă partir d'un contrat?
Vous ne comprenez pas la profondeur du problème si vous pensez qu'il est facile d'appeler un contrat à partir d'un contrat. Le fait est que la validité d'un autre contrat doit être confirmée par consensus et le fait de cet appel doit être signé sur la blockchain, en général, simplement compiler avec un autre contrat et invoquer sa méthode ne fonctionnera pas, bien que je le veuille vraiment. Mais nous sommes amis des programmeurs, nous devons donc leur donner la possibilité de tout faire directement et de cacher toutes les astuces sous le capot du système. Ainsi, le développement du contrat est comme avec des appels directs, et les contrats se tirent de manière transparente, mais lorsque nous collectons le contrat pour publication, nous glissons un proxy au lieu d'un autre contrat, qui ne connaît que son adresse et appelle les signatures du contrat.
Comment organiser tout ça? - Nous devrons stocker d'autres contrats dans un répertoire spécial que notre générateur pourra reconnaître et créer des proxys pour chaque contrat importé.
Autrement dit, si nous nous rencontrions:
import “ContractsDir/ContractAddress"
Nous l'écrivons sur la liste des contrats importés.
Soit dit en passant, pour cela, vous n'avez pas besoin de connaître le code source du contrat, il vous suffit de connaître la description que nous avons déjà collectée, donc si nous publions une telle description quelque part, et que tous les appels passent par le système principal, alors nous ne nous soucions pas de ce que un autre contrat est écrit dans le langage, si nous pouvons y appeler des méthodes, nous pouvons écrire un stub pour lui sur Go, qui ressemblera à un package avec un contrat qui peut être appelé directement. Plans napoléoniens, commençons.
En principe, nous avons déjà une méthode d'assistance proxy, avec cette signature:
RouteCall(ref Address, method string, args []byte) ([]byte, error)
Cette méthode peut être appelée directement à partir du contrat, elle appelle le contrat à distance, renvoie une réponse sérialisée que nous devons analyser et retourner à notre contrat.
Mais il faut que l'utilisateur ressemble Ă tout:
ret := contractPackage.GetObject(Address).Method(arg1,arg2, …)
Commençons, tout d'abord, dans le proxy, vous devez répertorier tous les types qui sont utilisés dans les signatures des méthodes de contrat, mais comme nous nous en souvenons, pour chaque nœud AST, nous pouvons prendre sa représentation textuelle, et maintenant le temps est venu pour ce mécanisme.
Ensuite, nous devons créer un type de contrat, en principe, il connaît déjà sa classe, seule une adresse est nécessaire.
type {{ .ContractType }} struct { Reference Address }
Ensuite, nous devons en quelque sorte implémenter la fonction GetObject, qui à l'adresse sur la blockchain retournera une instance de proxy qui sait comment travailler avec ce contrat, et pour l'utilisateur, cela ressemble à une instance de contrat.
func GetObject(ref Address) (r *{{ .ContractType }}) { return &{{ .ContractType }}{Reference: ref} }
Fait intéressant, la méthode GetObject en mode de débogage utilisateur est directement une méthode de structure BaseContract, mais rien, rien ne nous empêche, en respectant le SLA, de faire ce qui nous convient. Nous pouvons maintenant créer un contrat proxy dont nous contrôlons les méthodes. Reste à créer réellement des méthodes.
{{ 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 }}
Voici la même histoire avec la construction de la liste des arguments, car nous sommes paresseux et stockons exactement le noeud ast.Node de la méthode, pour les calculs, il faut beaucoup de conversions de types que les modèles ne connaissent pas, donc tout est préparé à l'avance. Avec les fonctions, tout est sérieusement plus compliqué, et c'est le sujet d'un autre article.
Les fonctions que nous avons sont des constructeurs d'objets et l'accent est mis sur la façon dont les objets sont réellement créés dans notre système, le fait de la création est enregistré sur un artiste distant, l'objet est transféré vers un autre artiste, il est vérifié et effectivement stocké là -bas, et il existe de nombreuses façons de sauvegarder ce domaine de connaissance est appelé crypte. Et l'idée est fondamentalement simple, un wrapper à l'intérieur duquel seule l'adresse est stockée, et des méthodes qui sérialisent l'appel et tirent notre processeur singleton, qui fait le reste. Nous ne pouvons pas utiliser l’assistant de proxy transmis, car l’utilisateur ne nous l’a pas transmis, nous avons donc dû en faire un singleton.
Une autre astuce - en fait, nous utilisons toujours le contexte d'appel, c'est un tel objet qui stocke des informations sur qui, quand, pourquoi, pourquoi notre contrat intelligent a été appelé, sur la base de ces informations, l'utilisateur prend la décision de donner l'exécution du tout, et si possible alors comment.
Auparavant, nous passions simplement le contexte, c'était un champ non exprimable dans le type BaseContract avec un setter et un getter, et le setter ne permettait de définir le champ qu'une seule fois, donc le contexte était défini avant l'exécution du contrat et l'utilisateur ne pouvait que le lire.
Mais voici le problème, l'utilisateur ne lit que ce contexte, s'il appelle une sorte de fonction système, par exemple, un appel proxy vers un autre contrat, cet appel proxy ne reçoit aucun contexte, car personne ne le lui passe. Et puis le stockage local de goroutine entre en scène. Nous avons décidé de ne pas écrire le notre, mais d'utiliser github.com/tylerb/gls.
Il vous permet de définir et de prendre le contexte pour le goroutine actuel. Ainsi, si aucune goroutine n'a été créée à l'intérieur du contrat, nous définissons simplement le contexte en gls avant de démarrer le contrat, maintenant nous donnons à l'utilisateur non pas une méthode, mais juste une fonction.
func GetContext() *core.LogicCallContext { return gls.Get("ctx").(*core.LogicCallContext) }
Et il l'utilise avec plaisir, mais nous l'utilisons dans RouteCall (), par exemple, pour comprendre quel contrat appelle actuellement quelqu'un.
En principe, l'utilisateur peut créer goroutine, mais s'il le fait, le contexte est perdu, nous devons donc faire quelque chose avec cela, par exemple, si l'utilisateur utilise le mot-clé go, nous devons encapsuler ces appels dans notre wrapper, que le contexte se souviendra et créera goroutine et restaurer le contexte en elle, mais c'est le sujet d'un autre article.
Tous ensemble
Nous aimons fondamentalement le fonctionnement de la chaîne d'outils du langage GO, en fait, c'est un tas de commandes différentes qui font une chose, qui sont exécutées ensemble lorsque vous allez construire, par exemple. Nous avons décidé de faire de même, une équipe place un fichier de contrat dans un répertoire temporaire, la seconde met un wrapper pour lui et appelle une troisième fois, ce qui crée un proxy pour chaque contrat importé, la quatrième compile tout, la cinquième le publie sur la blockchain. Et il y a une commande pour les exécuter tous dans le bon ordre.
Hourra, nous avons maintenant une chaîne d'outils et un runtime pour lancer GO depuis GO. Il existe encore de nombreux problèmes, par exemple, vous devez en quelque sorte décharger le code inutilisé, vous devez déterminer en quelque sorte qu'il se bloque et redémarrer le processus suspendu, mais ce sont des tâches qui expliquent clairement comment le résoudre.
Oui, bien sûr, le code que nous avons écrit ne prétend pas être une bibliothèque, il ne peut pas être utilisé directement, mais lire un exemple de génération de code de travail est toujours génial, à une époque je l'ai manqué. Par conséquent, une partie de la génération de code peut être visualisée dans le
compilateur , mais comment elle démarre dans l'
exécuteur .