资讯专栏INFORMATION COLUMN

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

Tony / 1832人阅读

摘要:图中,转帐表单是如何把转帐数据提交到后台的由于是前端,所以我们要去从前端的代码库中寻找。继续看这段代码内容还是比较多的,但总体基本上还是对参数进行验证补全和转换,然后交给后面的方法处理。

作者:freewind

比原项目仓库:

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

Gitee地址:https://gitee.com/BytomBlockc...

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

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

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

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

(上图为图1)

转帐成功后,如下:

(上图为图2)

我们看一下这个交易的详细信息,由于太长,截成了两个图:


(上面两图合称为图3)

我们今天(以及往后的几天)就是把这一块流程搞清楚。

由于上面展示的操作还是有点多的,所以我们还是按之前的套路,先把它分解成多个小问题,一一解决:

图1中,转帐界面是如何把转帐数据提交到后台的?

图1中,后台是如何接收到转帐数据并执行转帐操作的?

图2中,前台是如何拿到后台的数据并展示出来的?

图3中,前台是如何拿到后台的数据并展示出来的?

今天的文章,我们主要是研究前两个问题,即跟图1相关的逻辑。

图1中,转帐表单是如何把转帐数据提交到后台的?

由于是前端,所以我们要去从前端的代码库中寻找。通过搜索“简单交易”这个词,我们很快定位到下面这块代码:

src/features/transactions/components/New/New.jsx#L275-L480

    return (
      
      // ...
      
    )

由于上面的代码实在太长太细节,全是一些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处代码就是对应我们“简单交易”的情况,它会调用buildPromise,这里面应该包括了对后台的访问

第2处就是buildPromise的定义,可以看到会访问/build-transaction

第3处是如果前一个访问是正常的,那么会继续调用signAndSubmitTransaction

第4处就进入到signAndSubmitTransaction内部了,可以看到,它会访问一个新的接口/sign-transaction

第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处的filterAliases主要是对传进来的参数进行验证和补全。比如像account和asset,一般都有id和alias这两个属性,如果只提交了alias而没有提交id的话,则filterAliases就会从数据库或者缓存中查找到相应的id补全。如果过程中出了错,比如alias不存在,则报错返回

第2处的onlyHaveSpendActions是检查如果这个交易中,只存在资金来源方,而没有资金目标方,显示是不对的,报错返回

第3处的mergeActions是把请求数据中的spend_account进行分组累加,把相同account的相同asset的数量累加到一起

第4处的代码看着挺多,实际上只是把刚才处理过的请求数据由JSON转换成相应的Go对象。在actionDecoder(typ)里通过手动比较type的值返回相应的Decoder

第5处的ttl是指Time To Live,指的这个请求的存活时间,如果没指明的话(本例就没有),则设为默认值5分钟

第6处就是转交给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,我们很快定位到以下代码:

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处代码是两个大循环,基本上做了两件事:

把用户提交上来的数据中需要签名的部分取出来,运行相关的签名函数sw.sign,生成相关的签名signatures

raw_transaction处添加了一些操作符和约束条件,把它变成了一个合约(这块还需要以后确认)

第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处代码是对交易对象签名相关的内容进行严格的检查,比如参数个数、签名、甚至某些对应虚拟机的操作码,这一块挺复杂的,你一定不会想看blockchain/txbuilder/finalize.go#L66-L113

第2处代码是把交易数据解码,从看起来奇怪的16进制字符串变成正常的内容

第3处代码是把解析出来的内容的长度赋值给tx中的某些字段

第4处代码是对交易内容再次进行详细的检查,最后还包括了对gas的检查,如果全部正常,则会把它提交到txPool(用来在内存中保存交易的对象池),等待广播出去以及打包到区块中。我觉得这个名字ValidateTx有点问题,因为它即包含了验证,还包含了提交到池子中,这是两个不同的操作,应该分开

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

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

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/24177.html

相关文章

  • 剥开原看代码12:比原如何通过/create-account-receiver创建地址的?

    摘要:继续看生成地址的方法由于这个方法里传过来的是而不是对象,所以还需要再用查一遍,然后,再调用这个私有方法创建地址该方法可以分成部分在第块中主要关注的是返回值。 作者:freewind 比原项目仓库: Github地址:https://github.com/Bytom/bytom Gitee地址:https://gitee.com/BytomBlockc... 在比原的dashboard中...

    oneasp 评论0 收藏0
  • 剥开原看代码03:比原如何监听p2p端口的

    摘要:启动直到进入所以我们首先需要知道,比原在源代码中是如何启动,并且一步步走进了的世界。后面省略了一些代码,主要是用来获取当前监听的实际以及外网,并记录在日志中。 比原是如何监听p2p端口的 我们知道,在使用bytomd init --chain_id mainnet/testnet/solonet初始化比原的时候,它会根据给定的chain_id的不同,使用不同的端口(参看config/t...

    layman 评论0 收藏0
  • 剥开原看代码11:比原如何通过接口/create-account创建帐户的

    摘要:而本文将继续讨论,比原是如何通过接口来创建帐户的。把各信息打包在一起,称之为另外,在第处还是一个需要注意的。比原在代码中使用它保存各种数据,比如区块帐户等。到这里,我们已经差不多清楚了比原的是如何根据用户提交的参数来创建帐户的。 作者:freewind 比原项目仓库: Github地址:https://github.com/Bytom/bytom Gitee地址:https://git...

    haobowd 评论0 收藏0
  • 剥开原看代码06:比原如何把请求区块数据的信息发出去的

    摘要:作者比原项目仓库地址地址在前一篇中,我们说到,当比原向其它节点请求区块数据时,会发送一个把需要的区块告诉对方,并把该信息对应的二进制数据放入对应的通道中,等待发送。这个就是真正与连接对象绑定的一个缓存区,写入到它里面的数据,会被发送出去。 作者:freewind 比原项目仓库: Github地址:https://github.com/Bytom/bytom Gitee地址:https:...

    CloudwiseAPM 评论0 收藏0
  • 剥开原看代码10:比原如何通过/create-key接口创建密钥的

    摘要:如果传的是,就会在内部使用默认的随机数生成器生成随机数并生成密钥。使用的是,生成的是一个形如这样的全球唯一的随机数把密钥以文件形式保存在硬盘上。 作者:freewind 比原项目仓库: Github地址:https://github.com/Bytom/bytom Gitee地址:https://gitee.com/BytomBlockc... 在前一篇,我们探讨了从浏览器的dashb...

    ccj659 评论0 收藏0

发表评论

0条评论

最新活动
阅读需要支付1元查看
<