合约调用机制与并行合约
# 合约调用机制与并行合约
学校: 深圳职业技术学院
作者:钟文慧
# 1. 智能合约的调用机制
智能合约的调用机制是 Solidity 合约中进行函数调用和交互的方式。在 Solidity 中,有多种方式可以在不同的合约之间进行调用和交互,包括外部调用、内部调用、库函数等。分别为如下几种:
- 外部调用:合约A需要和合约B或者地址进行交互的时候,可以使用
address.call()
方法进行外部调用。在这种情况下,发送方的状态不会改变,因为所有数据都是通过参数传递的。外部调用允许向任何地址发送消息,并以任何字节序列形式返回结果,但结果必须与 Solidity 函数声明类型匹配才能被解释。 - 内部调用:与外部调用不同,在 Solidity 合约内部进行的函数调用会直接修改执行合约的状态。在内部调用过程中,Solidity 编译器会优化代码以避免额外的 GAS 消耗,并保证编译时类型检查以避免类型不匹配的错误。
- 库函数:Solidity 允许将经常使用的代码片段封装成 library,使得其它合约可以引用并重复使用。这些函数不占用调用合约的存储空间,同时也不会影响当前合约的状态。Library 可以视作一种抽象合约,但它的
function
关键字被用于声明函数而不是contract
的。
# 2. 合约调用合约
# 2.1 两种简单的方式
合约经常需要调用其他的合约。这可以通过调用已经部署好的合约或实例化新的合约来完成。
根据如下的案例,OwnerContract
合约需要调用 OtherContract
合约的函数并获得返回结果。在 Solidity 中,我们首先需要创建 otherContract
实例并保证它已经被正确部署和初始化。接着,我们就可以对其公共函数进行调用,如下所示:
// 在 OwnerContract 合约中实例化 OtherContract 合约并调用其函数
OtherContract otherContract = OtherContract(0x123...); // 传入部署后的合约地址
uint timestamp = otherContract.getNowTime(); // 获取当前时间戳
// 或者通过使用新的实例化方法
OtherContract newOtherContract = new OtherContract();
string memory title = newOtherContract.getTitle(); // 获取标题内容
根据如上的代码可以分为两种:
直接将部署后的合约地址传递给智能合约构造函数中的
OtherContract()
函数,并赋值给otherContract
变量。然后,我们在该变量上调用getNowTime()
函数以获取当前时间戳。通过调用
new OtherContract()
构造函数来实例化新的OtherContract
,并将其赋值给newOtherContract
变量。之后,我们可以利用newOtherContract
对象调用其公共函数来访问该合约。特别需要注意的是,创建新实例时,必须首先确保OtherContract
合约已经被正确部署和初始化。
# 2.2 详细的合约案例
详细的合约案例如下:
pragma solidity ^0.4.25;
contract OtherContract {
// 自定义变量
string private title = "test";
// 获取当前的时间戳
function getNowTime() public view returns(uint256){
return block.timestamp;
}
// 获取当前的标题
function getTitleInfo(string memory _title) public view returns(string memory){
title = _title;
return title;
}
}
部署当前的OtherContract合约拿到当前的地址。在OwnerContract的合约的初始化构造函数中进行传参地址。
pragma solidity ^0.4.25;
import "./OtherContract.sol";
contract OwnerContract {
// OtherContract合约的私有变量
OtherContract private otherContract;
// OtherContract合约的私有变量
OtherContract private newOtherContract;
constructor() public {
// 实例化一个新的OtherContract只能约合
otherContract = new OtherContract();
// 传入部署好的合约地址 实例化了一个新合约
newOtherContract = OtherContract(0x59b2de3137a09b197851866ad8560fede6c9280d);
}
// 测试调用实例化的OtherContract合约
function getTime() public view returns(uint256){
return otherContract.getNowTime();
}
// 测试调用部署好的OtherContract合约
function getTitle() public view returns(string memory){
return newOtherContract.getTitleInfo("zhangsan");
}
}
如下是使用WeBASE-Front调用OwnerContract合约的方法交易回执:
# 3. Call函数调用合约
# 3.1 Call函数介绍
call
函数可以在智能合约之间进行调用。call
函数允许合约之间建立更为灵活的通信方式,并通过提供更多的参数控制智能合约调用过程。
使用call合约不会像上面的那样简单,call
是address
类型的低级成员函数,它用来与其他合约交互。它的返回值为(bool, data)
,分别对应call
是否成功以及目标函数的返回值。
因为 call
函数的返回值既可以返回成功/失败状态,所以也可以返回调用的结果。
我在Solidity0.4.25中尝试发现有些问题,ABI编码签名调用方式不一样需要自己进行转换,所以使用0.8.0的时候会比较方便。
这里还是继续使用上面的OtherContract智能合约。OwnerContract合约用于测试。
// SPDX-License-Identifier: GPL-3.0
pragma solidity ^0.8.0;
contract OtherContract {
// 自定义变量
string private title = "test";
// 获取当前的时间戳
function getNowTime() public view returns(uint256){
return block.timestamp;
}
// 获取当前的标题
function getTitleInfo(string memory _title) public returns(string memory){
title = _title;
return title;
}
}
# 3.2 Call函数使用
这里需要了解的几个点是:
call
是solidity
官方推荐的通过触发fallback
或receive
函数发送ETH
的方法。- 所以
call
的主要目的不是用于调用合约进行传参。这样对合约不太安全。 - 使用
call
调用合约的时候,需要知道被调用的合约的函数方法。
call的使用方法:
(bool success, bytes memory returnData) = someAddress.call{value: msg.value}(abi.encodeWithSignature("foo(uint256)", arg));
其中:
someAddress
表示需要调用的合约地址。success
是返回的布尔值,用来指示函数调用是否成功,并防止调用任意合约时产生攻击风险。returnData
是返回结果的存储器。{value: msg.value}
用于在调用目标合约的时候给目标函数进行支付。abi.encodeWithSignature()
函数用于对待调用函数的参数进行编码,并生成函数签名。
类似于二进制编码利用结构化编码函数abi.encodeWithSignature,有如下几种:
abi.encode()
用于以紧密排列的形式对多个参数进行编码。abi.encodePacked()
对多个参数按照紧密的顺序压缩成一个字节数组。abi.encodeWithSelector()
函数与abi.encode()
类似,但需要为函数名生成一个 4 字节哈希值作为前置符号。encodeWithParameter()
可通过指定一个 Solidity 函数参数的 Keccak-256 哈希值来编码其输入参数。该工具是针对那些尚未作为合约编译器引用的函数所设计的,因此它在一些情况下可能非常有用。
如下是OwnerContract合约,使用call的函数进行调用。
// SPDX-License-Identifier: GPL-3.0
pragma solidity ^0.8.0;
contract OwnerContract {
// 调用其他合约的地址
address public otherContractAddress;
// 初始化构造函数初始化合约地址
constructor(address _bAddr) {
otherContractAddress = _bAddr;
}
// Log查看事件
event LogTime(bool indexed _isOk,uint256 indexed _data);
event LogTitle(bool indexed _isOk,string _title);
// 调用OtherContract合约 获取当前的时间戳
function getOtherContractTime() public returns(uint256){
(bool success, bytes memory data) = otherContractAddress.call(abi.encodeWithSignature("getNowTime()"));
require(success, unicode"获取当前的时间戳失败");
uint256 nowTime = abi.decode(data, (uint256));
emit LogTime(success,nowTime);
return nowTime;
}
// 调用OtherContract合约 获取当前合约的标题
function getOtherContractTitle(string memory _title) public returns (string memory) {
(bool success, bytes memory data) = otherContractAddress.call(abi.encodeWithSignature("getTitleInfo(string)",_title));
require(success, unicode"获取当前的标题失败");
string memory title = abi.decode(data, (string));
emit LogTitle(success,title);
return title;
}
}
# 3.3 合约测试
1.部署当前的合约拿到该地址。
2.部署OwnerContract合约传入合约地址。
3.调用getOtherContractTime
函数,查看控制台输出的日志以及触发的事件信息。
4.调用getOtherContractTitle
函数,查看控制台输出的日志以及触发的事件信息。
# 4. 并行合约
# 4.1 什么是并行合约
知乎: https://zhuanlan.zhihu.com/p/595020482
并行合约是一种能够在以太坊区块链上支持多线程计算的智能合约,以提高计算速度和性能。传统的智能合约只能够按顺序执行代码,即一个操作完成后才能开始另一个操作,这样的串行方式在处理大量数据时效率较低。而并行合约则采用多线程技术,最大限度地减少了不必要的等待时间,同时充分利用可用资源。例如,在处理大量数据时,可以将数据分成多个部分进行并行计算,从而大大提高计算速度。并行合约可以更高效地处理各种复杂业务,如金融、游戏、人工智能等。
在以太坊上实现并行合约需要一些特定的技术和工具。使用 Solidity 语言创建的并行合约不同于传统的编程架构,因为它可以同时执行多个任务,而无需考虑它们之间的顺序。在并行合约中,通常会声明一个或多个线程函数,每个线程函数实现一个独立的计算任务,并且可以同时运行该函数。
同时,为避免各个线程对共享变量产生死锁和竞态条件等问题,开发人员必须仔细处理共享数据的访问控制。这些挑战需要一个可靠的机制来确保并行执行任务的相互独立性和安全性,以及在完成计算时保证合约状态和数据的一致性。
并行合约和串行合约的区别:
调用合约是一种串行处理操作,即每个操作只能在上一个操作完成后才能开始执行。智能合约中的调用是通过使用函数来实现的,而且这些函数都是依靠主调函数,以递归方式执行的。
与之相反,为了提高计算性能并允许智能合约同时处理多项任务,而不必按顺序执行,我们可以使用并行合约。在并行合约中,可以使用多个线程函数来同时计算和完成独立的任务(例如,分布式负载平衡系统中的数据分割和并行计算)。
# 4.2 ParallelContract并行合约
FISCO-BCOS 平台提供的名为 "ParallelConfigPrecompiled"
的预编译合约,可以通过注册并行函数来开启 Solidity 并行合约的功能。其中,registerParallelFunctionInternal 和 unregisterParallelFunctionInternal 函数用于注册和注销并行函数。
在 Solidity 并行合约中,每个线程都应该是独立、无状态的,并且能够在多个线程之间共享数据。执行结果可能会因为不同执行顺序或并发条件而产生变化,所以需要注意避免竞态条件的出现。
pragma solidity ^0.4.25;
// 预编译合约 ParallelConfigPrecompiled
contract ParallelConfigPrecompiled {
// 注册并行函数
// 参数1:并行函数地址
// 参数2:并行函数名称
// 参数3:致命区间值,用于限制并行计算区间大小
function registerParallelFunctionInternal(address, string, uint256) public returns (int);
// 注销并行函数
// 参数1:并行函数地址
// 参数2:并行函数名称
function unregisterParallelFunctionInternal(address, string) public returns (int);
}
// 并行合约基类
contract ParallelContract {
// 实例化并使用 ParallelConfigPrecompiled 合约
ParallelConfigPrecompiled precompiled = ParallelConfigPrecompiled(0x1006);
// 注册并行函数,注册后可以在并行计算中使用
// 参数1:并行函数名称
// 参数2:致命区间值,用于限制并行计算区间大小
function registerParallelFunction(string functionName, uint256 criticalSize) public
{
precompiled.registerParallelFunctionInternal(address(this), functionName, criticalSize);
}
// 注销并行函数,在停用并行计算前建议注销所有已注册的并行函数
// 参数1:并行函数名称
function unregisterParallelFunction(string functionName) public
{
precompiled.unregisterParallelFunctionInternal(address(this), functionName);
}
// 启用并行计算功能
function enableParallel() public;
// 关闭并行计算功能
function disableParallel() public;
}
# 4.3 并行合约使用
定义了两个并行函数:thread1
和 thread2
。这两个函数分别计算给定范围内的累加和和平方和,然后将结果加到 totalSum
变量上。在启用并行计算功能后,可以通过调用 thread1
和 thread2
函数以并行方式执行这些线程。最后,可以使用 getTotalSum
函数查看总和的值。
pragma solidity ^0.4.25;
// 导入 ParallelContract 合约,以便使用其功能
import "./ParallelContract.sol";
// 定义并行合约 ParallelInstance,并继承 ParallelContract 合约(即 ParallelInstance 类型是 ParallelContract 类型的子类型)
contract ParallelInstance is ParallelContract {
// 声明私有变量 totalSum ,用于存储所有线程函数的计算结果的总和
uint private totalSum = 0;
// 声明线程函数 thread1,以计算给定范围内的累加和
function thread1(uint start, uint end) public {
uint sum = 0;
for (uint i = start; i <= end; ++i) {
sum += i;
}
// 将当前线程计算的结果累加到总和上
totalSum += sum;
}
// 声明线程函数 thread2,以计算给定范围内的平方和
function thread2(uint start, uint end) public {
uint sum = 0;
for (uint i = start; i <= end; ++i) {
sum += i * i;
}
// 将当前线程计算的结果累加到总和上
totalSum += sum;
}
// 声明函数 enableParallel,用于启用并行计算功能
function enableParallel() public {
// 分别注册 thread1 和 thread2 线程函数,以便后续调用
registerParallelFunction("thread1(uint,uint)",10);
registerParallelFunction("thread2(uint,uint)",10);
}
// 声明函数 disableParallel,用于禁用并行计算功能
function disableParallel() public {
// 首先,分别注销 thread1 和 thread2 线程函数
unregisterParallelFunction("thread1");
unregisterParallelFunction("thread2");
}
// 声明函数 getTotalSum,用于获取总和的计算结果
function getTotalSum() public constant returns (uint) {
return totalSum;
}
}
将ParallelInstance合约进行部署,分别给thread1和thread2方法给定计算的范围内的平方和。我这里分别是1到10。
调用enablePrarallel
函数开启并行计算。
调用getTotalSum
函数,查看当前的计算结果。