V神·以太坊上的分片
譯者序
2017年8月,比特幣網路進行了硬分叉,產生了比特幣現金(Bitcoin Cash),這個硬分叉的技術解釋就是對比特幣網路進行“擴容”。比特幣現金網路中的區塊大小為8M,是比特幣網路區塊大小的8倍(比特幣網路區塊大小為1M),從而提高了每個區塊的交易容量,反映為網路整體吞吐量的提高。而其他使用了與比特幣網路類似的資料儲存形式的加密貨幣網路,也必將伴隨交易量的增加逐漸開始需要面對“擴容”的問題。“分片”(Sharding)就是以太坊網路為了解決擴容問題而設計的一種技術方案。
“分片”的大致設計思路是:將區塊鏈網路中的每個區塊變為一個子區塊鏈,子區塊鏈中可以容納若干(目前為100個)打包了交易資料的Collation(大概可以稱為“校驗塊”,為了在分片的情景中將其與區塊的概念區分開),這些Collation最終組成一個在主鏈上區塊;因為這些Collation是整體作為區塊存在的,所以其資料必定是全部由某個特定的礦工所打包生成,本質上和現有協議中的區塊沒有區別,所以不再需要增加額外的網路確認。這樣,每個區塊的交易容量就大概擴大了100倍;而且這種設計還有利於未來的繼續擴充套件,整個擴充套件計劃目前也被大致分為4個階段;本文所介紹的僅僅是第一階段的相關實現細節。
這是一篇關於以太坊的“分片”技術改進的細節說明文件,供以太坊開發者和有興趣的技術同行參考。
(編者注:本文於 2018 年 6 月 12 日經過重新編輯,同步了譯者在 5 月 18 日完成的新譯本。)
目錄
序言
本文的目的是為那些希望理解分片建議詳情,乃至去實現它的朋友提供一份相對完整的細節說明和介紹。本文僅作為二次方分片(quadratic sharding)的第一階段的描述;第二、三、四階段目前不在討論範圍,同樣,超級二次方分片(super-quadratic sharding)(“Ethereum 3.0”) 也不在討論範圍。
假設用變數 c
來表示一個節點的有效計算能力,那麼在一個普通的區塊鏈裡,交易容量就被限定為 O(c),因為每個節點都必須處理所有的交易。二次方分片的目的,就是通過一種雙層的設計來增加交易容量。第一層不需要硬分叉,主鏈就保持原樣。不過,一個叫做 校驗器管理和約
分片執行在一個普通的符合最長鏈規則的權益證明系統中,權益資料將儲存在主鏈上(具體來說,是在 VMC 中)。所有分片共享一個通用驗證器池,這也意味著:任何通過 VMC 註冊的驗證器,理論上都可以在任意時間被授權來在任意分片上建立區塊。每個分片會有一個 O(c) 的區塊大小 / gas 上限(block size/gas limit),這樣,系統的整體容量就變成了 O(c^2) 。
分片系統中的大多數使用者都會執行兩部分程式。(i) 一個在主鏈上的全節點(需要 O(c) 資源)或輕量節點(需要 O(log(c)) 資源)。 (ii) 一個通過 RPC 與主鏈互動的“分片客戶端”(由於這個客戶端同樣執行在當前使用者的計算機中,所以它被認為是可信的);它也可以作為任意分片的輕客戶端、作為特定分片的全客戶端(使用者需要指定他們正在“監視”某個特定的分片),或者作為一個驗證器節點。在這些情況下,一個分片客戶端的儲存和計算需求也將不會超過 O(c) (除非使用者指定他們正在監視 每個 分片;區塊瀏覽器和大型的交易所可能會這麼做)。
在本文中,術語 Collation
被用來與 Block
(區塊)相區別,因為: (i) 它們是不同的 RLP(Recursive Length Prefix)物件:交易是第 0 層的物件,collation 是用來打包交易的第一層的物件,而 block 則是用來打包 collation(header)的第二層的物件; (ii) 在分片的情景中這更加清晰。通常,Collation
必須由 CollationHeader
和 TransactionList
(交易列表)組成;Collation
的詳細格式和 Witness
(見證人)會在 無狀態客戶端 那節定義。Collator
(即用來打包 transaction 生成 collation 的某個地址,譯者注)是由主鏈上 驗證器管理合約 的 getEligibleProposer
函式所生成的示例。演算法會在隨後的章節中介紹。
Main Chain | Shard Chain |
---|---|
Block | Collation |
BlockHeader | CollationHeader |
Block Proposer (or Miner in PoW chain) |
Collator |
二次方分片(Quadratic Sharding)
常量
LOOKAHEAD_PERIODS
: 4PERIOD_LENGTH
: 5COLLATION_GASLIMIT
: 10,000,000 gasSHARD_COUNT
: 100SIG_GASLIMIT
: 40000 gasCOLLATOR_REWARD
: 0.001 ETH
驗證器管理合約(Validator Manager Contract,VMC)
我們假定 VMC 存在於地址 VALIDATOR_MANAGER_ADDRESS
上(在已有的“主分片”上),它支援下列函式:
deposit() returns uint256
:新增一個驗證器到驗證器集合中,驗證器的大小就是函式呼叫時的msg.value
(也就是存入的以太幣數量)。這個函式會返回驗證器的索引序號。withdraw(uint256 validator_index) returns bool
:驗證msg.sender == validators[validator_index].addr
,如果相等,它會將驗證器從驗證器集合中移除,並退還存入的以太幣。get_eligible_proposer(uint256 shard_id, uint256 period) returns address
:使用一個區塊雜湊(block hash)作為種子,基於預設的演算法從驗證器集合中選擇一個簽名者(signer)。驗證器被選中機率應該與其存款數量成正比。這個函式應該可以返回一個當前週期內的值或者以LOOKAHEAD_PERIODS
為上限的任意未來週期內的值。add_header(uint256 shard_id, uint256 expected_period_number, bytes32 period_start_prevhash, bytes32 parent_hash, bytes32 transaction_root, address coinbase, bytes32 state_root, bytes32 receipt_root, uint256 number) returns bool
:嘗試處理一個 collation header,成功時返回 true,失敗時返回 false。get_shard_head(uint256 shard_id) returns bytes32
:返回驗證器管理合約內由引數所指定的分片的 header 雜湊。
這裡也有一個日誌型別:
CollationAdded(indexed uint256 shard_id, bytes collation_header_bytes, bool is_new_head, uint256 score)
其中的 collation_header_bytes
可以用 vyper 語言來構造:
collation_header_bytes = concat(
as_bytes32(shard_id),
as_bytes32(expected_period_number),
period_start_prevhash,
parent_hash,
transaction_root,
as_bytes32(collation_coinbase),
state_root,
receipt_root,
as_bytes32(collation_number),
)
注意:因為 coinbase
和 number
在 vyper 語言中是保留字,所以它們被重新命名為 collation_coinbase
和 collation_number
。
Collation Header
我們首先以一個有下列內容的 RLP 列表來定義一個“collation header”:
[
shard_id: uint256,
expected_period_number: uint256,
period_start_prevhash: bytes32,
parent_hash: bytes32,
transaction_root: bytes32,
coinbase: address,
state_root: bytes32,
receipt_root: bytes32,
number: uint256,
]
這裡:
shard_id
分片的ID;expected_period_number
是 collation 希望被包含進的週期序號,這是由period_number = floor(block.number / PERIOD_LENGTH)
計算出來的;period_start_prevhash
前一區塊,即區塊PERIOD_LENGTH * expected_period_number - 1
的區塊雜湊(這其實就是希望被包含進的週期起始區塊之前的最後一個區塊的雜湊)。分片中使用區塊資料的操作碼(例如 NUMBER 和 DIFFICULTY)會使用這個區塊的資料;只有 COINBASE 操作碼會使用分片的 coinbase;parent_hash
是父 collation 的雜湊;transaction_root
是包含了當前 collation 中的所有交易資料的樹(trie)的根雜湊;state_root
是分片中當前 collation 之後的新狀態根;(也就是當前 collation 中包含的所有交易執行之後,且記賬收益分配之後得到的狀態樹根節點雜湊,譯者注)receipt_root
是收據樹(receipt trie)根雜湊;number
是 collation 編號,現在也是分叉選擇規則中的分值;且
當 addHeader(header)
的呼叫返回 true 時, collation header 有效。驗證器管理合約會在滿足下列條件時這麼做:
shard_id
是 0 到SHARD_COUNT
之間的數值;expected_period_number
與當前週期號相等(即floor(block.number / PERIOD_LENGTH)
)- 在相同的分片中,一個具有
parent_hash
的 collation 已經被接受;(即當前 collation 的父 collation 已經被接受,譯者注) - 在相同分片中,當前週期內還沒有一個同樣的 collation 被提交;(也就是檢查要新增的 collation 是否已經新增過了,譯者注)
add_header
函式呼叫者的地址與get_eligible_proposer(shard_id, expected_period_number)
所返回的地址相同。(即判斷要新增這個 collation 的 proposer 是否是給定分片、給定週期的合法記賬人,譯者注)
當滿足以下條件時, collation 有效: (i) 它的“collation header”有效; (ii) 在 parent_hash
的 state_root
上執行 collation 的結果為給定的 state_root
和 receipt_root
;並且 (iii) 總共使用的 gas 小於等於 COLLATION_GASLIMIT
。
Collation 狀態轉換函式
執行一個 collation 時的狀態轉換處理如下:
- 按順序執行由
transaction_root
所指定的樹上的每個交易;並且 - 將
COLLATOR_REWARD
的獎勵分配給 coinbase。
`getEligibleProposer` 的細節
這裡是用 Viper 寫的一個簡單實現:
def getEligibleProposer(shardId: num, period: num) -> address:
assert period >= LOOKAHEAD_PERIODS
assert (period - LOOKAHEAD_PERIODS) * PERIOD_LENGTH < block.number
assert self.num_validators > 0
h = as_num256(
sha3(
concat(
blockhash((period - LOOKAHEAD_PERIODS) * PERIOD_LENGTH),
as_bytes32(shardId)
)
)
)
return self.validators[
as_num128(
num256_mod(
h,
as_num256(self.num_validators)
)
)
].addr
無狀態客戶端(Stateless Clients)
當驗證器被要求在一個給定的分片上建立區塊時,一個驗證器僅會被給予數分鐘的通知(準確地說,就是持續 LOOKAHEAD_PERIODS * PERIOD_LENGTH
個區塊的通知)。在 Ethereum 1.0 中,建立一個區塊需要為驗證交易而訪問全部的狀態。這裡,我們的目標是避免需要驗證器保留整個系統的狀態(因為這樣就將使運算資源需求變為 O(c^2) 了)。取而代之,我們允許驗證器在僅知曉根狀態(state root)的情況下建立 collation,而將其他責任交給交易傳送者,由他們提供“見證資料”(witness data)(也就是 Merkle 分支),以此來驗證交易對賬戶產生影響的“前狀態”(pre-state),並提供足夠的資訊來計算交易執行後的“後狀態根”(post-state root)。
(應該注意到,使用非無狀態正規化(non-stateless paradigm)來實現分片,理論上是可能的;然而,這需要: (i) 租用儲存空間來保持儲存的有界性;並且 (ii) 驗證器需要使用 O(c) 的時間在一個分片中建立區塊。上述方案避免了對這些犧牲的需求。)
資料格式
我們修改了交易的格式,以使交易必須指定一個 訪問列表 來列舉出它可以訪問的狀態(後邊我們會更精確的描述這點,這裡不妨把它想象為是一個地址列表)。任何在 VM 執行過程中試圖讀寫交易所指定的訪問列表以外的狀態,都會返回一個錯誤。這可以防止這樣的攻擊:某人傳送了一個消耗 500 萬 gas 的隨機執行,然後試圖訪問一個交易傳送者和 collator 都沒有見證人的隨機賬戶;也可以防止 collator 包含進像這樣浪費 collator 時間的交易。
交易傳送者必須指定“見證人”(witness),這在被簽名的交易體 之外 ,但也被打包進交易。這裡的見證人是一個 Merkle 樹節點的 RLP 編碼的列表(RLP-encoded list),它是由交易在其訪問列表中所指定的狀態的組成部分。這使 collator 僅使用狀態根就可以處理交易。在釋出 collation 的時候,collator 也會發送整個 collation 的見證人。
交易打包格式
[
[nonce, acct, data....], # transaction body (see below for specification)
[node1, node2, node3....] # witness
]
Collation格式
[
[shard_id, ... , sig], # header
[tx1, tx2 ...], # transaction list
[node1, node2, node3...] # witness
]
也請參考 ethresearch 上的帖子 無狀態客戶端的概念 。
無狀態客戶端狀態轉換函式
通常,我們可以將傳統的“有狀態”客戶端執行狀態轉換的函式描述為: stf(state, tx) -> state'
(或 stf(state, block) -> state'
)。在無狀態客戶端模型中,節點不儲存狀態,所以 apply_transaction
和 apply_block
可以寫為:
apply_block(state_obj, witness, block) -> state_obj', reads, writes
這裡,state_obj
是一個數據元組,包含了狀態根和其他 O(1) 大小的狀態資料(已使用的 gas、receipts、bloom filter 等等);witness
就是見證人;block
就是區塊的餘下部分。其返回的輸出是:
- 一個新的
state_obj
包含了新的狀態根和其他變數; - 從見證人那裡讀取的物件集合(用於區塊建立);和
- 為了組成新的狀態樹(state trie)而被建立的一組新的狀態物件。
這使得函式是“單純性的”(pure),僅處理小尺寸物件(small-sized objects)(相反的例子就是現行的以太坊狀態資料,現在已經 數百G位元組 ),從而使他們可以方便地在分片中使用。
客戶端邏輯
一個客戶端應該有一個如下格式的配置:
{
validator_address: "0x..." OR null,
watching: [list of shard IDs],
...
}
如果指定了 validator 地址,那麼客戶端會在主鏈上檢查這個地址是否是有效的 validator。如果是,那麼在每次在主鏈上開始一個新週期時(例如,當 floor(block.number / PERIOD_LENGTH)
變化的時候),客戶端將為所有分片的週期 floor(block.number / PERIOD_LENGTH) + LOOKAHEAD_PERIODS
呼叫 getEligibleProposer
。如果這個呼叫返回了某個分片 i
的驗證器地址,客戶端會執行演算法 CREATE_COLLATION(i)
(參考下文)。
對於 watching
列表中的每個分片 i
,每當一個新 collation header 出現在主鏈上,它就會從分片網路中下載完整的 collation,並對其進行校驗。它將內部保持追蹤所有有效的 header(這裡的有效性是回溯的,也就是說,一個 header 如果是有效的,那麼他的父 header 也應該是有效的),並且將 head 具有最高得分的分片連結受為主分片鏈,同時從創世(genesis)collation 到 head 的所有 collation 都是有效的和可用的。注意,這表示主鏈的重組 和 分片鏈的重組都將影響分片的 head。
逆向匹配候選 head
為了實現監視分片的演算法和建立 collation,我們要做的第一件事就是使用下面的演算法來按由高到低的順序匹配候選 head。首先,假設存在一個(非單純的、有狀態的)方法 getNextLog()
,它可以取得某個還沒有被匹配的給定分片的最新的 CollationAdded
日誌。這可以通過逆向匹配最新的區塊的所有日誌來達成,即從 head 開始,反方向掃描 receipt 中的每個區塊。我們定義一個有狀態的方法 fetch_candidate_head
:
unchecked_logs = []
current_checking_score = None
def fetch_candidate_head():
# Try to return a log that has the score that we are checking for,
# checking in order of oldest to most recent.
for i in range(len(unchecked_logs)-1, -1, -1):
if unchecked_logs[i].score == current_checking_score:
return unchecked_logs.pop(i)
# If no further recorded but unchecked logs exist, go to the next
# isNewHead = true log
while 1:
unchecked_logs.append(getNextLog())
if unchecked_logs[-1].isNewHead is True:
break
o = unchecked_logs.pop()
current_checking_score = o.score
return o
用普通的語言重新表述,這裡就是反向掃描 CollationAdded
日誌(對正確的分片),直到獲得一個 isNewHead = True
的日誌。首先返回那個日誌,然後用從老到新的順序返回所有與那個日誌分值相同的且 isNewHead = False
的所有最新日誌。隨後到前一個 isNewHead = True
的日誌(即確保分值會比前一個 NewHead 低,但比其他人高),再到這個日誌之後的所有具有該分值的最新 collation,而後到第四個。
這就是說這個演算法確保了首先按照分值的由高到低、然後按照從老到新的順序檢查潛在的候選 head。
例如,假定 CollationAdded
日誌具有以下雜湊和分值:
... 10 11 12 11 13 14 15 11 12 13 14 12 13 14 15 16 17 18 19 16
那麼,isNewHead
將被按如下賦值:
... T T T F T T T F F F F F F F F T T T T F
如果我們將 collation 命名為 A1..A5、 B1..B5、 C1..C5 和 D1..D5 ,那麼精確的返回順序將是:
D4 D3 D2 D1 D5 B2 C5 B1 C1 C4 A5 B5 C3 A3 B4 C2 A2 A4 B3 A1
監視一個分片
如果一個客戶端在監視一個分片,它應該去嘗試下載和校驗那個分片中的所有 collation(檢查任何給定的 collation,僅當其父 collation 已經被校驗過)。要取得 head,需要持續呼叫 fetch_candidate_head()
,直到它返回一個被校驗過的 collation,也就是 head。通常情況下它會立即返回一個有效的 collation,或者最多因為網路延遲或小規模的攻擊導致生成過幾個無效或者不可用的 collation,而需要稍微嘗試幾次。只有在遭遇一個真正長時間執行的 51% 攻擊時,這個演算法會惡化到 O(N) 的時間。
`CREATE_COLLATION`
這個處理由三部分組成,第一部分可以被叫做 GUESS_HEAD(shard_id)
,其示意程式碼如下:
# Download a single collation and check if it is valid or invalid (memoized)
validity_cache = {}
def memoized_fetch_and_verify_collation(c):
if c.hash not in validity_cache:
validity_cache[c.hash] = fetch_and_verify_collation(c)
return validity_cache[c.hash]
def main(shard_id):
head = None
while 1:
head = fetch_candidate_head(shard_id)
c = head
while 1:
if not memoized_fetch_and_verify_collation(c):
break
c = get_parent(c)
fetch_and_verify_collation(c)
包含了從分片網路取得 c
的所有資料(包括見證人資訊)並校驗它們的處理。上述演算法等價於“選取最長有效鏈,儘可能的檢查有效性,如果其資料無效,則轉而處理已知的次長鏈”。這個演算法應該僅當校驗器執行超時時才會停止,這就是該建立 collation 的時候了。每個 fetch_and_verify_collation
的執行都應該返回一個“寫集合”(參考上文的“無狀態客戶端”那節)。儲存所有這些“寫集合”,把它們組合在一起,就構成了 recent_trie_nodes_db
。
我們現在可以來定義 UPDATE_WITNESS(tx, recent_trie_nodes_db)
了。在執行 GUESS_HEAD
的過程中,某節點會接收到一些交易。當它要把交易(嘗試)包含進 collation 的時候,這個演算法需要先執行交易。假定交易有一個訪問列表 [A1 ... An]
和一個見證人 W
,對於每個 Ai
使用當前狀態樹的根取得 Ai
的 Merkle 分支,使用 recent_trie_nodes_db
和 W
一起作為資料庫。如果原始的 W
正確,並且交易不是在客戶端做這些檢查之前就已經發出的話,那麼這個取得 Merkle 分支的操作總是會成功的。在將交易包含進 collation 之後,狀態變動的“寫集合”也應該被新增到 recent_trie_nodes_db
中。
下面我們就要來 CREATE_COLLATION
了。作為例證,這裡是這個方法中可能的、收集交易資訊處理的完整示意程式碼。
# Sort by descending order of gasprice
txpool = sorted(copy(available_transactions), key=-tx.gasprice)
collation = new Collation(...)
while len(txpool) > 0:
# Remove txs that ask for too much gas
i = 0
while i < len(txpool):
if txpool[i].startgas > GASLIMIT - collation.gasused:
txpool.pop(i)
else:
i += 1
tx = copy.deepcopy(txpool[0])
tx.witness = UPDATE_WITNESS(tx.witness, recent_trie_nodes_db)
# Try to add the transaction, discard if it fails
success, reads, writes = add_transaction(collation, tx)
recent_trie_nodes_db = union(recent_trie_nodes_db, writes)
txpool.pop(0)
最後,有一個額外的步驟,最終確定collation(給 collator 發放獎勵,也就是 COLLATOR_REWARD
的 ETH)。這需要詢問網路以獲得 collator 賬戶的 Merkle 分支。當得到網路對此的迴應之後,發放獎勵之後的“後狀態根”(post-state root)就可以被計算出來了。Collator 就可以用 (header, txs, witness) 的形式打包這個 collation 了。這裡,見證人(witness)就是由所有交易的見證和 collator 賬戶的 Merkle 分支組成的。
協議變動
交易的格式
交易的格式現在將變為(注意這裡包含了 賬戶抽象 和 讀/寫列表 ):
[
chain_id, # 1 on mainnet
shard_id, # the shard the transaction goes onto
target, # account the tx goes to
data, # transaction data
start_gas, # starting gas
gasprice, # gasprice
access_list, # access list (see below for specification)
code # initcode of the target (for account creation)
]
完成交易的處理過程也將變為:
- 校驗
chain_id
和shard_id
是正確的; - 從
target
賬戶中減去start_gas * gasprice
wei; - 檢查目標
account
是否有程式碼,如果沒有,校驗sha3(code)[12:] == target
; - 如果目標賬戶為空,使用
code
作為初始程式碼,在target
中執行一個合約的建立;否則,跳過這個步驟; - 執行一個訊息,使用:剩餘的氣作為 startgas,
target
作為地址,0xff...ff 作為傳送者,0 作為 value,以及當前交易的data
作為 data; - 如果上述任何一個執行失敗,並且消耗了 <= 200000 的 gas(即
start_gas - remaining_gas <= 200000
),那麼這個交易是無效的; - 否則,
remaining_gas * gasprice
將被退還,已支付的交易費將被新增到一個交易費計數(注意:交易費 不會 被直接加入 coinbase 餘額,而是在區塊最終確認時立即新增)。
雙層樹(two-layer trie)重新設計
現存的賬戶模型將被替換為:在一個單層樹中收錄進所有賬戶的餘額、程式碼和儲存。具體來講,這個對映為:
- 賬戶 X 的餘額:
sha3(X) ++ 0x00
- 賬戶 X 的程式碼:
sha3(X) ++ 0x01
- 賬戶 X 的儲存鍵值 K:
sha3(X) ++ 0x02 ++ K
請參考 ethresearch 上的帖子 單層樹中的雙層賬戶樹 。
訪問列表
一個賬號的訪問列表看起來大概像這樣:
[[address, prefix1, prefix2...], [address, prefix1, prefix2...], ...]
從根本上說,這意味著:“這個交易可以訪問這裡給定的所有賬戶的餘額和程式碼,並且賬戶列表中給出的每個賬戶的字首中至少有一個是該賬戶儲存的一個鍵的字首”。我們可以將其轉換為“字首列表格式”,基本上就是一個賬戶的內部儲存樹(storage trie)的字首列表(參考前面的章節):
def to_prefix_list_form(access_list):
o = []
for obj in access_list:
addr, storage_prefixes = obj[0], obj[1:]
o.append(sha3(addr) + b'\x00')
o.append(sha3(addr) + b'\x01')
for prefix in storage_prefixes:
o.append(sha3(addr) + b'\x02' + prefix)
return o
我們可以通過取得交易的訪問列表,將其變換為字首列表格式,然後對字首列表中的每個字首執行 get_witness_for_prefix
,並將這些呼叫結果組成一個集合;以此來計算某個交易見證人。
在 EVM 中,任何嘗試對訪問列表以外的賬戶的訪問(直接呼叫、SLOAD 或者通過類似 BALANCE
或 EXTCODECOPY
的 opcode 的操作)都會導致執行這種程式碼的 EVM 例項丟擲異常。
請參考 ethresearch 上的帖子 賬戶讀/寫列表 。
Gas 的消耗
待定。
後續的階段
通過分離區塊 proposer 和 collator,我們實現了二次方擴充套件,這是一種快速、不徹底的中等安全權益證明分片,以此在不對協議或軟體架構做太多更改的情況下增加了大約 100 倍的吞吐量。這也被用來作為一個完整的二次方分片多階段計劃的第一階段,後續階段大致如下:
- 第二階段(two-way pegging,即雙向限定) :參考
USED_RECEIPT_STORE
章節,仍在撰寫; - 第三階段,選項a :將 collation header 作為 uncle 加入,而不是交易;
- 第三階段,選項b :將 collation header 加入一個數組,陣列中的元素
i
必須為分片i
的 collation header 或者空字串,並且額外的資料必須為這個陣列的雜湊(軟分叉); - 第四階段(tight coupling,即緊耦合) :如果區塊指向無效或不可用的 collation,那麼區塊也將變為無效;增加資料可用性證明。