剝開比原看代碼15:比原是如何轉賬的

做者:freewind前端

比原項目倉庫:git

Github地址:https://github.com/Bytom/bytom程序員

Gitee地址:https://gitee.com/BytomBlockchain/bytomgithub

在前面幾篇中,咱們作了足夠了準備,如今終於能夠試一試轉賬功能了!golang

這裏的轉賬最好使用solonet,再按前一篇文章的辦法修改代碼後產生單機測試幣,而後再試。在此以前,若是須要的話,請先備份好你以前的賬戶,而後刪除(或重命名)你的數據目錄,再使用bytomd init --chain_id=solonet從新初始化。算法

下面是我經過dashboard進行的轉賬操做,在操做以前,我先創建了兩個賬戶,而後把錢從一個賬戶轉到另外一個賬戶的地址中:數據庫

新建一個交易,填上把哪一個賬戶的哪一種資產轉到某個地址上。能夠看到還要消耗必定的gas:json

(上圖爲圖1)後端

轉賬成功後,以下:api

(上圖爲圖2)

咱們看一下這個交易的詳細信息,因爲太長,截成了兩個圖:

(上面兩圖合稱爲圖3)

咱們今天(以及日後的幾天)就是把這一塊流程搞清楚。

因爲上面展現的操做仍是有點多的,因此咱們仍是按以前的套路,先把它分解成多個小問題,一一解決:

  1. 圖1中,轉賬界面是如何把轉賬數據提交到後臺的?
  2. 圖1中,後臺是如何接收到轉賬數據並執行轉賬操做的?
  3. 圖2中,前臺是如何拿到後臺的數據並展現出來的?
  4. 圖3中,前臺是如何拿到後臺的數據並展現出來的?

今天的文章,咱們主要是研究前兩個問題,即跟圖1相關的邏輯。

圖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

src/actions.js#L15-L29

// ...
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)
      })
  }
  // ...
}

上面的代碼通過了個人簡化,其實它原本是有不少分支的(由於表單中除了「簡單交易」還有「高級交易」等狀況)。即便如此,也能夠看出來這個過程仍是比較複雜的,通過了好幾回的後臺接口訪問:

  1. 第1處代碼就是對應咱們「簡單交易」的狀況,它會調用buildPromise,這裏面應該包括了對後臺的訪問
  2. 第2處就是buildPromise的定義,能夠看到會訪問/build-transaction
  3. 第3處是若是前一個訪問是正常的,那麼會繼續調用signAndSubmitTransaction
  4. 第4處就進入到signAndSubmitTransaction內部了,能夠看到,它會訪問一個新的接口/sign-transaction
  5. 第5處是在前一個正常的狀況下,進行最後的提交,訪問接口/submit-transaction。後面的dealSignSubmitResp是一些對前端的操做,因此就不看它了

能夠看到,這一個表單的提交,在內部對應着好幾個接口的訪問,每一個提交的數據也不同,代碼跟蹤起來不太方便。可是好在只要咱們知道了這一條主線,那麼尋找其它的信息就會簡單一些。不過咱們也沒有必要執着於所有從源代碼中找到答案,由於咱們的目的並非學習React/Redux,而是理解比原的邏輯,因此咱們能夠藉助別的工具(好比Chrome的Developer Tools),來捕獲請求的數據,從而推理出邏輯。

我已經從Chrome的開發工具中取得了前端向下面幾個接口發送的數據:

  • /build-transaction
  • /sign-transaction
  • /submit-transaction

可是因爲咱們在這個小問題中,關注的重點是前端如何把數據提交給後臺的,因此對於這裏提交的數據的意義暫時不討論,留待下個小問題中一一解答。

圖1中,後臺是如何接收到轉賬數據並執行轉賬操做的?

因爲在圖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。

另外,前兩個的typespend_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,很快找到了它的定義處:

api/api.go#L164-L244

func (a *API) buildHandler() {
    // ...
    if a.wallet != nil {
        // ...
        m.Handle("/build-transaction", jsonHandler(a.build))
        // ...
}

能夠看到它對就的方法是a.build,其代碼爲:

api/transact.go#L167-L176

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是這樣定義的:

api/request.go#L21-L26

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

api/transact.go#L101-L164

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
}

這段代碼內容仍是比較多的,但整體基本上仍是對參數進行驗證、補全和轉換,而後交給後面的方法處理。我分紅了多塊,依次講解大意:

  1. 第1處的filterAliases主要是對傳進來的參數進行驗證和補全。好比像account和asset,通常都有id和alias這兩個屬性,若是隻提交了alias而沒有提交id的話,則filterAliases就會從數據庫或者緩存中查找到相應的id補全。若是過程當中出了錯,好比alias不存在,則報錯返回
  2. 第2處的onlyHaveSpendActions是檢查若是這個交易中,只存在資金來源方,而沒有資金目標方,顯示是不對的,報錯返回
  3. 第3處的mergeActions是把請求數據中的spend_account進行分組累加,把相同account的相同asset的數量累加到一塊兒
  4. 第4處的代碼看着挺多,實際上只是把剛纔處理過的請求數據由JSON轉換成相應的Go對象。在actionDecoder(typ)裏經過手動比較type的值返回相應的Decoder
  5. 第5處的ttl是指Time To Live,指的這個請求的存活時間,若是沒指明的話(本例就沒有),則設爲默認值5分鐘
  6. 第6處就是轉交給txbuilder.Build繼續處理

在這幾處裏提到的方法和函數的代碼我就不貼出來了,由於基本上都是一些針對map的低級操做,大片大片的看着很累,實際上沒作多少事。這種類型的代碼反覆出現,在別的語言中(甚至Java)均可以抽出來不少工具方法,可是在Go裏因爲語言特性(缺乏泛型,麻煩的錯誤處理),彷佛不是很容易。看一眼廣大Go程序員的期盼:

https://github.com/golang/go/issues/15292

看看在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究竟是作什麼的呢?經過上面我分析,咱們能夠知道它有兩個做用:

  1. 一是檢查各參數是否正確。由於用戶填寫的數據不少,並且裏面的數據看起來專業性很強,容易出錯,早點發現早點提醒
  2. 二是補全一些信息,如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,咱們很快定位到如下代碼:

api/api.go#L164-L244

func (a *API) buildHandler() {
    // ...
    if a.wallet != nil {
        // ...
        m.Handle("/sign-transaction", jsonHandler(a.pseudohsmSignTemplates))
        // ...
}

/sign-transaction對應的handler是a.pseudohsmSignTemplates,讓咱們跟進去:

api/hsm.go#L53-L63

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. 第1處代碼是兩個大循環,基本上作了兩件事:
    1. 把用戶提交上來的數據中須要簽名的部分取出來,運行相關的簽名函數sw.sign,生成相關的簽名signatures
    2. raw_transaction處添加了一些操做符和約束條件,把它變成了一個合約(這塊還須要之後確認)
  2. 第2處代碼若是發現前面簽名過程正確,就調用materializeWitnesses函數。它主要是在檢查沒有數據錯誤以後,把第1步中生成的簽名signatures添加到tpl對象上去。

因爲sw.SignmaterializeWitnesses基本上都是一些算法或者合約相關的東西,咱們這裏就暫時忽略,之後再研究吧。

這個接口/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,能夠在代碼中找到:

api/api.go#L164-L244

func (a *API) buildHandler() {
    // ...
    if a.wallet != nil {
        // ...
        m.Handle("/submit-transaction", jsonHandler(a.submit))
        // ...
}

那麼/submit-transaction所對應的handler就是a.submit了。咱們跟進去:

api/transact.go#L182-L191

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)
    // ...
}

這一個方法總體上仍是各類驗證

  1. 第1處代碼是對交易對象簽名相關的內容進行嚴格的檢查,好比參數個數、簽名、甚至某些對應虛擬機的操做碼,這一塊挺複雜的,你必定不會想看blockchain/txbuilder/finalize.go#L66-L113
  2. 第2處代碼是把交易數據解碼,從看起來奇怪的16進制字符串變成正常的內容
  3. 第3處代碼是把解析出來的內容的長度賦值給tx中的某些字段
  4. 第4處代碼是對交易內容再次進行詳細的檢查,最後還包括了對gas的檢查,若是所有正常,則會把它提交到txPool(用來在內存中保存交易的對象池),等待廣播出去以及打包到區塊中。我以爲這個名字ValidateTx有點問題,由於它即包含了驗證,還包含了提交到池子中,這是兩個不一樣的操做,應該分開

這裏涉及到的更細節的代碼就不進去了,主線咱們已經有了,感興趣的同窗能夠自行進去深刻研究。

那咱們今天關於提交交易的這個小問題就算是完成了,下次會繼續研究剩下的幾個小問題。

相關文章
相關標籤/搜索