## 跨合约调用 读者对象:ChainMaker不仅支持同虚拟机内跨合约调用,也支持不同类型虚拟机间的跨合约调用。例如,wasmer合约既可以调用wasmer合约,也可以调用docker-go合约。本章节主要介绍在合约内调用其他合约的方法,主要面向有跨合约调用需求的开发者。 ### solidity合约内创建合约 ChainMaker evm虚拟机支持solidity合约静态创建solidity合约,solidity合约动态创建solidity合约,以及solidity合约动态创建其他类型合约。 #### 静态创建 所谓静态创建,是指在合约创建之前,创建者合约必须掌握被创建合约的代码。二者在编译之前就已经被绑定,并一同编译生成可部署的字节码,不能在运行时改变被创建者。 **静态创建示例** 合约static_create.sol,实现功能: 1、部署Creator合约后,可调用Creator合约的createSalted方法创建Creature合约; 2、创建时,可通过name参数,为Creature合约指定合约名。注:部署合约时,ChainMaker会根据合约名计算合约地址,并将地址返回给用户,合约部署完成后,用户即可使用合约名调用合约,也可使用合约地址调用合约。 ```solidity // SPDX-License-Identifier: GPL-3.0 pragma solidity >=0.7.0 <0.9.0; //被创建合约 contract Creature { uint public x; constructor(uint a) payable { x = a; } function get() public view returns(uint){ return x; } } //创建者合约 contract Creator { function bytesCopy(bytes memory src, bytes memory dst, uint begin) internal pure returns (bytes memory){ uint len = src.length < 31 ? src.length : 31; for(uint i = 0; i < len; i++){ dst[begin + i] = src[i]; } return dst; } function createSalted(uint arg, string calldata name) public { bytes memory bytesname = bytes(name); bytes memory custom = new bytes(32); uint8 rtTypeEvm = 5; //evm的虚拟机类型是5,所以该值必须是5 custom[0] = bytes1(rtTypeEvm); //虚拟机类型必须放入custom的首字节,custom将赋值给创建选项salt custom = bytesCopy(bytesname, custom, 1); //将合约名拷贝到后31字节内 bytes32 n; assembly{ n := mload(add(custom, 32)) } //花括号内的salt为创建选项,salt为solidity语法关键字,赋值类型为byte32数据的值 //如果不指定为salt选项,写法为: Creature d = new Creature(arg) Creature d = new Creature{salt: n}(arg); } } ``` 如示例所示,创建者合约与被创建者合约位于同一文件内(也可以import导入),一同编译,执行创建动作时,无法更改被创建合约。 创建时,可通过salt选项指定被创建合约名,salt为byte32类型,但第一个字节必须赋值为evm虚拟机类型的枚举值5,后31字节才用于赋值合约名,所以合约名长度不得超过31字节。evm虚拟机会根据合约名计算地址,对于Chainmaker来说,所有类型的合约,都同时支持通过合约名和合约地址两种方式调用。 创建时,如果不指定salt选项,则evm虚拟机将为被创建者合约自动生成合约名和合约地址。 #### 动态创建 所谓动态创建,是指创建者合约不需要提前知道被创建合约的代码,只需要在创建时,告诉创建者被创建合约编译后的字节码即可,相比静态创建,动态创建更加灵活。 **动态创建示例** 合约dynamic_create.sol,实现功能: 1、部署Factory合约后,可调用Factory合约的create方法创建任意合约; 2、创建时,可指定合约虚拟机类型和合约名,并根据提供的合约字节码创建合约。 ```solidity // SPDX-License-Identifier: GPL-3.0 pragma solidity >=0.8.0; contract Factory { function bytesCopy(bytes memory src, bytes memory dst, uint begin) internal pure returns (bytes memory){ uint len = src.length < 31 ? src.length : 31; for(uint i = 0; i < len; i++){ dst[begin + i] = src[i]; } return dst; } function create(uint8 rtType, string calldata name, bytes calldata code) public returns (address addr){ assert(0 < rtType && rtType < 8);//ChainMaker支持7种虚拟机 bytes memory bytesname = bytes(name); bytes memory bytesCode = code; bytes memory custom = new bytes(32); custom[0] = bytes1(rtType); custom = bytesCopy(bytesname, custom, 1); assembly { //create2指令参数1为转账额,参数2为合约字节码长度,参数3为字节码,参数4为虚拟机类型+合约名 addr := create2(0, add(bytesCode,0x20), mload(bytesCode), mload(add(custom, 32))) } } } ``` 如示例所示,创建者合约不需要事先知道被创建合约,只需要在调用create方法时,以参数将被创建合约字节码传入即可。因为是动态创建,被创建合约不需要在编译前写入同一文件,所以,非solidity合约同样进入了被创建者的可选范围。 #### 异构创建 异构创建是指,solidity合约创建其他虚拟机类型的合约。异构创建方式和动态创建相同,只需要为调用者合约提供虚拟机类型、被调合约名和合约字节码即可,注意,这三个参数都是必不可少的。 ### solidity合约内调用合约 ChainMaker evm虚拟机支持solidity合约动态调用solidity合约,solidity合约动态调用solidity合约,以及solidity合约动态调用其他类型合约。 #### 静态调用 静态调用,是指在调用之前,调用者必须知道被调用者合约的接口。即,必须将被调用者的接口和调用者合约一同编译。在实际执行使,调用者不能调用接口以外的方法。 **静态调用示例** 合约callee.sol为被调用者合约,实现了一个简单的加法功能。 ```solidity // SPDX-License-Identifier: GPL-3.0 pragma solidity > 0.5.21; //被调用者合约 contract Callee { function Adder(uint256 x, uint256 y) public returns(uint256) { return x + y; } } ``` 合约caller.sol为调用者合约,可以调用callee.sol合约的方法。 ```solidity // SPDX-License-Identifier: GPL-3.0 pragma solidity >=0.5.21; //被调用合约Callee的接口 abstract contract ICallee { //Callee.Adder的方法签名 function Adder(uint256 x, uint256 y) public virtual returns(uint256); } //调用者合约 contract Caller { function crossCall(address addr, uint256 x, uint256 y) public returns(uint256) { ICallee callee = ICallee(addr); //将被调用合约的地址转化为ICalle接口 return callee.Adder(x, y); //根据接口和方法签名调用 } } ``` 如示例所示,调用者合约Caller所在文件内,声明了一个被调用者合约Callee的接口ICallee,接口内有Callee合约的方法签名。调用者合约获得被调用合约的地址后,即可根据实现声明的接口,调用该地址的方法了。注意,在调用者合约调用被调用者之前,必须确保被调用者合约已经部署完成。 #### 动态调用 所谓动态调用,是指调用者合约不需要提前知道被调用合约的接口,只需要在调用时,告诉调用者被调合约的方法签名即可。调用者合约不需要事前绑定被调者的接口,可以在调用时,指定任意被调用合约及其方法,更加灵活。 **动态调用示例** 合约dynamic_call.sol。部署Caller合约后,可以调用任意solidity合约及其方法,不限定接口。 ```solidity // SPDX-License-Identifier: GPL-3.0 pragma solidity >= 0.8.0; contract Caller { string[10] x; string[8] params; function cross_call(address callee, string calldata method, string calldata key, int256 value) public { //注意,method必须为方法签名格式,且方法签名的参数间不能有空格,例如"save(string,int256)" callee.call((abi.encodeWithSignature(method, key, value)); } } ``` 如示例所示,调用者合约Caller可以调用任意合约的任意方法,只需要将被调用合约的地址、方法签名和参数传给调用者合约即可。 注意,被调合约的方法必须是方法签名样式,且方法签名的参数间不能有空格,如示例中注释所示,否则无法编译为准确的ABI类型的calldata,evm将无法识别。 #### 异构调用 异构调用是指,solidity合约调用其他虚拟机类型的合约,比如wasm、docker-go等,只要在ChainMaker支持范围内,都可以异构调用。 solidity执行跨合约调用时,会将参数编码为ABI格式的calldata。但,对其他虚拟机合约来说,它们的参数并不需要ABI编码,也没有ABI文件,所以无法解析从调用者solidity合约那里传递来的参数。针对这种情况,ChainMaker单独实现了一套异构调用逻辑。 ​ 1 作为调用者的solidity合约需要在合约内声明一个string数组类型的状态变量,用做异构调用的**参数缓冲区**; ​ 2 在异构调用之前,需要将带传递的参数按顺序写入参数缓冲区; ​ 3 参数缓冲区的第一个元素(下标为0)必须是**"CrossVMCall"**字符串,用于让ChainMaker evm识别这是跨虚拟机的异构合约调用; ​ 4 参数缓冲区的第二个元素必须是待调用的方法名(此方法名不需要方法签名格式); ​ 5 从第三个参数开始,每两个元素一组,标识一个待传递参数,前一个元素为参数的类型或key(视被调合约而定),后一个元素为参数的值。 关于进一步理解异构调用的内容,可以参考接下来的异构调用示例。 **异构调用示例** 合约cross_vm_call.sol。部署CrossCall合约后,可以通过cross_call方法调用其他虚拟机类型的合约。。 ```solidity // SPDX-License-Identifier: GPL-3.0 pragma solidity >= 0.8.0; contract CrossCall { string[10] x; string[8] params; //参数缓冲区 //address类型的参数callee为被调用合约地址 function cross_call(address callee, string calldata method, string calldata time, string calldata name, string calldata hash) public { //CrossVMCall is reserved key word params[0] = "CrossVMCall";//CrossVMCall为固定值,第一个参数必须是此值 params[1] = method; //方法名,例如"save" params[2] = "time"; //参数1的类型或key params[3] = time; //参数1的value params[4] = "file_name"; //参数2的类型或key params[5] = name; //参数2的value params[6] = "file_hash"; //参数3的类型或key params[7] = hash; //参数2的value callee.call("");//跨虚拟机调用callee合约,callee.call的参数为空,evm虚拟机会从参数缓冲区读取对应的参数 } } ``` 如示例所示,solidity跨虚拟机异构调用时,必须提供被调合约的地址,而不能是方法名,因为合约调用方法call是solidity语法中,地址类型的成员方法,使用合约名则无法识别。 ### 其他虚拟机跨合约调用 相比于solidity,ChainMaker的其他虚拟机类型合约,都不支持合约创建合约,只支持合约调用合约。但同构异构都支持,且相比solidity也更加简单,只需要调用该类型合约所提供的跨合约调用接口即可。 **调用示例** 合约go_cross_call_evm.go 为go语言合约跨虚拟机调用soldity合约示例。 ```solidity /* SPDX-License-Identifier: Apache-2.0 */ package main //export init_contract func initContract() {} //export upgrade func upgrade() {} //export crossCallEvmContract func crossCallEvmContract() { ctx := NewSimContext() // 获取参数 name, _ := ctx.ArgString("name") method, _ := ctx.ArgString("method") //调用solidity合约时,需要使用ABI将被调合约的参数编码为calldata calldata, _ := ctx.Arg("calldata") params := make(map[string][]byte, 1) params["data"] = calldata //执行跨虚拟机调用 if result, resultCode := ctx.CallContract(name, method, params); resultCode != SUCCESS { // 返回结果 ctx.ErrorResult("failed to cross call evm contract: " + name) } else { // 返回结果 ctx.SuccessResultByte(result) // 记录日志 ctx.Log("cross call evm contract result:" + string(result)) } } func main() { } ``` 如示例所示,go合约调用solidity合约时,只需要调用go合约内置的SimContext.CallContract接口即可。对于被调用者合约来说,传递合约名或合约地址均可,而不必像solidity合约那样限制必须使用地址。 注意,跨虚拟机调用solidity合约时,同样需要使用ABI将被调合约的参数编码为calldata,详见[使用Solidity进行智能合约开发](./使用Solidity进行智能合约开发.md)的**calldata生成示例**。