合约规范
概述
编写智能合约需要遵循一些基本的规则和约定,只有这样的合约才能够发挥最大的能力。在合约简介当中已经介绍了HVM合约的组成部分,下文将会对这些组成部分的编写进行详细介绍。
需要注意的是,在HVM中,我们推荐在合约中仅保留一些简单的原子操作,常常更新的合约的较为复杂的业务逻辑则在InvokeBean
(下面会详细介绍)中编写。
合约主体类
我们约定一个继承了BaseContract并实现了一系列BaseContractInterface的子接口(合约接口)的类被称为一个合约主体类。 该类内的所有实现自合约可见接口的方法代表合约方法,可以被外界调用。
要求该合约主体类中必须提供无参构造方法,且不要在构造函数中写合约逻辑。为了解决可能存在的初始化的需求,我们提供了一个钩子函数onInit()
来代替原构造函数的作用(见下文)。
以下是合约主体类的一个简单示例:
public class DemoContract extends BaseContract implements IDemoA, IDemoB {
public DemoContract() {} // 无参构造方法
}
合约生命周期
合约的生命周期如下:
deploy
:合约部署阶段,该阶段只会在合约部署时出现一次。create
:构建合约对象阶段,无论部署还是调用均有该阶段。execute
:执行invoke bean阶段,该阶段仅仅在调用合约时出现。commit
:持久化阶段会自动扫描合约对象所有的持久化变量并做持久化操作(即数据真正上链),该阶段仅仅在调用合约时出现。
用户可以通过按需重写BaseContract中四个钩子方法onInit()
、onCreated()
、onPreCommit()
和onCommitted()
来在合约生命周期中的不同阶段加入自定代码。
各钩子函数的调用顺序如下:
部署合约
: onInit() -> onCreate() -> onPreCommit() -> onCommit()调用合约
: onCreate() -> onPreCommit() -> onCommit()
注:上面提到的四个钩子函数除了
onInit()
外,无论是在合约部署还是合约调用的时候均会被调用。
用户可以在钩子函数中添加自定义的逻辑,比如可以在onCreate()
中添加权限检查或者非持久化变量的初始化,在onPreCommit()
中添加执行结果的校验,在onCommitted()
中添加日志的打印等。
以下是一个重写了生命周期钩子函数的代码示例:
public class DemoContract extends BaseContract implements IDemoA, IDemoB {
public DemoContract() {} // 无参构造方法
@Override
public Boolean foo(String arg1, int arg2) {/*...*/}
@Override
public String bar(MyObject arg1, boolean arg2) {/*...*/}
@Override
public void onInit() {/*...*/}
@Override
public void onCommitted() {/*...*/}
}
合约成员属性
该节中主要介绍用户自定义成员变量(包含持久化变量和非持久化变量)。
以下代码是包含持久化属性和非持久化属性的代码示例:
public class DemoContract extends BaseContract implements IDemoA, IDemoB {
@StoreField
public String name; // 持久化属性
public int version; // 非持久化属性
public DemoContract() {} // 无参构造方法
}
持久化属性
合约持久化变量指的是在合约中增加了@StoreField
注解的成员变量,针对这些变量的修改,将会提交到区块链平台并影响世界状态,例如:
@StoreField
public String name = "sbank";
@StoreField
public HyperMap<String, Integer> accounts = new HyperMap<>();
持久化变量会在合约执行前从Ledger读取最新数据依赖注入进合约相应变量,同时在执行后自动向账本写入修改的新的数据数据,读取、修改和持久化的过程对用户透明。
同时,针对区块链场景,我们实现了两个自定义的数据集合:HyperMap
和HyperList
。顾名思义,即一个Map和List的自定义实现,在合约中需要使用与账本交互的集合时使用HVM提供的两个数据集合,可以做到比普通集合在大数据量时有更好的效率。
注意:
- HyperMap和HyperList作为持久化成员变量,必须在声明时就初始化,不可以在构造函数或者钩子函数中初始化;
- 集合内用户自定义的泛型类型不可以在带有泛型类型,例如HyperMap<String, Map<String, Integer>>是不允许的;
- 不要把HyperMap和HyperList作为非StoreField来使用(比如局部变量,返回值,函数入参等等),只在需要与账本交互时使用它们。
合约非持久化属性
合约中非@StoreField
的普通变量,例如:
public String key;
public int n = 1;
该类型变量若不在合约成员变量声明时初始化,则在每次调用前都会修改为改类型数据的零值,同时在合约执行完成后,也不会将数据写入账本,但是可以通过hook onCreated()
来改变非持久化类型的值,提供给在合约调用时使用。
@Override
public void onCreated() {
this.key = "key";
this.n = 2;
}
合约可见接口
可见接口是应用和合约进行交互的约定,可见接口通过部分声明和完全声明实现一定程度上的权限控制。
可见接口设计
合约可见接口是指在合约主类中实现的合约方法接口。一个合约主类可以实现多个合约方法接口,每个合约方法接口可以有自己的方法集合。
如在SBank
合约中实现了ISBank
和ISBank2
接口,ISBank
接口内容为
public interface ISBank extends BaseContractInterface {
boolean transfer(String from, String to, int val);
boolean deposit(String from, int val);
}
ISBank
接口仅声明了transfer
和deposit
两个方法,应用持有这接口的话,仅仅能够调用这两个合约方法。
ISBank2
接口内容为
public interface ISBank2 extends BaseContractInterface {
boolean transfer(String from, String to, int value);
boolean withdraw(String from, int val);
boolean deposit(String from, int val);
}
ISBank
接口仅声明了transfer
、deposit
和withdraw
三个方法,应用持有这接口的话,能够调用三个合约方法。
通过上面的方式,在部署合约完成后,部署者只需要把调用者需要调用的接口给调用者即可。
比如,部署者并不希望调用者A知道withdraw
方法,则可以将SBank
给调用者A以供调用,但部署者希望调用者B知道withdraw
方法,则可以将SBank2
给调用者B。
这种方式能让合约调用更加准确便捷,能依靠编译器的类型检查极大的避免方法调用时因为方法名以及参数传入错误而导致的失败,而且可以在一定程度上屏蔽其他的合约方法。
合约调用类 InvokeBean
InvokeBean
:这是HVM中提出的一个新概念,指代实现了BaseInvoke接口的类,其实现的invoke方法中包含了调用合约方法的业务逻辑。
注意:
InvokeBean
是在应用代码当中编写的,不是在合约代码中编写。
需要强调的是,InvokeBean
需要一个空参空体的构造函数。
这种写法有如下几个优点:
- 能够在一个invoke bean中同时调用合约的多个方法。
- 能够使得在不更新合约的情况下更新合约业务逻辑(通过更新invoke方法内的逻辑,或者实现多个用于不同业务场景的invoke bean),以此适应不断变化的业务需求。
- 能够保证整个业务逻辑为一个事务,即整个方法要么全部执行要么全部不执行。
- 能够在编译阶段就进行参数类型检查,而非运行时检查,提升了安全性。
调用合约需要编写一个合约调用类InvokeBean,且该调用类需要实现BaseInvoke
接口,该接口的具体声明如下:
// 泛型T表示返回的类型,V表示合约类
public interface BaseInvoke<T, V extends BaseContractInterface> {
T invoke(V obj);
}
实现接口的invoke
来编写执行合约的具体逻辑,obj
即为合约对象,其中对于该调用类必须有一个空参空体的构造函数。
以调用之前SBank
为例,编写BankInvoke
来进行转账操作,调用类具体示例如下:
public class BankInvoke implements BaseInvoke<Boolean, ISBank> {
/**
* 调用参数(也可使用单个Bean)
*/
public String from;
public String to;
public int value;
// 必须提供的空参空体构造函数
public BankInvoke(){}
// 方便初始化参数的构造函数
public BankInvoke(String from, String to, int value){
this.from = from;
this.to = to;
this.value = value;
}
// 实现invoke接口方法
@Override
public Boolean invoke(ISBank obj) {
boolean a = obj.transfer(from, to, value);
if(a){
// 转账成功在进行后续操作
obj.deposit(from,value);
}
return a;
}
// setter or getter
public String getFrom() {return from;}
public String getTo() {return to;}
public int getValue() {return value;}
}
在SDK协助调用的过程当中,需要将InvokeBean
的实例作为参数传入,即可完成对应的合约调用,调用示例如下:
BankInvoke bankInvoke = new BankInvoke(fromAddr, toAddr, 100);
Transaction tx = new Transaction(from, contratAddr, bankInvoke, simulate, vmType);
tx.sign(account);
String ret = sdk.invoke(tx);
合约功能 Bean
合约中需要用到一些辅助的bean来帮助合约的开发,编写这些bean的时候需要注意必须重写hashCode()
函数和equals()
函数。
例如SBank中Account账户bean的编写:
public class Account {
private String name;
private int amount;
public Account(){}
public Account(String name, int amount){
this.name = name;
this.amount = amont;
}
@Override
public int hashCode() {
...
}
@Override
public boolean equals(Object o) {
...
}
}
需要注意: 需要作为参数使用的合约功能Bean需要在合约端和应用端都拥有,也就是说,合约功能Bean是需要同可见接口一起交付的。