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<Vec<u8>>
該方法需要兩個參數,第一個是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<T: crate::abi::VmValueEncoder>(
contract_address: &Address, param: T,
) -> Option<Vec<u8>> {
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 合約的實現源碼。在跨合約調用的過程中,參數的序列化是本文的重點。尤其在調用不同合約時,採用的序列化方法不一樣,這一點需要特別注意。
重點劃完啦~你是否有所收穫呢?如果你有任何疑問,歡迎掃描文末二維碼,加入本體技術社區,與技術愛好者共同學習探討哦!