為了實現兩條EOSIO體系區塊鏈間token的跨鏈轉移,首先需要解決兩個難題,1.輕客戶端如何實現,2.如何確保跨鏈交易的完整性和可靠性, 如何防止雙花和重放攻擊。下文均以EOS主網和BOS主網為例進行說明,但本文檔適用于任何兩條EOSIO體系的區塊鏈。
?
一、術語
BOSIBC:是指BOSCORE技術團隊開發的IBC合約及插件。
二、關鍵概念及數據結構
· Simple Payment Verification (SPV)
簡單支付驗證技術最早在中本聰的比特幣白皮書中提出Bitcoin,用于驗證一筆交易存在于區塊鏈中。 SPV client存儲著連續的區塊頭,但沒有區塊體,因此只需占用很小的存儲空間。當獲得一筆交易和這筆交易的Merkle path后,可以驗證這筆交易是否 存在于區塊鏈上。
· Lightwight client (lwc)
輕客戶端,即SPV client,即由區塊頭組成的一條輕量的鏈。
· Merkle path 默克爾路徑。為驗證一筆交易是否存在于某個區塊中,只需要提供交易原始數據和交易在所在區塊的merkle path,而無需提供整個區塊體,通過計算merkle path并和區塊頭中 記錄的merkle root對比,若相等,則說明此交易存在于此區塊中。merkle path也被稱為merkle branch。
· Block Producer Schedule
BP Schedule是基于DPOS機制的EOSIO體系公鏈用于決定生產區塊權利的技術,新的BP Schedule是由上一批BP Schedule包含的Block Producers認證通過后生效 以此確保嚴格的BP權利交接,在輕客戶端中跟隨對應主網的BP Schedule是IBC系統邏輯的一項核心技術。
· forkdb
EOSIO節點在運行時,有兩個底層的db用于存儲區塊信息,一個是blog即block log,用于存儲不可逆區塊,一個是forkdb,用于存儲可逆區塊。 forkdb存儲的是當前區塊鏈最頂端的一部分區塊信息,一個區塊首先要被forkdb接受才能最終進入不可逆區塊,IBC系統在合約實現的輕客戶端主要參考了forkdb的邏輯。
三、輕客戶端
為了解決跨鏈問題,首先要解決的是輕客戶端如何實現的問題。
1. 輕客戶端運行在哪里合理,合約中還是合約外,例如插件中;
2. 若運行在合約中,是實時同步對方鏈的全部區塊頭數據,還是根據需要同步一部分區塊數據來驗證交易,因為如果同步全部區塊數據會消耗兩條鏈大量cpu資源。
3. 若運行在合約中,如何確保輕客戶端的可信性,如何防止惡意攻擊,如何做到完全去中心化,不依賴對任何中繼節點的信任;
3.1 輕客戶端是否運行在合約中
比特幣的輕客戶端最早是運行在單個節點上(如個人電腦或去中心化比特幣手機錢包),用于驗證交易是否存,并查看交易所在區塊深度。
IBC和去中心化錢包對輕客戶端的需求是不同的,去中心化錢包一般運行在個人的手機App上,為用戶個人提供交易驗證服務,而IBC系統需要的輕客戶端 要對所有人公開可查可信,從這個角度看,一個能夠獲得大眾信任的輕客戶端只能運行在合約中,因為只有合約的數據是全局一致不可篡改的, 運行在合約外則無法實現一個可信的輕客戶端,因此BOSIBC將輕客戶端運行在合約中。
3.2 是否同步全量區塊頭信息
在比特幣和以太坊的輕客戶端中,會尋找一個起點和后續的一些驗證點,輕客戶端會同步起點后的全量的區塊頭信息。比特幣每年產生的所有區塊的區塊頭體積僅有4Mb, 按現在移動設備的存儲能力,是完全可以容納的,并且同步這些區塊頭也不會消耗移動設備大量計算資源。然而EOSIO的情況卻很不同, EOSIO每0.5秒一個區塊,實際測試可知,每添加一個區塊頭到合約中需要消耗0.5毫秒cpu,每刪除一個區塊頭需要0.2毫秒,因此每處理一個區塊頭需要0.7ms的cpu。 假設要同步對方EOSIO公鏈的全量區塊頭信息,按現在每個區塊總的cpu時間200ms計算,也就是需要一條鏈全部計算的0.7ms / 200ms = 0.35%才能實時全量同步 另一條鏈的所有區塊頭, 按實際全網抵押總量為4億token計算,如果再cpu繁忙時保證IBC系統正常工作,需要為push區塊信息的賬戶抵押 4億 * 0.35% = 140萬token,這是個很大的數目。又因為EOSIO倡導多側鏈的生態,假設未來有多條側鏈和EOSIO主網實現跨鏈,并且側鏈與側鏈間 也實現了一對多的跨鏈,按1對10計算,每條鏈需要維護10個輕客戶端,則只為了維護這些輕客戶端就需要消耗3.5%的單條鏈全網cpu,這個比例實在是太高了, 因此需要尋找更合理的方案。
設計跨鏈通信的過程是一種尋找可信證據的過程,有沒有一種方案即不需要同步全量區塊信息,又可以保證輕客戶端的可信性,EOSIO底層已經為實現這一目的有所準備。 我們先假設,如果BP schedule自始至終不會變化,那么任何時候,當ibc.chain合約中獲得一連串的簽名驗證通過的區塊頭,比如第n ~ n+336個, 并且有2/3以上的活躍bp在出塊,就可以確信第n個區塊已經是不可逆的,可以用于驗證跨鏈交易。 然后,就是需要考慮有BP schedule更換的情況了,當出現BP schedule更換時,不在接受交易驗證,直到更換完成,處理BP更換時相對復雜的過程,后續會更詳細介紹, 因此使用這個方案就可以大大降低需要同步的區塊頭數量,只有在BP列表更換或有跨鏈交易時才需要同步區塊。
為了實現這一目的,在ibc.chain中引入了概念section,一個section記錄的是一段連續的區塊頭信息,section結構不存儲具體的區塊頭信息,而是記錄這一段 區塊頭的第一個區塊編號(first)和最后一個區塊編號(last),具體區塊頭信息在chaindb中存儲,每個section都有一個valid值,在沒有bp schedule更替的時候, 只要有2/3的活躍BP在出塊,并且last - first 》 lib_depth則認為first ~ last - lib_depth的區塊是不可逆的,可以用于驗證跨鏈交易, 當遇到BP schedule 更替,section的valid變為false,不再接受交易驗證,直到schedule更替完成,valid重新變為true之后,繼續驗證跨鏈交易。
3.3 如何確保輕客戶端的可信性
3.3.1 forkdb
1.一個新的區塊是如何追加到forkdb的
一個運行的nodeos節點維護著兩個底層數據結構blog 和forkdb, blog用于存儲不可逆的區塊信息,其存儲的數據是序列化的signed_block,forkdb用于存儲可逆區塊信息,其存儲的數據是block_state, block_state比signed_block包含更多區塊相關信息。一個區塊首先要被追加到forkdb,才可能最終變為不可逆區塊而移除forkdb進入blog, 一個區塊的block_state信息是如何獲得的呢,并非生產區塊的BP將所生產區塊的block_state通過p2p網絡傳遞給其他bp和全節點,p2p網絡 只傳遞signed_block, 當一個節點通過p2p網絡接收到一個signed_block后,它會使用此signed_block構建block_state并驗證簽名 相關函數, 其中需要說明的幾個關鍵點是,1.blockroot_merkle,2.get_scheduled_producer(),3.verify_signee()。
blockroot_merkle
EOSIO在block_state::block_header_state結構中維護了一個blockroot_merkle的incremental_merkle數據,incremental_merkle實際是 一個完整的merkle樹的活躍節點,使用incremental_merkle只需維護極少的活躍節點信息即可不斷累加并獲得merkle_root,是block_state 中使用的一個關鍵技術。BOSIBC的ibc.chain合約同樣使用了此數據結構。
blockroot_merkle從創世區塊id不斷累加,但是signed_block和blog中并沒有這個數據,只有forkdb的每個block_state中記錄著當前block的 blockroot_merkle,并且此值被用于計算區塊簽名。
get_scheduled_producer()
此函數根據一個區塊的header.timestamp計算出應該生成此區塊的producer_key(見block_header_state::next()),為后續驗證簽名做準備。
驗證簽名相關函數如下
digest_type block_header_state::sig_digest()const {
auto header_bmroot = digest_type::hash( std::make_pair( header.digest(), blockroot_merkle.get_root() ) );
return digest_type::hash( std::make_pair(header_bmroot, pending_schedule_hash) );
}
public_key_type block_header_state::signee()const {
return fc::crypto::public_key( header.producer_signature, sig_digest(), true );
}
void block_header_state::verify_signee( const public_key_type& signee )const {
EOS_ASSERT( block_signing_key == signee, wrong_signing_key, “block not signed by expected key”,
(“block_signing_key”, block_signing_key)( “signee”, signee ) );
}
驗證簽名的第一步是獲得區塊摘要,即sig_digest(),此函數中用到了header.digest(),blockroot_merkle.get_root()和pending_schedule_hash; 第二步是獲得簽名公鑰,即signee(),通過區塊的producer_signature和sig_digest()計算BP公鑰; 第三步是驗證公鑰是否正確,即verify_signee(),此函數在block_header_state::next()被調用;驗證通過后,一個區塊被追加的forkdb中的分支中。
所以在forkdb中每添加一個區塊都經過了非常嚴格全面的效驗,核心是包括blockroot_merkle,get_scheduled_producer()和verify_signee(), 在ibc.chain合約完全繼承了forkdb嚴格的效驗。
2.forkdb如何處理分叉
當添加一個新的區塊導致fordb的head.id和controller_impl的head.id不同時,則重新選擇分支。 源碼參考eosio::chain::controller_impl的push_block()和maybe_switch_forks();
3.LIB如何確定
EOSIO目前使用的共識方式是dpos,當構造一個區塊的block_header_state時會設定required_confs,此值為當前活躍BP數量的2/3+1, 在21個BP的情況下,required_confs為15。每個區塊頭中都有header.confirmed,用于對前面的區塊進行確認,每個區塊得到一個確認, 其required_confs會減1,當某個區塊的required_confs減少到零時,此區塊會被最新區塊(即forkdb的head)提名為dpos_proposed_irreversible_blocknum, 當某個區塊獲得了2/3的BP提名后,其變為不可逆區塊,即進入LIB。由于確認的信息是在header中傳遞的,因此一個區塊從產生到進入LIB總共需要 兩個2/3輪,也就是 12 *( 14 * 2 ) = 336才會進入LIB,考錄到BP每次都是連續出12個區塊,只有第一個區塊的header.confirmed為非零, 因此當一個BP開始出塊時,只有第一個區塊會提升LIB,因此實際head和LIB的差距在325至336之間,但是在有BP丟塊的情況下,head和LIB的差距可能出現小于 325至336。
4.BP列表是如何更換的
在pow的區塊鏈中,比如比特幣和以太坊,是選擇算力累加最大的分叉作為主鏈,一個區塊只有包含一定的算力才有可能被認可,最終變為不可逆。 而在以dpos為共識算法的EOS中,區塊被認可的標記是BP簽名,因此BP列表在EOSIO中具有至關重要的地位。IBC的輕客戶端同樣需要維護BP列表和BP列表的更換, 因此需要透徹分析BP列表的更換邏輯。
第一步,在系統合約eosio.system的onblock()函數中,系統會每分鐘一次嘗試更新bp列表update_elected_producers( timestamp ),此函數最終調用 wasm接口set_proposed_producers(),通過一系列檢查后,會將新的schedule和當前區塊編號存到global_property_object對象中。
第二部,當此區塊變為不可逆之后,會在當前的pending區塊中設置新的名單header.new_producers,并重置global_property_object對象,當前 pending區塊編號會被記錄到pending_schedule_lib_num,此時在nodeos日志中可以看到新的名單;具體邏輯參考controller::start_block() // Promote proposed schedule to pending schedule.。 也就是說新的名單從proposed schedule變為pending schedule大約需要經歷325至336個區塊。從這里開始, 后面區塊block_header_state的pending_schedule.version會比active_schedule.version大1.
第三步,當pending_schedule_lib_num變為不可逆后,active_schedule會被pending schedule替換,整個的BP更換過程完成。 從pending schedule出現到其變為active_schedule同樣需要經歷約325至336區塊。
IBC系統的輕客戶端同樣需要繼承forkdb的這些邏輯,才能實現可信的輕客戶端。然而,輕客戶端是在合約中實現,需要充分考慮合約的特性和限制, 因此在實現細節上,需要做諸多調整。
3.3.2 eosio::table(“chaindb”), ibc.chain合約中的forkdb
1.輕客戶端(lwc)的LIB如何確定
有兩種方案,一種是完全按forkdb的邏輯,維護一整套confirm_count和confirmations等block_header_state相關信息,每添加一個區塊 計算一次LIB,這樣做的優點是可以準確獲得實時LIB值,然而對于輕客戶端來說,其關心的是區塊已經不可逆,而并非實時的精確LIB值。有沒有更簡單的方案呢, 根據上述的邏輯,如果有活躍的2/3以上的BP在出塊,并且某個區塊的深度超過336,則此區塊一定是不可逆的,可以用于驗證跨鏈交易;使用這種方案, 可以簡化合約中forkdb的復雜度。
ibc.chain合約中表global的lib_depth是一個深度值,當在一段連續的區塊頭中,某個區塊頭的深度超過此值時,則認為不可逆,可以用于驗證交易了。 此值應該設置多少合適呢,可以設置成336,當輕客戶端檢查到有2/3以上的BP在出塊,則認為深度超過了336的區塊是不可逆的。然而在合約中添加和刪除區塊頭 是非常耗cpu的,實際測試可知,每添加一個區塊頭需要消耗0.5毫秒cpu,每刪除一個區塊頭需要0.2毫秒,因此每個區塊頭需要0.7ms的cpu。 是否會出現主網巨大波動,導致沒有進lib的區塊全部回滾呢,這是有可能的,實際也發生過,因此要必須確保一筆跨鏈交易所在區塊進入lib,再開始處理。 插件在此時可以起到一定的作用,在BOSCORE技術團隊研發的ibc_plugin中,設置了參數,只處理一定深度內的跨鏈交易,這樣,插件中的深度和合約中的深度相加 超過336即可。這只是BOSIBC初期的做法,目的是避免消耗大量cpu,后續可能會考慮將合約中的深度設置為336,從而完全不依賴中繼的深度,然而無論是插件 還是合約中增加深度值,都會直接延長跨鏈交易到賬時間,從而影響用戶體驗,因此需要根據實際情況確定一個合理的值,從而即保證足夠安全又不失良好的用戶體驗。
2.表chaindb
表chaindb是ibc.chain合約中的forkdb,ibc.chain合約中沒有blog結構,和eosio中forkdb使用的block_header_state不同,chaindb進行了大量精簡,只保留了 block_num、block_id、header、active_schedule、pending_schedule、blockroot_merkle、block_signing_key7個數據,又因為 bp schedule需要占用大量空間,因此在另外的表prodsches中存儲實際schedule信息,在chaindb中用id引用,以節省內存占用和wasm cpu消耗。
3.輕客戶端的創世區塊
輕客戶端需要一個可信的起點,此起點是輕客戶端的創世區塊頭,后面所有區塊頭的驗證都是基于創世區塊頭的信息。 創世區塊頭需要block_header_state中的blockroot_merkle,active_schedule和header信息,才能驗證區塊簽名。 源碼為chain::chaininit(),最重要一個限制是,創世區塊頭的pending_schedule必須和active_schedule相同,因為其不同意味著 此區塊是bp列表更替過程中的一個區塊,如果使用更替過程中的區塊,需要同步后續的區塊,直到active_schedule被pending_schedule替換, 增加了復雜度,因此這樣的區塊不適合作為創世區塊。
4.輕客戶端是如何添加新區塊的
輕客戶端添加header的方式和和eosio的forkdb非常類似。源碼見ibc.chain的chain::pushheader()。 第一步,通過區塊編號驗證是否能夠連接到最新的section 第二部,是否需要處理分叉,刪除舊數據。在ibc.chainz中不會同時保存多個分支,而是以后者替代前者的方式實現對分叉的處理。 第三步,通過區塊id驗證是否能夠連接到最新的section 第四部,構造block_header_state,并驗證BP簽名
其中最核心的是構造block_header_state,在這個過程中處理的BP schedule的更換,確保有2/3以上活躍BP(見section_type::add())。
5. Section Section是chaindb的核心概念和創新,意思是一段連續的區塊頭,Section的管理也是最ibc.chain的核心的邏輯。 使用section的目的是降低cpu消耗,只有在BP schedule有變化或有跨鏈交易時才需要同步一部分區塊頭。 任何section的起始區塊不能是 bp schedule 更換過程中的區塊,也就是說,任何一個section的起始區塊的pending_schedule.version必須等于 active_schedule.version,并且一個section的起始區塊頭的active_schedule必須和前一個section最后區塊的active_schedule相同, 這樣就保證了在任意兩個section之間一定不存在BP schedule的更換,每一此BP schedule更換的完整過程必須在某個section中完成,從而確保 section數據的可信性。
四、跨鏈交易
1. 跨鏈交易三部曲
一筆跨鏈交易分為三個過程,下面以將EOS從EOS主網跨鏈轉賬到BOS主網為例說明,首先用戶在EOS主網發起一筆跨鏈交易orig_trx,在EOS側ibc.token合約的 origtrx表中會記錄此交易信息,當這筆交易所在區塊進入lib后,EOS側IBC中繼(relay_eos)將此交易和交易相關信息(區塊信息及Merkle路徑) 傳遞到BOS側中繼(relay_bos);relay_bos構造cash交易并調用BOS側ibc.token合約的cash接口,如果調用成功, cash函數中會給目標用戶發行對應的token;等cash交易所在的區塊進入lib后,relay_bos會將cash_trx和此交易相關信息(區塊信息及Merkle路徑) 傳遞到relay_eos,relay_eos構造cashconfirm交易并調用EOS側ibc.token合約的cashconfirm接口,cashconfirm會刪除EOS側ibc.token合約 中對orig_trx的記錄,至此一筆完整的跨鏈交易完成。
2. 跨鏈失敗的交易
跨鏈交易是可能失敗的,比如指定的賬戶在對方鏈上不存在,或者由于網絡環境惡劣,導致調用cash接口失敗,未能成功跨鏈的交易會被回滾,即原路退還用戶的資產, 然而現在的IBC系統是交易驅動的,失敗的IBC交易需要等到有成功的IBC交易完成后才會被回滾。(注:后續版本升級會讓失敗的交易盡快回滾)
3. 如何防止replay攻擊,即雙花攻擊
防止雙花攻擊分為兩個階段:
1,一筆成功的跨鏈交易只能執行一次cash,否則會造成重復cash。
2,對于每一個cash交易,必須將其相關信息傳回原鏈執行cashconfirm,以消除合約中記錄的原始交易信息,否則會出現即在目的鏈上給用戶發行了token, 又將原鏈的token退還給了用戶。
cash函數是ibc.token的核心邏輯,ibc.token合約中記錄著最近執行cash的原始交易id,即orig_trx_id,并且新的cash的orig_trx的區塊編號必須 大于或等于所有orig_trx所在的區塊編號,也就是說必須按原始交易在原鏈按區塊的順序進行cash,(執行cash時,原鏈某個區塊內的跨鏈交易順序是無關緊要的) ,再結合trx_id檢查,可以確保一筆跨鏈交易只能執行一次cash。
同樣,cashconfirm接口會檢查cash交易的編號seq_num,此編號必須逐一遞增,以確保所有在目的鏈上的cash交易都會刪除在原鏈上的原始交易記錄, 從而確保不會出現雙花的情況。
五、插件
插件的作用分為兩部分:1,輕客戶端同步;2,跨鏈交易傳遞。
核心邏輯請參考ibc_plugin_impl::ibc_core_checker()
ibc_plugin主要參考了net_plugin的框架。
六、問答
問:IBC合約的多個action中用到了relay的權限,那么,本IBC系統是否依賴對中繼的信任。
答:目前出于安全以及快速功能迭代的考量,特意添加了中繼權限,隨著功能逐漸完善 BOS IBC 方案會支持多中繼機制,以避免單點風險。
驗證relay權限處于兩種考慮:1,ibc.chain合約使用了section的機制,現在的邏輯不允許為舊的section添加區塊,也不允許在一個section前面 添加區塊頭,如果任何人都可以調用pushsection接口,假設應該push的區塊范圍是1000-1300,故意搗亂的人可能會搶先push 1100-1300, 從而導致1000-1100無法被push,進而導致一些跨鏈交易無法成功,(注,此問題會在后續版本中考慮優化);2,考慮到IBC系統承載著 大量用戶資產,并且本系統還未經過長期市場考驗,因此增加了relay權限,以降低安全風險。
七、升級計劃
1. 兼容pbft
2. 以更優雅的方式支持多條側鏈&EOS主鏈互相跨鏈
3. 支持token以外其他類型數據的跨鏈
評論
查看更多