剥开比原看代码15:比原是如何转帐的

Posted bytom

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了剥开比原看代码15:比原是如何转帐的相关的知识,希望对你有一定的参考价值。

作者:freewind

比原项目仓库:

Github地址:https://github.com/Bytom/bytom

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

在前面几篇中,我们做了足够了准备,现在终于可以试一试转帐功能了!

这里的转帐最好使用solonet,再按前一篇文章的办法修改代码后产生单机测试币,然后再试。在此之前,如果需要的话,请先备份好你之前的帐户,然后删除(或重命名)你的数据目录,再使用bytomd init --chain_id=solonet重新初始化。

下面是我通过dashboard进行的转帐操作,在操作之前,我先建立了两个帐户,然后把钱从一个帐户转到另一个帐户的地址中:

新建一个交易,填上把哪个帐户的哪种资产转到某个地址上。可以看到还要消耗一定的gas:

技术分享图片

(上图为图1)

转帐成功后,如下:

技术分享图片

(上图为图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有点问题,因为它即包含了验证,还包含了提交到池子中,这是两个不同的操作,应该分开

这里涉及到的更细节的代码就不进去了,主线我们已经有了,感兴趣的同学可以自行进去深入研究。

那我们今天关于提交交易的这个小问题就算是完成了,下次会继续研究剩下的几个小问题。

以上是关于剥开比原看代码15:比原是如何转帐的的主要内容,如果未能解决你的问题,请参考以下文章

剥开比原看代码17:比原是如何显示交易的详细信息的?

剥开比原看代码13:比原是如何通过/list-balances显示帐户余额的?

剥开比原看代码16:比原是如何通过/list-transactions显示交易信息的

剥开比原看代码11:比原是如何通过接口/create-account创建帐户的

剥开比原看代码05:如何从比原节点拿到区块数据?

剥开比原看代码02:比原启动后去哪里连接别的节点