Eksekusi Kode Kustom di GO

Ini sebenarnya semua tentang kontrak pintar.


Tetapi jika Anda tidak cukup membayangkan apa itu kontrak pintar, dan secara umum jauh dari crypto, maka apa itu prosedur tersimpan dalam database, Anda bisa bayangkan sepenuhnya. Pengguna membuat potongan kode yang kemudian berfungsi di server kami. Adalah nyaman bagi pengguna untuk menulis dan menerbitkannya, dan aman bagi kami untuk mengeksekusinya.

Sayangnya, kami belum mengembangkan keamanan, jadi sekarang saya tidak akan menjelaskannya, tetapi saya akan memberikan beberapa petunjuk.

Kami juga menulis di Go, dan runtime-nya memberlakukan beberapa batasan yang sangat spesifik, yang utamanya adalah, pada umumnya, kami tidak dapat menautkan ke proyek lain yang ditulis tidak dalam perjalanan, ini akan menghentikan runtime kami setiap kali kami mengeksekusi kode pihak ketiga. Secara umum, kami memiliki opsi untuk menggunakan semacam juru bahasa, yang kami temukan Lua yang benar-benar waras dan WASM yang benar-benar waras, tetapi entah bagaimana saya tidak ingin menambahkan klien ke Lua, tetapi dengan WASM sekarang ada lebih banyak masalah daripada manfaat, itu dalam rancangan negara , yang diperbarui setiap bulan, jadi kami akan menunggu hingga spesifikasi selesai. Kami menggunakannya sebagai mesin kedua.

Sebagai hasil dari pertempuran panjang dengan hati nuraninya sendiri, diputuskan untuk menulis kontrak pintar di GO. Faktanya adalah bahwa jika Anda membangun arsitektur untuk mengeksekusi kode GO yang dikompilasi, Anda harus mentransfer eksekusi ini ke proses terpisah, seperti yang Anda ingat, untuk keamanan, dan mentransfer ke proses terpisah adalah hilangnya kinerja pada IPC, meskipun nanti, ketika kami memahami volume yang dapat dieksekusi kode, itu entah bagaimana menyenangkan bahwa kami memilih solusi ini. Masalahnya adalah itu dapat diskalakan, meskipun ia menambahkan penundaan untuk setiap panggilan individu. Kami dapat meningkatkan banyak runtimes jarak jauh.

Sedikit lagi tentang keputusan yang dibuat sehingga menjadi jelas. Setiap kontrak pintar terdiri dari dua bagian, satu bagian adalah kode kelas, dan yang kedua adalah data objek, jadi pada kode yang sama kita dapat, begitu kita menerbitkan kode, membuat banyak kontrak yang pada dasarnya akan berperilaku sama, tetapi dengan pengaturan berbeda , dan dengan status berbeda. Jika kita berbicara lebih jauh, maka ini sudah tentang blockchain dan bukan topik cerita ini.

Jadi, kami menjalankan GO


Kami memutuskan untuk menggunakan mekanisme plugin, yang tidak hanya siap dan bagus. Dia melakukan yang berikut, kita mengkompilasi apa yang akan menjadi plugin dengan cara khusus ke perpustakaan bersama, dan kemudian memuatnya, menemukan simbol di dalamnya dan meneruskan eksekusi di sana. Tetapi yang menarik adalah bahwa GO memiliki runtime, dan ini hampir satu megabyte kode, dan secara default runtime ini juga pergi ke perpustakaan ini, dan kami memiliki runtime raznipipenny di mana-mana. Tetapi sekarang kami memutuskan untuk melakukannya, yakin bahwa kami dapat mengalahkannya di masa depan.

Semuanya sederhana ketika Anda membangun perpustakaan Anda, Anda membangunnya dengan kunci - buildmode = plugin dan mendapatkan file .so, yang kemudian Anda buka.

p, err := plugin.Open(path) 

Mencari karakter yang Anda minati:

 symbol, err := p.Lookup(Method) 

Dan sekarang, tergantung pada apakah variabel itu fungsi atau fungsi, Anda bisa memanggilnya atau menggunakannya sebagai variabel.

Di bawah kap mekanisme ini adalah dlopen sederhana (3), kami memuat pustaka, memeriksa apakah itu plugin dan memberikan pembungkus di atasnya, saat membuat pembungkus, semua karakter yang diekspor dibungkus dengan antarmuka {} dan disimpan. Jika itu adalah fungsi, maka itu harus direduksi menjadi jenis fungsi yang benar dan cukup dipanggil, jika variabel - maka berfungsi seperti variabel.

Hal utama yang perlu diingat adalah bahwa jika simbol adalah variabel, maka simbol bersifat global selama seluruh proses dan Anda tidak dapat menggunakannya tanpa berpikir.

Jika suatu tipe telah dideklarasikan dalam plugin, maka masuk akal untuk meletakkan tipe ini dalam paket terpisah sehingga proses utama dapat bekerja dengannya, misalnya, meneruskan sebagai argumen ke fungsi-fungsi plugin. Ini opsional, Anda tidak dapat mengukus dan menggunakan refleksi.

Kontrak kami adalah objek dari "kelas" yang sesuai, dan pada awalnya instance dari objek ini disimpan dalam variabel yang diekspor, sehingga kami dapat membuat variabel yang sama:

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

Dan sudah di dalam variabel lokal ini dari jenis yang benar, deserialize keadaan objek. Setelah objek dipulihkan, kita dapat memanggil metode di atasnya. Setelah objek serial dan ditambahkan kembali ke toko, sorak-sorai kami memanggil metode pada kontrak.

Jika Anda tertarik dengan caranya, tetapi terlalu malas untuk membaca dokumentasinya, maka:

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

Di tengah, Anda masih perlu mengisi array dengan antarmuka kosong yang berisi jenis argumen yang benar, jika Anda tertarik, lihat sendiri bagaimana hal itu dilakukan, sumbernya terbuka, meskipun menemukan tempat ini dalam sejarah akan sulit.

Secara umum, semuanya bekerja untuk kami, Anda dapat menulis kode dengan sesuatu seperti kelas, meletakkannya di blockchain, membuat kontrak kelas ini lagi di blockchain, membuat panggilan metode di atasnya dan keadaan kontrak yang baru ditulis kembali ke blockchain. Hebat! Bagaimana cara membuat kontrak baru dengan kode di tangan? Sangat sederhana, kami memiliki fungsi konstruktor yang mengembalikan objek yang baru dibuat, yang merupakan kontrak baru. Sejauh ini, semuanya bekerja melalui refleksi dan pengguna harus menulis:

 var EXPORT ContractType 

Sehingga kita tahu simbol apa yang merupakan representasi dari kontrak, dan benar-benar menggunakannya sebagai templat.

Kami tidak begitu menyukainya. Dan kami memukul keras.

Parsing


Pertama, pengguna tidak boleh menulis sesuatu yang berlebihan, dan kedua, kami memiliki gagasan bahwa interaksi kontrak dengan kontrak harus sederhana, dan diuji tanpa meningkatkan blockchain, blockchain lambat dan sulit.

Oleh karena itu, kami memutuskan untuk membungkus kontrak dalam pembungkus, yang dihasilkan berdasarkan kontrak dan templat pembungkus, pada prinsipnya, solusi yang dapat dimengerti. Pertama, pembungkus membuat objek ekspor untuk kami, dan kedua, itu menggantikan perpustakaan dengan kontrak yang dikumpulkan ketika pengguna menulis kontrak, perpustakaan yayasan digunakan dengan mokas di dalamnya, dan ketika kontrak diterbitkan, itu diganti dengan pertempuran yang bekerja dengan blockchain itu sendiri .

Untuk memulainya, Anda perlu mengurai kode dan memahami apa yang umumnya kita miliki, menemukan struktur yang diwarisi dari BaseContract untuk menghasilkan pembungkus di sekitarnya.

Ini dilakukan cukup sederhana, kami membaca file dengan kode dalam [] byte, meskipun parser itu sendiri dapat membaca file, ada baiknya memiliki teks di suatu tempat yang semua elemen AST merujuk, mereka merujuk ke nomor byte dalam file, dan di masa depan kami ingin menerima kode struktur apa adanya, kami hanya mengambil sesuatu seperti.

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

Kami benar-benar mem-parsing file dan mendapatkan simpul AST paling atas dari mana kami akan menjelajah file.

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

Selanjutnya, kita berkeliling kode mulai dari simpul atas, dan mengumpulkan segala sesuatu yang menarik dalam struktur yang terpisah.

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

Decls, sudah diuraikan menjadi array, daftar semua yang didefinisikan dalam file, tetapi itu adalah array dari antarmuka Decl yang tidak menggambarkan apa yang ada di dalamnya, sehingga setiap elemen perlu dikonversi ke jenis tertentu, di sini penulis bahasa berangkat dari ide mereka menggunakan antarmuka, antarmuka di go / ast lebih merupakan kelas dasar.

Kami tertarik pada simpul tipe GenDecl dan FuncDecl. GenDecl adalah definisi dari variabel atau tipe, dan Anda perlu memeriksa apa sebenarnya tipe di dalamnya, dan sekali lagi dilemparkan ke tipe TypeDecl, yang sudah dapat Anda gunakan. FuncDecl lebih sederhana - ini adalah fungsi, dan jika memiliki bidang Recv diisi, maka ini adalah metode struktur yang sesuai. Kami mengumpulkan semua hal ini dalam penyimpanan yang nyaman, karena kami menggunakan teks / templat, dan tidak memiliki banyak kekuatan ekspresif.

Satu-satunya hal yang perlu kita ingat secara terpisah adalah nama tipe data yang diwarisi dari BaseContract, dan kita akan menari di sekitarnya.

Pembuatan Kode


Jadi, kita tahu semua jenis dan fungsi yang ada dalam kontrak kita dan kita harus bisa membuat pemanggilan metode pada objek dari nama metode yang masuk dan array argumen serial. Tapi bagaimanapun, pada saat pembuatan kode, kita tahu seluruh perangkat kontrak, jadi kita meletakkan di samping file kontrak kita di sebelah file lain, dengan nama paket yang sama, di mana kita memasukkan semua impor yang diperlukan, jenis sudah ditentukan dalam file utama dan tidak perlu.

Dan di sini adalah hal utama, pembungkus fungsi. Nama bungkusnya dilengkapi dengan semacam awalan dan sekarang bungkusnya mudah ditemukan.

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

Setiap bungkus memiliki tanda tangan yang sama, jadi ketika kita menyebutnya dari program utama, kita tidak perlu refleksi tambahan, satu-satunya hal adalah pembungkus fungsi berbeda dari pembungkus metode, mereka tidak menerima dan tidak mengembalikan keadaan objek.

Apa yang kita miliki di dalam bungkusnya?

Kami membuat array variabel kosong yang sesuai dengan argumen fungsi, memasukkannya ke dalam variabel tipe array antarmuka, dan membatalkan deserialisasi argumen ke dalamnya, jika kita adalah sebuah metode, kita juga harus membuat serial keadaan objek, umumnya seperti ini:

 {{ 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 }} 

Pembaca yang penuh perhatian akan tertarik pada apa itu pembantu proxy? - ini adalah objek gabungan yang masih kita butuhkan, tetapi untuk saat ini kita menggunakan kemampuannya untuk membuat serial dan deserialize.

Nah, siapa pun yang membaca akan bertanya, "Tapi ini argumen Anda, dari mana mereka berasal?" Ini juga jawaban yang bisa dimengerti, ya teks / templat di sana tidak cukup bintang dari langit, itu sebabnya kami menghitung garis-garis ini dalam kode, dan bukan di templat.

method.ArgumentsZeroList berisi sesuatu seperti

 var arg0 int = 0 Var arg1 string = β€œβ€ Var arg2 ackwardType = ackwardType{} Args := []interface{}{&arg0, &arg1, &arg2} 

Dan Argumen sesuai mengandung "arg0, arg1, arg2".

Dengan demikian, kita dapat memanggil apa saja yang kita inginkan, dengan tanda tangan apa saja.

Tetapi kami tidak dapat membuat serialisasi jawaban apa pun, faktanya adalah bahwa serializers bekerja dengan refleksi, dan itu tidak memberikan akses ke bidang struktur yang tidak diekspor, itu sebabnya kami memiliki metode pembantu proxy khusus yang mengambil objek antarmuka kesalahan dan membuat objek tipe pondasi darinya. Kesalahan, yang berbeda dari yang biasa di mana teks kesalahan ada di dalamnya di bidang yang diekspor, dan kita bisa membuat serial, meskipun dengan beberapa kerugian.

Tetapi jika kita menggunakan sterilisasi penghasil kode, maka kita bahkan tidak membutuhkannya, kita dikompilasi dalam paket yang sama, kita memiliki akses ke bidang yang tidak diekspor.

Tetapi bagaimana jika kita ingin memanggil suatu kontrak dari suatu kontrak?


Anda tidak mengerti kedalaman masalah jika Anda merasa mudah untuk memanggil kontrak dari kontrak. Faktanya adalah bahwa validitas kontrak lain harus dikonfirmasikan dengan konsensus, dan fakta panggilan ini harus ditandatangani di blockchain, secara umum, hanya mengkompilasi dengan kontrak lain dan menggunakan metode ini tidak akan berhasil, walaupun saya benar-benar ingin. Tapi kami adalah teman programmer, jadi kami harus memberi mereka kesempatan untuk melakukan semuanya secara langsung, dan menyembunyikan semua trik di bawah kap sistem. Dengan demikian, pengembangan kontrak seolah-olah dengan panggilan langsung, dan kontrak saling menarik secara transparan, tetapi ketika kami mengumpulkan kontrak untuk publikasi, kami menyelipkan proxy alih-alih kontrak lain, yang hanya mengetahui alamat dan tanda tangan panggilan tentang kontrak tersebut.

Bagaimana mengatur semua ini? - Kita harus menyimpan kontrak lain di direktori khusus yang generator kita dapat mengenali dan membuat proksi untuk setiap kontrak yang diimpor.

Yaitu, jika kita bertemu:

 import β€œContractsDir/ContractAddress" 

Kami menulisnya ke daftar kontrak impor.

Ngomong-ngomong, untuk ini Anda tidak perlu tahu kode sumber kontrak, Anda hanya perlu mengetahui deskripsi yang telah kami kumpulkan, jadi jika kami menerbitkan deskripsi seperti itu di suatu tempat, dan semua panggilan melalui sistem utama, maka kami tidak peduli apa kontrak lain ditulis dalam bahasa, jika kita dapat memanggil metode di atasnya, kita dapat menulis sebuah rintisan untuk itu di Go, yang akan terlihat seperti paket dengan kontrak yang dapat dipanggil secara langsung. Rencana Napoleon, mari kita mulai.

Pada prinsipnya, kami sudah memiliki metode pembantu proxy, dengan tanda tangan ini:

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

Metode ini dapat dipanggil langsung dari kontrak, itu disebut kontrak jarak jauh, mengembalikan respons berseri yang perlu kita uraikan dan kembali ke kontrak kita.

Tetapi penting bagi pengguna untuk terlihat seperti:

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

Mari kita mulai, pertama, di proksi, Anda perlu membuat daftar semua jenis yang digunakan dalam tanda tangan metode kontrak, tetapi seperti yang kita ingat, untuk setiap node AST kita dapat mengambil representasi tekstualnya, dan sekarang saatnya untuk mekanisme ini.

Selanjutnya, kita perlu membuat jenis kontrak, pada prinsipnya, dia sudah tahu kelasnya, hanya alamat yang diperlukan.

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

Selanjutnya, kita perlu mengimplementasikan fungsi GetObject, yang pada alamat di blockchain akan mengembalikan instance proxy yang tahu cara bekerja dengan kontrak ini, dan bagi pengguna itu tampak seperti instance kontrak.

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

Menariknya, metode GetObject dalam mode debugging pengguna secara langsung adalah metode struktur BaseContract, tetapi tidak ada, tidak ada yang menghalangi kita, mengamati SLA, untuk melakukan apa yang nyaman bagi kita. Sekarang kita dapat membuat kontrak proxy, metode yang kita kendalikan. Tetap benar-benar membuat metode.

 {{ 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 }} 

Di sini cerita yang sama dengan konstruksi daftar argumen, karena kita malas dan menyimpan persis ast. Metode ini, maka perhitungan memerlukan banyak jenis konversi yang tidak diketahui templat, sehingga semuanya disiapkan terlebih dahulu. Dengan fungsi, semuanya menjadi lebih rumit, dan ini adalah topik dari artikel lain.

Fungsi yang kami miliki adalah konstruktor objek dan ada banyak penekanan pada bagaimana objek sebenarnya dibuat dalam sistem kami, fakta penciptaan terdaftar pada pelaksana jarak jauh, objek ditransfer ke pelaksana lain, diperiksa dan benar-benar disimpan di sana, dan ada banyak cara untuk menyimpan, dengan sia-sia bidang pengetahuan ini disebut crypt. Dan idenya pada dasarnya sederhana, sebuah pembungkus di dalamnya hanya alamat yang disimpan, dan metode yang membuat serial panggilan dan menarik prosesor singleton kami, yang melakukan sisanya. Kami tidak dapat menggunakan pembantu proxy yang ditransmisikan, karena pengguna tidak memberikannya kepada kami, jadi kami harus menjadikannya singleton.

Trik lain - pada kenyataannya, kita masih menggunakan konteks panggilan, ini adalah objek yang menyimpan informasi tentang siapa, kapan, mengapa, mengapa kontrak pintar kita dipanggil, berdasarkan informasi ini, pengguna membuat keputusan apakah akan melakukan eksekusi sama sekali, dan jika mungkin lalu bagaimana.

Sebelumnya, kami hanya melewati konteks, itu adalah bidang yang tidak bisa diekspresikan dalam tipe BaseContract dengan setter dan pengambil, dan setter tersebut memungkinkan pengaturan bidang hanya sekali, jadi konteksnya ditetapkan sebelum kontrak dieksekusi, dan pengguna hanya bisa membacanya.

Tapi di sini masalahnya, pengguna hanya membaca konteks ini, jika ia membuat panggilan ke beberapa jenis fungsi sistem, misalnya, panggilan proxy ke kontrak lain, maka panggilan proxy ini tidak menerima konteks apa pun, karena tidak ada yang meneruskannya kepadanya. Dan kemudian penyimpanan lokal goroutine memasuki lokasi. Kami memutuskan untuk tidak menulis sendiri, tetapi gunakan github.com/tylerb/gls.

Ini memungkinkan Anda untuk mengatur dan mengambil konteks untuk goroutine saat ini. Jadi, jika tidak ada goroutine yang dibuat di dalam kontrak, kami hanya mengatur konteks di gls sebelum memulai kontrak, sekarang kami memberikan pengguna bukan metode, tetapi hanya sebuah fungsi.

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

Dan dia dengan senang hati menggunakannya, tetapi kami menggunakannya di RouteCall (), misalnya, untuk memahami kontrak mana yang saat ini meminta seseorang.

Pada prinsipnya, pengguna dapat membuat goroutine, tetapi jika dia melakukannya, maka konteksnya hilang, jadi kita perlu melakukan sesuatu dengan ini, misalnya, jika pengguna menggunakan kata kunci go, maka kita harus membungkus panggilan tersebut di bungkus kami, yang konteksnya akan mengingat dan membuat goroutine dan kembalikan konteks di dalamnya, tetapi ini adalah topik dari artikel lain.

Semuanya bersama


Kami pada dasarnya menyukai cara kerja toolchain bahasa GO, pada kenyataannya itu adalah banyak perintah berbeda yang melakukan satu hal, yang dieksekusi bersama ketika Anda membangun, misalnya. Kami memutuskan untuk melakukan hal yang sama, satu tim menempatkan file kontrak di direktori sementara, yang kedua menempatkan pembungkus untuknya di sebelahnya dan memanggil yang ketiga kalinya, yang membuat proxy untuk setiap kontrak yang diimpor, yang keempat mengkompilasi semuanya, yang kelima menerbitkannya di blockchain. Dan ada satu perintah untuk menjalankan semuanya dalam urutan yang benar.

Hore, kami sekarang memiliki toolchain dan runtime untuk meluncurkan GO dari GO. Masih ada banyak masalah, misalnya, Anda perlu membongkar kode yang tidak terpakai, Anda perlu menentukan apakah itu hang dan memulai kembali proses yang ditangguhkan, tetapi ini adalah tugas yang jelas bagaimana menyelesaikannya.

Ya, tentu saja, kode yang kami tulis tidak berpura-pura menjadi perpustakaan, itu tidak dapat digunakan secara langsung, tetapi membaca contoh pembuatan kode kerja selalu bagus, pada satu waktu saya melewatkannya. Dengan demikian, bagian dari pembuatan kode dapat dilihat di kompiler , tetapi bagaimana itu dimulai di pelaksana .

Source: https://habr.com/ru/post/id425985/


All Articles