3. Solidity智能合约开发

​ Solidity智能合约使用Spark-Evm引擎,脱胎于原生以太坊EVM架构实现。在星火链合约账户中,Solidity编译后生成的opCode指令码会存储到合约账户中,用于合约的执行。本目录的文档主要介绍在星火链合约平台中支持的 Solidity 合约的特性、语法、功能等。星火链平台支持的solidity语法基本与官方solidity基本一致,目前支持0.4.26版本,可以参考官方文档:

​ https://solidity.readthedocs.io/en/v0.4.26/

3.1 Solidity智能合约描述

3.1.1 特性说明

  • 星火链平台solidity对address关键字进行了重新开发,address表示的合约地址或账户地址,长度为24字节;而官方solidity中address表示的地址是20字节。

  • 在星火链链平台上,如果尝试在合约内向一个不存在的地址转账,合约会异常终止;而在以太坊官方 Solidity 合约内向不存在的地址转账时,系统会自动以该地址创建账户。

  • 星火链平台中,solidity不支持STATICCALL指令。

  • 星火链平台中,solidity不支持CALLCODE指令。

  • 星火链平台中,solidity不支持SELFDESTRUCT指令。

  • 星火链平台中,合约账户无codehash,solidity不支持EXTCODEHASH指令。

  • 星火链平台中,区块中未保存出块人信息,solidity不支持COINBASE指令。

  • 星火链平台中,无难度值概念,solidity不支持DIFFICULT指令。

  • 星火链平台中,自定义实现了STOI64CHECK指令。

  • 星火链EVM引擎处理交易时,使用星火令消耗上限+异常退出(递归深度,栈,内存)+超时。需要指定当前交易接受的最大星火令,执行合约时,按照具体指令扣减星火令。当星火令不足时,合约执行结束。

  • 当递归深度超过配置的最大深度时,合约执行结束。此处为兼容星火链框架,合约递归深度未使用以太坊原始最大递归深度1024,合约最大递归深度为4层。

3.1.2 指令集比较

指令表 星火链是否支持 以太坊是否支持
CREATE2
CREATE
DELEGATECALL
STATICCALL
CALL
CALLCODE
RETURN
REVERT
SELFDESTRUCT
STOP
MLOAD
MSTORE
MSTORE8
SHA3
LOG0
LOG1
LOG2
LOG3
LOG4
EXP
ADD
MUL
SUB
DIV
SDIV
MOD
SMOD
NOT
LT
GT
SLT
SGT
EQ
ISZERO
AND
OR
XOR
BYTE
SHL
SHR
SAR
ADDMOD
MULMOD
SIGNEXTEND
ADDRESS
ORIGIN
BALANCE
CALLER
CALLVALUE
CALLDATALOAD
CALLDATASIZE
RETURNDATASIZE
CODESIZE
EXTCODESIZE
CALLDATACOPY
RETURNDATACOPY
EXTCODEHASH
CODECOPY
EXTCODECOPY
GASPRICE
BLOCKHASH
COINBASE
TIMESTAMP
NUMBER
DIFFICULTY
GASLIMIT
CHAINID
SELFBALANCE
POP
PUSHC
PUSH1/32
JUMP
JUMPI
JUMPC
JUMPCI
DUP1/16
SWAP1/16
SLOAD
SSTORE
PC
MSIZE
GAS
JUMPDEST
INVALID

3.1.3 合约数据类型

  • 星火链交易支持的数据类型

    基本类型:

    uint<M>:M 位的无符号整数,0 < M <= 256、M % 8 == 0。例如:uint32,uint8,uint256。

    int<M>:以 2 的补码作为符号的 M 位整数,0 < M <= 256、M % 8 == 0。

    uint、int:uint256、int256 各自的同义词。在计算和 函数选择器 中,通常使用 uint256 和 int256。

    bool:等价于 uint8,取值限定为 0 或 1 。在计算和 函数选择器 中,通常使用 bool。

    bytes<M>:M 字节的二进制类型,0 < M <= 32。

    byte:等价于bytes1。

    bytes:动态大小的字节序列。

    string:动态大小的 unicode 字符串,通常呈现为 UTF-8 编码。

    一维数组:

    type[M](type=uint/int/address) 有 M 个元素的定长数组,M >= 0,数组元素为给定类型。

    二维数组:

    type[][](type=uint/int)

  • 平台建议使用数据类型

数据类型 参考样例 合约内部是否支持 输入参数是否支持
bool bool a = true
uint uint a = 1
uint8 ~ uint256 uint8 a = 1
int int a = 1
int8 ~ int256 int8 a = 1
bytes bytes a = “test”
bytes1 ~ bytes32 bytes1 a = “a”
string string a = “test”
int[] int256[] a = [1,2,3,4,5]
uint[] uint256[] a = [1,2,3,4,5]
bytes1[] ~ bytes32[] bytes4[] asd = new bytes4;
string[] string[] a = [“adbc”,”dg”]
enum enum a {a,b,c}
struct struct a { string name;}

3.2 Solidity合约工具

​ 星火链合约平台支持 Solidity 智能合约,由于Spark-Evm中存在的特性,不能使用以太坊提供的编译器编译处理Solidity合约 ,因此我们提供了星火链Solidity的编译工具docker镜像。

3.2.1 镜像下载

docker pull caictdevelop/bif-solidity:v0.4.26

3.2.2 使用solc

​ 镜像下载之后,需要启动镜像进入容器中,可以使用solc –help 来查看此工具支持的参数说明。

​ 常用选项说明:

--opcodes            Opcodes of the contracts.
--bin                Binary of the contracts in hex.
--abi                ABI specification of the contracts.

3.2.3 使用实例

  • 启动镜像

# docker相关操作需要root权限
docker run -it caictdevelop/bif-solidity:v0.4.26 /bin/bash
  • 编写合约test.sol

pragma solidity ^0.4.26;

contract test{
    function testfun() public returns(string){
        return "hello world";
    }
}
  • 编译合约

#cd /root/solidity/build/solc
#./solc --bin test.sol

======= test.sol:test =======
Binary: 
608060405234801561001057600080fd5b5061013f806100206000396000f300608060405260043610610041576000357c0100000000000000000000000000000000000000000000000000000000900463ffffffff168063031153c214610046575b600080fd5b34801561005257600080fd5b5061005b6100d6565b6040518080602001828103825283818151815260200191508051906020019080838360005b8381101561009b578082015181840152602081019050610080565b50505050905090810190601f1680156100c85780820380516001836020036101000a031916815260200191505b509250505060405180910390f35b60606040805190810160405280600b81526020017f68656c6c6f20776f726c640000000000000000000000000000000000000000008152509050905600a165627a7a723058201a4c9bfcbee5d683f6e46525cf17db2dd46a6ecf5c3f45cbdd148229639263480029

3.3 合约操作

​ 本节内容将以下述合约代码为例,说明合约部署、调用、查询的详细流程。

​ 合约实现了在星火链合约中存储和查询数据的接口。合约调用时,将变量设置到合约账户中进行存储。合约查询时,根据输入的参数“key”查询合约账户中存储的变量。

pragma solidity ^0.4.26;

contract Test {
	mapping (int256 => string) public keyMap;
	function setKey(int256 key, string val) public{
		keyMap[key] = val;
	}
	function getKey(int256 key) public returns(string){
		return keyMap[key];
	}
}

3.3.1 合约部署

​ 合约部署前,需要先使用solidity编译器生成合约opCode码,具体操作见Solidity合约工具。本节不再详述;

​ 生成的opCode指令码如下:

608060405234801561001057600080fd5b50610444806...

​ Java SDK代码如下:

public void contractCreate() {
    // 初始化参数
    String senderAddress = "did:bid:efuEAGFPJMsojwPGKzjD8vZX1wbaUrVV";
    String senderPrivateKey = "priSPKkAwBP7w1ajzwp16hNBHvz5psKsksmgZDapcaebzxCS42";
    String payload = "608060405234801561001057600080fd5b50610444806...";

    Long initBalance = ToBaseUnit.ToUGas("0.01");

    BIFContractCreateRequest request = new BIFContractCreateRequest();
    request.setSenderAddress(senderAddress);
    request.setPrivateKey(senderPrivateKey);
    request.setInitBalance(initBalance);
    request.setPayload(payload);
    request.setMetadata("create contract");
    request.setType(1);
    // 调用BIFContractCreate接口
    BIFContractCreateResponse response = sdk.getBIFContractService().contractCreate(request);
    if (response.getErrorCode() == 0) {
        System.out.println(JSON.toJSONString(response.getResult(), true));
    } else {
        System.out.println("error:      " + response.getErrorDesc());
    }
}

​ 合约部署完成后,查询合约账户返回结果

{
    "total_count":1,
    "transactions":[{
        "actual_fee":"1002548",
        "close_time":1630659261685103,
        "error_code":0,
        "error_desc":"[{\"contract_address\":\"did:bid:efrVoaxEQo9sDqQmv9BCnDstyu1FDTHE\",\"contract_evm_address\":\"6566b65e22eb91b439228dd2cab092cffeccdc2f4f06edd5\",\"operation_index\":0,\"vm_type\":1}]",
        "hash":"1f6f75935ddf164237cd56a5b8a678545ac74c92dece1c5c73873ba5345f0db1",
        "ledger_seq":20961,
        "signatures":[{
            "public_key":"b0656669660116b0b2ddfbba3f155962b311dc0afb671e2da069563a70fccabd9ff8c4",
            "sign_data":"12f528b7ba3c2722e4aad9f46374582c10548e3005ad4cd3d106e54b21e800942d6dd80e88d6f09e0482ce84b1425508ea48a69c6c2ecd4b9a61c9b35fb2390e"
        }],
        "transaction": {
            "fee_limit":2000000,
            "gas_price":1,
            "metadata":"create contract",
            "nonce":50,
            "operations": [{
                "create_account": {
                    "contract": {
                        "payload":"608060405234801561001057600080fd5b50610444806100206000396000",
                        "type":1
                    },
                    "init_balance":1000000,
                    "priv": {
                        "thresholds":{
                            "tx_threshold":1
                        }
                    }
                },
                "type":1
            }],
            "source_address":"did:bid:efuEAGFPJMsojwPGKzjD8vZX1wbaUrVV"
        },
        "tx_size":2285
    }]
}

3.3.2 合约调用

​ 当调用合约时,合约接口input按照指定格式输入参数:

{
	"function":"xxx",//待调用函数声明 入setKey(int256,string),仅包含函数名(形参类型)
	"args":{ 
		"xxx":""//待调用合约的参数,多个参数使用“,”分隔
	}
}

​ Java SDK代码如下:

public void contractInvokeByGas() {
    // 初始化参数
    String senderAddress = "did:bid:efuEAGFPJMsojwPGKzjD8vZX1wbaUrVV";
    String contractAddress = "did:bid:efrVoaxEQo9sDqQmv9BCnDstyu1FDTHE";
    String senderPrivateKey = "priSPKkAwBP7w1ajzwp16hNBHvz5psKsksmgZDapcaebzxCS42";
    Long amount = 1000L;

    BIFContractInvokeRequest request = new BIFContractInvokeRequest();
    request.setSenderAddress(senderAddress);
    request.setPrivateKey(senderPrivateKey);
    request.setContractAddress(contractAddress);
    request.setBIFAmount(amount);
    request.setMetadata("contract invoke");
     request.setInput("{\"function\":\"setKey(int256,string)\",\"args\":\"123,'hello world'\"}");
    // 调用 BIFContractInvoke 接口
    BIFContractInvokeResponse response = sdk.getBIFContractService().contractInvoke(request);
    if (response.getErrorCode() == 0) {
        System.out.println(JSON.toJSONString(response.getResult(), true));
    } else {
        System.out.println("error:      " + response.getErrorDesc());
    }
}

3.3.3 合约查询

​ 当调用合约时,合约接口input按照指定格式输入参数:

{
	"function":"xxx",//待调用函数声明 入getKey(int256),仅包含函数名(形参类型)
	"args":{ 
		"xxx":""//待调用合约的参数,多个参数使用“,”分隔
	},
	"return":"returns(xxx)"//待返回数据类型,returns(形参类型)
}

​ Java SDK代码如下:

public void callContract() {
    // Init variable
    // Contract address
    String contractAddress = "did:bid:efrVoaxEQo9sDqQmv9BCnDstyu1FDTHE";

    // Init request
    BIFContractCallRequest request = new BIFContractCallRequest();
    request.setContractAddress(contractAddress);
    request.setInput("{\"function\":\"getKey(int256)\",\"args\":123, \"return\":\"returns(string)\"}");

    // Call call
    BIFContractCallResponse response = sdk.getBIFContractService().contractQuery(request);
    if (response.getErrorCode() == 0) {
        BIFContractCallResult result = response.getResult();
        System.out.println(JSON.toJSONString(result, true));
    } else {
        System.out.println("error: " + response.getErrorDesc());
    }
}

3.4 Solidity智能合约示例

  • ERC20智能合约示例

具体合约示例见5.1 ERC20合约示例