组建交易的数据

交易一般从我们的钱包(比如MetaMask)开始,比如用户A想发送一笔转账到用户B,在和钱包的交互过程中用户只需要输入用户B的链上地址0xBBB,转账的金额Amount,点击确认为这笔交易签名就可以像节点发送交易信息了。 在这个过程中钱包为用户默认设置了很多参数来减少用户交互的复杂度,从vechain L1我们深入理解下具体的一笔交易的参数:

    body = {
        "chainTag": 246,                    # 主网固定 74 (十进制),testnet 是 39
        "blockRef": block_ref,               # 8字节 hex
        "expiration": 720,                   # 720个块后过期 ≈ 2小时
        "clauses": [
            {
                "to": TO_ADDRESS,
                "value": AMOUNT_WEI,         # wei 单位
                "data": "0x"                 # 普通转账填 0x
            }
        ],
        "gasPriceCoef": 128,                 # 0-255,128 是标准
        "gas": 21000,                        # 普通转账固定 21000
        "dependsOn": None,
        "nonce": int(time.time() * 1000)     # 随意,但不能重复(毫秒时间戳就行)
    }
  1. blockRef 在 VeChain 交易里是一个非常重要的字段,作用是防止重放攻击指定这笔交易最早可以被哪个区块打包。
  2. chainTag是根据测试或者主网的固定参数。
  3. clauses 是 VET 或 VTHO 交易的核心概念,它代表这笔交易里包含的一个或多个子操作。
  4. gasPriceCoef 是 VeChain 独有的一个字段,它决定了你这笔交易的 实际 gas 价格 是当前网络基础 gas 价格的多少倍。
  5. VeChain 里这个 gas 字段的作用是设置最大愿意消耗的 gas 数量上限。转账的时候设置21000,需要调用合约进行复杂的操作的时候需要提高上限来保证交易不会因为gas限制而revert。

    # 3. 用 thor-devkit 构建并签名交易
    tx = transaction.Transaction(body)
    
    private_key_bytes = bytes.fromhex(PRIVATE_KEY_HEX)
    signature = secp256k1.sign(tx.get_signing_hash(), private_key_bytes)
    
    tx.set_signature(signature)

    # 4. 得到 raw(发送用的十六进制字符串)
    raw = "0x" + tx.encode().hex()
    print("Raw transaction:")
    print(raw)
    print()

    # 5. 发送交易
    send_resp = requests.post(
        NODE_URL + "/transactions",
        json={"raw": raw}
    )
    
    if send_resp.status_code == 200:
        txid = send_resp.json()["id"]
        print("交易已广播!")
        print(f"TxID: {txid}")
    else:
        print("发送失败:")
        print(send_resp.status_code, send_resp.text)

我们使用python对数据打包签名,然后选择一个共用的节点,调用节点上的/transactions路由,发送raw数据就可以发送交易数据进行广播了。

tx_pool收到数据

在api/transactions.go中的handleSendTransaction函数负责来处理这些交易的请求:

func (t *Transactions) handleSendTransaction(w http.ResponseWriter, req *http.Request) error {
	var rawTx *api.RawTx
	if err := restutil.ParseJSON(req.Body, &rawTx); err != nil {
		return restutil.BadRequest(errors.WithMessage(err, "body"))
	}
	tx, err := rawTx.Decode()
	if err != nil {
		return restutil.BadRequest(errors.WithMessage(err, "raw"))
	}

	if err := t.pool.AddLocal(tx); err != nil { <@ add to tx_pool
		if txpool.IsBadTx(err) {
			return restutil.BadRequest(err)
		}
		if txpool.IsTxRejected(err) {
			return restutil.Forbidden(err)
		}
		return err
	}
	txID := tx.ID()
	return restutil.WriteJSON(w, &api.SendTxResult{ID: &txID})
}

通过AddLocal将交易数据存储的内存中的交易池中。 vechain上有黑名单列表,在这个过程中会检查origin也就是交易的发起方是否在黑名单列表之中,在列表中的地址就不进行后续处理了。 主要的流程:

  1. ChainTag验证 , 交易的大小验证。
  2. gas费用验证。
  3. 交易信息是否过期验证。
  4. 最早可打包区块(blockRef)验证,这个值需要在5分钟之内。

如果验证都通过了证明在这个阶段这个交易是可执行的,交易信息就会被放进tx pool中等待被执行。

packer打包

// Run runs the packer for solo
func (s *Solo) Run(ctx context.Context) error {
	goes := &co.Goes{}

    ...
    
	goes.Go(func() {
		s.loop(ctx)
	})

	return nil
}

func (s *Solo) loop(ctx context.Context) {
	for {
		select {
		case <-ctx.Done():
			logger.Info("stopping interval packing service......")
			return
		case <-time.After(time.Duration(1) * time.Second):
			if left := uint64(time.Now().Unix()) % s.options.BlockInterval; left == 0 {
				if txs, err := s.core.Pack(s.txPool.Executables(), false); err != nil {
					logger.Error("failed to pack block", "err", err)
				} else {
					for _, tx := range txs {
						s.txPool.Remove(tx.Hash(), tx.ID())
					}
				}
			}
		}
	}
}

在main函数中会通过cli的方式启动这个Run函数,run函数调用Loop,可以看到这是通过协程的方式启动的。 loop会一直执行循环知道收到ctx.Done()的推出指令。这个time.After在间隔1秒之后就会被调用,也每1秒执行一次这个s.core.Pack()函数。 s.core.Pack()还是会进行那些基础的验证比如:

  1. ChainTag
  2. BlockRef是否大于当前block.number
  3. 交易是否已经过期
  4. 当前的flow中总的gas使用量是否超出了 f.runtime.Context().GasLimit的限制

当检测都通过之后Runtime.ExecuteTransaction()函数就会被调用:

// ExecuteTransaction executes a transaction.
// If some clause failed, receipt.Outputs will be nil and vmOutputs may shorter than clause count.
func (rt *Runtime) ExecuteTransaction(tx *tx.Transaction) (receipt *tx.Receipt, err error) {
	executor, err := rt.PrepareTransaction(tx)
	if err != nil {
		return nil, err
	}
	for executor.HasNextClause() {
		exec, _ := executor.PrepareNext()
		if _, _, err := exec(); err != nil {
			return nil, err
		}
	}
	return executor.Finalize()
}

所有可执行的tx列表就会被放进:

flow.Pack(genesis.DevAccounts()[0].PrivateKey, 0, false)

来生成一个新的区块。新的区块内容会被广播到各个节点,然后进入到BFT的共识阶段。