做者:freewind前端
比原項目倉庫:git
Github地址:https://github.com/Bytom/bytom程序員
Gitee地址:https://gitee.com/BytomBlockc...github
在前面幾篇中,咱們作了足夠了準備,如今終於能夠試一試轉賬功能了!golang
這裏的轉賬最好使用solonet
,再按前一篇文章的辦法修改代碼後產生單機測試幣,而後再試。在此以前,若是須要的話,請先備份好你以前的賬戶,而後刪除(或重命名)你的數據目錄,再使用bytomd init --chain_id=solonet
從新初始化。算法
下面是我經過dashboard進行的轉賬操做,在操做以前,我先創建了兩個賬戶,而後把錢從一個賬戶轉到另外一個賬戶的地址中:數據庫
新建一個交易,填上把哪一個賬戶的哪一種資產轉到某個地址上。能夠看到還要消耗必定的gas:json
(上圖爲圖1)後端
轉賬成功後,以下:api
(上圖爲圖2)
咱們看一下這個交易的詳細信息,因爲太長,截成了兩個圖:
(上面兩圖合稱爲圖3)
咱們今天(以及日後的幾天)就是把這一塊流程搞清楚。
因爲上面展現的操做仍是有點多的,因此咱們仍是按以前的套路,先把它分解成多個小問題,一一解決:
今天的文章,咱們主要是研究前兩個問題,即跟圖1相關的邏輯。
因爲是前端,因此咱們要去從前端的代碼庫中尋找。經過搜索「簡單交易」這個詞,咱們很快定位到下面這塊代碼:
src/features/transactions/components/New/New.jsx#L275-L480
return ( <FormContainer onSubmit={handleSubmit(this.submitWithValidation)}> // ... </FormContainer> )
因爲上面的代碼實在太長太細節,全是一些jsx用於生成表單的代碼,咱們就跳過算了,有興趣的同窗能夠自行看細節。咱們須要關注的是,當咱們單擊了「提交交易」的按鈕之後,this.submitWithValidation
會被調用,而它對應的代碼是:
src/features/transactions/components/New/New.jsx#L159-L177
submitWithValidation(data) { return new Promise((resolve, reject) => { this.props.submitForm(Object.assign({}, data, {state: this.state})) .catch((err) => { // ... return reject(response) }) }) }
一般咱們應該會在這個函數裏找到一些線索,發現數據會提交到後臺哪一個接口。可是此次卻好像沒有有用的信息,只有一個來自於props
的看起來很是通用的submitForm
。看來須要多找找線索。
好在很快在同一個文件的最後面,看到了用於把React組件與Redux鏈接起來的代碼,很是有用:
src/features/transactions/components/New/New.jsx#L515-L572
export default BaseNew.connect( (state) => { // ... return { // ... } }, (dispatch) => ({ // ... ...BaseNew.mapDispatchToProps('transaction')(dispatch) }), // ... )(Form) )
我把不太關注的內容都省略了,須要關注的是BaseNew.mapDispatchToProps('transaction')(dispatch)
這一行。
爲何要關注mapDispatchToProps
這個方法呢?這是由於當咱們點擊了表單中的提交按鈕後,不論中間怎麼操做,最後必定要調用dispatch
來處理某個action。而在前面看到,點擊「提交交易」後,執行的是this.props.submitForm
,經過this.props.
能夠看出,這個submitForm
是從外部傳進來的,而mapDispatchToPros
就是把dispatch操做映射在props
上,讓props
中有咱們須要的函數。因此若是咱們不能從其它地方看到明顯的線索的時候,應該考慮去看看這個。
而BaseNew.mapDispatchToProps
是來自於BaseNew
,咱們又找到了相應的代碼:
src/features/shared/components/BaseNew.jsx#L9-L16
import actions from 'actions' // ... export const mapDispatchToProps = (type) => (dispatch) => ({ submitForm: (data) => { return dispatch(actions[type].submitForm(data)).then((resp) => { dispatch(actions.tutorial.submitTutorialForm(data, type)) return resp }) } })
果真在裏面找到了submitForm
的定義。在裏面第一個dispatch
處,傳入了參數actions[type].submitForm(data)
,這裏的type
應該是transaction
,而actions
應該是以前某處定義的各類action的集合。
根據import actions from 'actions'
,咱們發現from
後面的'actions'
不是相對路徑,那麼它對應的就是js的源代碼根目錄src
下的某個文件,好比actions.js
。
找到後打開一看,裏面果真有transaction
:
// ... import { actions as transaction } from 'features/transactions' // ... const actions = { // ... transaction, // ... }
咱們繼續進入features/transactions/
探索,很快找到:
src/features/transactions/actions.js#L100-L200
form.submitForm = (formParams) => function (dispatch) { // ... // 2. const buildPromise = connection.request('/build-transaction', {actions: processed.actions}) const signAndSubmitTransaction = (transaction, password) => { // 4. return connection.request('/sign-transaction', { password, transaction }).then(resp => { if (resp.status === 'fail') { throw new Error(resp.msg) } const rawTransaction = resp.data.transaction.rawTransaction // 5. return connection.request('/submit-transaction', {rawTransaction}) }).then(dealSignSubmitResp) } // ... if (formParams.submitAction == 'submit') { // 1. return buildPromise .then((resp) => { if (resp.status === 'fail') { throw new Error(resp.msg) } // 3. return signAndSubmitTransaction(resp.data, formParams.password) }) } // ... }
上面的代碼通過了個人簡化,其實它原本是有不少分支的(由於表單中除了「簡單交易」還有「高級交易」等狀況)。即便如此,也能夠看出來這個過程仍是比較複雜的,通過了好幾回的後臺接口訪問:
buildPromise
,這裏面應該包括了對後臺的訪問buildPromise
的定義,能夠看到會訪問/build-transaction
signAndSubmitTransaction
signAndSubmitTransaction
內部了,能夠看到,它會訪問一個新的接口/sign-transaction
/submit-transaction
。後面的dealSignSubmitResp
是一些對前端的操做,因此就不看它了能夠看到,這一個表單的提交,在內部對應着好幾個接口的訪問,每一個提交的數據也不同,代碼跟蹤起來不太方便。可是好在只要咱們知道了這一條主線,那麼尋找其它的信息就會簡單一些。不過咱們也沒有必要執着於所有從源代碼中找到答案,由於咱們的目的並非學習React/Redux,而是理解比原的邏輯,因此咱們能夠藉助別的工具(好比Chrome的Developer Tools),來捕獲請求的數據,從而推理出邏輯。
我已經從Chrome的開發工具中取得了前端向下面幾個接口發送的數據:
/build-transaction
/sign-transaction
/submit-transaction
可是因爲咱們在這個小問題中,關注的重點是前端如何把數據提交給後臺的,因此對於這裏提交的數據的意義暫時不討論,留待下個小問題中一一解答。
因爲在圖1中前端一共訪問了3個不一樣的後端接口,因此在這裏咱們就須要依次分開討論。
/build-transaction
下面是我經過Chrome的開發工具捕獲的數據,看起來還比較多:
/build-transaction
{ "actions": [{ "amount": 437400, "type": "spend_account", "receiver": null, "account_alias": "freewind", "account_id": "", "asset_alias": "BTM", "reference_data": null }, { "amount": 23400000000, "type": "spend_account", "receiver": null, "account_alias": "freewind", "account_id": "", "asset_alias": "BTM", "asset_id": "", "reference_data": null }, { "address": "sm1qe4z3ava34wv5njdgekcgdlrckc95gnljazezva", "amount": 23400000000, "type": "control_address", "receiver": null, "asset_alias": "BTM", "asset_id": "", "reference_data": null }] }
能夠看到前端向/build-transaction
發送的數據包含了三個元素,其中前兩個是來源賬戶的信息,第三個是目的賬戶地址。這三個元素都包含一個叫amount
的key,它的值對應的是相應資產的數量,若是是BTM
的話,這個數字就須要從右向左數8位,再加上一個小數點。也就是說,第一個amount對應的是0.00437400
個BTM,第二個是234.00000000
,第三個是234.00000000
。
第一個元素對應的費用是gas,也就是圖1中顯示出來的估算的手續費。第二個是要從相應賬戶中轉出234
個BTM,第三個是要轉入234
個BTM。
另外,前兩個的type
是spend_account
,代表了是賬戶,可是spend
是什麼意思目前還不清楚(TODO);第三個是control_address
,表示是一個地址。
經過這些數據,比原的後臺就知道該怎麼作了。
{ "status": "success", "data": { "raw_transaction": "070100010161015f643bef0936443042ccb1e94213ed52af72488088702d88e7fc3580359a19a522ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff8099c4d599010001160014108c5ba0934951a12755523f8a1fe42a6c24342f010002013dffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe8ebaabf4201160014b111c8114dc7ee02050598022b46855fd482d27300013dffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff80d4fe955701160014cd451eb3b1ab9949c9a8cdb086fc78b60b444ff200", "signing_instructions": [{ "position": 0, "witness_components": [{ "type": "raw_tx_signature", "quorum": 1, "keys": [{ "xpub": "f98b3a39b4eef67707cac85240ef07235c990301b2e0658001545bdb7fde3a21363a23682a1dfbb727dec7565624812c314ca9f31a7f7374101e0247d05cb248", "derivation_path": ["010100000000000000", "0100000000000000"] }], "signatures": null }, { "type": "data", "value": "b826dcccff76d19d097ca207e053e67d67e3da3a90896ae9fa2d984c6f36d16c" }] }], "allow_additional_actions": false } }
這個迴應信息是什麼意思呢?咱們如今開始研究。
咱們在比原的後端代碼庫中,經過查找/build-transaction
,很快找到了它的定義處:
func (a *API) buildHandler() { // ... if a.wallet != nil { // ... m.Handle("/build-transaction", jsonHandler(a.build)) // ... }
能夠看到它對就的方法是a.build
,其代碼爲:
func (a *API) build(ctx context.Context, buildReqs *BuildRequest) Response { subctx := reqid.NewSubContext(ctx, reqid.New()) tmpl, err := a.buildSingle(subctx, buildReqs) if err != nil { return NewErrorResponse(err) } return NewSuccessResponse(tmpl) }
其中的buildReqs
就對應着前端提交過來的參數,只不過被jsonHandler
自動轉成了Go代碼。其中BuildRequest
是這樣定義的:
type BuildRequest struct { Tx *types.TxData `json:"base_transaction"` Actions []map[string]interface{} `json:"actions"` TTL json.Duration `json:"ttl"` TimeRange uint64 `json:"time_range"` }
能夠看出來有一些字段好比base_transaction
, ttl
, time_range
等在本例中並無提交上來,它們應該是可選的。
繼續看a.buildSingle
:
func (a *API) buildSingle(ctx context.Context, req *BuildRequest) (*txbuilder.Template, error) { // 1. err := a.filterAliases(ctx, req) // ... // 2. if onlyHaveSpendActions(req) { return nil, errors.New("transaction only contain spend actions, didn't have output actions") } // 3. reqActions, err := mergeActions(req) // ... // 4. actions := make([]txbuilder.Action, 0, len(reqActions)) for i, act := range reqActions { typ, ok := act["type"].(string) // ... decoder, ok := a.actionDecoder(typ) // ... b, err := json.Marshal(act) // ... action, err := decoder(b) // ... actions = append(actions, action) } // 5. ttl := req.TTL.Duration if ttl == 0 { ttl = defaultTxTTL } maxTime := time.Now().Add(ttl) // 6. tpl, err := txbuilder.Build(ctx, req.Tx, actions, maxTime, req.TimeRange) // ... return tpl, nil }
這段代碼內容仍是比較多的,但整體基本上仍是對參數進行驗證、補全和轉換,而後交給後面的方法處理。我分紅了多塊,依次講解大意:
filterAliases
主要是對傳進來的參數進行驗證和補全。好比像account和asset,通常都有id和alias這兩個屬性,若是隻提交了alias而沒有提交id的話,則filterAliases
就會從數據庫或者緩存中查找到相應的id
補全。若是過程當中出了錯,好比alias不存在,則報錯返回onlyHaveSpendActions
是檢查若是這個交易中,只存在資金來源方,而沒有資金目標方,顯示是不對的,報錯返回mergeActions
是把請求數據中的spend_account
進行分組累加,把相同account的相同asset的數量累加到一塊兒actionDecoder(typ)
裏經過手動比較type
的值返回相應的Decoderttl
是指Time To Live
,指的這個請求的存活時間,若是沒指明的話(本例就沒有),則設爲默認值5分鐘txbuilder.Build
繼續處理在這幾處裏提到的方法和函數的代碼我就不貼出來了,由於基本上都是一些針對map的低級操做,大片大片的看着很累,實際上沒作多少事。這種類型的代碼反覆出現,在別的語言中(甚至Java)均可以抽出來不少工具方法,可是在Go裏因爲語言特性(缺乏泛型,麻煩的錯誤處理),彷佛不是很容易。看一眼廣大Go程序員的期盼:
https://github.com/golang/go/...
看看在Go2中會不會實現。
讓咱們繼續看txbuilder.Build
:
blockchain/txbuilder/txbuilder.go#L40-L79
func Build(ctx context.Context, tx *types.TxData, actions []Action, maxTime time.Time, timeRange uint64) (*Template, error) { builder := TemplateBuilder{ base: tx, maxTime: maxTime, timeRange: timeRange, } // Build all of the actions, updating the builder. var errs []error for i, action := range actions { err := action.Build(ctx, &builder) // ... } // If there were any errors, rollback and return a composite error. if len(errs) > 0 { builder.rollback() return nil, errors.WithData(ErrAction, "actions", errs) } // Build the transaction template. tpl, tx, err := builder.Build() // ... return tpl, nil }
這塊代碼通過簡化後,仍是比較清楚的,基本上就是想盡辦法把TemplateBuilder
填滿。TemplateBuilder
是這樣的:
blockchain/txbuilder/builder.go#L17-L28
type TemplateBuilder struct { base *types.TxData inputs []*types.TxInput outputs []*types.TxOutput signingInstructions []*SigningInstruction minTime time.Time maxTime time.Time timeRange uint64 referenceData []byte rollbacks []func() callbacks []func() error }
能夠看到有不少字段,可是隻要清楚了它們的用途,咱們也就清楚了交易transaction
是怎麼回事。可是我發現一旦深刻下去,很快又觸及到比原的核心部分,因此就停在這裏不去深究了。前面Build
函數裏面提到的其它的方法,好比action.Build
等,咱們也不進去了,由於它們基本上都是在想盡辦法組裝出最後須要的對象。
到這裏,咱們能夠認爲buildSingle
就走完了,而後回到func (a *API) build(...)
,把生成的對象返回給前端。
那麼,這個接口/build-transaction
究竟是作什麼的呢?經過上面我分析,咱們能夠知道它有兩個做用:
id
,公鑰等等,方便前端進行後面的操做在這個接口的分析過程當中,咱們仍是忽略了不少內容,好比返回給客戶端的那一大段JSON代碼中的數據。我想這些東西仍是留着咱們研究到比原的核心的時候,再一塊兒學習吧。
/sign-transaction
在前一步/build-transaction
成功完成之後,會進行下一步操做/sign-transaction
。
下面是經過Chrome的開發工具捕獲的內容:
{ "password": "my-password", "transaction": { "raw_transaction": "070100010161015f643bef0936443042ccb1e94213ed52af72488088702d88e7fc3580359a19a522ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff8099c4d599010001160014108c5ba0934951a12755523f8a1fe42a6c24342f010002013dffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe8ebaabf4201160014b111c8114dc7ee02050598022b46855fd482d27300013dffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff80d4fe955701160014cd451eb3b1ab9949c9a8cdb086fc78b60b444ff200", "signing_instructions": [{ "position": 0, "witness_components": [{ "type": "raw_tx_signature", "quorum": 1, "keys": [{ "xpub": "f98b3a39b4eef67707cac85240ef07235c990301b2e0658001545bdb7fde3a21363a23682a1dfbb727dec7565624812c314ca9f31a7f7374101e0247d05cb248", "derivation_path": ["010100000000000000", "0100000000000000"] }], "signatures": null }, { "type": "data", "value": "b826dcccff76d19d097ca207e053e67d67e3da3a90896ae9fa2d984c6f36d16c" }] }], "allow_additional_actions": false } }
能夠看到這裏提交的請求數據,與前面/build-transaction
相比,基本上是同樣的,只是多了一個password
,即咱們剛纔在表單最後一處填寫的密碼。從這個接口的名字中含有sign
能夠推測,這一步應該是與簽名有關。
{ "status": "success", "data": { "transaction": { "raw_transaction": "070100010161015f643bef0936443042ccb1e94213ed52af72488088702d88e7fc3580359a19a522ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff8099c4d599010001160014108c5ba0934951a12755523f8a1fe42a6c24342f630240c52a057fa26322a48fdd88c842cf31a84c6aec54ae2dc62554dc3c7e0216986a0a4f4a5c935a5ae6d88b4c7a4d1ca1937205f5eb23089128cc6744fbd2b88d0520b826dcccff76d19d097ca207e053e67d67e3da3a90896ae9fa2d984c6f36d16c02013dffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe8ebaabf4201160014b111c8114dc7ee02050598022b46855fd482d27300013dffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff80d4fe955701160014cd451eb3b1ab9949c9a8cdb086fc78b60b444ff200", "signing_instructions": [{ "position": 0, "witness_components": [{ "type": "raw_tx_signature", "quorum": 1, "keys": [{ "xpub": "f98b3a39b4eef67707cac85240ef07235c990301b2e0658001545bdb7fde3a21363a23682a1dfbb727dec7565624812c314ca9f31a7f7374101e0247d05cb248", "derivation_path": ["010100000000000000", "0100000000000000"] }], "signatures": ["c52a057fa26322a48fdd88c842cf31a84c6aec54ae2dc62554dc3c7e0216986a0a4f4a5c935a5ae6d88b4c7a4d1ca1937205f5eb23089128cc6744fbd2b88d05"] }, { "type": "data", "value": "b826dcccff76d19d097ca207e053e67d67e3da3a90896ae9fa2d984c6f36d16c" }] }], "allow_additional_actions": false }, "sign_complete": true } }
回過來的消息也基本上跟提交的差很少,只是在成功操做後,raw_transaction
字段的內容也變長了,還添加上了signatures
字段。
咱們開始看代碼,經過搜索/sign-transaction
,咱們很快定位到如下代碼:
func (a *API) buildHandler() { // ... if a.wallet != nil { // ... m.Handle("/sign-transaction", jsonHandler(a.pseudohsmSignTemplates)) // ... }
則/sign-transaction
對應的handler是a.pseudohsmSignTemplates
,讓咱們跟進去:
func (a *API) pseudohsmSignTemplates(ctx context.Context, x struct { Password string `json:"password"` Txs txbuilder.Template `json:"transaction"` }) Response { if err := txbuilder.Sign(ctx, &x.Txs, x.Password, a.pseudohsmSignTemplate); err != nil { log.WithField("build err", err).Error("fail on sign transaction.") return NewErrorResponse(err) } log.Info("Sign Transaction complete.") return NewSuccessResponse(&signResp{Tx: &x.Txs, SignComplete: txbuilder.SignProgress(&x.Txs)}) }
能夠看到這個方法內容也是比較簡單的。經過調用txbuilder.Sign
,把前端傳來的參數傳進去,而後把結果返回給前端便可。那咱們只須要看txbuilder.Sign
便可:
blockchain/txbuilder/txbuilder.go#L82-L100
func Sign(ctx context.Context, tpl *Template, auth string, signFn SignFunc) error { // 1. for i, sigInst := range tpl.SigningInstructions { for j, wc := range sigInst.WitnessComponents { switch sw := wc.(type) { case *SignatureWitness: err := sw.sign(ctx, tpl, uint32(i), auth, signFn) // ... case *RawTxSigWitness: err := sw.sign(ctx, tpl, uint32(i), auth, signFn) // ... } } } // 2. return materializeWitnesses(tpl) }
能夠看到這段代碼邏輯仍是比較簡單:
第1處代碼是兩個大循環,基本上作了兩件事:
sw.sign
,生成相關的簽名signatures
raw_transaction
處添加了一些操做符和約束條件,把它變成了一個合約(這塊還須要之後確認)materializeWitnesses
函數。它主要是在檢查沒有數據錯誤以後,把第1步中生成的簽名signatures
添加到tpl
對象上去。因爲sw.Sign
和materializeWitnesses
基本上都是一些算法或者合約相關的東西,咱們這裏就暫時忽略,之後再研究吧。
這個接口/sign-transaction
的做用應該是對經過密碼以及公鑰對「交易」這個重要的操做進行驗證,否則你們都能隨便把別人的錢轉到本身賬戶裏了。
/submit-transaction
當前一步/sign-transaction
簽名成功以後,終於能夠進行最後一步/submit-transaction
進行最終的提交了。
下面是經過Chrome的開發工具捕獲的內容。
{ "raw_transaction": "070100010161015f643bef0936443042ccb1e94213ed52af72488088702d88e7fc3580359a19a522ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff8099c4d599010001160014108c5ba0934951a12755523f8a1fe42a6c24342f630240c52a057fa26322a48fdd88c842cf31a84c6aec54ae2dc62554dc3c7e0216986a0a4f4a5c935a5ae6d88b4c7a4d1ca1937205f5eb23089128cc6744fbd2b88d0520b826dcccff76d19d097ca207e053e67d67e3da3a90896ae9fa2d984c6f36d16c02013dffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe8ebaabf4201160014b111c8114dc7ee02050598022b46855fd482d27300013dffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff80d4fe955701160014cd451eb3b1ab9949c9a8cdb086fc78b60b444ff200" }
能夠看到,到了這一步,提交的數據就少了,直接把前一步生成的簽名後的raw_transaction
提交上去就好了。我想這裏的內容應該已經包含了所有須要的信息,而且通過了驗證,因此不須要其它數據了。
{ "status": "success", "data": { "tx_id": "6866c1ab2bfa2468ce44451ce6af2a83f3885cdb6a1673fec94b27f338acf9c5" } }
能夠看到成功提交後,會獲得一個tx_id
,即爲當前這個交易生成的惟一的id,能夠用來查詢。
咱們經過查找/submit-transaction
,能夠在代碼中找到:
func (a *API) buildHandler() { // ... if a.wallet != nil { // ... m.Handle("/submit-transaction", jsonHandler(a.submit)) // ... }
那麼/submit-transaction
所對應的handler就是a.submit
了。咱們跟進去:
func (a *API) submit(ctx context.Context, ins struct { Tx types.Tx `json:"raw_transaction"` }) Response { if err := txbuilder.FinalizeTx(ctx, a.chain, &ins.Tx); err != nil { return NewErrorResponse(err) } log.WithField("tx_id", ins.Tx.ID).Info("submit single tx") return NewSuccessResponse(&submitTxResp{TxID: &ins.Tx.ID}) }
能夠看到主要邏輯就是調用txbuilder.FinalizeTx
來「終結」這個交易,而後把生成的tx_id
返回給前端。
讓咱們繼續看txbuilder.FinalizeTx
:
blockchain/txbuilder/finalize.go#L25-L47
func FinalizeTx(ctx context.Context, c *protocol.Chain, tx *types.Tx) error { // 1. if err := checkTxSighashCommitment(tx); err != nil { return err } // This part is use for prevent tx size is 0 // 2. data, err := tx.TxData.MarshalText() // ... // 3. tx.TxData.SerializedSize = uint64(len(data)) tx.Tx.SerializedSize = uint64(len(data)) // 4. _, err = c.ValidateTx(tx) // ... }
這一個方法總體上仍是各類驗證
tx
中的某些字段txPool
(用來在內存中保存交易的對象池),等待廣播出去以及打包到區塊中。我以爲這個名字ValidateTx
有點問題,由於它即包含了驗證,還包含了提交到池子中,這是兩個不一樣的操做,應該分開這裏涉及到的更細節的代碼就不進去了,主線咱們已經有了,感興趣的同窗能夠自行進去深刻研究。
那咱們今天關於提交交易的這個小問題就算是完成了,下次會繼續研究剩下的幾個小問題。