Hi 游客

更多精彩,请登录!

比特池塘 区块链技术 正文

Solidity应用二进制接口(ABI)说明

一辛爱柏轿
103 0 0
基本设计% ]& ~% j# p1 i( C
在 |ethereum| 生态系统中, |ABI| 是从区块链外部与合约进行交互以及合约与合约间进行交互的一种标准方式。
+ j2 c) I4 O6 D0 I) ?7 E数据会根据其类型按照这份手册中说明的方法进行编码。这种编码并不是可以自描述的,而是需要一种特定的概要(schema)来进行解码。7 \& Q, b3 m3 j" ?- V' Y$ r
我们假定合约函数的接口都是强类型的,且在编译时是可知的和静态的;不提供自我检查机制。我们假定在编译时,所有合约要调用的其他合约接口定义都是可用的。
% F  ]1 ]5 K5 c3 d; u5 |这份手册并不针对那些动态合约接口或者仅在运行时才可获知的合约接口。如果这种场景变得很重要,你可以使用 |ethereum| 生态系统中其他更合适的基础设施来处理它们。
9 |( Y* w1 |" E$ _1 U; d… _abi_function_selector:2 x& ^# c" z+ x& P/ ~
|function_selector|+ Z& d: H& @1 q) F" Y8 ?
一个函数调用数据的前 4 字节,指定了要调用的函数。这就是某个函数签名的 Keccak(SHA-3)哈希的前 4 字节(高位在左的大端序)(译注:这里的“高位在左的大端序“,指最高位字节存储在最低位地址上的一种串行化编码方式,即高位字节在左)。+ e( C5 x8 ^2 @# v* }
这种签名被定义为基础原型的规范表达,基础原型即是函数名称加上由括号括起来的参数类型列表,参数类型间由一个逗号分隔开,且没有空格。3 P$ R# D, Y7 E$ R) H$ H  {2 O
… note::( E0 X, ~+ T9 o
函数的返回类型并不是这个签名的一部分。在 :ref:Solidity 的函数重载  中,返回值并没有被考虑。这是为了使对函数调用的解析保持上下文无关。( {% r6 W0 h. m" }- r7 L
然而 |ABI| 的 JSON 描述中包含了即包含了输入也包含了输出。(参考 :ref:JSON ABI )。: ^# x% ~, P+ o
参数编码6 j& G5 `- T% ~
从第5字节开始是被编码的参数。这种编码也被用在其他地方,比如,返回值和事件的参数也会被用同样的方式进行编码,而用来指定函数的4个字节则不需要再进行编码。2 @0 f( U& S) C$ U5 n( `- U
类型2 h3 ~5 |% O" o3 X- H5 q$ h+ R
以下是基础类型:8 B; B5 ~* q2 ~/ q8 R
  • $ m$ P$ s: g; h/ l, K% n
    uint:M 位的无符号整数,0 、M % 8 == 0。例如:uint32,uint8,uint256。8 ]* S, I$ p$ H( E

  •   _' W2 D5 D3 c/ y& ~int:以 2 的补码作为符号的 M 位整数,0 、M % 8 == 0。
    $ `) q+ }2 G9 i- @& B1 {; }
  • , U. Y' f+ y' ]; @- K0 x/ t. Y
    address:除了字面上的意思和语言类型的区别以外,等价于 uint160。在计算和 |function_selector| 中,通常使用 address。
    ) k% }: \1 f! w% f0 G  X
  • 4 a* J2 r6 F+ ?
    uint、int:uint256、int256 各自的同义词。在计算和 |function_selector| 中,通常使用 uint256 和 int256。4 v, v' N4 E, l8 ~: T, X
  • ; g8 @% M1 o! g( E
    bool:等价于 uint8,取值限定为 0 或 1 。在计算和 |function_selector| 中,通常使用 bool。0 M  X! R0 i' l, Y

  • 6 f$ j) \' D" W' R' W; D+ jfixedx:M 位的有符号的固定小数位的十进制数字 8 、M % 8 ==0、且 0 。其值 v 即是 v / (10 ** N)。(也就是说,这种类型是由 M 位的二进制数据所保存的,有 N 位小数的十进制数值。译者注。)
    + F% z0 X8 k: z
  • $ J+ x3 {' I7 A& W& T% q6 X. e
    ufixedx:无符号的 fixedx。
    7 x: G1 O0 w5 y6 u) O0 [+ |3 m( C
  • * r# s/ L7 C+ V3 i: d' q1 H
    fixed、ufixed:fixed128x18、ufixed128x18 各自的同义词。在计算和 |function_selector| 中,通常使用 fixed128x18 和 ufixed128x18。. j# O9 y6 w; A$ r* J

  • 0 a* A* F. c" w2 S0 R3 tbytes:M 字节的二进制类型,0 。
    % a. s4 C" }6 W8 J* s

  • " }" O% t# [# ^$ e" r6 N' {; Kfunction:一个地址(20 字节)之后紧跟一个 |function_selector| (4 字节)。编码之后等价于 bytes24。
    # G1 P" r1 D$ H+ `
    . b6 X) Q3 `6 q; ]1 ?
    " R9 g% C9 }4 h: u; |, p4 Y
    以下是定长数组类型:" i9 z& S+ {' \
  • [M]:有 M 个元素的定长数组,M >= 0,数组元素为给定类型。
    1 n9 N( b# D, @
    ' X# ?" F$ Q9 p, K+ a4 e( ~9 K8 }1 _
    以下是非定长类型:
    / j/ y4 n! ^. @

  • . e( H0 H. C: \. D' ~bytes:动态大小的字节序列。
    , D1 p: u. a( Z$ w' ?1 ~
  • 7 Y; g& l* P9 }4 i5 j
    string:动态大小的 unicode 字符串,通常呈现为 UTF-8 编码。. h; J6 u2 g' U( }; n

  • * \+ [7 b. O: w% o0 @, _[]:元素为给定类型的变长数组。
    * e& o# H) c# g' q1 @$ }
    4 g# j' A1 D& m1 _* O' r: c
    2 [6 d& f- f& _, P
    可以将若干类型放到一对括号中,用逗号分隔开,以此来构成一个 |tuple|:/ m& m) \* _1 M, Q5 y
  • (T1,T2,...,Tn):由 T1,…,Tn,n >= 0 构成的 |tuple|。# K- O; _8 _" o+ J) c* I
    . U: @: q6 x& R- i5 `4 s, ~% l
    用 |tuple| 构成 |tuple|、用 |tuple| 构成数组等等也是可能的。另外也可以构成“零元组(zero-tuples)”,就是 n = 0 的情况。8 r, T5 m9 d# q- ?! m" y; k# B
    … note::
    6 b1 o7 d. {' J) O- K0 D; `' g除了 |tuple| 以外,Solidity 支持以上所有类型的名称。ABI |tuple| 是利用 Solidity 的 structs 编码得到的。+ f5 B6 E1 L' p' Y; j0 ^
    编码的形式化说明  s" j- ?( e' {) R  X
    我们现在来正式讲述编码,它具有如下属性,如果参数是嵌套的数组,这些属性非常有用:
    0 z9 ]1 a9 ^0 [! X0 K1 ]9 f属性:
    $ }: m& L: N" o2 K3 l1、读取的次数取决于参数数组结构中的最大深度;也就是说,要取得 a_i[k][l][r] 需要读取 4 次。在先前的ABI版本中,在最糟的情况下,读取的次数会随着动态参数的总数而线性地增长。
    " @4 ?! H2 K4 w$ w3 ?2、一个变量或数组元素的数据,不会被插入其他的数据,并且是可以再定位的;也就是说,它们只会使用相对的“地址”。: E7 |+ f3 y; N+ G! C* f
    我们需要区分静态和动态类型。静态类型会被直接编码,动态类型则会在当前数据块之后单独分配的位置被编码。
    # d) W; K$ d0 F/ r- y* w" L定义: 以下类型被称为“动态”:
    3 S) C2 K, S$ {+ d8 G2 {3 A$ ~* u* H
  • bytes
  • string
  • 任意类型 T 的变长数组 T[]
  • 任意动态类型 T 的定长数组 T[k] (k >= 0)
  • 由动态的 Ti (1 )构成的 |tuple| (T1,...,Tk)
    - \+ M$ u* b3 O

    9 D" o( z3 k$ r& B所有其他类型都被称为“静态”。1 P. T( l9 o! v( g! `0 T6 }6 a# l
    定义: len(a) 是一个二进制字符串 a 的字节长度。len(a) 的类型被呈现为 uint256。3 D* \0 l8 P8 i3 ~- C
    我们把实际的编码 enc 定义为一个由ABI类型到二进制字符串的值的映射;因而,当且仅当 X 的类型是动态的,len(enc(X)) (即 X 经编码后的实际长度,译者注)才会依赖于 X 的值。
    7 Z" s0 h7 |4 ]9 N* ^定义: 对任意ABI值 X,我们根据 X 的实际类型递归地定义 enc(X)。. L# z1 g. o1 g8 q2 ~

  • 4 [0 @2 f5 p, e(T1,...,Tk) 对于 k >= 0 且任意类型 T1 ,…, Tk
    + p0 Q5 D/ g" ]enc(X) = head(X(1)) ... head(X(k)) tail(X(1)) ... tail(X(k))8 H: @8 k/ Z2 i8 ]
    这里,X = (X(1), ..., X(k)),并且( A/ v6 S, A: q. x1 p
    当 Ti 为静态类型时,head 和 tail 被定义为% ~7 Z# Z& ]7 ^. Y* W7 X' v) O
    head(X(i)) = enc(X(i)) and tail(X(i)) = "" (空字符串)" v8 j9 \$ P5 j, U3 ~
    否则,比如 Ti 是动态类型时,它们被定义为
    : t8 n2 v8 {  |head(X(i)) = enc(len(head(X(1)) ... head(X(k-1)) tail(X(1)) ... tail(X(i-1))))
    $ e. _3 D0 Q! \  c3 }tail(X(i)) = enc(X(i))2 r7 u8 \/ G% _- N& {/ E9 |+ n
    注意,在动态类型的情况下,由于 head 部分的长度仅取决于类型而非值,所以 head(X(i)) 是定义明确的。它的值是从 enc(X) 的开头算起的,tail(X(i)) 的起始位在 enc(X) 中的偏移量。3 `' x! V3 c: ?* C" h. d+ v

  • " e' y  X. T9 u* s: G( IT[k] 对于任意 T 和 k:3 a$ l6 `) Q& \+ m
    enc(X) = enc((X[0], ..., X[k-1]))- l0 N! [8 M4 C9 d
    即是说,它就像是个由相同类型的 k 个元素组成的 |tuple| 那样被编码的。
    + u2 a5 U( ?# w$ P- x
  • # D0 W, x. a: Z) I
    T[] 当 X 有 k 个元素(k 被呈现为类型 uint256):; H  u2 \3 t8 H9 O# P
    enc(X) = enc(k) enc([X[1], ..., X[k]])
    ' _8 b4 p* q3 m+ P1 P即是说,它就像是个由静态大小 k 的数组那样被编码的,且由元素的个数作为前缀。/ c6 P1 o( D( u5 J
  • & E$ L9 p. J$ p, |& o, r, C  l# _
    具有 k (呈现为类型 uint256)长度的 bytes:
    ! F. r4 m8 ^: c$ G% v( v% Cenc(X) = enc(k) pad_right(X),即是说,字节数被编码为 uint256,紧跟着实际的 X 的字节码序列,再在前边(左边)补上可以使 len(enc(X)) 成为 32 的倍数的最少数量的 0 值字节数据。; C; a  |# M( x2 o6 `
  • 2 [9 }/ H1 x8 P4 |" H
    string:7 E* ^, e" _; Z
    enc(X) = enc(enc_utf8(X)),即是说,X 被 utf-8 编码,且在后续编码中将这个值解释为 bytes 类型。注意,在随后的编码中使用的长度是其 utf-8 编码的字符串的字节数,而不是其字符数。7 m) K& k- {7 j9 e7 j% B8 a8 @
  • 6 X: Z3 Q& y5 p" H/ l$ O
    uint:enc(X) 是在 X 的大端序编码的前边(左边)补充若干 0 值字节以使其长度成为 32 字节。
    5 V8 J$ u! R" E

  • ) b$ d5 J* J( I; U. naddress:与 uint160 的情况相同。
    8 z4 L" P2 X( d& l6 y* X

  • # G. y8 T% M" I' x) `int:enc(X) 是在 X 的大端序的 2 的补码编码的高位(左侧)添加若干字节数据以使其长度成为 32 字节;对于负数,添加值为 0xff (即 8 位全为 1,译者注)的字节数据,对于正数,添加 0 值(即 8 位全为 0,译者注)字节数据。1 d4 m0 w" D, g9 `; W& f) ^
  •   I* d" G$ A5 G" h
    bool:与 uint8 的情况相同,1 用来表示 true,0 表示 false。, ?+ R( F8 t# b0 |0 ?3 ]
  • * Z$ v6 x' i. H& n9 ]( n
    fixedx:enc(X) 就是 enc(X * 10**N),其中 X * 10**N 可以理解为 int256。
    * c; g" {& T. r7 Y

  • 6 q7 C5 M' J5 x/ J( ]. zfixed:与 fixed128x18 的情况相同。: h9 o5 s% V2 c- \, I3 {& j

  • % [5 `- U' d: R* O* U3 Eufixedx:enc(X) 就是 enc(X * 10**N),其中 X * 10**N 可以理解为 uint256。' E  ?7 F. |  q

  • / R& N' g; c6 D' ^$ f- Fufixed:与 ufixed128x18 的情况相同。
    6 s0 Y' i# e4 }! s# I% I) Y2 d

  • 6 {" X, S: T( ?/ o: F( n# T3 Z! Lbytes:enc(X) 就是 X 的字节序列加上为使长度成为 32 字节而添加的若干 0 值字节。
    : k$ \! r9 R$ Z
    ( b$ @7 J) |6 u4 s" h- I
    ' `& g( g& M" r  d  [
    注意,对于任意的 X,len(enc(X)) 都是 32 的倍数。
    ) ~- p. G* G: v$ {+ n3 q|function_selector| 和参数编码
    9 D! I3 U8 R% y8 A% P+ {6 `* b大体而言,一个以 a_1, ..., a_n 为参数的对 f 函数的调用,会被编码为
    ; p9 `, B  }/ y+ xfunction_selector(f) enc((a_1, ..., a_n))* G% n0 r% G# }
    f 的返回值 v_1, ..., v_k 会被编码为
    4 U! G+ N7 H" E/ z. m; Venc((v_1, ..., v_k))
      i% C( R) Z$ X6 d( P也就是说,返回值会被组合为一个 |tuple| 进行编码。
    5 E3 d- w8 |/ Y& i; |6 `例子2 L# s6 T; ~, T& A
    给定一个合约:
    # X* v  L: M- [% m::1 B5 g! n2 r5 Q
    pragma solidity ^0.4.16;( b( O, _6 C; G
    contract Foo {5 D2 t, f5 ^$ v% e) c: A. u( ?
      function bar(bytes3[2]) public pure {}, \# s. u) m* x9 h: u2 e2 U
      function baz(uint32 x, bool y) public pure returns (bool r) { r = x > 32 || y; }+ Q: _* B, D" `$ v5 t
      function sam(bytes, bool, uint[]) public pure {}
    2 c" c# n) p  P; X0 D: j; t}
    # S0 q+ `, t% s/ Q" x这样,对于我们的例子 Foo,如果我们想用 69 和 true 做参数调用 baz,我们总共需要传送 68 字节,可以分解为:
    9 A7 F' ]. O- `3 o9 q4 f1 l% x
  • 0xcdcd77c0:方法ID。这源自ASCII格式的 baz(uint32,bool) 签名的 Keccak 哈希的前 4 字节。
  • 0x0000000000000000000000000000000000000000000000000000000000000045:第一个参数,一个被用 0 值字节补充到 32 字节的 uint32 值 69。
  • 0x0000000000000000000000000000000000000000000000000000000000000001:第二个参数,一个被用 0 值字节补充到 32 字节的 boolean 值 true。
    ( ^% s6 _; E' ^0 ?  A' M

    ( Q( D- f& g4 n/ `+ t! W合起来就是::* B" h, h) r0 L6 M$ X+ {  C
    0xcdcd77c000000000000000000000000000000000000000000000000000000000000000450000000000000000000000000000000000000000000000000000000000000001
    0 Y3 ]8 \! `. y) ^' Y/ f- i5 T它返回一个 bool。比如它返回 false,那么它的输出将是一个字节数组 0x0000000000000000000000000000000000000000000000000000000000000000,一个bool值。
    / h0 `/ C) {9 B% n2 \* \+ Q  W如果我们想用 ["abc", "def"] 做参数调用 bar,我们总共需要传送68字节,可以分解为:3 V" J: J' {9 k( r
  • 0xfce353f6:方法ID。源自 bar(bytes3[2]) 的签名。
  • 0x6162630000000000000000000000000000000000000000000000000000000000:第一个参数的第一部分,一个 bytes3 值 "abc" (左对齐)。
  • 0x6465660000000000000000000000000000000000000000000000000000000000:第一个参数的第二部分,一个 bytes3 值 "def" (左对齐)。* k: m5 L$ ~9 i9 L, V% d9 N
    ) w% L; |6 u8 l9 y: j  t3 p
    合起来就是::, Y0 ]' K1 S! X, }# Y* c
    0xfce353f661626300000000000000000000000000000000000000000000000000000000006465660000000000000000000000000000000000000000000000000000000000
    - {% i  \; V% O0 w  D如果我们想用 "dave"、true 和 [1,2,3] 作为参数调用 sam,我们总共需要传送 292 字节,可以分解为:& I+ a9 l6 }2 ?1 a  y1 t
  • 0xa5643bf2:方法ID。源自 sam(bytes,bool,uint256[]) 的签名。注意,uint 被替换为了它的权威代表 uint256。
  • 0x0000000000000000000000000000000000000000000000000000000000000060:第一个参数(动态类型)的数据部分的位置,即从参数编码块开始位置算起的字节数。在这里,是 0x60 。
  • 0x0000000000000000000000000000000000000000000000000000000000000001:第二个参数:boolean 的 true。
  • 0x00000000000000000000000000000000000000000000000000000000000000a0:第三个参数(动态类型)的数据部分的位置,由字节数计量。在这里,是 0xa0。
  • 0x0000000000000000000000000000000000000000000000000000000000000004:第一个参数的数据部分,以字节数组的元素个数作为开始,在这里,是 4。
  • 0x6461766500000000000000000000000000000000000000000000000000000000:第一个参数的内容:"dave" 的 UTF-8 编码(在这里等同于 ASCII 编码),并在右侧(低位)用 0 值字节补充到 32 字节。
  • 0x0000000000000000000000000000000000000000000000000000000000000003:第三个参数的数据部分,以数组的元素个数作为开始,在这里,是 3。
  • 0x0000000000000000000000000000000000000000000000000000000000000001:第三个参数的第一个数组元素。
  • 0x0000000000000000000000000000000000000000000000000000000000000002:第三个参数的第二个数组元素。
  • 0x0000000000000000000000000000000000000000000000000000000000000003:第三个参数的第三个数组元素。
    * P6 y  P- S8 N: D/ C, C

    ( f3 o$ ]; Y8 M# N+ Z合起来就是::
    7 v  [) U% s% m. H* C6 n2 T7 B' r0xa5643bf20000000000000000000000000000000000000000000000000000000000000060000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000a00000000000000000000000000000000000000000000000000000000000000004646176650000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000030000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000036 _2 j* N1 i+ A: ?' C3 {9 r1 p8 I
    动态类型的使用
    : P3 h+ }$ V0 [+ p; t. y用参数 (0x123, [0x456, 0x789], "1234567890", "Hello, world!") 进行对函数 f(uint,uint32[],bytes10,bytes) 的调用会通过以下方式进行编码:
    8 q* z$ E3 s  J% a+ Q取得 sha3("f(uint256,uint32[],bytes10,bytes)") 的前 4 字节,也就是 0x8be65246。
    1 h1 y* T9 f! p* O$ v4 P! K4 k, {然后我们对所有 4 个参数的头部进行编码。对静态类型 uint256 和 bytes10 是可以直接传过去的值;对于动态类型 uint32[] 和 bytes,我们使用的字节数偏移量是它们的数据区域的起始位置,由需编码的值的开始位置算起(也就是说,不计算包含了函数签名的前 4 字节),这就是:* q' ?2 j3 B( y! y* O, W7 V
  • 0x0000000000000000000000000000000000000000000000000000000000000123 (0x123 补充到 32 字节)
  • 0x0000000000000000000000000000000000000000000000000000000000000080 (第二个参数的数据部分起始位置的偏移量,4*32 字节,正好是头部的大小)
  • 0x3132333435363738393000000000000000000000000000000000000000000000 ("1234567890" 从右边补充到 32 字节)
  • 0x00000000000000000000000000000000000000000000000000000000000000e0 (第四个参数的数据部分起始位置的偏移量 = 第一个动态参数的数据部分起始位置的偏移量 + 第一个动态参数的数据部分的长度 = 4*32 + 3*32,参考后文)
    : f- m! R& V7 O' P

    ' D& O8 E  ^0 {( R在此之后,跟着第一个动态参数的数据部分 [0x456, 0x789]:
    , X6 k# ?$ M. {* M) Z
  • 0x0000000000000000000000000000000000000000000000000000000000000002 (数组元素个数,2)
  • 0x0000000000000000000000000000000000000000000000000000000000000456 (第一个数组元素)
  • 0x0000000000000000000000000000000000000000000000000000000000000789 (第二个数组元素)5 y3 Q; @, B) T; ^; @4 m

    - I: L0 V6 U; i1 W8 K6 [/ A1 Q. p最后,我们将第二个动态参数的数据部分 "Hello, world!" 进行编码:& H! O: t+ w, J# O( o+ k$ a; o7 X
  • 0x000000000000000000000000000000000000000000000000000000000000000d (元素个数,在这里是字节数:13)
  • 0x48656c6c6f2c20776f726c642100000000000000000000000000000000000000 ("Hello, world!" 从右边补充到 32 字节)3 T7 n9 m2 Y; l4 r" B

    ) p; r  Y' l; G/ {' z最后,合并到一起的编码就是(为了清晰,在 |function_selector| 和每 32 字节之后加了换行):
    2 d' y- E1 W0 K: d/ C::
    , u, w) V4 {1 `3 v- w  C6 b0x8be65246* Z+ i" f3 m" C
      0000000000000000000000000000000000000000000000000000000000000123$ E6 T# G7 r  l1 I! J5 {
      00000000000000000000000000000000000000000000000000000000000000802 \& H' [- h! ]: _# d" R
      3132333435363738393000000000000000000000000000000000000000000000% H+ U# P1 ~. [5 A0 A
      00000000000000000000000000000000000000000000000000000000000000e06 m1 s# v5 t$ j. _5 o
      0000000000000000000000000000000000000000000000000000000000000002
    0 R1 x, o1 y. Q$ G  0000000000000000000000000000000000000000000000000000000000000456& N0 K! O; Q) ]) ]6 Z9 J! ^
      0000000000000000000000000000000000000000000000000000000000000789' `4 Y  }6 N! @" K$ S* |( S
      000000000000000000000000000000000000000000000000000000000000000d0 [- Y* z; e7 `: \6 R- u
      48656c6c6f2c20776f726c6421000000000000000000000000000000000000000 O, f9 h+ |; U' t7 H- S
    让我们使用相同的原理来对一个签名为 g(uint[][],string[]),参数值为 ([[1, 2], [3]], ["one", "two", "three"]) 的函数来进行编码;但从最原子的部分开始:
    / @9 R' R8 b4 [) {4 ]* ?, H6 |首先我们将第一个根数组 [[1, 2], [3]] 的第一个嵌入的动态数组 [1, 2] 的长度和数据进行编码:
    - H' Q( }; x& M9 D+ x3 t
  • 0x0000000000000000000000000000000000000000000000000000000000000002 (第一个数组中的元素数量 2;元素本身是 1 和 2)
  • 0x0000000000000000000000000000000000000000000000000000000000000001 (第一个元素)
  • 0x0000000000000000000000000000000000000000000000000000000000000002 (第二个元素)) s1 o- {, _  l" r" p! y+ O

    ' v6 m, k1 N2 q7 Y; R然后我们将第一个根数组 [[1, 2], [3]] 的第二个潜入的动态数组 [3] 的长度和数据进行编码:
    / P3 L, F6 J* j- m5 F
  • 0x0000000000000000000000000000000000000000000000000000000000000001 (第二个数组中的元素数量 1;元素数据是 3)
  • 0x0000000000000000000000000000000000000000000000000000000000000003 (第一个元素)0 i' b7 Y7 N' C& a7 X

    ( f6 p% n& A! b% O% @& T1 d然后我们需要找到动态数组 [1, 2] 和 [3] 的偏移量。要计算这个偏移量,我们可以来看一下第一个根数组 [[1, 2], [3]] 编码后的具体数据:
    . s: W- @% s: a" K; j: d::7 k! ]- o& X1 d9 n$ d: `7 e8 |
    0 - a                                                                - [1, 2] 的偏移量
    / b8 _6 k" _/ |" l' b3 A: }/ t1 - b                                                                - [3] 的偏移量
    5 u! C& r( z2 x# N2 - 0000000000000000000000000000000000000000000000000000000000000002 - [1, 2] 的计数
    5 k. P2 W# P3 R8 s3 - 0000000000000000000000000000000000000000000000000000000000000001 - 1 的编码9 z$ v* K, A1 I  b5 N
    4 - 0000000000000000000000000000000000000000000000000000000000000002 - 2 的编码
    0 q& i1 }5 c% t9 b/ Q5 }6 {: a5 - 0000000000000000000000000000000000000000000000000000000000000001 - [3] 的计数: X) [1 T; |, K9 N
    6 - 0000000000000000000000000000000000000000000000000000000000000003 - 3 的编码
    & r! v$ K! n) @9 D7 U6 T偏移量 a 指向数组 [1, 2] 内容的开始位置,即第 2 行的开始(64 字节);所以 a = 0x0000000000000000000000000000000000000000000000000000000000000040。  r3 S( A' [8 s' k
    偏移量 b 指向数组 [3] 内容的开始位置,即第 5 行的开始(160 字节);所以 b = 0x00000000000000000000000000000000000000000000000000000000000000a0。5 P* ]6 X5 h- _4 k6 l& x; ^% W* [
    然后我们对第二个根数组的嵌入字符串进行编码:
    6 B; S$ ~. V! a2 t( B- k$ O7 P+ a- C
  • 0x0000000000000000000000000000000000000000000000000000000000000003 (单词 "one" 中的字符个数)
  • 0x6f6e650000000000000000000000000000000000000000000000000000000000 (单词 "one" 的 utf8 编码)
  • 0x0000000000000000000000000000000000000000000000000000000000000003 (单词 "two" 中的字符个数)
  • 0x74776f0000000000000000000000000000000000000000000000000000000000 (单词 "two" 的 utf8 编码)
  • 0x0000000000000000000000000000000000000000000000000000000000000005 (单词 "three" 中的字符个数)
  • 0x7468726565000000000000000000000000000000000000000000000000000000 (单词 "three" 的 utf8 编码)
    " @% \# b" h: c: Q0 J8 t+ k1 N- @

    ; L& x$ q0 f9 w; X8 b, N% t作为与第一个根数组的并列,因为字符串也属于动态元素,我们也需要找到它们的偏移量 c, d 和 e:) f* \# @4 g  ~
    ::2 B: p$ X6 K$ Z8 |0 K2 ]- k8 L" d
    0 - c                                                                - “one” 的偏移量% K0 r5 l# W) F- J( }
    1 - d                                                                - “two” 的偏移量3 q8 ^8 `/ K' m
    2 - e                                                                - “three” 的偏移量
    ! Z& |# O% }1 p8 e- p3 - 0000000000000000000000000000000000000000000000000000000000000003 - “one” 的字符计数9 G. w6 I7 j  n
    4 - 6f6e650000000000000000000000000000000000000000000000000000000000 - “one” 的编码
    0 _" {; R$ x/ F$ ~; I5 - 0000000000000000000000000000000000000000000000000000000000000003 - “two” 的字符计数. I8 ?& ^6 L" c7 M+ J# y
    6 - 74776f0000000000000000000000000000000000000000000000000000000000 - “two” 的编码
    & S1 i, q5 H  |& ~" M7 - 0000000000000000000000000000000000000000000000000000000000000005 - “three” 的字符计数7 V  O. G; j  m. i9 ]+ j4 k
    8 - 7468726565000000000000000000000000000000000000000000000000000000 - “three” 的编码& P. P$ G' Y) d5 L0 K
    偏移量 c 指向字符串 "one" 内容的开始位置,即第 3 行的开始(96 字节);所以 c = 0x0000000000000000000000000000000000000000000000000000000000000060。+ V. ^! j9 I7 R+ Q. @
    偏移量 d 指向字符串 "two" 内容的开始位置,即第 5 行的开始(160 字节);所以 d = 0x00000000000000000000000000000000000000000000000000000000000000a0。# j% T8 }7 L1 ?6 x8 l6 a
    偏移量 e 指向字符串 "three" 内容的开始位置,即第 7 行的开始(224 字节);所以 e = 0x00000000000000000000000000000000000000000000000000000000000000e0。
    6 N* |8 Z2 Y- Z) Z0 E* E注意,根数组的嵌入元素的编码并不互相依赖,且具有对于函数签名 g(string[],uint[][]) 所相同的编码。
    " \' ]! F4 P; S8 u" _6 B% l$ C然后我们对第一个根数组的长度进行编码:
    , d8 {' H2 Z% y5 O  g
  • 0x0000000000000000000000000000000000000000000000000000000000000002 (第一个根数组的元素数量 2;这些元素本身是 [1, 2] 和 [3]); q, t* U5 c) I+ ~# ^8 T

    " L5 P7 n+ R3 ^3 N/ e而后我们对第二个根数组的长度进行编码:: c# J5 n( V( g
  • 0x0000000000000000000000000000000000000000000000000000000000000003 (第二个根数组的元素数量 3;这些字符串本身是 "one"、"two" 和 "three")
    * x% ]6 Y8 o( W$ g4 J
      {  y- |/ ~3 f
    最后,我们找到根动态数组元素 [[1, 2], [3]] 和 ["one", "two", "three"] 的偏移量 f 和 g。汇编数据的正确顺序如下:
    # l& h" P  z% T  Y5 i1 n$ B! O- M::
      w# V8 {  A0 i  w, Y3 `0x2289b18c                                                            - 函数签名& O) @3 S# s$ E5 t& {
    0 - f                                                                - [[1, 2], [3]] 的偏移量
    , R" f$ Y8 k% U$ z/ Q. @% k& X1 - g                                                                - [“one”, “two”, “three”] 的偏移量
    2 ~7 v' D' g0 W7 I# p2 ?, T2 - 0000000000000000000000000000000000000000000000000000000000000002 - [[1, 2], [3]] 的元素计数: l8 p0 i) w0 B9 }% s& |
    3 - 0000000000000000000000000000000000000000000000000000000000000040 - [1, 2] 的偏移量
    % s. M6 d( L2 g4 - 00000000000000000000000000000000000000000000000000000000000000a0 - [3] 的偏移量
    " {& P8 n5 @3 }; h2 R6 |9 [5 - 0000000000000000000000000000000000000000000000000000000000000002 - [1, 2] 的元素计数
    7 I2 z" Z3 D  a0 `6 - 0000000000000000000000000000000000000000000000000000000000000001 - 1 的编码
    - o; r4 H3 k6 n8 ~- z+ X7 - 0000000000000000000000000000000000000000000000000000000000000002 - 2 的编码
    2 |/ V- c' t9 Y! [2 H0 i6 P  K8 - 0000000000000000000000000000000000000000000000000000000000000001 - [3] 的元素计数5 O/ O6 w! T& }+ Z2 A1 n
    9 - 0000000000000000000000000000000000000000000000000000000000000003 - 3 的编码% S9 r  h+ q' M" s5 L
    10 - 0000000000000000000000000000000000000000000000000000000000000003 - [“one”, “two”, “three”] 的元素计数' X  }$ c- p2 V
    11 - 0000000000000000000000000000000000000000000000000000000000000060 - “one” 的偏移量
    1 G% h2 C4 y. k12 - 00000000000000000000000000000000000000000000000000000000000000a0 - “two” 的偏移量
    / J! S& i. h$ R2 r13 - 00000000000000000000000000000000000000000000000000000000000000e0 - “three” 的偏移量
    + `9 \4 D& k9 g3 L  Y4 J14 - 0000000000000000000000000000000000000000000000000000000000000003 - “one” 的字符计数
    & Q% C2 |5 I: J3 v5 d! g15 - 6f6e650000000000000000000000000000000000000000000000000000000000 - “one” 的编码6 G- [. H& [& N0 l% T; I3 a
    16 - 0000000000000000000000000000000000000000000000000000000000000003 - “two” 的字符计数6 r# ]0 l, S6 R0 [
    17 - 74776f0000000000000000000000000000000000000000000000000000000000 - “two” 的编码) |8 R/ j+ [, A' A, h9 S4 {0 S
    18 - 0000000000000000000000000000000000000000000000000000000000000005 - “three” 的字符计数
    9 S+ U9 t# q# m19 - 7468726565000000000000000000000000000000000000000000000000000000 - “three” 的编码, `" Z) b0 @7 ?5 h$ Z+ k" k
    偏移量 f 指向数组 [[1, 2], [3]] 内容的开始位置,即第 2 行的开始(64 字节);所以 f = 0x0000000000000000000000000000000000000000000000000000000000000040。
    / A6 R3 M- V; `* ]! X偏移量 g 指向数组 ["one", "two", "three"] 内容的开始位置,即第 10 行的开始(320 字节);所以 g = 0x0000000000000000000000000000000000000000000000000000000000000140。$ x2 z* g$ C  y5 _
    事件
    ( j# Q+ ^  x& Q( Y" s+ Z事件,是 |ethereum| 的日志/事件监视协议的一个抽象。日志项提供了合约的地址、一系列的主题(最高 4 项)和一些任意长度的二进制数据。为了使用合适的类型数据结构来演绎这些功能(与接口定义一起),事件沿用了既存的 ABI 函数。
    8 W7 E+ q2 K) h1 ]; {  U" t给定了事件名称和事件参数之后,我们将其分解为两个子集:已索引的和未索引的。已索引的部分,最多有 3 个,被用来与事件签名的 Keccak 哈希一起组成日志项的主题。未索引的部分就组成了事件的字节数组。9 Q; l, n" _. R. f
    这样,一个使用 ABI 的日志项就可以描述为:5 \; h" A1 q8 W. X$ o5 j# V
  • address:合约地址(由 |ethereum| 真正提供);
  • topics[0]:keccak(EVENT_NAME+"("+EVENT_ARGS.map(canonical_type_of).join(",")+")") (canonical_type_of 是一个可以返回给定参数的权威类型的函数,例如,对 uint indexed foo 它会返回 uint256)。如果事件被声明为 anonymous,那么 topics[0] 不会被生成;
  • topics[n]:EVENT_INDEXED_ARGS[n - 1] (EVENT_INDEXED_ARGS 是已索引的 EVENT_ARGS);
  • data:abi_serialise(EVENT_NON_INDEXED_ARGS) (EVENT_NON_INDEXED_ARGS 是未索引的 EVENT_ARGS,abi_serialise 是一个用来从某个函数返回一系列类型值的ABI序列化函数,就像上文所讲的那样)。
    : U- ^& T8 e0 ?' j8 r$ F
    + M" p7 r. [$ M1 |3 j, M
    对于所有定长的Solidity类型,EVENT_INDEXED_ARGS 数组会直接包含32字节的编码值。然而,对于 动态长度的类型 ,包含 string、bytes 和数组,# U5 D" r1 r! A( H, g# c3 Y* x
    EVENT_INDEXED_ARGS 会包含编码值的 Keccak 哈希 而不是直接包含编码值。这样就允许应用程序更有效地查询动态长度类型的值(通过把编码值的哈希设定为主题),8 Z/ v# L  F0 h% z; C9 e
    但也使应用程序不能对它们还没查询过的已索引的值进行解码。对于动态长度的类型,应用程序开发者面临在对预先设定的值(如果参数已被索引)的快速检索和对任意数据的清晰处理(需要参数不被索引)之间的权衡。5 y+ n7 ^+ d, `- A9 q: s, O
    开发者们可以通过定义两个参数(一个已索引、一个未索引)保存同一个值的方式来解决这种权衡,从而既获得高效的检索又能清晰地处理任意数据。4 W2 Z6 _. C  m" ?4 V5 T: ?# j, Z
    … _abi_json:. l1 ~* j8 ^: u$ w5 D4 c- N
    JSON( d8 J+ u: _8 A2 o$ |/ }4 H* ^$ Y
    合约接口的JSON格式是由一个函数和/或事件描述的数组所给定的。一个函数的描述是一个有如下字段的JSON对象:
    3 ^# x( n- n% c' U* l1 ~( k

  • # _% f& X8 M: U0 P* xtype:"function"、"constructor" 或 "fallback" (:ref:未命名的 "缺省" 函数 )
    5 U: M$ D7 S/ D* i# D
  • - ~( d9 q" @0 B3 Z6 A& \
    name:函数名称;
    3 H' [  C7 p4 i7 J: g

  • : H( Z& B  V: Uinputs:对象数组,每个数组对象会包含:4 k4 l, l6 k8 _+ K
  • name:参数名称;
  • type:参数的权威类型(详见下文)
  • components:供 |tuple| 类型使用(详见下文); _" g' X" V  S- T: U5 x3 i, E
  • 7 B! X8 d8 o8 [
    outputs:一个类似于 inputs 的对象数组,如果函数无返回值时可以被省略;
    * y  y. |/ Y, Y3 @! \

  • & l7 H4 Z( Y4 L  X' |payable:如果函数接受 |ether| ,为 true;缺省为 false;/ c* I5 p4 b. U  m/ [
  • $ J- \* b# L4 j4 G7 r" B' @  v/ L
    stateMutability:为下列值之一:pure (:ref:指定为不读取区块链状态 ),view (:ref:指定为不修改区块链状态 ),nonpayable 和 payable (与上文 payable 一样)。# d. v" }0 t. V! i2 ]% R; c
  • ; ]8 e$ h4 A$ H8 R$ h0 n3 |
    constant:如果函数被指定为 pure 或 view 则为 true。
    6 W: V+ }# ~! F* G2 T3 B+ x; ~! A# k
    * I$ a6 `! j9 t+ s! e

    * u: l8 K* V6 B0 O2 ytype 可以被省略,缺省为 "function"。: H& ^  ?! G9 |! Y, y
    Constructor 和 fallback 函数没有 name 或 outputs。Fallback 函数也没有 inputs。
    " ?$ }' V8 x$ z" E% W向 non-payable(即不接受 |ether| )的函数发送非零值的 |ether| 会导致其丢失。不要这么做。! U+ q; @. @. {5 R' V6 b) [( G
    一个事件描述是一个有极其相似字段的 JSON 对象:
    3 C, \0 E8 R- o& [. e0 j9 r. i
  • 1 X; q( u' n7 D& w* i3 C- H  q
    type:总是 "event";
    9 ^" \# n, ^% G# Y3 e7 p2 s5 J& b

  • / k% N2 a6 P) Lname:事件名称;8 h" l) \* {1 ^: h$ K# D; f1 q- N6 m
  • 5 P# r; K9 ~$ E8 E: Q" M" j) d5 |7 Z+ r
    inputs:对象数组,每个数组对象会包含:( _9 I0 W: ?0 l) x% p" w* i+ H
  • name:参数名称;
  • type:参数的权威类型(相见下文);
  • components:供 |tuple| 类型使用(详见下文);
  • indexed:如果此字段是日志的一个主题,则为 true;否则为 false。+ d# @, a1 h: V

  • ) e* D$ z7 ~) A7 v* E2 }anonymous:如果事件被声明为 anonymous,则为 true。
    ( @4 y+ \0 ]3 Q! ]0 i
    ; M" Y- Z+ Z- C

    ! u) M- Y' R/ A$ Q例如,$ J' Y, c" v4 @5 a
    ::! Y- G. W1 @- I. P& W. F& f0 T
    pragma solidity ^0.4.0;' N$ C( {" U) M$ m
    contract Test {
    0 J& Q% I7 ^+ E" T  function Test() public { b = 0x12345678901234567890123456789012; }
    ; z* i. H' ^' q: G  ~  P2 F  event Event(uint indexed a, bytes32 b);
    1 |  U8 }9 L. M% i% \  event Event2(uint indexed a, bytes32 b);
    % z) `0 T/ E' @. H  function foo(uint a) public { Event(a, b); }0 F9 S; t. F+ Z; ~  J& Z
      bytes32 b;* E/ K  \. e7 b$ j3 U
    }
    1 W; e/ ?! M+ H# j可由如下 JSON 来表示:! H9 X- e6 e& Y& {; o
    … code:: json
    + _: s* B  K1 |[{* I. U( k0 a' T- B
    “type”:“event”,
    ; \3 I! |' k# ^* Z! ^“inputs”: [{“name”:“a”,“type”:“uint256”,“indexed”:true},{“name”:“b”,“type”:“bytes32”,“indexed”:false}],* U2 D: R- H* V# U% |, m, a1 ?0 M( a
    “name”:“Event”1 r- h7 o- q! M3 L! h$ {- D
    }, {
    1 T3 a. c" j) c. x5 \+ U+ Q8 N“type”:“event”,
    3 a; e, V5 J9 R9 m2 h  u“inputs”: [{“name”:“a”,“type”:“uint256”,“indexed”:true},{“name”:“b”,“type”:“bytes32”,“indexed”:false}],9 P$ Z& }6 K# g! o
    “name”:“Event2”% a7 Y, C) Y+ w/ s
    }, {
    , a; Z1 Z* p5 A# B4 o“type”:“function”,
    1 T5 Y5 M& ]; e* B1 C; k“inputs”: [{“name”:“a”,“type”:“uint256”}],. C! M( J. B! F# J
    “name”:“foo”,+ O7 ]2 u9 r% O
    “outputs”: [], f& q# x% {/ n- ^( d& A5 u
    }]
    , V- }0 i* s$ @1 p+ ]处理 |tuple| 类型- M+ x& t- x8 I8 P) _) S3 C& M
    尽管名称被有意地不作为 ABI 编码的一部分,但将它们包含进JSON来显示给最终用户是非常合理的。其结构会按下列方式进行嵌套:
    3 g1 o: z5 b$ {3 k1 b一个拥有 name、 type 和潜在的 components 成员的对象描述了某种类型的变量。
    & S/ Q# D" ?( N% `直至到达一个 |tuple| 类型且到那点的存储在 type 属性中的字符串以 tuple 为前缀,也就是说,在 tuple 之后紧跟一个 [] 或有整数 k 的 [k],才能确定一个 |tuple|。" A9 T/ B: L7 b* g4 I
    |tuple| 的组件元素会被存储在成员 components 中,它是一个数组类型,且与顶级对象具有同样的结构,只是在这里不允许已索引的(indexed)数组元素。
    & P. u1 U- }' J1 r/ a6 f, T作为例子,代码' }8 q7 ?% _1 d( ]
    ::0 O3 G. _: a6 t" m$ n
    pragma solidity ^0.4.19;
    $ v( }- ]# z' Z0 W2 h6 |  Cpragma experimental ABIEncoderV2;
    3 a& b# O; K( J% f" Z: ycontract Test {: K, \$ u- l: H( f* N6 t
      struct S { uint a; uint[] b; T[] c; }
      a2 W% U3 q' v9 e  struct T { uint x; uint y; }
    - h$ C: Q/ |6 P# w  function f(S s, T t, uint a) public { }3 ~/ D1 d2 h3 _& R* ^
      function g() public returns (S s, T t, uint a) {}' K4 q1 F. j( D! r, c- s* v3 f% i
    }
    8 f" }1 g; s; W- B可由如下 JSON 来表示:
    1 C6 }/ |& @7 M… code:: json. A# B. H; Q) C8 T) ^, Y
    [' b3 A8 h. c$ S
    {
    & J2 H. t' V6 t, w! c“name”: “f”,
    , C# o7 J( A- [7 H) ^3 m, v5 k2 E“type”: “function”,
    : r/ ]& P4 W  [8 Q2 ^“inputs”: [$ Y7 F; T6 Y0 n9 L0 U2 @
    {( H% v! C( F  F! s
    “name”: “s”,
    6 h  J& v5 }( x) u% l“type”: “tuple”,
    6 l. E: r6 n# j; F4 p“components”: [2 @2 O4 U$ I* L, e6 |! Q
    {
    2 p- |. o. q4 ~* _“name”: “a”,) Y1 p7 g5 ^+ f# j# x  L. z
    “type”: “uint256”
    4 E0 ?) L8 a) i% w) ]- U3 y, m0 i},
    ( k) U( ^) c2 V0 u$ X{  r' m# J% G: e  t. _/ o7 a" T
    “name”: “b”,
    * r: q# o, T3 F3 u8 u5 t  H“type”: “uint256[]”; z9 q" V8 U: p& |8 |
    },5 w  \* F3 \) Y& P' z% n0 r5 _
    {3 |. U* Z! G% \8 b7 v9 G
    “name”: “c”,. H# L. d/ g& O. e+ V9 B
    “type”: “tuple[]”,
    $ o" U) \5 w% O“components”: [9 ~& f3 y& B' J' l" w+ ]6 E
    {
    * y; T, C0 s2 t* w) Z# c2 {# a“name”: “x”,% y1 u& ?: P( t
    “type”: “uint256”
    ( C, U6 h$ U" X+ D, Q4 P6 M},
    * t6 A- p& m1 g7 z{/ `* J, \& l* u% m; C. Z; H/ L
    “name”: “y”,
    , j+ t% S# G  K7 X5 y“type”: “uint256”$ R! V+ Q/ P  g) q" d' m5 U
    }
    $ f! ^* W% b( ~+ [) W" |3 L' V) O]
    $ @1 k. v7 }( ?}# F! z$ I8 B7 _+ \3 p$ l
    ]
    # N  g5 a9 u( j2 q3 R/ L; C* Z},
    ; y1 }8 ^0 N+ a  c{1 ]2 W7 y. M, ^, A2 @
    “name”: “t”,+ A6 q8 @' k% T! `
    “type”: “tuple”,
    7 {! H. f8 ]. v' [8 K! w. X, [8 C“components”: [7 ?8 p/ M( ^5 e/ h1 B% }0 ]
    {
    4 k1 j1 C$ D/ ?( v“name”: “x”,
      D' [: }( w, B, {$ |# `“type”: “uint256”4 H5 A1 j, M" q6 |" T- z& W" }
    },
    % d& L5 C7 a) `, o! h1 E{$ p4 |- o" N6 D8 _$ V3 G& q( {. |
    “name”: “y”,
      ?- ^4 M% d2 e% g& ?' e“type”: “uint256”
    7 K) [# `9 `3 _& {6 A6 p2 M4 l}
    & h$ l/ I7 U& F  I7 M" r]0 G" |, A7 e9 g4 C& k5 {+ o' K* ~
    },
    " v+ j- M2 v& {$ a{4 @6 T4 d6 E0 i, r
    “name”: “a”,1 Z  C# ?' X4 D& w
    “type”: “uint256”2 U" d: `' y' a& {! i
    }: a* d' j5 D/ b
    ],2 _; w8 O! ?: U5 f
    “outputs”: []
    ) e- g7 f' R5 h( ]) E+ y- {; v}
    " h5 R5 Z4 S4 Y. T4 y  E]
    $ S3 E1 d& e% D- I… _abi_packed_mode:, ?+ X* s3 ~. J
    非标准打包模式
    0 F9 h+ g$ r% _& ~* qSolidity 支持一种非标准打包模式:
    " C2 X9 c' m# I" A: u
  • :ref:函数选择器 不进行编码,
  • 长度低于 32 字节的类型,既不会进行补 0 操作,也不会进行符号扩展,以及
  • 动态类型会直接进行编码,并且不包含长度信息。
    1 o/ I& Q; z3 I0 x9 g

    . E2 H2 J' d+ a) Z* T例如,对 int1, bytes1, uint16, string 用数值 -1, 0x42, 0x2424, "Hello, world!" 进行编码将生成如下结果 ::5 P3 Q6 x  w$ D# {# ^9 e4 t4 O
    0xff42242448656c6c6f2c20776f726c6421- ^8 y7 x9 _9 N
      ^^                                 int1(-1)1 \: y. @: Z' F& s1 S) o
        ^^                               bytes1(0x42)
    - `& C( m& ]  B* W6 h0 t      ^^^^                           uint16(0x2424)
    0 Z  c7 U: h, p          ^^^^^^^^^^^^^^^^^^^^^^^^^^ string("Hello, world!") without a length field
    ; p9 _$ I& w7 P更具体地说,每个静态大小的类型都尽可能多地按它们的数值范围使用了字节数,而动态大小的类型,像 string、 bytes 或 uint[],在编码时没有包含其长度信息。: {7 F" y" R7 O! a  S# _5 Y5 B
    这意味着一旦有两个动态长度的元素,编码就会变得有歧义了。
  • BitMere.com 比特池塘系信息发布平台,比特池塘仅提供信息存储空间服务。
    声明:该文观点仅代表作者本人,本文不代表比特池塘立场,且不构成建议,请谨慎对待。
    您需要登录后才可以回帖 登录 | 立即注册

    本版积分规则

    成为第一个吐槽的人

    一辛爱柏轿 小学生
    • 粉丝

      0

    • 关注

      0

    • 主题

      2