理解以太坊合约数据读取过程 | 函数式与区块链(二)

基于函数式语言的 Ethereum 智能合约数据读取全过程

**关于函数式编程:** > 函数式编程是有别于传统的面对对象范式的编程范式,函数式方向是目前编程语言发展的大方向,所有新设计的编程语言都或多或少的引入了函数式编程功能。 > > 笔者认为,「下一代计算机学科体系」将基于函数式编程语言。因此,打好函数式编程基础对于具备「长期主义」思维的程序员是必要的。 **关于本专栏:** > 本专栏将通过实战代码分析与经典著作解读,分享作者关于函数式编程与区块链的思考与实践。就目前而言,本专栏将基于两种函数式语言:**Rust 和 Elixir**,有时候会提及其它语言,作为辅助性参考。 **关于太上:** > 太上是笔者团队近期实践的一个函数式+区块链的项目。 > > 太上炼金炉在不改变原有 NFT 合约的基础上,通过附加「存证合约」,赋予 NFT 组合、拆解、生命周期、权益绑定等能力,锻造 NFT +,创造无限创新玩法与想象空间。 > > **项目地址:** https://github.com/WeLightProject/Tai-Shang > > **愿景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](https://solidity.readthedocs.io/en/develop/abi-spec.html) (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中挂载: ```elixir # 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: ```elixir # config.exs config :ethereumex, url: "http://localhost:8545" ``` # 交易结构 在 Elixir 中,我们可以通过代码简单清晰地理解结构体(Struct)。 我们可以把 Ethereum 中的交易用如下的 Elixir Struct 表示: ```elixir %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](https://solidity.readthedocs.io/en/latest/abi-spec.html) > > 1. `QUANTITY|TAG` - integer block number, or the string `"latest"`, `"earliest"` or `"pending"`, see the [default block parameter](https://eth.wiki/json-rpc/API#the-default-block-parameter) > > ##### Returns > > `DATA` - the return value of executed contract. > > ##### Example > > ```javascript > // Request > curl -X POST --data '{"jsonrpc":"2.0","method":"eth_call","params":[{see above}],"id":1}' > > // Result > { > "id":1, > "jsonrpc": "2.0", > "result": "0x" > } > ``` > > ——https://eth.wiki/json-rpc/API 在 Rust 中我们也有相似的结构: ```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 limit gas_price: None, value: Some(U256::from(10000)), data: None, nonce: None, condition: None }; ``` 我们现在只是要把流程跑通,所以可以先不用去管 gas_price 和 gas_limit,写死即可: ```elixir @gas %{price: 0, limit: 300_000} ``` 那么,现在只要搞定 2 个参数即可:to 和 data。 # 地址的 Binary 转换 在上一讲中,笔者讨论了 Binary 的基本知识。同样的,我们日常所使用的区块链中的地址,如`0x769699506f972A992fc8950C766F0C7256Df601f`,可以转换为 Binary: ```elixir @spec addr_to_bin(String.t()) :: Binary.t() def addr_to_bin(addr_str) do addr_str |> String.replace("0x", "") |> Base.decode16!(case: :mixed) end ``` # 从智能合约函数到 Data 通过「函数字符串标识」与参数列表(params list)生成 data: ```elixir @spec get_data(String.t(), List.t()) :: String.t() def get_data(func_str, params) do payload = func_str |> ABI.encode(params) |> Base.encode16(case: :lower) "0x" <> payload end ``` 函数字符串标识的例子: ```elixir @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` 函数的实现: ```elixir def encode(function_signature, data, data_type \\ :input) # 在这一步会把 string 格式的 function 解析为 function_selector # 然后再次调用 encode 方法,传入 function_selector def encode(function_signature, data, data_type) when is_binary(function_signature) do function_signature |> Parser.parse!() |> encode(data, data_type) end def encode(%FunctionSelector{} = function_selector, data, data_type) do TypeEncoder.encode(data, function_selector, data_type) end ``` FunctionSelector 结构体: ```elixir 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: ```elixir defmodule Utils.TypeTranslator do …… def data_to_int(raw) do raw |> hex_to_bin() |> ABI.TypeDecoder.decode_raw([{:uint, 256}]) |> List.first() end def data_to_str(raw) do raw |> hex_to_bin() |> ABI.TypeDecoder.decode_raw([:string]) |> List.first() end def data_to_addr(raw) do addr_bin = raw |> hex_to_bin() |> ABI.TypeDecoder.decode_raw([:address]) |> List.first() "0x" <> Base.encode16(addr_bin, case: :lower) end …… end ``` 具体采用哪种方式视返回值的类型而定,我们可以通过 ABI 判定返回值: ```json { "constant": true, "inputs": [], "name": "get", "outputs": [{ "name": "", "type": "string" # 返回值是string } ], "payable": false, "stateMutability": "view", "type": "function" } ``` # 合约调用函数—Elixir 现在只差最后一步了!我们只要将如上几个函数放在一个调用函数中,区块链数据读取就大功告成。 以`get_balance`函数为例: ```elixir @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`去调用合约的例子: ```rust 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

关于函数式编程:

函数式编程是有别于传统的面对对象范式的编程范式,函数式方向是目前编程语言发展的大方向,所有新设计的编程语言都或多或少的引入了函数式编程功能。

笔者认为,「下一代计算机学科体系」将基于函数式编程语言。因此,打好函数式编程基础对于具备「长期主义」思维的程序员是必要的。

关于本专栏:

本专栏将通过实战代码分析与经典著作解读,分享作者关于函数式编程与区块链的思考与实践。就目前而言,本专栏将基于两种函数式语言:Rust 和 Elixir,有时候会提及其它语言,作为辅助性参考。

关于太上:

太上是笔者团队近期实践的一个函数式+区块链的项目。

太上炼金炉在不改变原有 NFT 合约的基础上,通过附加「存证合约」,赋予 NFT 组合、拆解、生命周期、权益绑定等能力,锻造 NFT +,创造无限创新玩法与想象空间。

项目地址: https://github.com/WeLightProject/Tai-Shang

愿景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 (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.exsdeps中以外,还需要在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.exs
config :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: &lt;&lt;>>, # 机器码
  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
  1. QUANTITY|TAG - integer block number, or the string "latest", "earliest" or "pending", see the default block parameter
Returns

DATA - the return value of executed contract.

Example
// Request
curl -X POST --data '{"jsonrpc":"2.0","method":"eth_call","params":[{see above}],"id":1}'

// Result
{
  "id":1,
  "jsonrpc": "2.0",
  "result": "0x"
}

——https://eth.wiki/json-rpc/API

在 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 limit
        gas_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 转换

在上一讲中,笔者讨论了 Binary 的基本知识。同样的,我们日常所使用的区块链中的地址,如0x769699506f972A992fc8950C766F0C7256Df601f,可以转换为 Binary:

@spec addr_to_bin(String.t()) :: Binary.t()
def addr_to_bin(addr_str) do
  addr_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) do
  payload =
  func_str
  |> ABI.encode(params)
  |> Base.encode16(case: :lower)

  "0x" &lt;> payload
end

函数字符串标识的例子:

@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_selector
def encode(function_signature, data, data_type) when is_binary(function_signature) do
  function_signature
  |> Parser.parse!()
  |> encode(data, data_type)
end

def encode(%FunctionSelector{} = function_selector, data, data_type) do
  TypeEncoder.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) do
    raw
    |> hex_to_bin()
    |> ABI.TypeDecoder.decode_raw([{:uint, 256}])
    |> List.first()
  end

  def data_to_str(raw) do
    raw
    |> hex_to_bin()
    |> ABI.TypeDecoder.decode_raw([:string])
    |> List.first()
  end

  def data_to_addr(raw) do
    addr_bin =
      raw
      |> hex_to_bin()
      |> ABI.TypeDecoder.decode_raw([:address])
      |> List.first()

    "0x" &lt;> 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&lt;()> {
    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::&lt;String, _, _,_>("get", (), None, Options::default(), None).await?;
    println!("{}", result);

    Ok(())
}

这个例子的完整项目见:

https://github.com/leeduckgo/eth-interactor-rs

本文参与登链社区写作激励计划 ,好文好收益,欢迎正在阅读的你也加入。

  • 发表于 2021-08-26 09:38
  • 阅读 ( 476 )
  • 学分 ( 0 )
  • 分类:Solidity
  • 专栏:狗哥区块链精品内容集

评论