Hello World - 2020年11月
type: channel
link: https://wlwl.host/index.php/2020/11/
description: Just So So ...
items: Go CGO进阶
items: Go HTTP
items: 以太坊 智能合约升级

Go CGO进阶
type: item
date: 2020-11-15T08:42:00+00:00
description: 一、内存模型CGO是架接Go语言和C语言的桥梁，它使二者在二进制接口层面实现了互通，但是我们要注意因两种语言的内存模型的差异而可能引起的问题。如果在CGO处理的跨语言函数调用时涉及到了指针的传递，则可能会出现Go语言和C语言共享某一段内存的场景。我们知道C语言的内存在分配之后就是稳定的，但是Go语言因为函数栈的动态伸缩可能导致栈中内存地址的移动(这是Go和C内存模型的最大差异)。如果C语言持有的是移动之前的Go指针，那么以旧指针访问Go对象时会导致程序崩溃。1、Go访问C内存C语言空间的内存是稳定的，只要不是被人为提前释放，那么在Go语言空间可以放心大胆地使用。在Go语言访问C语言内存是最简单的情形，我们在之前的例子中已经见过多次。因为Go语言实现的限制，我们无法在Go语言中创建大于2GB内存的切片。不过借助cgo技术，我们可以在C语言环境创建大于2GB的内存，然后转为Go语言的切片使用：package main /* #include <stdlib.h> void* makeslice(size_t memsize) { return malloc(memsize); } */ import "C" import "unsafe" func makeByteSlize(n int) []byte { p := C.makeslice(C.size_t(n)) return ((*[1 << 31]byte)(p))[0:n:n] } func freeByteSlice(p []byte) { C.free(unsafe.Pointer(&p[0])) } func main() { s := makeByteSlize(1<<32+1) s[len(s)-1] = 255 print(s[len(s)-1]) freeByteSlice(s) } 例子中我们通过makeByteSlize来创建大于4G内存大小的切片，从而绕过了Go语言实现的限制。而freeByteSlice辅助函数则用于释放从C语言函数创建的切片。因为C语言内存空间是稳定的，基于C语言内存构造的切片也是绝对稳定的，不会因为Go语言栈的变化而被移动。2、C临时访问传入的Go内存cgo之所以存在的一大因素是为了方便在Go语言中接纳吸收过去几十年来使用C/C++语言软件构建的大量的软件资源。C/C++很多库都是需要通过指针直接处理传入的内存数据的，因此cgo中也有很多需要将Go内存传入C语言函数的应用场景。假设一个极端场景：我们将一块位于某goroutinue的栈上的Go语言内存传入了C语言函数后，在此C语言函数执行期间，此goroutinue的栈因为空间不足的原因发生了扩展，也就是导致了原来的Go语言内存被移动到了新的位置。但是此时此刻C语言函数并不知道该Go语言内存已经移动了位置，仍然用之前的地址来操作该内存——这将将导致内存越界。以上是一个推论（真实情况有些差异），也就是说C访问传入的Go内存可能是不安全的！当然有RPC远程过程调用的经验的用户可能会考虑通过完全传值的方式处理：借助C语言内存稳定的特性，在C语言空间先开辟同样大小的内存，然后将Go的内存填充到C的内存空间；返回的内存也是如此处理。下面的例子是这种思路的具体实现：package main /* void printString(const char* s) { printf("%s", s); } */ import "C" func printString(s string) { cs := C.CString(s) defer C.free(unsafe.Pointer(cs)) C.printString(cs) } func main() { s := "hello" printString(s) } 在需要将Go的字符串传入C语言时，先通过C.CString将Go语言字符串对应的内存数据复制到新创建的C语言内存空间上。上面例子的处理思路虽然是安全的，但是效率极其低下（因为要多次分配内存并逐个复制元素），同时也极其繁琐。为了简化并高效处理此种向C语言传入Go语言内存的问题，cgo针对该场景定义了专门的规则：在CGO调用的C语言函数返回前，cgo保证传入的Go语言内存在此期间不会发生移动，C语言函数可以大胆地使用Go语言的内存！根据新的规则我们可以直接传入Go字符串的内存：package main /* #include<stdio.h> void printString(const char* s, int n) { int i; for(i = 0; i < n; i++) { putchar(s[i]); } putchar('\n'); } */ import "C" func…

Go HTTP
type: item
date: 2020-11-09T14:28:00+00:00
description: 超文本传输协议（Hypertext Transfer Protocol、HTTP协议）是今天使用最广泛的应用层协议，1989年由Tim Berners-Lee在CERN起草的协议已经成为了互联网的数据传输的核心。在过去几年的时间里，HTTP/2和HTTP/3也对现有的协议进行了更新，提供更加安全和快速的传输功能。多数的编程语言都会在标准库中实现HTTP/1.1和HTTP/2.0已满足工程师的日常开发需求，Go语言的网络库也实现了这两个大版本的HTTP协议。一、设计原理HTTP协议是应用层协议，在通常情况下我们都会使用TCP作为底层的传输层协议传输数据包，但是HTTP/3在UDP协议上实现了新的传输层协议QUIC来传输数据，这也意味着HTTP既可以跑在TCP上，也可以跑在UDP上。Go语言标准库通过net/http包提供HTTP的客户端和服务端实现。请求和响应HTTP协议中最常见的概念是HTTP请求与响应，我们可以将它们理解成客户端和服务端之间传递的消息，客户端向服务端发送HTTP 请求，服务端收到HTTP请求后会做出计算后以HTTP响应的形式发送给客户端。与其他的二进制协议不同，作为文本传输协议，HTTP协议的协议头都是文本数据，HTTP请求头的首行会包含请求的方法、路径和协议版本，接下来是多个HTTP协议头以及携带的负载。GET / HTTP/1.1 User-Agent: Mozilla/4.0 (compatible; MSIE5.01; Windows NT) Host: draveness.me Accept-Language: en-us Accept-Encoding: gzip, deflate Content-Length: <length> Connection: Keep-Alive ... HTTP响应也有着比较类似的结构，其中也包含响应的协议版本、状态码、响应头以及负载。消息边界HTTP协议目前主要还是跑在TCP协议上的，TCP协议是面向连接的、可靠的、基于字节流的传输层通信协议，应用层交给TCP协议的数据并不会以消息为单位向目的主机传输，这些数据在某些情况下会被组合成一个数据段发送给目标的主机。因为TCP协议是基于字节流的，所以基于TCP协议的应用层协议都需要自己划分消息的边界。在应用层协议中，最常见的两种解决方案就是基于长度或者基于终结符（Delimiter）。HTTP 协议其实同时实现了上述两种方案，在多数情况下HTTP协议都会在协议头中加入Content-Length表示负载的长度，消息的接收者解析到该协议头之后就可以确定当前 HTTP请求/响应结束的位置，分离不同的HTTP消息，下面就是一个使用Content-Length划分消息边界的例子：HTTP/1.1 200 OK Content-Type: text/html; charset=UTF-8 Content-Length: 138 ... Connection: close An Example Page Hello World, this is a very simple HTML document. 不过HTTP协议除了使用基于长度的方式实现边界，也会使用基于终结符的策略，当HTTP使用块传输（Chunked Transfer）机制时，HTTP头中就不再包含Content-Length了，它会使用负载大小为0的HTTP消息作为终结符表示消息的边界。层级结构Go语言的net/http中同时包好了HTTP客户端和服务端的实现，为了支持更好的扩展性，它引入了net/http.RoundTripper和 net/http.Handler两个接口。net/http.RoundTripper是用来表示执行HTTP请求的接口，调用方将请求作为参数可以获取请求对应的响应，而net/http.Handler主要用于HTTP服务器响应客户端的请求：type RoundTripper interface { RoundTrip(*Request) (*Response, error) } HTTP请求的接收方可以实现net/http.Handler接口，其中实现了处理HTTP请求的逻辑，处理的过程中会调用net/http.ResponseWriter接口的方法构造HTTP响应，它提供的三个接口Header、Write和WriteHeader分别会获取HTTP响应、将数据写入负载以及写入响应头：type Handler interface { ServeHTTP(ResponseWriter, *Request) } type ResponseWriter interface { Header() Header Write([]byte) (int, e…

以太坊 智能合约升级
type: item
date: 2020-11-02T14:16:00+00:00
description: 以太坊的最大优势之一是其公共账本内交易记录的不可篡改性，这些交易包括Token的转移，合约的部署以及合约交易。以太坊网络上的任何节点都可以验证每笔交易的有效性和状态，从而使以太坊成为一个非常强大的去中心化系统。但最大的缺点是，智能合约一旦部署后，则无法更改合约源码。中心化应用程序的开发人员会经常对程序进行更新，修复bug或引入新功能。而这种方式在以太坊上是不可能做到的。著名的Parity Wallet事件，黑客盗取了150000个ETH，在这次的攻击中，Parity multisig钱包中一个合约的漏洞被黑客利用，盗取了钱包中的资金。在黑客攻击过程中，我们唯一能做的就是利用相同的漏洞，比黑客更快速的将钱包中的资金进行转移，并在事后归还给所有者。如果有一种方法可以在智能合约部署后，更新源代码……一、代理模式虽然无法更新已部署的智能合约代码，但是可以通过设置一个代理合约架构，进而部署新的合约，以实现合约升级的目的。代理模式使得所有消息调用都通过代理合约，代理合约会将调用请求重定向到最新部署的合约中。如要升级时，将升级后新合约地址更新到代理合约中即可。代理底层由delegatecall和fallback函数来实现：当调用的方法在合约中不存在时，合约会调用fallback函数。可以编写fallback函数的逻辑处理这种情况。代理合约使用自定义的fallback函数将调用请求重定向到逻辑合约中。每当合约A通过delegatecall到另一个合约B时，它都会在合约A的上下文中执行合约B的代码。这意味着将保留msg.value和msg.sender值，并且每次存储修改的是合约A。代理的核心实现代码（使用强大的汇编代码来解析参数和提供返回值）：pragma solidity ^0.6.6; abstract contract Proxy { function implementation() public virtual view returns (address); fallback() external { address _impl = implementation(); require(_impl != address(0)); assembly { let ptr := mload(0x40) calldatacopy(ptr, 0, calldatasize()) let result := delegatecall(gas(), _impl, ptr, calldatasize(), 0, 0) let size := returndatasize() returndatacopy(ptr, 0, size) switch result case 0 { revert(ptr, size) } default { return(ptr, size) } } } } 为了将调用请求代理到另一个合约中，我们必须将代理合约收到的msg.data传递给逻辑合约。由于msg.data的类型为bytes，大小是不固定的，数据大小存储在msg.data的第一个字长（32个字节）中。如果我们只想提取实际数据，则需要跳过前32字节，从 msg.data的0x20（32个字节）位置开始。这里，我们将利用两个操作码来执行该操作。使用calldatasize获得msg.data的大小，使用calldatacopy将其复制到ptr变量中。注意初始化ptr变量。在Solidity中，内存插槽0x40位置是比较特殊的，它包含了下一个可用的空闲内存指针的值。每次将变量直接保存到内存时，都应通过查询0x40位置的值，来确定变量保存在内存的位置。let ptr := mload(0x40) calldatacopy(ptr, 0, calldatasize()) 接下来看一下汇编模块中delegatecall操作码：let result := delegatecall(gas(), target, ptr, calldatasize(), 0, 0) 参数：gas() 传递执行合约所需要燃料target 所请求的目标合约地址ptr 请求数据在内存中的起始位置calldatasize() 请求数据的大小。0 用于表示目标合约的返回值。这是未使用的，因为此时我们尚不知道返回数据的大小，因此无法将其分配给变量。之后我们可以使用returndata操作码访问此信息0 表示目标合约返回值的大小。这是未使用的，因为在调用目标合约之前，我们是无法知道返回值的大小。之后我们可以通过returndatasize操作码来获得该值下一行，使用returndatasize操作码获取返回值的大小let size := returndatasize() 然后，我们使用returndataco…