前回の記事で、ステーキングとは?という部分がちょっとは理解できたので、今度はコードベースでもっと具体的に理解していこうと思います。
題材とさせていただくのは、リリース間近のCodablecashです!
コードはオープンソースなので、ステーキングが「中で何をしているのか」の実際のコードを追って確かめることができます。
前もってお知らせしておくと、私はPHPを少し触るくらいでC++の知識はありません。Codablecashのクラス名や短いコードを眺めながら「ふーむなるほど」と言いたい。というのが今回の趣旨なので、お気軽にお読みいただけると幸いです。
記事内のコード引用は、967b7eb(2026年9月8日)のコミットからお借りしています。
ステーキングでやっていること
ステーキングに関連する部分をチケット購入者の目線でコードベースで追ったところ、大きく以下のような流れになっていました。
- チケット価格の決定:256ブロックごとに
calcTicketPriceが「合計ロック額 ➗ 理想の4万枚」で再計算する - チケットの購入:ウォレットが
RegisterTicketTransactionを作る。投票先のノード・返却先アドレス・ロック額をTicketUtxoに書き込む - 購入の検証:各ノードが
validateFinalで「ロック額が現在価格以上か」「投票先のノードが(プール運営者によって)登録済みか」を確認する - 成熟待ち:買ったチケットは256ブロックの間、抽選に出られない
- 抽選:ブロックごとに
TicketVoteSelectorが候補を集め、ブロック高のSHA-256を乱数表にして5枚を選ぶ - 投票:選ばれた5枚のチケットが指名していた投票ノードが
VoteBlockTransactionで投票する。5票そろうとブロックが確定 - 報酬:
BlockRewardCalculatorがマイナーと当選チケットで報酬を頭割り。各シェアの0.5%が投票ノードへの手数料、残りと元本がチケットの返却先アドレスへ - ミスしたとき:
RevokeMissedTicketで元本は全額戻る。ただし投票ノードの台帳にミスが記録され、ミスが続くと抽選に出せる枠(capacity)が減る
ステーキング関連のクラス
関連するコードの中心は、src_blockchain/配下の、名前にfinalizerとつくディレクトリに集まっていました。
RegisterTicketTransaction— チケットの登録(購入)TicketUtxo— チケット本体(ロックされたコイン)。ロック額・投票先ノード・返却先アドレスのデータを持つVoteTicket— 投票ノード側に置いておくチケットの控え(買った高さなどの抽選に係る情報)TicketVoteSelector— 投票チケットの抽選VoterEntry— 投票ノードごとの台帳(抽選に出せる枠を管理)VoteBlockTransaction— ブロックへの投票RevokeMissedTicket— 投票ミスしたチケットの返金
このほか、別のディレクトリにある以下のクラスも登場します。
StatusCacheContext— チケット価格の再計算CodablecashSystemParam— 各種パラメータの初期値BlockRewardCalculator/BlockRewardStakeBase— 報酬の計算
うーん、クラスだけでいっぱいありますね!さっそく見てゆきましょう。
StatusCacheContext チケットの現在価格
まずはチケットの「現在価格」の定義から。CodablecashではDecredと同じく「買いたい人が多いと値上がりする自動調整」機能があるようで、それを担っているcalcTicketPrice()という関数がありました。
関数内、冒頭のheight・window・modのコードは、チケット価格の再計算のタイミングについてで、「一定のブロック数ごとにしか再計算しない」という線引きがあるようです。
// src_blockchain/bc_status_cache_context/StatusCacheContext.cpp
void StatusCacheContext::calcTicketPrice(const BlockHeader *header) {
uint64_t height = header->getHeight();
uint64_t window = this->config->getTicketPriceWindow(height);
uint64_t mod = (height + 1) % window;
if(mod != 0){
return;
}
uint64_t numTicketMax = this->config->getTicketIdealNumber(height);
uint64_t ticketPriceDefault = this->config->getTicketPriceDefault(height);
// ・・・・・(全投票ノードの台帳を list に集める)
BalanceUnit total(0L); // 合計を0から始める
int maxLoop = list->size();
for(int i = 0; i != maxLoop; ++i){ // 投票ノードを順に回って
VoterEntry* entry = list->get(i);
total += entry->getTicketPriceSum(); // そのノードが抱えるチケットのロック額の合計を足す
}
uint64_t price = total.getAmount() / numTicketMax;
price = price < ticketPriceDefault ? ticketPriceDefault : price;
this->ticketPrice = price;
}
numTicketMax:理想のチケット枚数ticketPriceDefault:チケット価格の初期値・下限total:現存するチケットの合計ロック額(全投票ノードの台帳を回って、各ノードが抱えるチケットのロック額を足したもの)
priceを導く計算は、合計ロック額 ➗ 理想のチケット枚数 = 新しいチケット価格。priceが下限より低ければ下限の値がセットされ、そうでなければ「新しいチケット価格」が採用される、という仕組みのようです。
また、パラメータ初期値のセットは以下に。
// src_blockchain/bc/CodablecashSystemParam.cpp
CodablecashSystemParam::CodablecashSystemParam() {
// ・・・・・
this->ticketPriceDefault = 2;
this->ticketPriceWindow = 256;
this->ticketIdealNumber = 40000;
// ・・・・・
}
チケット価格の下限は2、価格の再計算は256ブロックごと、理想のチケット枚数は40,000枚、となっていました。
RegisterTicketTransaction (チケット購入)
次に、チケットの登録(=購入)を担う、RegisterTicketTransactionを見てみます。ここではチケット購入の可否チェックをしているようです。
先に、出てくるワードについて。ticketUtxoのUTXOですが、こちらはUnspent Transaction Output(取引で生まれて、まだ消費されていないコイン)の頭文字だそうです。ticketUtxoに関して言うと「チケットとしてロックされたコイン」を表しています。
// src_blockchain/bc_finalizer_trx/RegisterTicketTransaction.cpp
TrxValidationResult RegisterTicketTransaction::validateFinal(
const BlockHeader *header, MemPoolTransaction *memTrx, IStatusCacheContext *context) const {
{
uint64_t priceUint = context->getTicketPrice();
BalanceUnit price(priceUint);
BalanceUnit amount = this->ticketUtxo->getAmount();
if(price.compareTo(&amount) > 0){
return TrxValidationResult::INVALID;
}
}
・・・・・
}
priceUint:今この時点でチケット1枚を買うのに必要な最低額(前述のcalcTicketPriceで決まる値)amount:this->ticketUtxo->getAmount()。このチケットにロックしようとしている額if(price.compareTo(&amount) > 0):条件チェック。チケット価格が、ロックしようとしている額より大きいなら(つまり、コインがチケット価格に満たないなら)
チケット価格(priceUint)とロック用に差し出した額(amount)を比較して、条件によって「無効」を返す、ということをしています。
price.compareTo(&amount)自体は0、1、-1、のいずれかを返すので、それを > 0かどうかで分岐しています。
簡単に言うと、「コインがチケット価格に足りていないならNOを返す」役割の関数ですね。
TicketUtxo
TicketUtxoというクラスの定義も見てみましょう。チケット(ロックされたコインそのもの)本体のクラスです。
// src_blockchain/bc_finalizer_trx/TicketUtxo.h
class TicketUtxo: public AbstractUtxo {
// ・・・・・
virtual BalanceUnit getAmount() const noexcept;
//・・・・・
private:
NodeIdentifier* nodeId; // Stake pool
AddressDescriptor* addressDesc; // registered ticket returned address
BalanceUnit amount;
};
定義されている変数です。
- nodeId:投票を任せるノード(コメントにStake poolとあります)
- addressDesc:元本を返してもらう先のアドレス
- amount:ロックする額
チケットを購入するとき、同時にプールも指定しますよね。それらの情報もチケットに記録されているのですね。
この変数に実際に値が入るのはどこかというと、RegisterTicketTransaction自身は空のTicketUtxoをnewし、それぞれのセッターが用意されていました。
// src_blockchain/bc_finalizer_trx/RegisterTicketTransaction.cpp
RegisterTicketTransaction::RegisterTicketTransaction() : AbstractFinalizerTransaction() {
this->ticketUtxo = new TicketUtxo();
}
// ・・・・・
void RegisterTicketTransaction::setNodeId(const NodeIdentifier *nodeId) noexcept {
this->ticketUtxo->setNodeIndentifier(nodeId);
}
void RegisterTicketTransaction::setAddressDescriptor(const AddressDescriptor *ticketReturnaddressDesc) noexcept {
this->ticketUtxo->setAddressDescriptor(ticketReturnaddressDesc);
}
void RegisterTicketTransaction::setAmounst(BalanceUnit amount) noexcept {
this->ticketUtxo->setAmounst(amount);
}
そして、そのセッターを呼んでいるのは、ノードではなくウォレット側でした。ウォレットの「チケットを買う」処理(RegisterTicketTransactionWalletHandler::createTransaction)が、投票先のノードID・ロック額・返却先アドレスを引数で受け取り、トランザクションを組み立てて署名、という流れのようです。
// src_blockchain/bc_wallet_trx/RegisterTicketTransactionWalletHandler.cpp
RegisterTicketTransaction* RegisterTicketTransactionWalletHandler::createTransaction(const NodeIdentifier *nodeId, const BalanceUnit& stakeAmount,
const BalanceUnit& feeRate, const AddressDescriptor *ticketReturnaddressDesc, const IWalletDataEncoder *encoder, ITransactionBuilderContext *context) {
// ・・・・・
RegisterTicketTransaction* trx = new RegisterTicketTransaction(); __STP(trx);
trx->setNodeId(nodeId); // 投票ノードのid
trx->setAddressDescriptor(ticketReturnaddressDesc);
trx->setAmounst(stakeAmount);
trx->build();
trx->sign(musigProvidor, &utxoFinder);
// ・・・・・
return __STP_MV(trx);
}
つまり「いくらロックするか」「どのノードに任せるか」を決定するのはウォレットで、ノード側はさきほどのvalidateFinalで「その額が現在価格以上か」「任せる先のノードが登録済みか」を検査する、という分担になっているようです。
チケットの成熟期
チケットが買えてもすぐに抽選に出られるわけではありません。パラメータの初期値に、こんな2行がありました。Matureとは"成熟"という意味です。
// src_blockchain/bc/CodablecashSystemParam.cpp
CodablecashSystemParam::CodablecashSystemParam() {
// ・・・・・
this->ticketMatureIntervalHeight = 256;
this->ticketExpireHeight = 20 * 24 * 30 * 3;
// ・・・・・
}
まずticketMatureIntervalHeight = 256について。買ったチケットが抽選の対象になるまで256ブロック待つという成熟期間のようです。Decredでも、買った直後のチケットはimmature(未成熟)と表示され、すぐには投票に参加できないですよね。あれと同じ概念のようです。
この256は、抽選の候補を集めるmakeList()で「成熟の境界線」として使われていました。
// src_blockchain/bc_finalizer/TicketVoteSelector.cpp
void TicketVoteSelector::makeList() {
// ・・・・・
uint64_t matureHeight = this->height - this->tiketMatureIntervalHeight; // 今のブロック高 − 256
// ・・・・・
const VoteTicket* ticket = entry->nextTicket(matureHeight); // 成熟期を過ぎたチケットの中から1枚出して
// ・・・・・
}
そして、ticketExpireHeightは20×24×30×3=43,200ブロック。チケットの失効に関する値?と思ったら、購入から一定のブロック数が過ぎても当選しないままのチケットを高優先リストに入れるための基準値でした!
// src_blockchain/bc_finalizer/TicketVoteSelector.cpp
void TicketVoteSelector::addList(const VoteTicket *ticket) {
uint64_t ticketHeight = ticket->getHeight(); // このチケットを買ったブロック高
uint64_t expire = this->height - ticketHeight; // 買ってから何ブロックたったか
if(expire < this->ticketExpireHeight){ // 43,200ブロック未満なら
this->candidateList->addElement(ticket); // 通常の候補リストへ
}
else{
this->expiredList->addElement(ticket); // 超えていたら優先リストへ
}
}
長く待たされたチケット(43,200ブロックは3分間隔で約90日)を優先して救済する仕組みのようです。そんな親切設計があるのですね。
TicketVoteSelector チケット投票権を得る流れ
さて、チケット価格が決まり、チケット購入まで進みました。成熟期をすぎたら投票に参加できる土台は十分です。
まず、投票の前提ですが、ブロックごとの投票に使われるチケットは5枚までです。
先ほど、理想のチケット枚数(ticketIdealNumber)は4万枚、とコードから見つけました。つまり4万枚ほどあるチケットのうち、各投票ノードが差し出した候補の中から、ブロックの投票権を得る「たった5枚」を選ぶ必要があります。
その5枚を選ぶ(抽選する)役割を担うのが、このTicketVoteSelectorクラスのようです。
makebuffer() 抽選用の乱数表を作る
TicketVoteSelectorクラスに、この抽選用の乱数表を作るmakebuffer()という関数がありました。
// src_blockchain/bc_finalizer/TicketVoteSelector.cpp
ByteBuffer* TicketVoteSelector::makebuffer() {
ByteBuffer* buff = ByteBuffer::allocateWithEndian(sizeof(uint64_t), true); __STP(buff);
buff->putLong(this->height); // 「何番目のブロックか」という数字を、8マスの箱に左から詰める。書き終わるとペン先は箱の右端にいる
buff->position(0); // ペン先を箱の左端に戻す。次に箱を読む人が最初から読めるように
// Sha
ByteBuffer* shabuff = Sha256::sha256(buff, true); __STP(shabuff);
shabuff->put((char)0);
shabuff->position(0);
return __STP_MV(shabuff);
}
ここではsha256という関数が使われています。sha256は「バイトの並び」を受け取る関数なので、sha256に渡すためにブロックの高さを8バイトに整形する、という作業を行なっています。その作業に該当するのがByteBuffer〜buff->position(0)のくだりで、ちょっと難しいですが以下に簡単にメモをしておきます。
ByteBuffer:バッファ(数字をバイトの並びに直すための、一時的な作業スペース)this->height:今から処理するブロックの高さ(何番目のブロックか)sizeof(uint64_t):ブロック高を入れる型の大きさ=8(作業スペースを8バイトで借りる指定)buff->putLong():ブロック高の数値を8バイトに直して、作業スペースに書き込む(書いた分だけペン先が右へ進む)buff->position(0):ペン先を左端に戻す(次のSHA-256が先頭から読めるように)
このような事務的?な作業を経て作成されるのが、shabuffです。
shabuff = Sha256::sha256(buff, true) ブロックの高さをSHA-256でハッシュした32バイトの乱数表を手に入れました。ここまではまだ乱数表を作っただけで、抽選は行われていませんね。
では、その乱数表からどうやってチケットを選ぶのかというと、同じクラスにdoSelect()と、selectFromList()という関数がありました。2つの関数は以下のような関係になっています。
selectFromList():実際に抽選を行う関数doSelect():必要な回数だけselectFromList()を呼び出す
doSelect() 抽選の土台
doSelect()から見てみましょう。count = 0 から始まって、まず優先組からexpCount回、続けて通常組からcandidateCount回(合計で最大5回)selectFromList()を呼んでいます。
前半で、expCount・candidateCountを定義していますね。
// src_blockchain/bc_finalizer/TicketVoteSelector.cpp
void TicketVoteSelector::doSelect() {
// high priority
int expCount = this->expiredList->size();
expCount = expCount >= this->votePerBlock ? this->votePerBlock : expCount;
// normal priority
int candidateCount = this->votePerBlock - expCount;
candidateCount = candidateCount > this->candidateList->size() ? this->candidateList->size() : candidateCount;
ByteBuffer* shabuff = makebuffer(); __STP(shabuff); // さきほどの「抽選用の乱数表」を作る
int count = 0; // 何枚目を選んでいるか。0から始まる
for(int i = 0; i != expCount; ++i){
const VoteTicket* ticket = selectFromList(this->expiredList, count, shabuff); // 長く待たされたチケットから先に選ぶ
this->selected->addElement(ticket);
count++; // 1枚選ぶたびに1増える
}
for(int i = 0; i != candidateCount; ++i){
const VoteTicket* ticket = selectFromList(this->candidateList, count, shabuff); // 残りの枠を通常の候補から選ぶ
this->selected->addElement(ticket);
count++;
}
}
votePerBlock:1ブロックで選ぶ枚数(初期値5)expiredList:長く待たされて優先扱いになったチケットのリスト。ここから先に、最大5枚まで選ぶcandidateList:通常の候補のリスト。優先組で埋まらなかった残りの枠をここから選ぶcount:何枚目の抽選かを数える通し番号(0〜4)。優先組と通常組で通しで数え、selectFromList()に渡して「乱数表のどこを読むか」をずらすのに使う
「1ブロックで選ぶ枚数」votePerBlockの初期値は5で、ここでセットされていました。
// src_blockchain/bc/CodablecashSystemParam.cpp
CodablecashSystemParam::CodablecashSystemParam() {
// ・・・・・
this->votePerBlock = 5;
// ・・・・・
}
selectFromList() 抽選本体
そして、抽選の本体であるselectFromList()です。doSelect()からは以下の2パターンで呼ばれていましたね。
selectFromList(this->expiredList, count, shabuff)selectFromList(this->candidateList, count, shabuff)
// src_blockchain/bc_finalizer/TicketVoteSelector.cpp
const VoteTicket* TicketVoteSelector::selectFromList(ArrayList<const VoteTicket> *list, int count, ByteBuffer* shabuff) {
uint16_t pos = count % 32;
pos = shabuff->getShort(pos); // 乱数表のcountバイト目から2バイトを、数字として読む
// ・・・・・
pos = pos % list->size(); // 候補の枚数で割った余り=「何番目のチケットか」
return list->remove(pos); // そのチケットを当選として抜き取る
}
pos:乱数表の何バイト目から読むかを定義し、それを2バイトの数値化し、当たりチケットの番号を保有、という段階による役割を持つlist->size(): 候補リストに今入っているチケットの枚数
32バイトの乱数表(shabuff)のcountバイト目から2バイトを数字として読み、「候補の枚数で割った余り」の番号のチケットを、当たったチケットとして抜いています。
ここで大事なのは、この計算に出てくるのが「乱数表」と「候補の枚数」だけという点です。チケットにいくらロックしたかは、式のどこにも出てきません。つまり選ばれ方はチケット1枚ごとの平等な抽選で、「たくさんロックした人ほど1枚あたりの当選確率が上がる」ような重み付けは見当たりませんでした。確率を上げたければチケットを複数枚買う、という素直な設計のようです。
抽選候補について
チケットの中から選ばれる候補のリストは、各投票ノードが自分の抱えるチケットを先頭から順に差し出して作るのですが、その差し出す側の関数にこうありました。
// src_blockchain/bc_finalizer/VoterEntry.cpp
const VoteTicket* VoterEntry::nextTicket(uint64_t matureHeight) noexcept {
int size = this->list->size();
size = size >= this->capacity ? this->capacity : size; // 出せるのはcapacity枚まで
if(this->pos >= size || this->list->get(this->pos)->getHeight() > matureHeight){
return nullptr; // 上限に達した、または次のチケットがまだ成熟していない
}
return this->list->get(this->pos++);
}
1回の抽選に出せる枚数には、投票ノードごとの上限(capacity)があり、この上限の範囲内で、成熟前のチケットは除外したものを候補として出しているようです。
5票そろうとブロックが確定
ここまでで、1ブロックにつき5枚のチケットが選ばれ、それぞれの投票ノードが投票するところまで来ました。集まった票がvotePerBlockに達したかを判定しているのがこちらです。
// src_blockchain/bc_block/BlockHeader.cpp
bool BlockHeader::isFinalizing(int votePerBlock) const noexcept {
const VotedHeaderIdGroup* group = this->votePart->getMaxVotedGroup();
return group != nullptr && group->size() == votePerBlock;
}
こうして1ブロックごとの投票が規定数に達すると、ブロックは確定作業へと進みます。
BlockRewardCalculator 報酬を計算する
やっとここまで来ました。嬉しい報酬関連です。
BlockRewardCalculatorクラスのcalcRewords()を見てみます。
// src_blockchain/bc_block_generator/BlockRewardCalculator.cpp
void BlockRewardCalculator::calcRewords(uint64_t height, uint16_t zone) noexcept {
BalanceUnit total = getTotalRewords4Shards(height, zone) + this->fee;
int totalShare = this->list.size();
if(this->pow != nullptr){
totalShare++;
}
if(totalShare == 0){
return;
}
BalanceUnit perShare = total / totalShare;
BalanceUnit remain = total - (perShare * BalanceUnit(totalShare));
int maxLoop = this->list.size();
for(int i = 0; i != maxLoop; ++i){
BlockRewardStakeBase* stake = this->list.get(i);
stake->setReward(perShare);
}
if(this->pow != nullptr){
BalanceUnit powShare = perShare + remain;
this->pow->setReward(powShare);
}
}
total: このブロックで配る総額。(直前の行で「ブロック報酬 + 手数料」として計算)this->list: このブロックに取り込まれた投票チケットのリスト(0〜5枚)this->pow: このブロックを掘ったマイナーの報酬の受け皿(受取アドレスが設定されていれば作られる。なければnullptr=空)
報酬総額を「投票の数+PoWマイナー1」で割っています。つまり最大6等分ですね。
計算で割り切れなかった端数(remain)に関しては、BalanceUnit powShare = perShare + remain;の箇所で、マイナーに乗せられます。
BlockRewardStakeBase ステーキング手数料
calcRewords()では、ブロック報酬をマイナー1人と当選したチケット各1枚に、perShareずつ配られるということがわかりました。
でも、当選チケット1枚には2人の関係者がいましたね。
- チケット保有者:コインをロックしてチケットを買った人
- 投票ノード:そのチケットが指名した、実際に投票するノード(プール運営者のノード。自前で立てるなら保有者本人のノード)
この2人に対してどのように報酬が分けられるかは、以下のcalcTicketOwnerBalance()で計算されていました。
// src_blockchain/bc_block_generator/BlockRewardStakeBase.cpp
BalanceUnit BlockRewardStakeBase::calcTicketOwnerBalance(uint64_t ticketVoterFeeBasisPoint) const {
BalanceUnit voterReword = this->reward * BalanceUnit(ticketVoterFeeBasisPoint) / 10000L;
BalanceUnit ticketReword = this->reward - voterReword;
BalanceUnit amount = ticketReword + this->ticketVotedUtxo->getAmount();
return amount;
}
ticketVoterFeeBasisPointは、以下で初期値が50にセットされていました。
// src_blockchain/bc/CodablecashSystemParam.cpp
CodablecashSystemParam::CodablecashSystemParam() {
// ・・・・・
this->ticketVoterFeeBasisPoint = 50;
// ・・・・・
}
つまり、報酬の内訳は以下です。
voterReword = this->reward * BalanceUnit(ticketVoterFeeBasisPoint) / 10000L:0.5%。投票を実行した側(プールやノード)ticketReword = this->reward - voterReword:残り99.5%がチケット所有者へ(チケット元本とともに)
この計算をもとに、当選して投票が済んだチケット1枚ごとに「投票済みチケットの精算」の取引が1本、ブロックに載ります(exportStakeBaseTransaction)。入力は投票済みチケットの小切手、出力は「元本+報酬の99.5%」をチケットの返却先へ、「報酬の0.5%」を投票ノードのアドレスへ、の2枚です。プール運営者が委任者の元本に触る場面はなく、精算はプロトコルが自動で振り分けます。
RevokeMissedTicket 投票ミスしたチケットの返金
投票をミスしたチケットの扱いを見てみます。
投票しそこねたチケットは、ロックしていた額がそのまま元のアドレスに払い戻されるようです。この取り消しのトランザクションを組み立てているのは、BlockGeneratorクラスのaddRevokeMissedTicket()でした。
// src_blockchain/bc_block_generator/BlockGenerator.cpp
void BlockGenerator::addRevokeMissedTicket(・・・・・, VoteCandidate *candidate) {
RevokeMissedTicket* revokeTrx = new RevokeMissedTicket(); __STP(revokeTrx);
// ・・・・・
BalanceUnit ticketPrice = candidate->getTicletPrice(); // ロックしていた額
const AddressDescriptor* desc = candidate->getAddressDescriptor(); // チケットに書いた返却先
BalanceUtxo utxo(ticketPrice);
utxo.setAddress(desc);
revokeTrx->addBalanceUtxo(&utxo); // 出力: 同じ額を、返却先宛ての普通の小切手として
// ・・・・・
block->addControlTransaction(revokeTrx); // ブロックに載せる
}
引数で渡されるcandidateは、取り消し対象である「当選したのに投票が届かなかったチケット」です。そのチケットのロック額、返却先アドレス宛てに返金する、という処理を行っています。
VoterEntry ミスのペナルティ
「投票ミスによるペナルティ」と聞くと、コインをロックしてチケットを買った人はどきっとしてしまうかもしれませんが、実際に投票するのは投票ノードです。
そのため、ミスによるペナルティは投票ノードに課せられます(個人でノードを立てている場合も同様)。チケット保有者の側は、その回の報酬をもらえないだけで、元本は前の節のとおり返金されます。
そして、その処理を担うVoterEntryにこんな関数がありました。
// src_blockchain/bc_finalizer/VoterEntry.cpp
void VoterEntry::handleMissed(int missingLimit) noexcept {
this->extendCount = 0;
this->missingCount++;
if(this->missingCount >= missingLimit){
this->capacity = this->capacity / 2;
this->missingCount = 0;
}
this->updated = true;
}
voteMissingLimitの初期値は2にセットされています。
// src_blockchain/bc/CodablecashSystemParam.cpp
CodablecashSystemParam::CodablecashSystemParam() {
// ・・・・・
this->voteMissingLimit = 2;
// ・・・・・
}
その投票ノードが抽選に出せるチケット枠の上限を、capacityが持っています。ミスが連続してmissingLimit(初期値2)に達すると、this->capacity / 2で上限が半分に減ってしまうようです。
(逆に、投票を成功させ続けると枠が1つ増えるhandleVotedという関数もありました)
投票ミス自体はコイン喪失などのペナルティとはならない代わりに、信用スコアが下がって参加枠を減らされるという仕組みでした。これはステーキングプールを運営する人にとっては要注意ポイントですね。
終わりに
追いきれていない部分もかなりあると思いますが、主だったコードを実際に見ることでぼや〜としていたステーキングへの理解が少し深まりました。
またメインネットができるころにはこの記事の内容を見返して、本番で変更された点なども確認しますね。