削减(懲罰)
摘要
本節規定了削減模塊。
削減模塊使得基於Cosmos SDK的區塊鏈能夠通過懲罰具有權益價值的協議認可的參與者來減少其可能導致的行為風險。
懲罰可能包括但不限於::
- 銷毀他們持有的一定數量的權益份額
- 在一段時間內取消他們對未來區塊的投票權限。
概念
狀態
在任何時候,狀態機中都有任意數量的驗證者註冊。 每個區塊,頂部的 MaxValidators(由 x/staking 定義)未被監禁的驗證者變成了 bonded,這意味著他們可以提出和投票支持區塊。 被 bonded 的驗證者 at stake,這意味著他們和他們的委託人的部分或全部權益份額會因為他們犯了協議錯誤而遭受風險。
對於每個驗證者,我們保留一個 ValidatorSigningInfo 記錄,其中包含有關驗證者活動狀態和其他違反相關屬性的信息。
墓碑上限
為了減輕最初可能出現的非惡意協議錯誤的影響,Cosmos Hub 為每個驗證者實現了一個 墓碑 上限,這只允許驗證者因雙重簽署錯誤而被懲罰一次。例如,如果您錯誤地配置了 HSM 並雙重簽署了一堆舊區塊,您只會因為第一次雙重簽署而受到懲罰(然後立即被墓碑化)。這仍然是相當昂貴的,因此需要盡量避免,但是墓碑上限在一定程度上減輕了意外錯誤配置的經濟影響。
活動性錯誤(Liveness faults)沒有上限,因為它們不能堆疊在一起。當違規發生時,活動性錯誤被“檢測”出來,並且驗證者立即被監禁,因此他們不可能在解監之前多次犯錯。
違規時間軸
為了說明 slashing
模塊如何通過 Tendermint 共識處理提交的證據,請考慮以下示例:
定義:
Cn : 違規行為 n
發生
Dn : 發現違規行為 n
Vb : 已綁定的驗證者
Vu : 未綁定的驗證者
單次或雙重簽名違規
<----------------->
當發現單次違規行為後,驗證者會被解除綁定並被扣除違規行為的全部懲罰。
多次雙重簽名違規
<--------------------------->
當發現多次違規行為後,驗證者會被拘留並被扣除其中一次違規行為的懲罰。由於驗證者已被墓碑化,因此無法重新加入驗證者集合。
狀態
簽名信息(活性)
每個區塊都包含前一個區塊的驗證者預提交集合,稱為 Tendermint 提供的 LastCommitInfo。 只要 LastCommitInfo 包含超過總投票權的 2/3 的預提交,就是有效的。這個機制可以確保網絡的安全性和活性。
提議者會受到激勵,將所有驗證者的預提交包含在 Tendermint 的 LastCommitInfo
中,通過根据包含在 LastCommitInfo
中的投票權與 2/3 投票權之差比例提高額外費用。
type LastCommitInfo struct {
Round int32
Votes []VoteInfo
}
如果驗證者未能在某些區塊的 L
astCommitInfo` 中被包含,他們將受到懲罰,自動被拘留,可能被削減獎勵,並解除綁定。
有關驗證者活性的信息通過 ValidatorSigningInfo 进行跟踪。它在存储库中按以下方式进行索引:
- ValidatorSigningInfo:
0x01 | ConsAddrLen (1 byte) | ConsAddress -> ProtocolBuffer(ValSigningInfo)
- MissedBlocksBitArray:
0x02 | ConsAddrLen (1 byte) | ConsAddress | LittleEndianUint64(signArrayIndex) -> VarInt(didMiss)
(varint 是一種數字編碼格式。)
第一個映射允許我們根據驗證者的共識地址輕鬆查找驗證者的最近簽名信息。
第二個映射 (MissedBlocksBitArray
) 充當大小為 SignedBlocksWindow
的位數組,告訴我們驗證者是否錯過了位數組中給定索引的區塊。
位數組中的索引以小端 uint64 的形式給出。
結果是一個 varint
,取值為 0
或 1
,其中 0
表示驗證者未錯過(已簽署)相應的區塊,而 1
表示他們錯過了該區塊(未簽署)。
請注意,MissedBlocksBitArray
不是在前面明確初始化的。
當我們在新綁定的驗證者的前 SignedBlocksWindow
個區塊中進行進度時,將添加鍵。
SignedBlocksWindow
參數定義用於跟踪驗證者活性的滑動窗口的大小(區塊數)。
跟踪驗證者活性存儲的信息如下:
+++ https://github.com/cosmos/cosmos-sdk/blob/v0.40.0/proto/cosmos/slashing/v1beta1/slashing.proto#L11-L33
Messages
在本節中,我們描述了針對“懲罰”模塊的消息處理過程。
Unjail
如果驗證者因停機而被自動解除綁定,並希望重新上線並可能重新加入綁定集,則必須發送 MsgUnjail
:
// MsgUnjail is an sdk.Msg used for unjailing a jailed validator, thus returning
// them into the bonded validator set, so they can begin receiving provisions
// and rewards again.
message MsgUnjail {
string validator_addr = 1;
}
Below is a pseudocode of the MsgSrv/Unjail
RPC:
unjail(tx MsgUnjail)
validator = getValidator(tx.ValidatorAddr)
if validator == nil
fail with "No validator found"
if getSelfDelegation(validator) == 0
fail with "validator must self delegate before unjailing"
if !validator.Jailed
fail with "Validator not jailed, cannot unjail"
info = GetValidatorSigningInfo(operator)
if info.Tombstoned
fail with "Tombstoned validator cannot be unjailed"
if block time < info.JailedUntil
fail with "Validator still jailed, cannot unjail until period has expired"
validator.Jailed = false
setValidator(validator)
return
如果驗證者擁有足夠的股份以進入前 n = MaximumBondedValidators 名
,它將自動重新綁定,
仍然委託給該驗證者的所有委託者將重新綁定並開始再次收集佣金和獎勵。
開始區塊
Liveness Tracking
在每個區塊開始時,我們會更新每個驗證者的 ValidatorSigningInfo
,並檢查它們是否在一個滑動窗口內跨越了存活閾值。
這個滑動窗口由 SignedBlocksWindow
定義,並且此窗口中的索引由驗證者的 ValidatorSigningInfo
中的 IndexOffset
確定。
對於每個處理的區塊,無論驗證者是否簽署,都會增加 I
ndexOffset。一旦確定索引,就會相應地更新
MissedBlocksBitArray和
MissedBlocksCounter`。
最後,為了確定驗證者是否跨越了存活閾值,我們會取得最大錯過區塊數 maxMissed,
它等於 SignedBlocksWindow - (MinSignedPerWindow * SignedBlocksWindow)
,
以及可以確定存活性的最小高度 minHeight。如果當前區塊高度大於 minHeight,
並且驗證者的 MissedBlocksCounter
大於 maxMissed
,則會對其進行懲罰,
懲罰方式是削減 SlashFractionDowntime
的貢獻值,並將其關押在 DowntimeJailDuration
的時間內
同時重置以下值:MissedBlocksBitArray
、MissedBlocksCounter
和 IndexOffset
。
注意: 存活性懲罰不會導致墓碑化。
height := block.Height
for vote in block.LastCommitInfo.Votes {
signInfo := GetValidatorSigningInfo(vote.Validator.Address)
// This is a relative index, so we counts blocks the validator SHOULD have
// signed. We use the 0-value default signing info if not present, except for
// start height.
index := signInfo.IndexOffset % SignedBlocksWindow()
signInfo.IndexOffset++
// Update MissedBlocksBitArray and MissedBlocksCounter. The MissedBlocksCounter
// just tracks the sum of MissedBlocksBitArray. That way we avoid needing to
// read/write the whole array each time.
missedPrevious := GetValidatorMissedBlockBitArray(vote.Validator.Address, index)
missed := !signed
switch {
case !missedPrevious && missed:
// array index has changed from not missed to missed, increment counter
SetValidatorMissedBlockBitArray(vote.Validator.Address, index, true)
signInfo.MissedBlocksCounter++
case missedPrevious && !missed:
// array index has changed from missed to not missed, decrement counter
SetValidatorMissedBlockBitArray(vote.Validator.Address, index, false)
signInfo.MissedBlocksCounter--
default:
// array index at this index has not changed; no need to update counter
}
if missed {
// emit events...
}
minHeight := signInfo.StartHeight + SignedBlocksWindow()
maxMissed := SignedBlocksWindow() - MinSignedPerWindow()
// If we are past the minimum height and the validator has missed too many
// jail and slash them.
if height > minHeight && signInfo.MissedBlocksCounter > maxMissed {
validator := ValidatorByConsAddr(vote.Validator.Address)
// emit events...
// We need to retrieve the stake distribution which signed the block, so we
// subtract ValidatorUpdateDelay from the block height, and subtract an
// additional 1 since this is the LastCommit.
//
// Note, that this CAN result in a negative "distributionHeight" up to
// -ValidatorUpdateDelay-1, i.e. at the end of the pre-genesis block (none) = at the beginning of the genesis block.
// That's fine since this is just used to filter unbonding delegations & redelegations.
distributionHeight := height - sdk.ValidatorUpdateDelay - 1
Slash(vote.Validator.Address, distributionHeight, vote.Validator.Power, SlashFractionDowntime())
Jail(vote.Validator.Address)
signInfo.JailedUntil = block.Time.Add(DowntimeJailDuration())
// We need to reset the counter & array so that the validator won't be
// immediately slashed for downtime upon rebonding.
signInfo.MissedBlocksCounter = 0
signInfo.IndexOffset = 0
ClearValidatorMissedBlockBitArray(vote.Validator.Address)
}
SetValidatorSigningInfo(vote.Validator.Address, signInfo)
}
Hooks
本節包含模組的 hooks 描述。Hooks 是當事件觸發時自動執行的操作。
Staking hooks
削減模組實現了在 x/staking
中定義的 StakingHooks
,用於記錄驗證者的信息。
在應用程序初始化期間,這些 hooks
應該在權益綁定模組結構中註冊。
以下 hooks 影響削減狀態:
AfterValidatorBonded
會根據以下部分的描述創建一個V
alidatorSigningInfo` 實例。AfterValidatorCreated
存儲驗證者的共識密鑰。AfterValidatorRemoved
刪除驗證者的共識密鑰。
驗證者綁定
在新驗證者首次成功綁定時,我們為新綁定的驗證者創建一個新的 ValidatorSigningInfo
結構,其 StartHeight
為當前區塊的高度。
onValidatorBonded(address sdk.ValAddress)
signingInfo, found = GetValidatorSigningInfo(address)
if !found {
signingInfo = ValidatorSigningInfo {
StartHeight : CurrentHeight,
IndexOffset : 0,
JailedUntil : time.Unix(0, 0),
Tombstone : false,
MissedBloskCounter : 0
}
setValidatorSigningInfo(signingInfo)
}
return
Tags
削減模組發出以下事件/標籤:
MsgServer
MsgUnjail
Type | Attribute Key | Attribute Value |
---|---|---|
message | module | slashing |
message | sender | {validatorAddress} |
Keeper
BeginBlocker: HandleValidatorSignature
Type | Attribute Key | Attribute Value |
---|---|---|
slash | address | {validatorConsensusAddress} |
slash | power | {validatorPower} |
slash | reason | {slashReason} |
slash | jailed [0] | {validatorConsensusAddress} |
- [0] 只有在驗證者被投入監獄時才會包括。
Type | Attribute Key | Attribute Value |
---|---|---|
liveness | address | {validatorConsensusAddress} |
liveness | missed_blocks | {missedBlocksCounter} |
liveness | height | {blockHeight} |
Slash
- 與
HandleValidatorSignature
中的slash
事件相同,但不包括jailed
屬性。
Jail
Type | Attribute Key | Attribute Value |
---|---|---|
slash | jailed | {validatorAddress} |
權益墓碑
摘要
在當前的 slashing 模組實現中,當共識引擎通知狀態機出現驗證者共識錯誤時,該驗證者會被部分削減,並進入“監獄期”, 在此期間不允許其重新加入驗證者集合。 然而,由於共識錯誤和 ABCI 的性質,可能會存在一段時間的延遲,使得違規行為發生後的證據到達狀態機(這是存在解除綁定期的主要原因之一)。
注意:權益墓碑的概念僅適用於存在延遲的錯誤,例如由於不可預測的證據傳播層延遲以及驗證者有選擇性地揭示雙簽名(例如向不經常在線的輕客戶端揭示), 證據表明驗證者雙重簽名可能需要一段時間才能到達狀態機。 另一方面,活性削減會在違規行為發生後立即檢測到,因此不需要削減期。 驗證者立即進入監獄期,他們不能再犯活性錯誤,直到解除監獄。 將來可能會出現其他類型的拜占庭錯誤,例如將無效提案作為交易提交的證據。 當實施時,必須決定這些未來的拜占庭錯誤是否會導致墓碑(如果不是,削減金額將不會受到削減期的限制)。
在目前的系统设计中, 一旦验证者由于共识错误而被关进监狱,经过JailPeriod(监狱时期)后,他们可以发送一个unjail(解除监禁)交易来解除自己的监禁状态,从而重新加入验证者集合。 这样的设计允许验证者在被惩罚并完成监禁期后有机会恢复其验证者身份。
slashing模块的一个“设计愿望”是,如果在执行证据(并将验证者关进监狱)之前发生了多次违规行为, 那么验证者应该只受到最严重违规行为的惩罚,而不是累积惩罚 例如,如果事件的顺序是:
1.驗證者A犯下違規行為1(價值30%削減) 2.驗證者A犯下違規行為2(價值40%削減) 3.驗證者A犯下違規行為3(價值35%削減) 4.違規行為1的證據到達狀態機(並將驗證者關入監獄) 5.違規行為2的證據到達狀態機 6.違規行為3的證據到達狀態機
只有違規行為2應該生效其削減,因為它是最高的。 這樣做是為了在驗證者的共識密鑰被破壞的情況下,即使黑客對多個區塊進行雙重簽名,他們也只會被懲罰一次 因為,解除監禁必須使用驗證者的操作密鑰,他們有機會重新保護他們的共識密鑰,然後使用他們的操作密鑰發出信號表明他們已經準備好。 我們將在這段期間內僅追踪最大違規行為,稱為"削減期"。
一旦驗證者通過解除監禁重新加入,我們將開始一個新的削減期; 如果他們在解除監禁後犯下新的違規行為,則將在前一個削減期的最嚴重違規行為基礎上累計削減。
然而,雖然違規行為是根據削減期進行分組,但由於證據可以在違規行為之後的"解除綁定期"內提交, 我們仍然必須允許提交先前削減期的證據。 例如,如果事件的順序是:
1.驗證者A犯下違規行為1(價值30%削減) 2.驗證者A犯下違規行為2(價值40%削減) 3.違規行為1的證據到達狀態機(驗證者A被監禁) 4.驗證者A解除監禁
我們現在進入了一個新的削減期,但是我們仍然必須為之前的違規行為留下空間,因為違規行為2的證據可能仍會出現。 隨著削減期數量的增加,它會帶來更多的複雜性,因為我們必須跟踪每個削減期的最高違規行為金額。 這可能會增加一些複雜性,但這是必要的,以確保整個網絡的安全和穩定。
注意:根據“削減”模塊的規範,每當驗證器取消綁定然後重新綁定時,就會創建一個新的削減期。這可能應該更改為被監禁/解除監禁。有關更多詳細信息,請參見問題 #3205。 在接下來的內容中,我將假設我們只有在驗證器被解除監禁時才開始一個新的削減期。
最大削减期数是 len(UnbondingPeriod) / len(JailPeriod)
。
Gaia 中 UnbondingPeriod
和 JailPeriod
的默认值分别为 3 周和 2 天。这意味着每个验证器可能同时被跟踪多达 11 个削减期。
如果我们将 JailPeriod >= UnbondingPeriod
,我们只需要跟踪 1 个削减期(即不需要跟踪削减期)。
目前,在監禁期實現中,一旦驗證器解除監禁,所有委託給他們的委託人(沒有取消綁定/重新委託)都會留在他們身邊。 鑒於共識安全故障非常嚴重(比活性故障嚴重得多),讓委託人不自動重新委託給驗證器可能是明智的選擇。
提議:無限監禁
我們建議將共識安全故障的“監禁時間”設置為無限(即墓碑狀態)。 這實際上將驗證器踢出驗證器集合,並且不允許它們重新進入驗證器集合。 他們的所有委託人(包括操作員本身)必須取消綁定或重新委託。 如果驗證器操作員願意,可以創建一個新的驗證器,帶有新的操作員密鑰和共識密鑰,但是他們必須“重新贏得”他們的委託。
實現墓碑系統並且擺脫懲罰期追踪將使 slashing
模塊變得更簡單,
特別是因為我們可以刪除staking模塊中使用的slashing
模塊定義的所有鉤子(slashing
模塊仍然使用staking
中定義的鉤子
单次惩罚金额
另一個可以進行的優化是,如果我們假設所有Tendermint共識的ABCI故障都會被削減相同的金額, 我們就不必跟踪“最大削減金額”。 一旦發生ABCI故障,我們就不必擔心比較未來可能發生的故障,以找到最大削減金額。
目前唯一的 Tendermint ABCI 故障是:
- 不合理的預提交(雙重簽名)
目前計劃在不久的將來包括以下故障:
- 在未綁定期間簽署預提交(需要使輕客戶端二分法安全)
鑑於這些故障都是可歸因的拜占庭故障,我們可能希望對它們進行相同的懲罰,因此我們可以實施上述更改。
注意:此更改可能對當前的Tendermint共識算法有意義, 但對於不同的共識算法或未來可能希望以不同級別進行懲罰(例如部分懲罰)的Tendermint版本可能沒有意義。
Parameters(參數)
懲罰模組包含以下參數
Key | Type | Example |
---|---|---|
SignedBlocksWindow | string (int64) | "100" |
MinSignedPerWindow | string (dec) | "0.500000000000000000" |
DowntimeJailDuration | string (ns) | "600000000000" |
SlashFractionDoubleSign | string (dec) | "0.050000000000000000" |
SlashFractionDowntime | string (dec) | "0.010000000000000000" |
CLI
用戶可以使用命令行界面(CLI)查詢和與slashing
模組進行互動。
Query
query
命令允許用戶查詢slashing
狀態。
stcd query slashing --help
params
params命令允許用戶查詢slashing模組的創世參數。
stcd query slashing params [flags]
範例:
stcd query slashing params
範例輸出:
downtime_jail_duration: 600s
min_signed_per_window: "0.500000000000000000"
signed_blocks_window: "100"
slash_fraction_double_sign: "0.050000000000000000"
slash_fraction_downtime: "0.010000000000000000"
signing-info
signing-info
命令允許用戶使用共識公鑰查詢驗證者的簽名信息。
stcd query slashing signing-infos [flags]
範例:
stcd query slashing signing-info '{"@type":"/cosmos.crypto.ed25519.PubKey","key":"Auxs3865HpB/EfssYOzfqNhEJjzys6jD5B6tPgC8="}'
範例輸出:
address: cosmosvalcons1nrqsld3aw6lh6t082frdqc84uwxn0t958c
index_offset: "2068"
jailed_until: "1970-01-01T00:00:00Z"
missed_blocks_counter: "0"
start_height: "0"
tombstoned: false
signing-infos
signing-infos
命令允許用戶查詢所有驗證者的簽名信息。
stcd query slashing signing-infos [flags]
範例:
stcd query slashing signing-infos
範例輸出:
info:
- address: cosmosvalcons1nrqsld3aw6lh6t082frdqc84uwxn0t958c
index_offset: "2075"
jailed_until: "1970-01-01T00:00:00Z"
missed_blocks_counter: "0"
start_height: "0"
tombstoned: false
pagination:
next_key: null
total: "0"
交易
tx
命令允許用戶與slashing
模組進行互動。
tx slashing --help
unjail
unjail
命令允許用戶解禁先前因停機時間而被禁用的驗證者。
simd tx slashing unjail --from mykey [flags]
範例:
stcd tx slashing unjail --from mykey
gRPC
用戶可以使用gRPC端點查詢slashing
模組。
Params
Params
端點允許用戶查詢削減模組的參數。
cosmos.slashing.v1beta1.Query/Params
範例:
grpcurl -plaintext localhost:9090 cosmos.slashing.v1beta1.Query/Params
範例輸出:
{
"params": {
"signedBlocksWindow": "100",
"minSignedPerWindow": "NTAwMDAwMDAwMDAwMDAwMDAw",
"downtimeJailDuration": "600s",
"slashFractionDoubleSign": "NTAwMDAwMDAwMDAwMDAwMDA=",
"slashFractionDowntime": "MTAwMDAwMDAwMDAwMDAwMDA="
}
}
SigningInfo
SigningInfo查詢給定共識地址的簽名信息。
cosmos.slashing.v1beta1.Query/SigningInfo
範例:
grpcurl -plaintext -d '{"cons_address":"cosmosvalcons1nrqsld3aw6lh6t082frdqc84uwxn0t958c"}' localhost:9090 cosmos.slashing.v1beta1.Query/SigningInfo
範例輸出:
{
"valSigningInfo": {
"address": "cosmosvalcons1nrqsld3aw6lh6t082frdqc84uwxn0t958c",
"indexOffset": "3493",
"jailedUntil": "1970-01-01T00:00:00Z"
}
}
SigningInfos
SigningInfos
查詢所有驗證者的簽名信息。
cosmos.slashing.v1beta1.Query/SigningInfos
範例:
grpcurl -plaintext localhost:9090 cosmos.slashing.v1beta1.Query/SigningInfos
範例輸出:
{
"info": [
{
"address": "cosmosvalcons1nrqslkwd3pz096lh6t082frdqc84uwxn0t958c",
"indexOffset": "2467",
"jailedUntil": "1970-01-01T00:00:00Z"
}
],
"pagination": {
"total": "1"
}
}
REST
用戶可以使用REST端點查詢slashing
模組。
Params
/cosmos/slashing/v1beta1/params
範例:
curl "localhost:1317/cosmos/slashing/v1beta1/params"
範例輸出:
{
"params": {
"signed_blocks_window": "100",
"min_signed_per_window": "0.500000000000000000",
"downtime_jail_duration": "600s",
"slash_fraction_double_sign": "0.050000000000000000",
"slash_fraction_downtime": "0.010000000000000000"
}
signing_info
/cosmos/slashing/v1beta1/signing_infos/%s
範例:
curl "localhost:1317/cosmos/slashing/v1beta1/signing_infos/cosmosvalcons1nrqslkwd3pz096lh6t082frdqc84uwxn0t958c"
範例輸出:
{
"val_signing_info": {
"address": "cosmosvalcons1nrqslkwd3pz096lh6t082frdqc84uwxn0t958c",
"start_height": "0",
"index_offset": "4184",
"jailed_until": "1970-01-01T00:00:00Z",
"tombstoned": false,
"missed_blocks_counter": "0"
}
}
signing_infos
/cosmos/slashing/v1beta1/signing_infos
範例:
curl "localhost:1317/cosmos/slashing/v1beta1/signing_infos
範例輸出:
{
"info": [
{
"address": "cosmosvalcons1nrqslkwd3pz096lh6t082frdqc84uwxn0t958c",
"start_height": "0",
"index_offset": "4169",
"jailed_until": "1970-01-01T00:00:00Z",
"tombstoned": false,
"missed_blocks_counter": "0"
}
],
"pagination": {
"next_key": null,
"total": "1"
}
}