CKB 交易驗證生命週期
RPC
首先,傳送方會構造一筆交易,透過 RPC 提交。交易由提交到的 outputs_validator (從 0.27.0 版本引入)進行驗證。
預設的驗證邏輯包括檢查各種東西:
transaction.outputs.all{ |output|
let script = output.script
(script.code_hash == secp256k1_blake160_sighash_all && script.hash_type == "type" && script.args.size == 20) ||
(script.code_hash == secp256k1_blake160_multisig_all && script.hash_type == "type" && (script.args.size == 20 || (script.args.size == 28 && script.args[20..28].is_valid_since_format))
}
transaction.outputs.all{ |output|
let script = output.type
script.is_null || script.code_hash == dao && script.hash_type == "type"
|| (script.has_lock_period() && since.is_absolute())
}
此驗證旨在防止格式不正確的交易,例如在 Common Gotchas 中提到的交易。
https://github.com/nervosnetwork/ckb/wiki/Common-Gotchas#nervos-dao
此外,可以將其配置為 passthrough 以跳過此驗證。
交易提交到本地節點後,節點還會輸出交易 id,您可以使用該 id 跟蹤交易的狀態。
驗證
在廣播交易並進入 mempool 之前,交易將在本地驗證和執行。
步驟 1——Resolve
本質上,交易 input 只是指標,如下所示:
struct OutPoint {
tx_hash: Byte32,
index: Uint32,
}
我們在交易執行之前透過指標收集引用的資料,這個過程稱為「解析交易」。我們還需要檢查這個交易的所有輸入都是有效的(沒有重複或雙花)。
步驟 2——驗證
驗證步驟需要檢查如下要素:
1、版本(目前必須是 0)
2、serialized_size 必須小於如下限制:
pub fn serialized_size(&self) -> usize { // the offset in TransactionVec header is u32
self.as_slice().len() + molecule::NUMBER_SIZE
// molecule::NUMBER_SIZE = size_of::<u32>() 4
}
3、inputs 不是空的
inputs().is_empty() || outputs().is_empty()
4、inputs 是成熟的
對於每個 input 和 dep,如果引用的 output 交易是 cellbase,那麼它必須至少經過 4 個 epoch 確認。
5、capacity
input capacity 的和必須小於或等於 output capacity
6、duplicate_deps
deps 不能重複
7、outputs_data_verifier
「output data」欄位的數量必須等於 outputs 的數量
8、since
since 值必須遵循 RFC:Transaction valid since
https://github.com/nervosnetwork/rfcs/blob/master/rfcs/0017-tx-valid-since/0017-tx-valid-since.md
CKB VM 將執行交易指令碼,並輸出所消耗的 cycles 個數。
向網路廣播
如果驗證成功,當前節點將交易(帶有 cycles 值)廣播給它的所有對等節點(它所連線的節點)。
在驗證失敗的情況下,將不再廣播交易。交易流經各個「完整節點」,這些節點重複前面步驟中描述的驗證過程,並檢查 cycle 值是否與驗證交易時使用的實際 cycle 相匹配。
Tx-pool
CKB 使用 two-step 進行交易確認。交易將在 tx-pool 中劃分為不同的狀態(pending 狀態和 proposed 狀態)。當一個塊上鍊時,交易的狀態將改變。當最新的塊更改時,將重新掃描 tx-pool 中的所有交易,以確保它們仍然有效。
BlockAssembler 將從 pending 池和 proposed 池中為塊模板獲取 proposal 和交易,詳細可參考:Two-Step Transaction Confirmation:
https://github.com/nervosnetwork/rfcs/blob/master/rfcs/0020-ckb-consensus-protocol/0020-ckb-consensus-protocol.md#two-step-transaction-confirmation
CKB VM 驗證規則
在編寫程式時,確定操作環境和執行時行為非常重要,以便程式的執行結構儘可能接近程式建立者的期望。例如:在編寫 Python 程式時,需瞭解 Python 中 GIL 的影響、硬體指令的預期執行時間、流水線規劃等。
我們都知道 CKB VM 是基於 RISC-V 指令集開發的虛擬環境。下面,我們將介紹在 CKB 驗證期間 VM 的環境,包括提供的一些系統呼叫等相關背景知識。
環境
在 CKB 中,每個交易都是單獨執行的,即每個交易都有自己獨立的 VM 環境。雖然執行了多交易並行驗證,但是 VM 內部沒有多執行緒環境。
執行單元
當每個單獨的交易進行驗證時,指令碼將首先被分成 group,然後以 script group 的單位順序執行。每個 group 都是將具有相同指令碼 hash 的交易分組在一起而建立的。請注意,output 的 lock script 將不會在交易驗證期間執行。
無論執行哪個 script group,script 都可以在執行期間訪問整個交易資料。這種設計的一個優點是 script 記錄屬於當前 script 的cell 的索引。這相當於刪除交易資料並驗證相同的 lock script 或 type script,但只需執行一次即可完成多個 cell 的驗證。減少了驗證資源的消耗,併為交易的 data set 提供了公有環境。
程式碼如下:
class ScriptGroup:
def __init__(self, script):
self.script = script
self.input_indices = []
self.output_indices = []
def split_group(tx):
lock_groups: Dict[Hash, ScriptGroup] = dict()
type_groups: Dict[Hash, ScriptGroup] = dict()
for index, input in enumerate(tx.inputs):
if lock_groups.get(hash(input.lock)):
lock_groups.get(hash(input.lock)).input_indices.append(index)
else:
script_group = ScirptGroup(input.lock)
script_group.input_indices.append(index)
lock_groups[hash(input.lock)] = script_group
if input.type:
if type_groups.get(hash(input.type)):
type_groups.get(hash(input.type)).input_indices.append(index)
else:
script_group = ScriptGroup(input.type)
script_group.input_indices.append(index)
type_groups[hash(input.type)] = script_group
for index, output in enumerate(tx.outputs):
if output.type:
if type_groups.get(hash(input.type)):
type_groups.get(hash(input.type)).output_indices.append(index)
else:
script_group = ScriptGroup(input.type)
script_group.output_indices.append(index)
type_groups[hash(input.type)] = script_group
return list(lock_groups.values()) + list(type_groups.values())
def run():
for group in split_group(tx):
if vm_run(group) != 0:
return error()
當執行每個 script group 時,將記錄 script 的執行成本,並將所有資源消耗的總和與允許的 max_block_cycles 上限進行比較。
假設有一個交易:
Transaction {
input: [cell_1 {lock: A, type: B}, cell _2 {lock: A, type: B}, cell_3 {lock: C, type: None}]
output: [cell_4 {lock: D, type: B}, cell_5 {lock: C, type: B}, cell_6 {lock: G, type: None}, cell_7(lock: A, type: F)]
}
它將按這樣的方式分組:
[
group(A, input:[0, 1], output:[]),
group(C, input:[2], output:[]),
group(B, input:[0, 1], output:[0, 1]),
group(F, input:[], output:[3])
]
VM 的 syscall 可以透過 group(input/output index) 載入這些對應的 cell,以完成一次性驗證。
CKB 將執行所有根據返回值驗證的 script group。此處遵循類 unix 系統中程序退出狀態的約定:返回值為 0 代表驗證透過,而其他返回值代表驗證異常。
請注意:在執行 script 時,script 不知道它是 type script 還是 lock script。script 需要透過檢查 args 或 witness data 來自己解決這個問題。
特定規則
除了一種型別的合約,即 TypeId 合約之外,大多數合約都是如上所述進行驗證的。TypeId 合約使用特殊的規則,直接在指令碼程式碼中編寫,並且不啟動 VM。有關更多資訊,請參見程式碼:
https://github.com/nervosnetwork/ckb/blob/44b0d3595c31a29aef81e74360ba8613cd0dd27f/script/src/type_id.rs
Syscall
關於 Syscall 的內容,請參考:RFC:VM Syscalls 和相關程式碼:
https://github.com/nervosnetwork/rfcs/blob/master/rfcs/0009-vm-syscalls/0009-vm-syscalls.md
https://github.com/nervosnetwork/ckb-system-scripts/blob/865f4d7697cc979d62111e49f2fb12a3607a4eb9/c/ckb_syscalls.h