Bytom交易说明(账户管理模式)
V刘晨曦
发表于 2022-11-13 23:52:49
163
0
0
Github地址:https://github.com/Bytom/bytom
Gitee地址:https://gitee.com/BytomBlockchain/bytom$ e' @6 O, \, X" [0 J
该部分主要针对用户使用bytom自带的账户模式发送交易
1、构建交易
API接口 build-transaction,代码api/transact.go#L120) Q, J6 j# Y ?6 p
以标准的非BTM资产转账交易为例,资产ID为全F表示BTM资产,在该示例中BTM资产仅作为手续费,该交易表示花费99个特定的资产到指定地址中。其中构建交易的输入请求json格式如下:
{9 b5 i) q! o5 p* @: Z6 Q* x
"base_transaction": null,
"actions": [
{# `- I6 Z8 V" }! i
"account_id": "0ER7MEFGG0A02",- n: X* X& ~ w) M: n) e; K
"amount": 20000000,
"asset_id": "ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff",, g2 Y! j5 q& y$ a. n
"type": "spend_account"( l% r& D6 ?6 W
},8 s9 D+ K+ c5 p2 h4 ?
{
"account_id": "0ER7MEFGG0A02",
"amount": 99, F8 }7 r* y( r9 R: h' {+ S$ R( I
"asset_id": "42275aacbeda1522cd41580f875c3c452daf5174b17ba062bf0ab71a568c123f",
"type": "spend_account"8 q! i, g" e. Y+ l! R' q
},4 ~0 q( `3 ^+ M
{1 a' _' Q. e% w# ?; ^+ @
"amount": 99,! `+ A: f# k+ y h/ H$ ^ T- G" A! Q
"asset_id": "42275aacbeda1522cd41580f875c3c452daf5174b17ba062bf0ab71a568c123f",
"address": "sm1qxe4jwhkekgnxkezu7xutu5gqnnpmyc8ppq98me",% p4 H( ?& v Y* A9 d3 A6 o, |" V
"type": "control_address"7 Y! K& s9 d7 s& M+ [
}
],7 w) T- [/ {: O3 }- E* I
"ttl": 0,
"time_range": 00 H5 J! b! W7 R
}8 j' ~* P7 A" i$ w2 c
对应源代码的请求对象如下:
// BuildRequest is main struct when building transactions# u# g2 ^- r7 e3 v
type BuildRequest struct {/ l- n# ] o& T" v0 x ^6 I' j
Tx *types.TxData `json:"base_transaction"`6 s0 L' R* w7 o9 M1 y: m
Actions []map[string]interface{} `json:"actions"`5 A9 s0 ]0 A5 s( p
TTL json.Duration `json:"ttl"`
TimeRange uint64 `json:"time_range"`
}; {: V: A) f0 |" o+ E- c/ W$ y
结构字段说明如下:4 K& j G( n$ Y1 n8 w2 K+ n
Tx 交易的TxData部分,该字段为预留字段,为空即可
TTL 构建交易的生存时间(单位为毫秒),意味着在该时间范围内,已经缓存的utxo不能用于再一次build交易,除非剩余的utxo足以构建一笔新的交易,否则会报错。当ttl为0时会被默认设置为600s,即5分钟% T) i- J* B- o3 a; g* B
TimeRange 时间戳,意味着该交易将在该时间戳(区块高度)之后不会被提交上链,为了防止交易在网络中传输延迟而等待太久时间,如果交易没有在特定的时间范围内被打包,该交易便会自动失效
Actions 交易的actions结构,所有的交易都是由action构成的,map类型的interface{}保证了action类型的可扩展性。其中action中必须包含type字段,用于区分不同的action类型,action主要包含input和output两种类型,其详细介绍如下:
input action 类型:# S; S+ \7 S+ |2 ]& \) G
issue 发行资产3 k1 [$ p; M. x& F: H# g7 O4 `
spend_account 以账户的模式花费utxo' @4 k4 K O' R# ~3 c
spend_account_unspent_output 直接花费指定的utxo
output action 类型:# b6 K7 b9 R4 ?# h' D
control_address 接收方式为地址模式
control_program 接收方式为(program)合约模式
retire 销毁资产8 r/ f+ H! ?' S! r9 y: q) _- C- \
5 C( K) ?) c- g' V0 W" r
' F+ X* @3 B4 M& Q
注意事项:, y+ Z( r. T9 M) u
一个交易必须至少包含一个input和output(coinbase交易除外,因为coinbase交易是由系统产生,故不在此加以描述),否则交易将会报错。
除了BTM资产(所有交易都是以BTM资产作为手续费)之外,其他资产在构建input和output时,所有输入和输出的资产总和必须相等,否则交易会报出输入输出不平衡的错误信息。" F) \( H- |) v9 R. \9 W
交易的手续费: 所有inputs的BTM资产数量 - 所有outputs的BTM资产数量
交易中的资产amount都是neu为单位的,BTM的单位换算如下:1 BTM = 1000 mBTM = 100000000 neu
$ q+ g7 k+ b. k9 `
action简介
下面对构建交易时用到的各种action类型进行详细说明:
issue) U2 `; r! j# Y- y
issueAction结构体源代码如下:
type issueAction struct {
assets *Registry
bc.AssetAmount2 ~# K) p7 z ^6 o( F# U+ |* C
}
type AssetAmount struct {8 o2 V7 X# M# l _
AssetId *AssetID `protobuf:"bytes,1,opt,name=asset_id,json=assetId" json:"asset_id,omitempty"`
Amount uint64 `protobuf:"varint,2,opt,name=amount" json:"amount,omitempty"`9 }2 P& y% R. u& O+ U* J- b
}- i# e4 g2 y* t* A2 Z' P- a
结构字段说明如下:
assets 主要用于资产的管理,无需用户设置参数
AssetAmount 表示用户需要发行的资产ID和对应的资产数目,这里的AssetID需要通过create-asset创建,并且这里不能使用BTM的资产ID8 T5 K# |: p6 k' S o: k) I" @$ P6 b
, `! K. v. e# ?. G
issueAction的json格式为:1 N' s, K9 Q! h; {/ X1 ] E
{
"amount": 100000000,5 R! e, j$ Z! D$ Q- T
"asset_id": "3152a15da72be51b330e1c0f8e1c0db669269809da4f16443ff266e07cc43680",
"type": "issue"
}
例如发行一笔资产的交易示例如下:
(该交易表示发行数量为900000000个assetID的42275aacbeda1522cd41580f875c3c452daf5174b17ba062bf0ab71a568c123f的资产到接收地址sm1qxe4jwhkekgnxkezu7xutu5gqnnpmyc8ppq98me中, 其中手续费为20000000neu的BTM资产) E0 g6 N- S+ V# h: d9 a
{
"base_transaction": null,. ^8 y& A# a+ u: i1 _
"actions": [, U, ?7 V4 D9 w9 Z% k- z7 ~
{3 d! Y4 K9 G+ K* x5 ^% _
"account_id": "0ER7MEFGG0A02",* t" e M, @. P( l; y
"amount": 20000000,1 d4 {* F7 F+ R" d5 W3 ~
"asset_id": "ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff",
"type": "spend_account"
},( I: |% m5 O' [" a! \
{
"amount": 900000000,( P( @) O4 Z% Q5 c5 t
"asset_id": "42275aacbeda1522cd41580f875c3c452daf5174b17ba062bf0ab71a568c123f",
"type": "issue"% @$ t4 ]) m# M: C. P L/ r" d
},
{& U! c% X3 }" i- B
"amount": 900000000,& ^" x* `# E6 H
"asset_id": "42275aacbeda1522cd41580f875c3c452daf5174b17ba062bf0ab71a568c123f",% ^" I; m/ M! H
"address": "sm1qxe4jwhkekgnxkezu7xutu5gqnnpmyc8ppq98me",: s) m/ p! S) }; `9 E
"type": "control_address"
}. C \& {- q3 G/ S: [5 ?6 ]3 w& F
],
"ttl": 0,
"time_range": 06 u, C& l. h5 @3 D& X, X
}
spend_account
spendAction结构体源代码如下:1 m4 q8 |( W: O
type spendAction struct {/ \" x( N8 ]! S9 f( y7 x
accounts *Manager' f6 F$ f% }: L: w7 r0 J
bc.AssetAmount
AccountID string `json:"account_id"`7 O/ i( [4 @( P" c
ClientToken *string `json:"client_token"`7 x" _) {. y9 J4 y* z' D' f% o! W
}
type AssetAmount struct {# E/ C6 ?3 W2 P7 @2 q4 e4 u
AssetId *AssetID `protobuf:"bytes,1,opt,name=asset_id,json=assetId" json:"asset_id,omitempty"`1 I) T0 I4 W3 G3 _+ `
Amount uint64 `protobuf:"varint,2,opt,name=amount" json:"amount,omitempty"`5 n a& x- X& F; y0 }3 Y0 T
}
结构字段说明如下:
accounts 主要用于账户的管理,无需用户设置参数) w0 f- E& B) r& G
AccountID 表示需要花费资产的账户ID/ {6 ~ a# P+ `* y7 F7 \5 Q) |0 V& z
AssetAmount 表示花费的资产ID和对应的资产数目
ClientToken 表示Reserve用户UTXO的限制条件,目前不填或为空即可 y1 i$ m( q8 u& R. D* X. x
& o& M8 {7 N: w& p
spendAction的json格式为:% ?2 ~" i9 }" g l- r; o4 ?
{
"account_id": "0BF63M2U00A04", n6 H+ _ _7 m3 [
"amount": 2000000000,
"asset_id": "ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff",
"type": "spend_account"' k4 V/ v1 [; o2 k Z0 G1 a- j$ ?- A+ ]
}
例如转账一笔资产的交易示例如下:5 T7 W. v7 h9 t* w% S9 Q9 G
(该交易表示通过账户的方式转账100000000neu的BTM资产到地址sm1qxe4jwhkekgnxkezu7xutu5gqnnpmyc8ppq98me中, 其中手续费20000000neu = 输入BTM资产数量 - 输出BTM资产数量)9 j/ U3 ` U" |3 ]
{
"base_transaction": null,+ m% }1 L; N8 o3 y
"actions": [
{
"account_id": "0ER7MEFGG0A02",
"amount": 120000000,
"asset_id": "ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff",
"type": "spend_account"2 j( Z0 a% n4 t* W5 W
},# [& F$ q4 I; J- X+ B% O
{
"amount": 100000000,
"asset_id": "ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff",% |/ L" n& x. M8 [) Z
"address": "sm1qxe4jwhkekgnxkezu7xutu5gqnnpmyc8ppq98me",9 p- `$ j# h, P/ B1 Y. @
"type": "control_address"# F& C3 u+ G& y( L2 T' g
}+ ^3 G! Y" H$ {" S3 j/ n2 f
],+ |7 f! `5 v/ C& D/ {" }- a
"ttl": 0,
"time_range": 0
}4 z) b: r; @. i8 v+ G5 O& g
* u8 y, I: I. y# L7 [( s
spend_account_unspent_output
spendUTXOAction结构体源代码如下:
type spendUTXOAction struct {
accounts *Manager
OutputID *bc.Hash `json:"output_id"`
ClientToken *string `json:"client_token"`" r4 i( J& j+ A& C' o& b) J# ?! Z
}
结构字段说明如下:
accounts 主要用于账户的管理,无需用户设置参数7 Z7 P2 W$ V/ S4 u
OutputID 表示需要花费的UTXO的ID,可以根据list-unspent-outputs查询可用的UTXO,其中OutputID对应该API返回结果的id字段& g1 Y2 o* g8 z- i
ClientToken 表示Reserve用户UTXO的限制条件,目前不填或为空即可
spendUTXOAction的json格式为:8 K& P ~& }/ P# B2 Y
{
"type": "spend_account_unspent_output",
"output_id": "58f29f0f85f7bd2a91088bcbe536dee41cd0642dfb1480d3a88589bdbfd642d9"
}4 {4 j! X: m9 q- i
例如通过花费UTXO的方式转账一笔资产的交易示例如下:' i& S" y( u* L2 V% R: C% a
(该交易表示通过直接花费UTXO的方式转账100000000neu的BTM资产到地址sm1qxe4jwhkekgnxkezu7xutu5gqnnpmyc8ppq98me中, 其中手续费 = 输入BTM资产的UTXO值 - 输出BTM资产数量)
{8 g8 v; O0 [! J2 X7 {8 y' u
"base_transaction": null,$ p5 w, f1 z( h
"actions": [
{, M. J5 M8 O% { h. C& Y0 p
"output_id": "58f29f0f85f7bd2a91088bcbe536dee41cd0642dfb1480d3a88589bdbfd642d9",
"type": "spend_account_unspent_output"
},
{5 j' k3 `2 w' Q& S2 \$ [; J8 f$ g
"amount": 100000000,
"asset_id": "ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff",+ {7 e g9 [ t" O$ N
"address": "sm1qxe4jwhkekgnxkezu7xutu5gqnnpmyc8ppq98me",
"type": "control_address") `0 ]' y& a# J6 t* z. a
}' P8 C: M# A/ N- P+ ^
],
"ttl": 0,
"time_range": 01 o* p' ^+ E z, e( T9 t' i
}
$ T. U& D- B; F e) r+ b
control_address' v1 {- K& k' M0 ^! t: I; P+ ]
controlAddressAction结构体源代码如下:, I8 L6 e a0 i' l" U6 ~4 a
type controlAddressAction struct {7 X$ r5 O F& n0 m# S6 m. E7 `, ~& G
bc.AssetAmount
Address string `json:"address"`
}
type AssetAmount struct {4 A# |0 C, R" F: u& O4 b4 Q
AssetId *AssetID `protobuf:"bytes,1,opt,name=asset_id,json=assetId" json:"asset_id,omitempty"`
Amount uint64 `protobuf:"varint,2,opt,name=amount" json:"amount,omitempty"`5 a+ D: i+ f+ i6 W
}8 ?) h- a1 g/ i7 c, p) j+ q% L
结构字段说明如下:4 Q+ @5 n. z' S, F' Z( B, I
Address 表示接收资产的地址,可以根据 create-account-receiver API接口创建地址- k2 S; ?9 F) Y- w
AssetAmount 表示接收的资产ID和对应的资产数目+ Y5 j% W( w+ T0 L* n9 h
controlAddressAction的json格式为:
{
"amount": 100000000,
"asset_id": "ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff",
"address": "bm1q50u3z8empm5ke0g3ngl2t3sqtr6sd7cepd3z68",4 T& c$ Z9 y' o+ d/ k! M
"type": "control_address"
}- z$ P7 X3 D1 z
例如转账一笔资产的交易示例如下:# ]6 J* G j) t3 Y& O$ m
(该交易表示通过账户的方式转账100000000neu的BTM资产到地址sm1qxe4jwhkekgnxkezu7xutu5gqnnpmyc8ppq98me中, 其中control_address类型表示以地址作为接收方式)' i2 i D% B; q: i" B. A, ?
{& a( ]: n7 g; X6 u2 L, w4 V
"base_transaction": null,% ^4 Z, |% H' s! e" l; [
"actions": [
{. G* ~3 S% r$ c
"account_id": "0ER7MEFGG0A02"," b. e& e6 Q, t& L+ w7 v$ e
"amount": 120000000,
"asset_id": "ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff",: X2 d' m E) G) |$ _: z
"type": "spend_account"1 s4 c U& ]8 m7 e s3 O, P# {/ P
},
{
"amount": 100000000,
"asset_id": "ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff",! @' ]3 W* ^. o9 l( H
"address": "sm1qxe4jwhkekgnxkezu7xutu5gqnnpmyc8ppq98me",* I; O$ E& @9 d+ m( Z
"type": "control_address"$ F }3 Y4 U. ^( ^' A8 p' h
}
],/ Z+ Z6 y9 k7 P4 p9 u' A8 J
"ttl": 0,% j% P! U' S4 D, @2 m9 c3 C
"time_range": 01 R0 J; e! ~2 y V6 a
}
control_program
controlProgramAction结构体源代码如下:3 @6 N: \; {' d/ a' s- H; x6 Z8 r0 q
type controlProgramAction struct {
bc.AssetAmount: a& Y& I3 ?- b# N1 O
Program json.HexBytes `json:"control_program"`
}& i2 \! g+ M: B4 [- G
type AssetAmount struct {
AssetId *AssetID `protobuf:"bytes,1,opt,name=asset_id,json=assetId" json:"asset_id,omitempty"`& u7 Y5 x l& z9 c- e7 u, e
Amount uint64 `protobuf:"varint,2,opt,name=amount" json:"amount,omitempty"`
}$ [4 r* _. |5 ?. e$ [2 T
结构字段说明如下:9 S( ~ Q! N' x/ A' M S/ r0 N
Program 表示接收资产的合约脚本,可以根据 create-account-receiver API接口创建接收program(返回结果的 program 和 address 是一一对应的)
AssetAmount 表示接收的资产ID和对应的资产数目
( E3 j: g1 P+ {! J. q
controlProgramAction的json格式为:6 m( t, |3 Q1 P2 H8 u' M& A4 P7 {/ v
{
"amount": 100000000,
"asset_id": "ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff",4 g4 Y' r+ l/ q+ Y. I2 f9 [8 n
"control_program":"0014a3f9111f3b0ee96cbd119a3ea5c60058f506fb19",9 |1 ~( `. y g' t t
"type": "control_program"/ m& ]1 P! r1 ?2 r
}
例如转账一笔资产的交易示例如下:
(该交易表示通过账户的方式转账100000000neu的BTM资产到接收program(跟address是一一对应的)0014a3f9111f3b0ee96cbd119a3ea5c60058f506fb19中, 其中control_program类型表示以program作为接收方式)
{" Y e9 s6 g- q' R [
"base_transaction": null,
"actions": [
{
"account_id": "0ER7MEFGG0A02",
"amount": 120000000,9 M2 o8 G, H2 ?( Q9 W% j
"asset_id": "ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff",
"type": "spend_account"
},
{$ f! l( M0 f) V. y+ c
"amount": 100000000,
"asset_id": "ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff",
"control_program": "0014a3f9111f3b0ee96cbd119a3ea5c60058f506fb19",: V1 M5 P) Y0 L
"type": "control_program"
}
],
"ttl": 0,0 a3 e0 T" I, Q; D. V& Z
"time_range": 0
}7 g, o( }, `4 N& [; o" o' l
: G+ I3 i6 a* b/ p& }
retire
retireAction结构体源代码如下:2 P# V, [$ a3 i7 h) `* M
type retireAction struct { W" z1 k0 h9 b! l/ `+ s4 {
bc.AssetAmount
}
type AssetAmount struct {# M d. u5 I: b4 U& D& t# q) N4 e: A/ w
AssetId *AssetID `protobuf:"bytes,1,opt,name=asset_id,json=assetId" json:"asset_id,omitempty"`
Amount uint64 `protobuf:"varint,2,opt,name=amount" json:"amount,omitempty"`* u6 ~' D8 ~" D+ o1 P, l
}) g5 U y! N& {0 H
结构字段说明如下:9 L& w* n2 }8 Q
AssetAmount 表示销毁的资产ID和对应的资产数目
; R# g! s: J, `
retireAction的json格式为:; s+ [3 S' g6 Q, b
{& W- L+ [; e$ k2 B K& d
"amount": 900000000,4 z0 v" I. H q2 {- T
"asset_id": "3152a15da72be51b330e1c0f8e1c0db669269809da4f16443ff266e07cc43680",
"type": "retire"
}
例如销毁一笔资产的交易示例如下:
(该交易表示通过账户的方式将100000000neu的BTM资产销毁, retire表示销毁指定数量的资产)
{
"base_transaction": null,
"actions": [. B; Z \( l1 Q$ V8 _6 E
{* x! }) C$ x* F. l9 _' B
"account_id": "0ER7MEFGG0A02",6 M- d2 [5 e3 X! m
"amount": 120000000,# ~# `& l0 F2 K9 r l2 W/ ]2 y0 l; P
"asset_id": "ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff",/ p) f: G8 t+ i) g `8 @( U
"type": "spend_account"1 w. i ]; U$ |2 N6 m
},3 C+ U, J6 h$ d( W! U# G
{1 z, L1 v% O: G1 ^' ]2 z! G
"amount": 100000000,
"asset_id": "ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff",2 ~; b% A: b$ A5 q9 D) D _
"type": "retire", j! e, t$ r$ j* K- ~0 W
}
],% {, T% k, V% ^4 M1 B
"ttl": 0,9 N1 r& O j4 t) j8 b$ p9 m
"time_range": 0
} ~% D6 }' P! a: p0 D5 d5 F+ f
build-transaction的输入构造完成之后,便可以通过http的调用方式进行发送交易,构建交易请求成功之后返回的json结果如下:
{! x9 s, m& S/ F
"allow_additional_actions": false," e5 F5 n4 c+ M) \3 p/ K
"raw_transaction": "070100020161015f1190c60818b4aff485c865113c802942f29ce09088cae1b117fc4c8db2292212ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff8099c4d599010001160014a86c83ee12e6d790fb388345cc2e2b87056a077301000161015fb018097c4040c8dd86d95611a13c24f90d4c9d9d06b25f5c9ed0556ac8abd73442275aacbeda1522cd41580f875c3c452daf5174b17ba062bf0ab71a568c123f80a094a58d1d0101160014068840e56af74038571f223b1c99f1b60caaf456010003013effffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff80bfffcb9901011600140b946646626c55a52a325c8bb48de792284d9b7200013e42275aacbeda1522cd41580f875c3c452daf5174b17ba062bf0ab71a568c123f9d9f94a58d1d01160014c8b4391bab4923a83b955170d24ee4ca5b6ec3fb00013942275aacbeda1522cd41580f875c3c452daf5174b17ba062bf0ab71a568c123f6301160014366b275ed9b2266b645cf1b8be51009cc3b260e100",
"signing_instructions": [: F$ c+ D; W5 r* c$ u
{7 {% x/ k8 N0 K r. ^
"position": 0,) G l* ^& \2 X' D' u ]
"witness_components": [
{+ f+ G) ^; G# x9 w. F8 m' {
"keys": [) s q% I- Y5 r2 U% p# W5 V
{: k+ z( x; y) w
"derivation_path": [
"010100000000000000",
"0100000000000000"
],% u2 K1 T# _! N% }5 ~! T& j$ |
"xpub": "de0db655c091b2838ccb6cddb675779b0a9a4204b122e61699b339867dd10eb0dbdc926882ff6dd75c099c181c60d63eab0033a4b0a4d0a8c78079e39d7ad1d8"5 q- T' M( k' n
}
],
"quorum": 1,- j6 [1 d* h; z+ K0 v+ m. {
"signatures": null,
"type": "raw_tx_signature"
},9 O+ R( P/ ~- @! M: i7 i
{7 p* n- O5 O% x4 f
"type": "data",) h3 \# R" ~, T% w$ W
"value": "d174db6506e35f2decb5be148c2984bfd0f6c67f043365bf642d1af387c04fd5"' G6 n: s1 B" j* h3 l2 R* I& R7 | k" j
}3 X7 X2 J* M$ X4 M3 d/ U- K
]* f& A$ Y A, V2 f% d
},$ M0 Z9 X* d) n- n" S
{
"position": 1,+ @ ` t4 P$ L) b3 i' p) j
"witness_components": [ [! ?/ j/ a! m+ t, F O3 X
{
"keys": [
{# ~2 i; ^. r# D% M$ Z( n' H
"derivation_path": [* |9 d- N9 u! m3 V" T0 p# H
"010100000000000000",
"0800000000000000"
]," c8 U+ o3 G- H2 Z5 R/ B! J! g
"xpub": "de0db655c091b2838ccb6cddb675779b0a9a4204b122e61699b339867dd10eb0dbdc926882ff6dd75c099c181c60d63eab0033a4b0a4d0a8c78079e39d7ad1d8"9 E0 S- h4 ?1 \. g1 e6 J
}- N2 F5 p i1 ^8 Z' n% ?
],2 B. m/ |9 t( x N
"quorum": 1,
"signatures": null,
"type": "raw_tx_signature"
},
{* t/ s+ {! O, W1 P S& y/ h
"type": "data",* Q9 l3 T3 E* l1 O
"value": "05cdbcc705f07ad87521835bbba226ad7b430cc24e5e3f008edbe61540535419"
}2 q$ O/ Q4 A2 @% s
]/ K7 X: C, N8 A0 Y3 ]
}
]
}
对应响应对象的源代码如下:
// Template represents a partially- or fully-signed transaction.8 v2 l4 S u3 ]" _5 Y# N
type Template struct {
Transaction *types.Tx `json:"raw_transaction"`$ M" c6 A/ h3 r
SigningInstructions []*SigningInstruction `json:"signing_instructions"`
// AllowAdditional affects whether Sign commits to the tx sighash or x R8 D( ?2 K3 o5 Q* h
// to individual details of the tx so far. When true, signatures
// commit to tx details, and new details may be added but existing
// ones cannot be changed. When false, signatures commit to the tx! |( {5 \: S4 B& f3 M, C
// as a whole, and any change to the tx invalidates the signature.
AllowAdditional bool `json:"allow_additional_actions"`
}
结构字段说明如下:3 o! _7 ^2 t1 B( W e1 u) r- q
Transaction 交易相关信息,该字段包含TxData和bc.Tx两个部分:
TxData 表示给用户展示的交易数据部分,该部分对用户可见2 y h5 z9 c4 u/ D
Version 交易版本( l, e( z7 P/ y6 J% A
SerializedSize 交易序列化之后的size
TimeRange 交易提交上链的最大时间戳(区块高度)(主链区块高度到达该时间戳(区块高度)之后,如果交易没有被提交上链,该交易便会失效)
Inputs 交易输入 e2 A, b& q$ [. _# k3 n
Outputs 交易输出
bc.Tx 表示系统中处理交易用到的转换结构,该部分对用户不可见,故不做详细描述
SigningInstructions 交易的签名信息7 E+ n) G, ]/ _" Y8 j
Position 对input action签名的位置7 D) a9 j {) ~; s
WitnessComponents 对input action签名需要的数据信息,其中build交易的signatures为null,表示没有签名; 如果交易签名成功,则该字段会存在签名信息。该字段是一个interface接口,主要包含3种不同的类型:' M2 {, H" N4 ~+ T4 K% R- b
SignatureWitness 对交易模板Template中交易input action位置的合约program进行哈希,然后对hash值进行签名; z! }9 P7 J% S9 z9 O/ ^7 [
signatures (数组类型)交易的签名,sign-transaction执行完成之后才会有值存在
keys (数组类型)包含主公钥xpub和派生路径derivation_path,通过它们可以在签名阶段找到对应的派生私钥child_xprv,然后使用派生私钥进行签名
quorum 账户key 的个数,必须和上面的keys的长度相等。如果quorum 等于1,则表示单签账户,否则为多签账户
program 签名的数据部分,program的hash值作为签名数据。如果program为空,则会根据当前交易ID和对应action位置的InputID两部分生成一个hash,然后把它们作为指令数据自动构造一个program
RawTxSigWitness 对交易模板Template的交易ID和对应input action位置的InputID(该字段位于bc.Tx中)进行哈希,然后对hash值进行签名" M1 _$ G" U( a, x$ ?) c
signatures (数组类型)交易的签名,sign-transaction执行完成之后才会有值存在
keys (数组类型)包含主公钥xpub和派生路径derivation_path,通过它们可以在签名阶段找到对应的派生私钥child_xprv,然后使用派生私钥进行签名
quorum 账户key的个数,必须和上面的keys 的长度相等。如果quorum 等于1,则表示单签账户,否则为多签账户
DataWitness 该类型无需签名,验证合约program的附加数据% Z+ H4 c1 M( j4 t5 p: z
& a& h0 b b$ k/ X" Y
AllowAdditional 是否允许交易的附加数据,如果为true,则交易的附加数据会添加到交易中,但是不会影响交易的执行的program脚本,对签名结果不会造成影响; 如果为false,则整个交易作为一个整体进行签名,任何数据的改变将影响整个交易的签名* T/ i4 r* X# e% H3 X, {
0 a3 n( d o7 G
估算手续费
估算手续费接口estimate-transaction-gas是对build-transaction的结果进行手续费的预估,估算的结果需要重新加到build-transaction的结果中,然后对交易进行签名和提交。其主要流程如下:
build - estimate - build - sign - submit
估算手续费的输入请求json格式如下:8 l" f. v& A/ B- Q
{
"transaction_template": {
"allow_additional_actions": false,
"raw_transaction": "070100020161015f1190c60818b4aff485c865113c802942f29ce09088cae1b117fc4c8db2292212ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff8099c4d599010001160014a86c83ee12e6d790fb388345cc2e2b87056a077301000161015fb018097c4040c8dd86d95611a13c24f90d4c9d9d06b25f5c9ed0556ac8abd73442275aacbeda1522cd41580f875c3c452daf5174b17ba062bf0ab71a568c123f80a094a58d1d0101160014068840e56af74038571f223b1c99f1b60caaf456010003013effffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff80bfffcb9901011600140b946646626c55a52a325c8bb48de792284d9b7200013e42275aacbeda1522cd41580f875c3c452daf5174b17ba062bf0ab71a568c123f9d9f94a58d1d01160014c8b4391bab4923a83b955170d24ee4ca5b6ec3fb00013942275aacbeda1522cd41580f875c3c452daf5174b17ba062bf0ab71a568c123f6301160014366b275ed9b2266b645cf1b8be51009cc3b260e100",8 E9 y* f" g! {, h- K- \( w: a* [6 {
"signing_instructions": [
{
"position": 0,$ N- {' p& k2 b+ m
"witness_components": [7 s% g J0 e4 b# E& k
{
"keys": [. M. e' H/ L1 H
{" K3 y! O7 r9 i! [$ M) Y ~
"derivation_path": [0 d1 v% x: }4 [2 M: T# x% h. D- ~
"010100000000000000",# H9 s) |) Q5 b, z- p9 H
"0100000000000000"9 |2 g5 N: W9 y5 {9 w+ `
],9 u3 y# c% f" T7 n8 z }) z
"xpub": "de0db655c091b2838ccb6cddb675779b0a9a4204b122e61699b339867dd10eb0dbdc926882ff6dd75c099c181c60d63eab0033a4b0a4d0a8c78079e39d7ad1d8"6 d# b5 R2 w3 W) i3 B0 ~( n; B
}$ }# J& x5 \1 G) H* z4 P
],
"quorum": 1,5 c f! p8 Z" j8 L
"signatures": null,7 l: K# U. l2 I$ B" O7 q
"type": "raw_tx_signature"
},5 q. F9 Z9 a) k% m) V
{
"type": "data",. W- W8 H4 E! J" s# D! N- T* |
"value": "d174db6506e35f2decb5be148c2984bfd0f6c67f043365bf642d1af387c04fd5"# r6 I& g- }: v, d+ o8 g
}; h3 y, _4 f4 p0 P% V. J# o7 W7 q
]
},
{" v6 o# n# Z& T* W# _7 k& E: u
"position": 1,& j, ^& O1 U- U& N# S. D1 S* D
"witness_components": [7 t- _1 D C* ~
{) M! I, L; ?* J! m
"keys": [9 H1 P: N% n# B* q% g
{
"derivation_path": [
"010100000000000000",3 y8 t; Z& C" n+ X1 t( ?6 ~4 K9 L) m
"0800000000000000"
],
"xpub": "de0db655c091b2838ccb6cddb675779b0a9a4204b122e61699b339867dd10eb0dbdc926882ff6dd75c099c181c60d63eab0033a4b0a4d0a8c78079e39d7ad1d8" y: u; H. L8 i Q+ J/ ~
}
],$ W) L3 l/ P9 I8 V
"quorum": 1,
"signatures": null,
"type": "raw_tx_signature"
},
{1 V# D( K7 g( b( o8 O, X. i
"type": "data",# h' d6 s/ t, e, a1 g2 e
"value": "05cdbcc705f07ad87521835bbba226ad7b430cc24e5e3f008edbe61540535419") f. r) w: I/ A, q# ~$ q# q
}
]* u- u% t. q O$ B
}/ q' q7 U' O3 @+ S% e( W: C" N: ]
]8 T. c. B$ n' L1 G5 q% a$ {
}3 h9 {/ f5 T0 ]3 }8 Z% C2 S
}9 M8 U1 F) c8 ?) @9 _( Q0 F3 ^
对应响应对象的源代码如下:
type request struct{
TxTemplate txbuilder.Template `json:"transaction_template"`7 u& k( u( P0 Z: H. H
}
// Template represents a partially- or fully-signed transaction.
type Template struct {
Transaction *types.Tx `json:"raw_transaction"` U3 O4 e& k7 ?# c
SigningInstructions []*SigningInstruction `json:"signing_instructions"`
// AllowAdditional affects whether Sign commits to the tx sighash or
// to individual details of the tx so far. When true, signatures
// commit to tx details, and new details may be added but existing- a3 G1 a% l$ ~( @
// ones cannot be changed. When false, signatures commit to the tx
// as a whole, and any change to the tx invalidates the signature.
AllowAdditional bool `json:"allow_additional_actions"`
}) o# h5 `/ I m
其中TxTemplate相关字段的说明见build-transaction的结果描述: [) ^, \" W# R* r m V U
调用estimate-transaction-gas接口成功之后返回的json结果如下:5 g6 i; F! ?3 m# N( {& O
{0 P Y, Q0 k( i1 O' O
"total_neu": 5000000,, ]8 v9 J9 ^5 }: M2 \) Y& }
"storage_neu": 3840000,3 S" T, Z0 R# B6 y1 V
"vm_neu": 14190004 C2 I' ]1 M% ?( d
}( ^% T0 E. P# Z* ?; w% t* b
对应响应对象的源代码如下:1 Q: Z8 D4 j" f4 M& t/ x: z
// EstimateTxGasResp estimate transaction consumed gas8 d8 n" {5 v8 z l% E
type EstimateTxGasResp struct {6 s. h4 p" l! \! c
TotalNeu int64 `json:"total_neu"`9 I1 v- K w: q$ y8 ]" x
StorageNeu int64 `json:"storage_neu"`
VMNeu int64 `json:"vm_neu"`4 b3 I0 j$ o5 z! x }/ h8 z
}
结构字段说明如下:
TotalNeu 预估的总手续费(单位为neu),该值直接加到build-transaction的BTM资产输入action中即可( Y9 C2 Q1 t# U; v
StorageNeu 存储交易的手续费
VMNeu 运行虚拟机的手续费
2、签名交易3 B4 k0 Q9 n {
API接口 sign-transaction,代码api/hsm.go#L53
签名交易的输入请求json格式如下:% A( J3 C; I! d
{# R- Y: s* {# s3 n: J
"password": "123456",2 U( w, s% P) i; o0 v
"transaction": {
"allow_additional_actions": false,/ R% h# A; F) L. h
"raw_transaction": "070100020161015f1190c60818b4aff485c865113c802942f29ce09088cae1b117fc4c8db2292212ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff8099c4d599010001160014a86c83ee12e6d790fb388345cc2e2b87056a077301000161015fb018097c4040c8dd86d95611a13c24f90d4c9d9d06b25f5c9ed0556ac8abd73442275aacbeda1522cd41580f875c3c452daf5174b17ba062bf0ab71a568c123f80a094a58d1d0101160014068840e56af74038571f223b1c99f1b60caaf456010003013effffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff80bfffcb9901011600140b946646626c55a52a325c8bb48de792284d9b7200013e42275aacbeda1522cd41580f875c3c452daf5174b17ba062bf0ab71a568c123f9d9f94a58d1d01160014c8b4391bab4923a83b955170d24ee4ca5b6ec3fb00013942275aacbeda1522cd41580f875c3c452daf5174b17ba062bf0ab71a568c123f6301160014366b275ed9b2266b645cf1b8be51009cc3b260e100", \6 a2 }% z# t5 P9 l
"signing_instructions": [
{
"position": 0,: \) x( P1 p1 x0 @. ?
"witness_components": [: o2 F* k9 `5 J. `' g/ l
{
"keys": [
{8 P: J. Q8 m; N0 C
"derivation_path": [
"010100000000000000",0 Z6 A$ @7 q) J# R y+ `! s
"0100000000000000"
],
"xpub": "de0db655c091b2838ccb6cddb675779b0a9a4204b122e61699b339867dd10eb0dbdc926882ff6dd75c099c181c60d63eab0033a4b0a4d0a8c78079e39d7ad1d8"" I/ V5 z0 O8 ]3 u7 W& `4 x
}0 M# Y+ H5 F+ H0 ?. O1 l
],) T5 ~* i$ X4 A1 u
"quorum": 1, H$ W& j9 ?2 Z5 |
"signatures": null,- U9 G" O8 M; E- _" D6 R" a
"type": "raw_tx_signature"
},+ @. P4 l) T; Q* z, b4 R
{
"type": "data",
"value": "d174db6506e35f2decb5be148c2984bfd0f6c67f043365bf642d1af387c04fd5" @! u# S0 v! M" ~
}7 `! ^* k9 ? p$ E+ D& B3 z
]5 [: B) S( c. C9 v$ ?3 Z
},: F' r P6 o8 H) Y" }" \* @* d
{
"position": 1,
"witness_components": [/ | o7 I2 r# c ^! n
{
"keys": [6 v6 U! F: }) T9 L
{" D; t& U7 y7 ~/ [$ j; z$ S+ u
"derivation_path": [8 t7 G2 a! w, c9 O+ b, k
"010100000000000000",( v4 y7 Z& S* o9 B+ W
"0800000000000000"# t$ n$ S3 e& W* ~, `
], }3 O4 e9 K A: Z: ^. v
"xpub": "de0db655c091b2838ccb6cddb675779b0a9a4204b122e61699b339867dd10eb0dbdc926882ff6dd75c099c181c60d63eab0033a4b0a4d0a8c78079e39d7ad1d8"! x# o% H+ ^" H4 e1 Q, Q- R
}
],3 U' l" V, A$ z& e, B4 T
"quorum": 1,
"signatures": null,
"type": "raw_tx_signature"
},: F9 j- r, {9 v4 a% b( a! H& z( l. `
{
"type": "data",
"value": "05cdbcc705f07ad87521835bbba226ad7b430cc24e5e3f008edbe61540535419"
}( t1 U7 ~4 X2 ?6 r) o" A7 J
]
}
]: o, f8 k; z5 W/ L
}& x) s5 H, G0 X( f' P3 H
}) i5 `$ M2 N: H* e
对应请求对象的源代码如下:
type SignRequest struct { //function pseudohsmSignTemplates request/ K) g! I# c# u
Password string `json:"password"`5 d$ a0 D. G1 F4 U8 Z" d
Txs txbuilder.Template `json:"transaction"`& i* B# N g$ }* [
}& R+ S$ _; p. }$ u3 I
结构字段说明如下:
Password 签名的密码,根据密码可以从节点服务器上解析出用户的私钥,然后用私钥对交易进行签名
Txs 交易模板,build-transaction的返回结果,结构类型为 txbuilder.Template,相关字段的说明见build-transaction的结果描述
签名交易sign-transaction请求成功之后返回的json结果如下:7 w9 `; J% u/ d% i( w
{! R& r0 X0 L' _) S9 Z! Q
"sign_complete": true,
"transaction": {
"allow_additional_actions": false,
"raw_transaction": "070100020161015f1190c60818b4aff485c865113c802942f29ce09088cae1b117fc4c8db2292212ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff8099c4d599010001160014a86c83ee12e6d790fb388345cc2e2b87056a0773630240273d5fc4fb06909fbc2968ea91c411fd20f690c88e74284ce2732052400129948538562fe432afd6cf17e590e8645b80edf80b9d9581d0a980d5f9f859e3880620d174db6506e35f2decb5be148c2984bfd0f6c67f043365bf642d1af387c04fd50161015fb018097c4040c8dd86d95611a13c24f90d4c9d9d06b25f5c9ed0556ac8abd73442275aacbeda1522cd41580f875c3c452daf5174b17ba062bf0ab71a568c123f80a094a58d1d0101160014068840e56af74038571f223b1c99f1b60caaf4566302400cf0beefceaf9fbf1efadedeff7aee5b38ee7a25a20d78b630b01613bc2f8c9230555a6e09aaa11a82ba68c0fc9e98a47c852dfe3de851d93f9b2b7ce256f90d2005cdbcc705f07ad87521835bbba226ad7b430cc24e5e3f008edbe6154053541903013effffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff80bfffcb9901011600140b946646626c55a52a325c8bb48de792284d9b7200013e42275aacbeda1522cd41580f875c3c452daf5174b17ba062bf0ab71a568c123f9d9f94a58d1d01160014c8b4391bab4923a83b955170d24ee4ca5b6ec3fb00013942275aacbeda1522cd41580f875c3c452daf5174b17ba062bf0ab71a568c123f6301160014366b275ed9b2266b645cf1b8be51009cc3b260e100",! ^) X2 N P3 o" g6 z. p2 I! I
"signing_instructions": [& I2 ^$ F9 T4 ~5 R7 D) G% d
{4 K+ R6 b1 `( ?& h; s. R) l
"position": 0,4 p" B/ e$ E( g+ ^' H- ~
"witness_components": [# d0 c W# `! @- i# ~8 F
{ s3 k4 \. o! A& ?+ O# l/ o% e
"keys": [
{
"derivation_path": [
"010100000000000000",; I- ^5 ?* ~8 e/ w
"0100000000000000"
],
"xpub": "de0db655c091b2838ccb6cddb675779b0a9a4204b122e61699b339867dd10eb0dbdc926882ff6dd75c099c181c60d63eab0033a4b0a4d0a8c78079e39d7ad1d8"
}
],
"quorum": 1,9 U; r: B3 J0 T/ P3 {' W- I* V) W
"signatures": [
"273d5fc4fb06909fbc2968ea91c411fd20f690c88e74284ce2732052400129948538562fe432afd6cf17e590e8645b80edf80b9d9581d0a980d5f9f859e38806"* M: _& Y& g! U
],
"type": "raw_tx_signature"3 j+ f0 b; x) T7 U9 ^
},
{
"type": "data",. @. k3 r Z, q9 J1 \. z
"value": "d174db6506e35f2decb5be148c2984bfd0f6c67f043365bf642d1af387c04fd5"
}4 t& B/ T+ c5 l! }
]& T1 ?. T6 W/ m* [ C3 E3 a/ P! F
},
{6 i, h N# }) O$ U8 n4 M: K
"position": 1,
"witness_components": [7 X" o' c' T: g3 y4 X+ U7 b
{
"keys": [
{
"derivation_path": [: Q$ p" [' X E% r( g/ {
"010100000000000000",. _+ B/ @5 M) Y7 C) I/ `
"0800000000000000": G; \$ Y. D! ~* w6 c- n) w
],3 B; ~2 w& Z( [; B/ E3 r2 s
"xpub": "de0db655c091b2838ccb6cddb675779b0a9a4204b122e61699b339867dd10eb0dbdc926882ff6dd75c099c181c60d63eab0033a4b0a4d0a8c78079e39d7ad1d8"
}
],/ r- @ w& K/ T( K& O9 s! F
"quorum": 1,! @4 N( [ G5 `/ V# C' ?
"signatures": [& W' W2 j% [( y% P- r' S
"0cf0beefceaf9fbf1efadedeff7aee5b38ee7a25a20d78b630b01613bc2f8c9230555a6e09aaa11a82ba68c0fc9e98a47c852dfe3de851d93f9b2b7ce256f90d", ^* H; I7 ?7 V5 U, {& J2 V
]," z# X( H3 f7 F9 h
"type": "raw_tx_signature"
},, G$ u& W2 I% `3 h7 `
{
"type": "data",/ ?6 B7 z: N( f9 ]# Z5 ~4 V4 g
"value": "05cdbcc705f07ad87521835bbba226ad7b430cc24e5e3f008edbe61540535419"
}, c& o5 `/ v" X
], c' B+ Q6 z+ }
}5 E, q' n6 ]; P. Z
] k5 D. ]! k' t: c
}! D! @- v3 p! u2 B# ~9 N
}# V- L. K1 F, u
对应响应对象的源代码如下:
type signResp struct {1 {5 h- y/ {( H" u
Tx *txbuilder.Template `json:"transaction"`
SignComplete bool `json:"sign_complete"`; ^) s& v! y9 e9 P; d
}2 ?+ p* H3 a) Y3 z! k. a, e
结构字段说明如下:: Q' t, s. ~ {8 \
Tx 签名之后的交易模板txbuilder.Template,如果签名成功则signatures会由null变成签名的值,而raw_transaction的长度会变长,是因为bc.Tx部分添加了验证签名的参数信息
SignComplete 签名是否完成标志,如果为true表示签名完成,否则为false表示签名未完成,单签的话一般可能为签名密码错误; 而多签的话一般为还需要其他签名。签名失败只需将签名的交易数据用正确的密码重新签名即可,无需再次build-transaction构建交易
( ]! f$ {8 z9 m% ?# G
3、提交交易7 ~4 J7 Y+ }! c" C2 M$ K& p
API接口 submit-transaction,代码api/transact.go#L135
提交交易的输入请求json格式如下:
{+ K; X, j4 ?/ }! R) r
"raw_transaction": "070100020161015f1190c60818b4aff485c865113c802942f29ce09088cae1b117fc4c8db2292212ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff8099c4d599010001160014a86c83ee12e6d790fb388345cc2e2b87056a0773630240273d5fc4fb06909fbc2968ea91c411fd20f690c88e74284ce2732052400129948538562fe432afd6cf17e590e8645b80edf80b9d9581d0a980d5f9f859e3880620d174db6506e35f2decb5be148c2984bfd0f6c67f043365bf642d1af387c04fd50161015fb018097c4040c8dd86d95611a13c24f90d4c9d9d06b25f5c9ed0556ac8abd73442275aacbeda1522cd41580f875c3c452daf5174b17ba062bf0ab71a568c123f80a094a58d1d0101160014068840e56af74038571f223b1c99f1b60caaf4566302400cf0beefceaf9fbf1efadedeff7aee5b38ee7a25a20d78b630b01613bc2f8c9230555a6e09aaa11a82ba68c0fc9e98a47c852dfe3de851d93f9b2b7ce256f90d2005cdbcc705f07ad87521835bbba226ad7b430cc24e5e3f008edbe6154053541903013effffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff80bfffcb9901011600140b946646626c55a52a325c8bb48de792284d9b7200013e42275aacbeda1522cd41580f875c3c452daf5174b17ba062bf0ab71a568c123f9d9f94a58d1d01160014c8b4391bab4923a83b955170d24ee4ca5b6ec3fb00013942275aacbeda1522cd41580f875c3c452daf5174b17ba062bf0ab71a568c123f6301160014366b275ed9b2266b645cf1b8be51009cc3b260e100"
}3 Q+ w- |8 x4 a+ x$ a( c
对应源代码的请求对象如下:
type SubmitRequest struct { //function submit request2 [( c _" ~0 Q" R7 o
Tx types.Tx `json:"raw_transaction"`# u) q0 H+ T. @8 X6 M2 n/ p
}! j n1 R% Z- ]0 N+ x( k! N3 W
结构字段说明如下:
Tx 签名完成之后的交易信息。这里需要注意该字段中的raw_transaction不是签名交易sign-transaction的全部返回结果,而是签名交易返回结果中transaction中的raw_transaction字段。# e* D# y% a$ J, q4 {) O3 P
submit-transaction请求成功之后返回的json结果如下:
{' S. r# }9 H0 Q4 X$ K
"tx_id": "2c0624a7d251c29d4d1ad14297c69919214e78d995affd57e73fbf84ece361cd"
}
对应源代码的响应对象如下:# q6 j! v0 _% m q% r: y' Z
type submitTxResp struct {
TxID *bc.Hash `json:"tx_id"`
}5 V3 o; G7 C( I' h$ S8 s/ T' Y
结构字段说明如下:
TxID 交易ID,当交易被提交到交易池之后会显示该信息,否则表示交易失败& H: {- E# \& d
$ U- Z# e7 C, d; q: h4 G9 z4 H! q
成为第一个吐槽的人