本體技術視點 | 聊聊Ontology上三種合約“交相輝映”的故事

10月29日,Ontology v1.8.0重磅發佈!經過數月在測試網上的穩定運行,Wasm 功能也正式登陸了主網。目前爲止,Ontology v1.8.0已支持 NativeNeoVM 和 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 庫中封裝了ontong合約調用接口,只需要通過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 合約時,要求傳遞的參數類型實現VmValueEncoderVmValueDecoder接口,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 類型的數據實現VmValueEncoderVmValueDecoder接口,所以在調用的時候,我們傳進來的是 Tuple 類型的數據 ("init",())。

 

結語

本文主要主要講了 Ontology Wasm 合約如何調用 Native 合約和 NeoVM 合約。其中以ont::transfer爲例講解了 Wasm 調用 Native合約的實現源碼;以neo::call_contract爲例講解了 Wasm 合約調用 NeoVM 合約的實現源碼。在跨合約調用的過程中,參數的序列化是本文的重點。尤其在調用不同合約時,採用的序列化方法不一樣,這一點需要特別注意。

重點劃完啦~你是否有所收穫呢?如果你有任何疑問,歡迎掃描文末二維碼,加入本體技術社區,與技術愛好者共同學習探討哦!


發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章