2024年4月 | シークレットロック
この記事は「April, 2024 | Secret Locks」を翻訳したものです。
Background
3月30日の午前11時(UTC)、Toshiは彼のノードで問題が発生していることをNEM&Symbol Discordでアラートしました。問題は、ブロック高が3,191,538の時点でフォークが発生しているというものでした。
私たちが彼に返信するまでに、データフォルダーはすでに削除され、追加の診断を行うためのデータがありませんでした。彼のノードが回復したと誤って考え、問題を深く考えることはありませんでした。
コミュニティの中で数人が同様の問題を報告しましたが、具体的な原因を見つけることができず、彼らが最終的にネットワークと同期すると想定しました。
4月1日、Toshiが同じ問題が発生したと報告しました。今度は心配になり、問題に対して人員を投入しました。
づ~さん、トシさん、そしてコミュニティの他のメンバーが行動に移り、診断や可能な原因に関する提案を提供しました。この時点で、フォークしたノードはごくわずか(10〜20個)であり、ネットワーク全体のわずか2%未満を占めていました。したがって、大きなリスクプロファイルは存在しませんでした。
私たちはツイートを投稿して、問題に取り組んでいることを皆に知らせ、作業を開始しました。
Diving In For Data
フォークしたノードのログには、次のような一般的なエラーが報告されていました:processing of block 3191538 failed with Failure_LockSecret_Hash_Already_Exists
このエラーは、アクティブなSECRET_LOCKを置換しようとしたことを意味しており、ネットワークのルールでは許可されていませんでした。それにもかかわらず、このブロックは多数のフォークで確認され、確定されました。これが分岐したブロックであるため、私たちは注意を集中すべき場所を把握しました。
私たちが直面していた最大の問題は、ゼロから同期またはバックアップ(チェックポイントとも呼ばれる)からのノードが、ブロック3191538を超えて進まないことでした。私たちはバックアップがコミュニティによって維持されていることに気付かなかったので、それらを見直しましたが、異常はありませんでした。
すぐに、合成ハッシュ「0D0D03B478CBFA3A9C0A0A292E2FECB2C09A5DA01659CFF20C199823D0E79E81」の秘密ロックに何か奇妙な点があることが明らかになりました。Symbolの熟練した学習者は、秘密ロックの合成ハッシュが秘密ロックの秘密と受信者から派生することを覚えているでしょう。どちらかが変更されると、秘密ロックの合成ハッシュも異なるものになります。
その合成ハッシュを使ったシークレット・ロックについて、ブロックチェーンで次のような使い方が確認されました:
Block 3,191,088: ACTIVATE | SECRET_LOCK created, with a 420 block expiration;
Block 3,191,098: DEACTIVATE | SECRET_LOCK completed;
Block 3,191,525: - ACTIVATE | SECRET_LOCK created, with a 420 block expiration;
Block 3,191,538: - ACTIVATE | SECRET_LOCK created, with a 420 block expiration;
Block 3,191,576: - DEACTIVATE | SECRET_LOCK completed.
ふむ。興味深い。
Symbolは、前のロックが「COMPLETED」または「EXPIRED」になっている場合、合成ハッシュの再利用を許可しています。そのため、ブロック高3191525でのロックの再作成は有効であり、意味があります。なぜなら、前のロックがブロック高3191098で完了したからです。しかし、ブロック高3191538でのロックの再作成は予期しないものです。なぜなら、前のロックはまだアクティブであるはずだからです。
もう1つ重要な観察結果は、この重複したロック作成の記録が、長時間稼働しているノードにのみ存在するようであるということでした。ゼロから同期されたノードは、ブロック高さ3,191,538で作成された重複したロックを拒否しました。これが、この問題を診断し修正するためのすべての情報でした。私たちは、何が起こっているのかを把握するために、多くのアドホックなテストと分析を行う必要がありました。
最初に考えたのは、これがロールバックの問題である可能性があるということでしたが、長期間稼働しているノードのみが影響を受けた理由を説明できませんでした。ロールバックの問題は通常、ごく少数のノードにしか影響しません。私たちはこの考え方をすぐに排除し、次の可能性の手掛かりに移りました:取引です。
何かがシークレットロック(および最終化)のプルーニングで起こっていました。
まず、背景を説明します:Symbolは内部でコンポジットハッシュの再利用を許可するため、各コンポジットハッシュのシークレットロックのリストが保存されます。このデータ構造は基本的にスタックです。
さらに、シークレットロックは時間制限があるため、ブロックチェーンの状態を変更できなくなったときにキャッシュから削除されることがあります。実際には、シークレットロックは、そのブロックでの有効期限が切れる可能性があるときにプルーニングされます。
これを考慮すると、ブロック 3,191,098 で作成されたシークレットロックは、ブロック高 3,191,508(420 + 58)でプルーニングされます。
我々は多数派のフォークで起こったシークエンスを推測しました:
3,191,525 で SECRET_LOCK が作成されました。
ブロック 3,191,508 が確定しました。
すべての一致する SECRET_LOCK が削除されました。
3,191,538 で SECRET_LOCK が作成されました。
しかし、バグは、1つのシークレットロックが期限切れになった場合に、シークレットロックの全履歴が削除されていたことでした。
The Solution
問題のコードはこれです:
auto groupIter = groupedSet.find(key);
const auto* pGroup = groupIter.get();
if (!pGroup)
return;
for (const auto& identifier : pGroup->identifiers())
set.remove(identifier);
groupedSet.remove(key);
pGroup->identifiers()は、指定された高さ(key)で期限切れになるすべてのコンポジットハッシュを返します。
set.remove(identifier); は、期限切れの部分だけでなく、全体のシークレットロック履歴を削除します。
修正箇所を見つけましたか?私たちは、シークレットロックを個別に削除する必要があります。完全な履歴全体を削除するのではなく。新しいコードは次のとおりです:
std::unordered_set<typename TDescriptor::KeyType, utils::ArrayHasher<typename TDescriptor::KeyType>> pendingRemovalIds;
ForEachIdentifierWithGroup(*m_pDelta, *m_pHeightGroupingDelta, height, [height, &pendingRemovalIds](auto& history) {
history.prune(height);
if (history.empty())
pendingRemovalIds.insert(history.id());
});
for (const auto& identifier : pendingRemovalIds)
m_pDelta->remove(identifier);
m_pHeightGroupingDelta->remove(height);
ここでは、残っているシークレットロックのない履歴のみが削除されます。重要なのは、履歴が空の場合にのみ削除をマークすることです(`history.empty()`)。
新しい設定項目として、すでにチェーン上にある重複したシークレットロック(ブロック*538)を回避するために、skipSecretLockUniquenessCheckが追加されました。
メインネットでは3'191'538に設定し、他のすべてのチェーンでは0に設定する必要があります。
シークレット・ロックの一意性チェックは、それが設定されている高さではスキップされます。
シークレットロック履歴の不適切な処理により、バグの2つの追加の現れが特定されました。
まず、秘密ロックが複数回期限切れになる可能性がある状況があります。
例えば、次のシーケンスを考えてみてください:
ブロック3197 038 - 420ブロックの有効期限で秘密ロックが作成されました
ブロック3197 075 - 秘密ロックが完了しました
ブロック3197 446 - 420ブロックの有効期限で秘密ロックが作成されました
最初のロックは、高さ*(038 + 420)で期限切れになるはずですが、これは完了しているため、状態に変更が生じるべきではありません。
残念ながら、期限が切れる時点では、最も最近のロックの状態のみがチェックされます。つまり、*446で作成されたロックのみがチェックされます。このロックが未完了であるため、Symbolは誤って期限切れの受領書と転送を生成します。
これは、期限切れのロックが最も最近のものであるかどうかを明示的に確認し、それがそうである場合にのみ処理を継続することで修正されました。
if (height == lockInfo.EndHeight || forceHeights.cend() != forceHeights.find(height))
accountStateConsumer(accountStateCache.find(lockInfo.OwnerAddress).get());
このバグを利用した2つのロックがメインネットで確認されました:
ブロック3197038で作成されたロック、モザイク`0BCF7F87A4175ABE`の1単位を持つ
ブロック3197361で作成されたロック、モザイク`00E26AEDCE86A630`の1単位を持つ
`forceSecretLockExpirations`設定は、同期ノードが元の(誤った)動作を模倣できるように追加されました。
二番目に、秘密のロックに対して有効期限の受領が登録されない場合があります。
たとえば、次のシーケンスを考えてみてください:
ブロック3197 038 - 420ブロックの有効期限でシークレットロックが作成されました
ブロック3197 075 - シークレットロックが完了しました
ブロック3197 446 - 420ブロックの有効期限でシークレットロックが作成されました
最初のロックは、ブロック*(038 + 420)で剪定の対象となりますが、2番目のロックはブロック*(446 + 420)で剪定の対象となります。主要なバグのため、ブロック781が最終化されると、キャッシュから全履歴が削除されます。これには、ブロック440で作成された2番目の(アクティブな)ロックも含まれます。その結果、ロックは完了できなくなります。
最初のロックが誤って完了し、また失効したため、これらはバランスがとれます。
これら2つの現れは関連しています。
実際には、これらは2番目のロックの期間を最初のロックの期間に短縮します。
メインネットでこのバグの影響を受けた2つのロックがありました:
`skipSecretLockExpirations`設定が追加され、同期ノードが元の(誤った)動作をエミュレートできるようになりました。
ゼロから正しく同期するために、メインネットでは以下の設定を推奨します:
skipSecretLockUniquenessChecks = 3'191'538
skipSecretLockExpirations = 3'197'860, 3'197'866
forceSecretLockExpirations = 3'197'458, 3'197'781
可能な限り早く1.0.3.7にアップグレードしてください。
これは強制アップグレードであり、将来同様の問題が発生しないようにします!
この問題の解決に尽力してくれたすべてのコミュニティメンバーに感謝します。特にtoshi、dusan、bootaru(およびNFTDriveEXチーム)、nelutaに感謝します!
この記事が気に入ったらサポートをしてみませんか?