理解以太坊合约数据读取过程 | 函数式与区块链编程(二)
关于函数式编程:
函数式编程是有别于传统的面对对象范式的编程范式,函数式方向是目前编程语言发展的大方向,所有新设计的编程语言都或多或少的引入了函数式编程功能。
笔者认为,「下一代计算机学科体系」将基于函数式编程语言。因此,打好函数式编程基础对于具备「长期主义」思维的程序员是必要的。
关于本专栏:
本专栏将通过实战代码分析与经典著作解读,分享作者关于函数式编程与区块链的思考与实践。就目前而言,本专栏将基于两种函数式语言:Rust 和 Elixir,有时候会提及其它语言,作为辅助性参考。
关于太上:
太上是笔者团队近期实践的一个函数式+区块链的项目。
太上炼金炉在不改变原有 NFT 合约的基础上,通过附加「存证合约」,赋予 NFT 组合、拆解、生命周期、权益绑定等能力,锻造 NFT +,创造无限创新玩法与想象空间。
愿景0x01:助力所有 NFT 及其相关项目,让其具备无限商业想象空间与无限玩法。
愿景0x02:成为下一代区块链基础设施
太上是本系列用以探讨函数式编程的第一个项目。
本篇是系列的第二篇,通过描述基于 Elixir 与 Ethereum 的交互方法,描述以太坊数据读取过程。需要注意的是,虽然名字是以太坊,但是这个过程对任何支持 EVM 的区块链都是适用的,例如:FISCO BCOS、Moobeam。
Ethereumex 与 ExABI
在 Elixir 中,使用到的两个 Repo,一个是 Ethereumex:
https://github.com/mana-ethereum/ethereumex
Elixir JSON-RPC client for the Ethereum blockchain.
另一个是 ExABI:
https://github.com/poanetwork/ex_abi
The Application Binary Interface[1] (ABI) of Solidity describes how to transform binary data to types which the Solidity programming language understands.
小 Tips:
ABI 是与 EVM 上的合约进行交互的标准方法,
.abi文件中包含了函数接口描述与事件描述,呈现方式为json。Hello World 合约的 ABI 如下:
[{"constant": true,"inputs": [],"name": "get","outputs": [{"name": "","type": "string"}],"payable": false,"stateMutability": "view","type": "function"}]
需要注意的是,我们在引入 Ethereumex 的时候,除了要常规的写入mix.exs的deps中以外,还需要在applications中挂载:
# mix.exs:def application do[mod: {TaiShang.Application, []},extra_applications: [:logger, :runtime_tools, :ethereumex]]end……defp deps do[{:ethereumex, "~> 0.7.0"}]end
还需要在config.exs中设置访问的区块链节点url:
# config.exsconfig :ethereumex,url: "http://localhost:8545"
交易结构
在 Elixir 中,我们可以通过代码简单清晰地理解结构体(Struct)。
我们可以把 Ethereum 中的交易用如下的 Elixir Struct 表示:
%Transaction{nonce: nonce, # 确保交易顺序的累加器gas_price: @gas.price, # gas 费用gas_limit: @gas.limit, # gas 上限to: bin_to, # Binary 形式的地址value: 0, # 要发送的以太币init: <<, # 机器码data: data # 要发送给to地址的数据}
需要注意的是,我们现在只做数据的读取,因此 nonce 这个参数是不需要的,nonce 参数只有在写操作时才会需要,也才会发生改变。
eth_call
Executes a new message call immediately without creating a transaction on the block chain.
Parameters
1.
Object- The transaction call object•
from:DATA, 20 Bytes - (optional) The address the transaction is sent from.•to:DATA, 20 Bytes - The address the transaction is directed to.•gas:QUANTITY- (optional) Integer of the gas provided for the transaction execution. eth_call consumes zero gas, but this parameter may be needed by some executions.•gasPrice:QUANTITY- (optional) Integer of the gasPrice used for each paid gas•value:QUANTITY- (optional) Integer of the value sent with this transaction•data:DATA- (optional) Hash of the method signature and encoded parameters. For details see Ethereum Contract ABI in the Solidity documentation[2]1.
QUANTITY|TAG- integer block number, or the string"latest","earliest"or"pending", see the default block parameter[3]Returns
DATA- the return value of executed contract.Example
——https://eth.wiki/json-rpc/API
// Requestcurl -X POST --data '{"jsonrpc":"2.0","method":"eth_call","params":[{see above}],"id":1}'// Result{"id":1,"jsonrpc": "2.0","result": "0x"}
在 Rust 中我们也有相似的结构:
from: https://kauri.io/#collections/A%20Hackathon%20Survival%20Guide/sending-ethereum-transactions-with-rust/let tx = TransactionRequest {from: accounts[0],to: Some(accounts[1]),gas: None, // 即 gas limitgas_price: None,value: Some(U256::from(10000)),data: None,nonce: None,condition: None};
我们现在只是要把流程跑通,所以可以先不用去管 gas_price 和 gas_limit,写死即可:
@gas %{price: 0, limit: 300_000}
那么,现在只要搞定 2 个参数即可:to 和 data。
地址的 Binary 转换
@spec addr_to_bin(String.t()) :: Binary.t()def addr_to_bin(addr_str) doaddr_str|> String.replace("0x", "")|> Base.decode16!(case: :mixed)end
从智能合约函数到 Data
通过「函数字符串标识」与参数列表(params list)生成 data:
@spec get_data(String.t(), List.t()) :: String.t()def get_data(func_str, params) dopayload =func_str|> ABI.encode(params)|> Base.encode16(case: :lower)"0x" <> payloadend
函数字符串标识的例子:
@func %{balance_of: "balanceOf(address)",token_of_owner_by_index: "tokenOfOwnerByIndex(address, uint256)",token_uri: "tokenURI(uint256)",get_evidence_by_key: "getEvidenceByKey(string)",new_evidence_by_key: "newEvidenceByKey(string, string)",mint_nft: "mintNft(address, string)",owner_of: "ownerOf(uint256)"}
简单来说就是「函数名(参数1类型, 参数2类型, …)」。
我们可以跳转过去,查看 encode 函数的实现:
def encode(function_signature, data, data_type \\ :input)# 在这一步会把 string 格式的 function 解析为 function_selector# 然后再次调用 encode 方法,传入 function_selectordef encode(function_signature, data, data_type) when is_binary(function_signature) dofunction_signature|> Parser.parse!()|> encode(data, data_type)enddef encode(%FunctionSelector{} = function_selector, data, data_type) doTypeEncoder.encode(data, function_selector, data_type)end
FunctionSelector 结构体:
iex(5)> ABI.Parser.parse!("baz(uint8)")%ABI.FunctionSelector{function: "baz",input_names: [],inputs_indexed: nil,method_id: nil,returns: [],type: nil,types: [uint: 8]}
TypeEncoder.encode 最终负责把 data, function_selector 和 data_type 编译为 data_type,详见:
https://github.com/poanetwork/ex_abi/blob/57ba7eb1703d8b0cd0353a0a588feef139b7edf3/lib/abi/type_encoder.ex
返回数据的转换
调用合约时返回的数据需要从hex形态的data转换为对应的格式,所以我们要写个 TypeTransalator:
defmodule Utils.TypeTranslator do……def data_to_int(raw) doraw|> hex_to_bin()|> ABI.TypeDecoder.decode_raw([{:uint, 256}])|> List.first()enddef data_to_str(raw) doraw|> hex_to_bin()|> ABI.TypeDecoder.decode_raw([:string])|> List.first()enddef data_to_addr(raw) doaddr_bin =raw|> hex_to_bin()|> ABI.TypeDecoder.decode_raw([:address])|> List.first()"0x" <> Base.encode16(addr_bin, case: :lower)end……end
具体采用哪种方式视返回值的类型而定,我们可以通过 ABI 判定返回值:
{"constant": true,"inputs": [],"name": "get","outputs": [{"name": "","type": "string" # 返回值是string}],"payable": false,"stateMutability": "view","type": "function"}
合约调用函数—Elixir
现在只差最后一步了!我们只要将如上几个函数放在一个调用函数中,区块链数据读取就大功告成。
以get_balance函数为例:
@spec balance_of(String.t(), String.t()) :: Integer.t()def balance_of(contract_addr, addr_str) do{:ok, addr_bytes} = TypeTranslator.hex_to_bytes(addr_str)data = get_data("balanceOf(address)", [addr_bytes]){:ok, balance_hex} =Ethereumex.HttpClient.eth_call(%{ # 交易结构被Ethereumex 封装过了!data: data,to: contract_addr})TypeTranslator.data_to_int(balance_hex)end
合约调用函数—Rust
最后是一个用rust-web3去调用合约的例子:
extern crate hex;use hex_literal::hex;use web3::{contract::{Contract, Options},types::{U256, H160, Bytes},};#[tokio::main]async fn main() -> web3::contract::Result<()> {let _ = env_logger::try_init();let http = web3::transports::Http::new("https://ropsten.infura.io/v3/9aa3d95b3bc440fa88ea12eaa4456161")?;let web3 = web3::Web3::new(http);let addr_u8 = hex::decode("7Ad11de6d4C3DA366BC929377EE2CaFEcC412A10").expect("Decoding failed");let addr_h160 = H160::from_slice(&addr_u8);let contra = Contract::from_json(web3.eth(),addr_h160,include_bytes!("../contracts/hello_world.json"),)?;// let acct:[u8; 20] = hex!("f24ff3a9cf04c71dbc94d0b566f7a27b94566cac").into();let result = contra.query::<String, _, _,_>("get", (), None, Options::default(), None).await?;println!("{}", result);Ok(())}
这个例子的完整项目见:
https://github.com/leeduckgo/eth-interactor-rs
References
[1] Application Binary Interface: https://solidity.readthedocs.io/en/develop/abi-spec.html[2] Ethereum Contract ABI in the Solidity documentation: https://solidity.readthedocs.io/en/latest/abi-spec.html[3] default block parameter: https://eth.wiki/json-rpc/API#the-default-block-parameter
