聊聊Ontology上三种合约“交相辉映”的故事

10月29日,Ontology v1.8.0重磅发布!经过数月在测试网上的稳定运行,Wasm 功能也正式登陆了主网。目前为止,Ontology v1.8.0已支持 Native、NeoVM 和 Wasm 三种类型的合约,不同类型的合约之间可以无缝交互。

1. Native 合约

也是 Ontology 的原生合约,直接由 Golang 语言实现,目前已有的原生合约均在创世块中部署,执行速度快。

2. NeoVM 合约

运行于 NeoVM 虚拟机上,具有合约文件小、字节码简单及高性能的特点。

3. Wasm 合约

这种合约支持多种高级语言开发的程序直接编译成 Wasm 字节码,功能更加丰富,可以直接引用很多优秀的第三方库,且 Wasm 社区也十分活跃。

今天我们来聊聊 Ontology 上的这三种合约是如何“交相辉映”的。你将会了解到 Wasm 合约与 Native 合约及 NeoVM 合约调用的实现。在介绍下面的调用过程中,大家可以先把我们的合约模板 clone 下来,然后修改lib.rs文件,进行测试。

Runtime 模块中跨合约调用接口介绍

ontology-wasm-cdt-rust 库中封装了跨合约调用的一个通用接口,如下:

pub fn call_contract(addr: &Address, input: &[u8]) -> Option>
该方法需要两个参数,第一个是addr目标合约地址,第二个是input调用的目标合约的方法名和方法参数。在跨合约调用的过程中,要按照正确的方式序列化方法名和方法参数。其中调用 NeoVM 合约和调用 Native 合约序列化方法名和方法参数是不一样的,下面我们会详细介绍如何正确地序列化方法名和方法参数。

Wasm 合约调用 Native 合约

ontology-wasm-cdt-rust 库中封装了ont和ong合约调用接口,只需要通过use ostd::contract::ont;引入即可。调用比较简单,现仅列出ont转账的合约调用示例:

use ostd::contract::ont;
...
let (from, to, amount) = source.read().unwrap();
sink.write(ont::transfer(from, to, amount));

现在,我们看一下ont::transfer方法的实现源码:

pub fn transfer(from: &Address, to: &Address, val: U128) -> bool {
let state = [TransferParam { from: *from, to: *to, amount: val }];
super::util::transfer_inner(&ONT_CONTRACT_ADDRESS, state.as_ref())
}

由上面的源码可以看到,我们先构造了一个TransferParam类型的实例,然后构造了一个数组,这一步是为了支持一笔交易中的多笔转账功能。最后将ont的合约地址和构造好的数组引用传给util::transfer_inner方法,我们接着看util::transfer_inner的实现源码如下:

pub(crate) fn transfer_inner(
contract_address: &Address, transfer: &[super::TransferParam],
) -> bool {
let mut sink = Sink::new(64);
sink.write_native_varuint(transfer.len() as u64);

for state in transfer.iter() {
sink.write_native_address(&state.from);
sink.write_native_address(&state.to);
sink.write(u128_to_neo_bytes(state.amount));
}
let mut sink_param = Sink::new(64);
sink_param.write(VERSION);
sink_param.write("transfer");
sink_param.write(sink.bytes());
let res = runtime::call_contract(contract_address, sink_param.bytes());
if let Some(data) = res {
if !data.is_empty() {
return true;
}
}
false
}

由之前的教程可知,在合约中进行参数序列化的工具是Sink实例,因此需要先构造一个Sink实例。由于要序列化的参数是个数组,所以要先序列化该数组的长度。在序列化数组长度时,要将数组长度转换成u64的数据类型,然后调用sink.write_native_varuint方法进行序列化。数组长度序列化完成后,开始序列化数组中的每个元素。对于Address类型的数据,需要调用sink.write_native_address方法进行序列化;对于U128类型的数据,需要先将其转换成 bytearray 类型,然后进行序列化;而对于U128转换成 bytearray 类型的数据,需要调用u128_to_neo_bytes方法。至此,方法参数序列化完成。

下面我们要开始序列化方法名,此时需要重新构造一个序列化实例,用这个新的实例序列化方法名。在序列化方法名之前,先序列化Version。该字段默认是0,然后序列化方法名,最后再序列化刚才已完成序列化的方法参数。至此,调用 Native 合约方法的参数构造过程已完成,可以调用runtime接口中的方法进行调用了。

Wasm 合约调用 NeoVM 合约

Wasm 合约调用 NeoVM 合约时,要求传递的参数类型实现VmValueEncoder和VmValueDecoder接口,ontology-wasm-cdt-rust 库已经为常用的数据类型实现了该接口,例如:&str、&[u8]、bool、H256、U128和 Address。contract模块中封装好了neo模块,开发者可以直接使用neo模块来调用 NeoVM 合约,使用示例如下:

use ostd::contract::neo;
...
let res = neo::call_contract(&NEO_CONTRACT_ADDR, ("init", ()));
match res {
Some(res2) => {
let mut parser = VmValueParser::new(res2.as_slice());
let r = parser.bool();
sink.write(r.unwrap_or(false));
}
_ => sink.write(false),
}

neo::call_contract需要两个参数,第一个是调用的目标合约地址,第二个是调用的合约方法需要的方法名和方法参数。在上面的例子中,方法名是init,方法参数是一个空的 Tuple 类型的数据。Wasm 合约中调用 NeoVM 合约时,得到的返回值需要使用VmValueParser进行反序列化,拿到合约返回结果。下面我们看一下neo::call_contract的实现源码,示例如下:

pub fn call_contract(
contract_address: &Address, param: T,
) -> Option> {
let mut builder = crate::abi::VmValueBuilder::new();
param.serialize(&mut builder);
crate::runtime::call_contract(contract_address, &builder.bytes())
}

从上面的源码可以看到neo::call_contract方法需要两个参数,第一个参数是目标合约地址,第二个就是方法名和方法参数,并且要求方法名和方法参数必须实现VmValueEncoder接口。此外,在对方法名和方法参数序列化时用的是VmValueBuilder而不是Sink,这一点值得开发者注意。得益于 Rust 强大的宏功能,我们使用宏为 Tuple 类型的数据实现VmValueEncoder和VmValueDecoder接口,所以在调用的时候,我们传进来的是 Tuple 类型的数据 ("init",())。

结语

本文主要讲了 Ontology Wasm 合约如何调用 Native 合约和 NeoVM 合约。其中以ont::transfer为例讲解了 Wasm 调用 Native合约的实现源码;以neo::call_contract为例讲解了 Wasm 合约调用 NeoVM 合约的实现源码。在跨合约调用的过程中,参数的序列化是本文的重点。尤其在调用不同合约时,采用的序列化方法不一样,这一点需要特别注意。

重点划完啦~你是否有所收获呢?如果你有任何疑问,欢迎扫描文末二维码,加入本体技术社区,与技术爱好者共同学习探讨哦!

分享到:

相关文章