简介Swagger本质上是一种用于描述使用JSON表示的RESTful API的接口描述语言。Swagger与一组开源软件工具一起使用，以设计、构建、记录和使用RESTful Web服务。Swagger包括自动文档，代码生成和测试用例生成。在前后端分离的项目开发过程中，如果后端能够提供一份清晰明了的接口文档，那么就能极大地提高大家的沟通效率和开发效率。可是编写接口文档历来都是令人头痛的，而且后续接口文档的维护也十分耗费精力。最好是有一种方案能够既满足我们输出文档的需要又能随代码的变更自动更新，而Swagger正是那种能帮我们解决接口文档问题的工具。集成想要使用gin-swagger为代码自动生成接口文档，一般需要下面三个步骤：按照swagger要求给接口代码添加声明式注释，具体参照声明式注释格式。使用swag工具扫描代码自动生成API接口文档数据使用gin-swagger渲染在线接口文档页面第一步：添加注释在程序入口main函数上以注释的方式写下项目相关介绍信息。package main // @title 这里写标题 // @version 1.0 // @description 这里写描述信息 func main() { r := gin.New() r.Run() }在你代码中处理请求的接口函数（通常位于controller层）按如下方式写上注释：// GetPostListHandler2 升级版帖子列表接口 // @Summary 升级版帖子列表接口 // @Description 可按社区按时间或分数排序查询帖子列表接口 // @Tags 帖子相关接口 // @Accept application/json // @Produce application/json // @Param Authorization header string false "Bearer 用户令牌" // @Param object query models.ParamPostList false "查询参数" // @Security ApiKeyAuth // @Success 200 {object} _ResponsePostList // @Router /posts2 [get] func GetPostListHandler2(c *gin.Context) { p := &models.ParamPostList{ Page: 1, Size: 10, Order: models.OrderTime, } ... }上面注释中参数类型使用了object，models.ParamPostList具体定义如下：// bluebell/models/params.go // ParamPostList 获取帖子列表query string参数 type ParamPostList struct { CommunityID int64 `json:"community_id" form:"community_id"` // 可以为空 Page int64 `json:"page" form:"page" example:"1"` // 页码 Size int64 `json:"size" form:"size" example:"10"` // 每页数据量 Order string `json:"order" form:"order" example:"score"` // 排序依据 }响应数据类型也使用的object，在controller层专门定义一个docs_models.go文件来存储文档中使用的响应数据model。// bluebell/controller/docs_models.go // _ResponsePostList 帖子列表接口响应数据 type _ResponsePostList struct { Code ResCode `json:"code"` // 业务响应状态码 Message string `json:"message"` // 提示信息 Data []*models.ApiPostDetail `json:"data"` // 数据 }第二步：生成接口文档数据编写完注释后，使用以下命令安装swag工具：go get -u github.com/swaggo/swag/cmd/swag # 1.16 及以上版本 go install github.com/swaggo/swag/cmd/swag@latest在项目根目录执行以下命令，使用swag工具生成接口文档数据。swag init执行完上述命令后，如果你写的注释格式没问题，此时你的项目根目录下会多出一个docs文件夹。./docs ├── docs.go ├── swagger.json └── swagger.yaml第三步：引入gin-swagger渲染文档数据然后在项目代码中注册路由的地方按如下方式引入gin-swagger相关内容：import ( "github.com/gin-gonic/gin" "github.com/swaggo/gin-swagger" "github.com/swaggo/gin-swagger/swaggerFiles" _ "bluebell/docs" )注册swagger api相关路由func main() { r := gin.Default() ... r.GET("/docs/*any", ginSwagger.WrapHandler(swaggerFiles.Handler)) r.Run(":8080") }把程序运行起来，打开浏览器访问 http://localhost:8080/docs/index.html 就能看到Swagger 2.0 Api文档了。通用API信息示例注释说明示例title必填 应用程序的名称。// @title Swagger Example APIversion必填 提供应用程序API的版本。// @version 1.0description应用程序的简短描述。// @description This is a sample server celler server.tag.name标签的名称。// @tag.name This is the name of the tagtag.description标签的描述。// @tag.description Cool Descriptiontag.docs.url标签的外部文档的URL。// @tag.docs.url https://example.comtag.docs.description标签的外部文档说明。// @tag.docs.description Best example documentationtermsOfServiceAPI的服务条款。// @termsOfService http://swagger.io/terms/contact.name公开的API的联系信息。// @contact.name API Supportcontact.url联系信息的URL。 必须采用网址格式。// @contact.url http://www.swagger.io/supportcontact.email联系人/组织的电子邮件地址。 必须采用电子邮件地址的格式。// @contact.email support@swagger.iolicense.name必填 用于API的许可证名称。// @license.name Apache 2.0license.url用于API的许可证的URL。 必须采用网址格式。// @license.url http://www.apache.org/licenses/LICENSE-2.0.htmlhost运行API的主机（主机名或IP地址）。// @host localhost:8080BasePath运行API的基本路径。// @BasePath /api/v1acceptAPI 可以使用的 MIME 类型列表。 请注意，Accept 仅影响具有请求正文的操作，例如 POST、PUT 和 PATCH。 值必须如“Mime类型”中所述。// @accept jsonproduceAPI可以生成的MIME类型的列表。值必须如“Mime类型”中所述。// @produce jsonquery.collection.format请求URI query里数组参数的默认格式：csv，multi，pipes，tsv，ssv。 如果未设置，则默认为csv。// @query.collection.format multischemes用空格分隔的请求的传输协议。// @schemes http httpsx-name扩展的键必须以x-开头，并且只能使用json值// @x-example-key {"key": "value"}API操作示例注释描述description操作行为的详细说明。description.markdown应用程序的简短描述。该描述将从名为endpointname.md的文件中读取。id用于标识操作的唯一字符串。在所有API操作中必须唯一。tags每个API操作的标签列表，以逗号分隔。summary该操作的简短摘要。acceptAPI 可以使用的 MIME 类型列表。 请注意，Accept 仅影响具有请求正文的操作，例如 POST、PUT 和 PATCH。 值必须如“Mime类型”中所述。produceAPI可以生成的MIME类型的列表。值必须如“Mime类型”中所述。param用空格分隔的参数。param name,param type,data type,is mandatory?,comment attribute(optional)security每个API操作的安全性。success以空格分隔的成功响应。return code,{param type},data type,commentfailure以空格分隔的故障响应。return code,{param type},data type,commentresponse与success、failure作用相同header以空格分隔的头字段。 return code,{param type},data type,commentrouter以空格分隔的路径定义。 path,[httpMethod]x-name扩展字段必须以x-开头，并且只能使用json值。
solidity编译后的字节码运行在以太坊虚拟机EVM中，指令修改需要同时修改编译器和虚拟机关键文件Solidity编译器项目地址：https://github.com/ethereum/solidity.gitToken的定义：solidity/liblangutil/Token.h；词法定义，比如 token{Add,+}，将+与Add对应起来Instruction的定义：solidity/libevmasm/Instruction.h；虚拟机执行的指令,需要在EVM里实现Type的定义：solidity/libsolidity/ast/Types.h；数据类型定义，包括魔术变量（block，msg...）的定义TypeProvider的定义：solidity/libsolidity/ast/TypeProvider.h；所有数据类型声明Semantic的定义：solidity/libevmasm/SemanticInformation.h;语义信息定义，主要是指令对函数pure,view，payable属性的影响ExpressionCompiler的定义：solidity/libsolidity/codegen/ExpressionCompiler.cpp；将AST语法树翻译成指令以太坊区块链项目地址：https://github.com/ethereum/go-ethereum.gitEVM字节码定义：go-ethereum/core/vm/opcodes.goEVM字节码属性定义：go-ethereum/core/vm/jump_table.go，将字节码和具体的操作绑定起来EVM指令操作定义：go-ethereum/core/vm/instructions.go添加RANDOM指令编译器添加和block.number一样的block.random语法，虚拟机支持和NUMBER一样操作的RANDOM指令编译器修改1.指令定义，代码位置libevmasm/Instruction.h。在enum calss Instruction中找到block的相关属性，并在其后追加RANDOM指令，注意添加的指令不能与其他的冲突。/// Virtual machine bytecode instruction. enum class Instruction: uint8_t { ...... BLOCKHASH = 0x40, ///< get hash of most recent complete block COINBASE, ///< get the block's coinbase address TIMESTAMP, ///< get the block's timestamp NUMBER, ///< get the block's number DIFFICULTY, ///< get the block's difficulty GASLIMIT, ///< get the block's gas limit CHAINID, ///< get the config's chainid param SELFBALANCE, ///< get balance of the current account BASEFEE, ///< get the block's basefee RANDOM, ...... }上述定义为16进制，同时也需要有一个字符串的"RANDOM"与指令对应，代码位置libevmasm/Instruction.cpp中。std::map<std::string, Instruction> const solidity::evmasm::c_instructions = { ...... { "DIFFICULTY", Instruction::DIFFICULTY }, { "GASLIMIT", Instruction::GASLIMIT }, { "CHAINID", Instruction::CHAINID }, { "SELFBALANCE", Instruction::SELFBALANCE }, { "BASEFEE", Instruction::BASEFEE }, { "RANDOM", Instruction::RANDOM }, ...... } static std::map<Instruction, InstructionInfo> const c_instructionInfo = { ...... { Instruction::DIFFICULTY, { "DIFFICULTY", 0, 0, 1, false, Tier::Base } }, { Instruction::GASLIMIT, { "GASLIMIT", 0, 0, 1, false, Tier::Base } }, { Instruction::CHAINID, { "CHAINID", 0, 0, 1, false, Tier::Base } }, { Instruction::SELFBALANCE, { "SELFBALANCE", 0, 0, 1, false, Tier::Low } }, { Instruction::BASEFEE, { "BASEFEE", 0, 0, 1, false, Tier::Base } }, { Instruction::RANDOM, { "RANDOM", 0, 0, 1, false, Tier::Base } }, ...... }后面的0,0,1，false,Tier::Base 是可变的，根据指令的需要。第一个默认为0即可，第二个0表示参数个数，第三个1表示需要1个返回值。false可理解为只在虚拟机内部使用，如果涉及到数据的读写，这里要填成true。最后的Tier::Base是gasprice的级别，根据需要填写即可。2.指令的处理，代码位置libsolidity/codegen/ExpressionCompiler.cppbool ExpressionCompiler::visit(MemberAccess const& _memberAccess) { ...... case Type::Category::Magic: // we can ignore the kind of magic and only look at the name of the member if (member == "coinbase") m_context << Instruction::COINBASE; else if (member == "timestamp") m_context << Instruction::TIMESTAMP; else if (member == "difficulty") m_context << Instruction::DIFFICULTY; else if (member == "number") m_context << Instruction::NUMBER; else if (member == "random") m_context << Instruction::RANDOM; ...... }3.对函数、存储的影响。确定数据类型，代码位置libsolidity/ast/Types.cppMemberList::MemberMap MagicType::nativeMembers(ContractDefinition const*) const { //指定存储的数据类型 ...... case Kind::Block: return MemberList::MemberMap({ {"coinbase", TypeProvider::payableAddress()}, {"timestamp", TypeProvider::uint256()}, {"blockhash", TypeProvider::function(strings{"uint"}, strings{"bytes32"}, FunctionType::Kind::BlockHash, false, StateMutability::View)}, {"difficulty", TypeProvider::uint256()}, {"number", TypeProvider::uint256()}, {"random", TypeProvider::uint256()}, {"gaslimit", TypeProvider::uint256()}, {"chainid", TypeProvider::uint256()}, {"basefee", TypeProvider::uint256()} }); ...... }对函数的影响：代码位置libevmasm/Semanticlnformation.cppbool SemanticInformation::invalidInPureFunctions(Instruction _instruction) { switch (_instruction) { ...... case Instruction::TIMESTAMP: case Instruction::NUMBER: case Instruction::RANDOM: //增加的random指令影响函数的Pure属性，表示该函数不能使用pure关键字。 case Instruction::DIFFICULTY: case Instruction::GASLIMIT: case Instruction::STATICCALL: case Instruction::SLOAD: return true; default: break; } return invalidInViewFunctions(_instruction); }修改虚拟机代码1.random指令的定义，代码位置hvm/evm/opcodes.goconst ( // 0x40 range - block operations BLOCKHASH OpCode = 0x40 + iota COINBASE TIMESTAMP NUMBER DIFFICULTY GASLIMIT RANDOM //新增 ) var opCodeToString = map[OpCode]string{ ...... NUMBER: "NUMBER", DIFFICULTY: "DIFFICULTY", GASLIMIT: "GASLIMIT", RANDOM: "RANDOM", //新增 ...... } var stringToOp = map[string]OpCode{ ...... "NUMBER": NUMBER, "DIFFICULTY": DIFFICULTY, "GASLIMIT": GASLIMIT, "RANDOM": RANDOM, //新增 ...... }2.指令操作的定义，代码位置hvm/evm/jump_table.go ,添加指令的操作属性 instructionSet[RANDOM] = operation{ execute: opRandom, gasCost: constGasFunc(GasQuickStep), validateStack: makeStackFunc(0, 1), valid: true, }3.上述操作码对应函数opRandom的定义，代码位置hvm/evm/instrucitons.go，可参考number函数的定义func opNumber(pc *uint64, evm *EVM, contract *Contract, memory *Memory, stack *Stack) ([]byte, error) { stack.push(math.U256(new(big.Int).Set(evm.BlockNumber))) return nil, nil } func opRandom(pc *uint64, evm *EVM, contract *Contract, memory *Memory, stack *Stack) ([]byte, error) { stack.push(math.U256(new(big.Int).Set(evm.BlockNumber))) return nil, nil }注：编译器和虚拟机里的字节码定义要一致，这样编译出来的程序才能正常运行
打包数据abi.encode打包数据时每个变量都补齐到32字节大小，比如说address类型的变量，需要在前面补12个0字节来凑齐32字节，而abi.encodePacked接口不需要，直接根据变量所占字节大小按顺序拼接成字节数组即可。solidity语言： function abiEncode() public pure returns (bytes memory) { address a = 0x1000000000000000000000000000007000006000; bytes32 b = "hello" ; return abi.encode(a, b); } function abiEncodePacked() public pure returns (bytes memory) { address a = 0x1000000000000000000000000000007000006000; bytes32 b = "hello" ; return abi.encodePacked(a, b); }go语言：import ( "fmt" "github.com/ethereum/go-ethereum/accounts/abi" "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/common/hexutil" ) func abiEncode() { addressTy, _ := abi.NewType("address", "", nil) bytes32Ty, _ := abi.NewType("bytes32", "", nil) arguments := abi.Arguments{ { Type: addressTy, }, { Type: bytes32Ty, }, } bytes, _ := arguments.Pack( common.HexToAddress("0x1000000000000000000000000000007000006000"), [32]byte{'h', 'e', 'l', 'l', 'o'}, ) fmt.Println(hexutil.Encode(bytes)) } func abiEncodePacked() { bytes = append( common.HexToAddress("0x1000000000000000000000000000007000006000").Bytes(), common.RightPadBytes([]byte{'h', 'e', 'l', 'l', 'o'}, 32)..., ) fmt.Println(hexutil.Encode(bytes)) }输出：0x000000000000000000000000100000000000000000000000000000700000600068656c6c6f000000000000000000000000000000000000000000000000000000 0x100000000000000000000000000000700000600068656c6c6f000000000000000000000000000000000000000000000000000000哈希数据：solidity使用keccak256接口将字节数组来生成32字节的哈希，部分前端js库在签名或哈希时自动在数据前加特殊的前缀，为了使solidity计算带前缀的哈希方便，将数据编码并哈希成32字节后再加前缀来计算哈希（这时前缀固定为\x19Ethereum Signed Message:\n32）solidity语言： function hash() public pure returns (bytes32) { address a = 0x1000000000000000000000000000007000006000; bytes32 b = "hello" ; return keccak256(abi.encodePacked(a,b)); } function prefixedHash() public pure returns (bytes32) { address a = 0x1000000000000000000000000000007000006000; bytes32 b = 'hello'; return keccak256(abi.encodePacked('\x19Ethereum Signed Message:\n32', keccak256(abi.encodePacked(a, b)))); }go语言：import ( "fmt" "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/crypto" ) func hashAndPrefixed() { hash := crypto.Keccak256Hash( common.HexToAddress("0x1000000000000000000000000000007000006000").Bytes(), common.RightPadBytes([]byte{'h', 'e', 'l', 'l', 'o'}, 32), ) fmt.Println(hash) prefixedHash := crypto.Keccak256Hash( []byte("\x19Ethereum Signed Message:\n32"), hash.Bytes(), ) fmt.Println(prefixedHash) }输出：0xd91dd3198b92de9e6c8678e55c5b62c026dce4d101c1b4bf2de31878c3bc08ba 0x589fec5d88151e9489222a3234a28adf42c0f640bba53cce3329c06d014f2237签名生成与验证使用crypto.Sign(digestHash, privateKey)接口来生成签名（65字节），使用crypto.SigToPub(hash, sig)接口恢复签名的公钥，使用crypto.PubkeyToAddress(*rpk)接口获取公钥对应的地址，对比恢复出来的地址和签名的账户地址一致来验证签名。使用crypto.HexToECDSA(hexkey)接口将私钥字符串实列化为可使用私钥对象。注：使用crypto.SigToPub接口时，如果签名的最后一个字节大于等于27，需要减去27；需要引用github.com/ethereum/go-ethereum/crypto包