普通の静的ブログは「どう公開するか」という問題は解決しても、「どう証明するか」という問題までは解決できていない。記事が改ざんされていないか、あるバージョンがいつ登場したのか、サイトが閉鎖された後も元の内容を検証できるのか——こうしたことはすべて運営者とホスティングサービス任せになっている。 そこで生まれたのがこの仕組みだ。サイトの生成は Hexo、自動ビルドは GitHub Actions、ホームページと記事データの保存は Polygon PoS、そして通常の閲覧体験は GitHub Pages がそれぞれ担う。 最終的なアーキテクチャ 公開の流れは次のようにまとめられる。 ここには互いに独立した2つのコントラクトがある。 HomepageArchive トップページの HTML、コンテンツハッシュ、公開時刻、バージョン番号を保存する。 ArticleArchive 記事 slug のハッシュをキーとして、記事の JSON、コンテンツハッシュ、バージョン情報を保存する。 ページはこれまでどおり CDN か GitHub Pages から読み込むので、表示速度は普通の Hexo ブログと変わらない。ブロックチェーンは、公開され、検証可能で、こっそり書き換えられることのない公開記録を提供する役割を担う。 環境を準備する ローカルには Node.js、pnpm、Git、それに Polygon ウォレットが必要だ。 BASHコピーnode --version pnpm --version git --version 依存関係をインストールし、Hexo を確認する。 BASHコピーpnpm install --frozen-lockfile pnpm clean pnpm build pnpm server 本番の公開には Polygon メインネットを使う。チェーン ID は 137、Gas トークンは POL。公開専用のウォレットを別途作成するのが望ましい。主要な資産を保管しているウォレットをそのまま使ってはいけない。 まず記事のオンチェーン形式を設計する 記事は、Hexo が生成した HTML をそのままコントラクトに放り込むわけにはいかない。HTML にはテーマの構造やビルドメタデータ、スタイルの細部が混ざり込むので、テーマを少し調整するだけで全記事が新しいバージョンになってしまう。 最終的に私が使ったのは、以下の正規化 JSON だ。 JSONコピー{ "title": "Article title", "slug": "4", "date": "2026-08-10T12:00:00Z", "content": "H4sIAAAAA...", "images": [ { "name": "architecture.png", "hash": "5d41402abc4b2a76b9719d911017c592" } ] } content は普通の Markdown ではなく、次の処理を経た結果だ。 改行を LF に統一し、前後の空白を削除して、末尾の改行が1つだけになるようにする。 Markdown を gzip level 9 で圧縮する。 圧縮結果を Base64 でエンコードし、JSON を安定してシリアライズできるようにする。 JSON 全体を UTF-8 でエンコードしてから、コントラクトの bytes。 決定性はとても重要だ。同じ記事は、Windows でビルドしても Linux でビルドしても、同じバイト列にならなければならない。そうでなければ、GitHub Actions が実行されるたびに新しい内容だと誤判定され、Gas を毎回支払うことになる。 記事のコンテンツハッシュは、圧縮後の Base64 を直接計算するのではなく、正規化 Markdown を含む意味論的 JSON に対して keccak256 を計算する。こうすることで、ハッシュは記事の内容そのものを表し、特定の圧縮ツールのバージョンによる出力の細部を表すことはない。 JavaScriptコピーconst semanticJson = JSON.stringify({ ...payload, content: markdown }); const contentHash = keccak256(toUtf8Bytes(semanticJson)); 画像についてローカルの画像は実際のバイナリを読み取ってハッシュを計算できるが、リモート画像には見落とされがちな問題がある。ビルドマシンが安定してダウンロードできるとは限らないし、リモート側にはホットリンク防止やタイムアウト、内容の動的な変化もありうる。 現在の実装が外部リンク画像に対して計算しているのは「画像 URL 文字列の MD5」で、これが示せるのは記事がどのアドレスを参照しているかだけだ。そのアドレスが返す画像の内容は証明できないし、画像が永久に利用可能であることも保証できない。 厳密に画像を永久アーカイブしたいなら、次のような選択肢がある。 画像を IPFS にアップロードし、CID を記事に書き込む。 Arweave で画像のバイナリを保存する。 公開の段階で画像をダウンロードし、SHA-256 または Keccak-256 を計算して、ファイルをイミュータブルなオブジェクトストレージに同期する。 ごく小さな画像なら直接オンチェーンに載せる手もあるが、コストは普通に見合わない。 つまり、「記事の Markdown がオンチェーンに載った」ことと「記事が参照するすべてのリソースが永久に保存された」ことは別の話だ。 Solidity コントラクトの設計 記事コントラクトの中心となる構造はそれほど複雑ではない。 SOLIDITYコピーstruct Publication { bytes payload; bytes32 contentHash; bytes32 payloadHash; uint256 version; uint256 publishedAt; uint256 publishedAtBlock; } mapping(bytes32 => Publication) private publications; 書き込み時は文字列 slug をそのまま使うのではなく、まず次を計算する。 JavaScriptコピーconst slugHash = keccak256(toUtf8Bytes(slug)); コントラクトの publish 関数では、いくつかのチェックを済ませる必要がある。 SOLIDITYコピーfunction publish( bytes32 slugHash, bytes calldata payload, bytes32 contentHash ) external { if (msg.sender != owner) revert NotOwner(); if (payload.length == 0) revert EmptyPayload(); if (payload.length > MAX_ARTICLE_BYTES) revert PayloadTooLarge(...); if (publications[slugHash].contentHash == contentHash) { revert UnchangedContent(); } // 保存数据、增加版本,并发出事件 } 1記事の上限は 24,576 バイトに設定した。ここで制限しているのは最終的な JSON の UTF-8 バイト数であって、Markdown ファイルのサイズではない。gzip は本文を小さくするが、Base64 がさらに約3分の1ほどサイズを増やすので、トランザクションを送る前に必ず最終的な payload を確認したい。 コントラクトは2つのハッシュを同時に保存する。 contentHash は記事の意味的な内容が変化したかどうかを判定するために使う。 payloadHash は、コントラクトが実際に書き込んだ bytes に対して計算した keccak256 で、オンチェーンの生のバイト列を検証するために使う。 「永久」とは結局何を指すのか 現在の構造では、更新のたびに publications[slugHash].payload が上書きされる。そのためコントラクトの読み取りインターフェースは最新バージョンしか直接返せない。旧バージョンはブロックチェーンの履歴やトランザクションの calldata、イベントの中には残っているが、現在の mapping からは直接照会できない。 過去のバージョンをすべてコントラクトのメソッドから直接読み取れるようにしたいなら、次のように変えればいい。 SOLIDITYコピーmapping(bytes32 => mapping(uint256 => Publication)) private versions; mapping(bytes32 => uint256) public latestVersion; 代償はコントラクトストレージが増え続けることと、Gas が高くなることだ。個人ブログなら、最新の読めるバージョンを残しつつ、旧バージョンの証明はトランザクション履歴に頼るほうが現実的な妥協点になる。 CREATE2 でコントラクトアドレスを固定する CI はどこかにローカル保存されたデプロイ結果に依存できないので、私は CREATE2 を使ってコントラクトアドレスを事前に計算した。 JavaScriptコピーconst address = getCreate2Address( deterministicDeployer, salt, keccak256(initCode) ); initCode にはコントラクトのバイトコードとコンストラクタ引数が入る。デプロイヤー、salt、コンパイル結果、owner が完全に一致していれば、最終的なアドレスも一致する。公開スクリプトはまず eth_getCode を呼び出す。アドレスにコードがなければデプロイし、コードがあればそのまま再利用する。 ここでの落とし穴は、公開用ウォレットを変えるとコントラクトアドレスも変わる という点だ。owner がコンストラクタ引数の一部だからだ。Solidity のバージョンを上げたり、最適化パラメータを変えたり、コントラクトコードや salt を調整したりしても、同じように新しいアドレスになる。移行計画がないまま、これらの値を気軽に変更してはいけない。 公開スクリプトはどうやって重複トランザクションを避けるか 記事の公開スクリプトは source/_posts を走査し、ファイル名が数字の slug と一致することを求める。 TEXTコピーsource/_posts/1.md -> /archives/1/ source/_posts/2.md -> /archives/2/ source/_posts/4.md -> /archives/4/ 数字 slug は正の整数でなければならず、重複も許されない。これは Web ページのパーマリンクの一部であり、オンチェーン記事の論理主キーでもある。一度公開した後で元の記事を別の slug に移してはいけない。そうするとオンチェーンではまったく新しい記事と見なされる。 記事ごとに、スクリプトはまずオンチェーンの記録を読み取る。 JavaScriptコピーconst previous = await contract.publication(article.slugHash); if (previous.contentHash === article.contentHash) { console.log(`Article ${article.slug} is unchanged`); } else { await contract.publish( article.slugHash, article.bytes, article.contentHash ); } つまり、CI のたびに全記事を走査できるが、トランザクションを送るのは新規追加か本当に変更された記事だけだ。変化のない記事については、公開時のブロックを手がかりに過去のイベントを照会し、トランザクションハッシュを復元して表示用メタデータを生成する。 送信前には estimateGas も実行し、gas limit のバッファを 20% 確保する。 JavaScriptコピーconst estimated = await wallet.estimateGas(transaction); const gasLimit = (estimated * 120n) / 100n; 残高チェックには、ネットワークが返す maxFeePerGas または gasPrice を使わなければならない。コントラクトの初回デプロイは通常、その後の公開より高くつき、完全な HTML や JSON を保存するコストはバイト数とともに目に見えて増える。 トップページになぜ個別の正規化が必要なのか トップページに保存されるのは Hexo が生成した完全な HTML だが、ページ下部にはビルドハッシュとビルド時刻が含まれ、ページ内には前回公開時のブロック高とトランザクションハッシュも表示される。HTML をそのままハッシュ化すると、循環が生じてしまう。 TEXTコピー构建时间变化 -> HTML 哈希变化 -> 再次上链 链上信息变化 -> HTML 再次变化 -> 再次上链 解決方法は、contentHash を計算する前に2つの動的領域を取り除くことだ。 JavaScriptコピーnormalized = normalized.replace( /<footer class="site-footer">[\s\S]*?<\/footer>/g, '<footer class="site-footer"></footer>' ); normalized = normalized.replace( /<!-- onchain-metadata:start -->[\s\S]*?<!-- onchain-metadata:end -->/g, '<!-- onchain-metadata -->' ); 置換ロジックは、各領域がちょうど1回だけ現れることを要求すべきだ。テンプレート構造の変化を黙って無視するのは危険で、ある時テーマを変更したことで動的フィールドが再びハッシュに混入し、ビルドのたびに繰り返し支払う羽目になりかねない。 注意点として、コントラクトが実際に保存するのはそのビルドの完全な HTML であり、正規化は内容ハッシュの計算と重複排除にしか使われない。ビルド時刻だけが変わってもオンチェーンの古い HTML は上書きされない。これは想定どおりの動作だ。 GitHub Actions の二段階ビルドフロー ワークフローは main ブランチだけを監視し、 contents: write 権限を与える。 YAMLコピーon: push: branches: [main] workflow_dispatch: permissions: contents: write 手順は次のとおりだ。 固定バージョンの pnpm と Node.js をインストールする。 依存関係は pnpm install --frozen-lockfile でインストールする。 README の記事インデックスを更新し、テストを実行する。 1回目の Hexo ビルドを実行し、オンチェーンに書き込むホームページ HTML を生成する。 ホームページと記事を Polygon に公開する。 ブロック高、トランザクションハッシュ、コントラクトアドレスを一時環境変数と .onchain/articles.json に書き込む。 2回目の Hexo ビルドを実行し、今取得したオンチェーン情報をページに表示させる。 public を孤立した pages ブランチに公開する。 中核となるコマンドは次のように書ける。 YAMLコピー- name: Build site run: | pnpm clean pnpm build - name: Publish to Polygon env: POLYGON_PRIVATE_KEY: ${{ secrets.POLYGON_PRIVATE_KEY }} POLYGON_RPC_URL: ${{ vars.POLYGON_RPC_URL }} run: | pnpm onchain:publish pnpm onchain:publish:articles - name: Rebuild with on-chain metadata run: | pnpm clean pnpm build .onchain/articles.json は同じワークフローの2回目のビルドでしか使わないので、 .gitignore に追加すべきだ。本当に信頼できるデータソースはあくまで Polygon であり、JSON ファイルはページを生成するときのキャッシュにすぎない。 Secrets と権限の設定 リポジトリの Settings -> Secrets and variables -> Actions で設定する必要がある項目は次のとおり。 種類名前用途Secret`POLYGON_PRIVATE_KEY`公開用ウォレットの秘密鍵Variable`POLYGON_RPC_URL`任意の Polygon メインネット RPCPOLYGON_PRIVATE_KEY はウォレットの 32 バイトの秘密鍵で、通常は 64 桁の16進数文字列として表される。スクリプトは 0x プレフィックスの有無に対応しているが、ウォレットアドレスやニーモニックの名称、API Key を記入してはいけない。 秘密鍵は必ず Secret に置き、リポジトリのファイルや Actions Variable、ビルドログ、フロントエンドの JavaScript に置いてはいけない。GITHUB_TOKEN は GitHub Actions が自動で提供するので、手動で作成する必要はない。 公開用ウォレットには、次のことをおすすめする。 数回分の公開をまかなえるだけの POL だけを残す。 普段使いのウォレットや主要資産と共用しない。 Actions のログに不審なトランザクションが出ていないか定期的に確認する。 Pull Request のワークフローでメインネットへの公開ステップを開放しない。 オンチェーン前後の検証 まずローカルでビルドと payload を検証する。 BASHコピーpnpm clean pnpm build pnpm test コントラクトのコンパイル、アドレス計算、記事のサイズだけをシミュレートしてもいい。 BASHコピーPOLYGON_WALLET_ADDRESS=0xYourPublisherAddress \ pnpm onchain:check:articles 公開が完了したら、コントラクトから記事を読み取って解凍できる。 JavaScriptコピーconst { Contract, JsonRpcProvider, keccak256, toUtf8Bytes, toUtf8String } = require('ethers'); const { gunzipSync } = require('node:zlib'); const provider = new JsonRpcProvider('https://polygon.drpc.org'); const contract = new Contract(contractAddress, abi, provider); const slugHash = keccak256(toUtf8Bytes('4')); const rawPayload = await contract.article(slugHash); const payload = JSON.parse(toUtf8String(rawPayload)); const markdown = gunzipSync( Buffer.from(payload.content, 'base64') ).toString('utf8'); console.log(payload.title); console.log(markdown); 検証のときは、Web ページ上の文字だけを見てはいけない。少なくとも Polygonscan で次を確認しよう。 ネットワークが Polygon PoS メインネットで、チェーン ID が 137 であること。 トランザクションの送信者が想定した公開用ウォレットであること。 トランザクションの宛先が想定したコントラクトアドレスであること。 トランザクションが成功し、正しいブロック高が記録されていること。 コントラクトが返す payloadHash が実際の payload の Keccak-256 と一致していること。 実際に踏んだ落とし穴 1. テストネットの Polygon は無制限に無料ではない Amoy テストネットはフローの検証に向いているが、フォーセットには通常、受け取りの頻度と数量に制限がある。完全な HTML や記事 payload の calldata とストレージはどちらも高くつくので、初回デプロイに初回書き込みが加わると、その日に受け取ったテストトークンを簡単に超えてしまう。 テストのときはまず payload を短くし、「デプロイ」と「公開」を分けて検証し、トランザクションを送る前に gas estimate を出力しよう。メインネットに切り替えるときは、chain ID、RPC、ブロックエクスプローラーのアドレス、Gas トークンを同時に確認する必要があり、RPC URL だけを差し替えて済ませてはいけない。 2. ビルド時刻のせいでトップページが無限に繰り返しオンチェーンされる HTML に現在時刻、Git commit、リアルタイムのブロック高、あるいはランダム値が含まれていれば、生の HTML ハッシュは安定しない。どのフィールドが内容を表し、どれが表示用メタデータにすぎないかをまず定義してから、厳密に正規化する必要がある。 3. RPC が不安定で CI がときどき失敗する 公共 RPC はレート制限されたり、タイムアウトしたり、バッチリクエストに対応していなかったりすることがある。記事公開ツールは不要な JSON-RPC batching を無効にし、ネットワークを明示的に設定し、ログで「RPC 失敗」「残高不足」「トランザクションのロールバック」「確認待ちのタイムアウト」を区別すべきだ。 失敗した後で無条件に再送してはいけない。まずオンチェーンの contentHash を読み取ろう。前のトランザクションはすでにブロードキャストに成功していて、CI がレシートを待っている途中で中断しただけかもしれないからだ。 参考リンク プロジェクトのソースコード:github.com/Moitr/homepage 公開サイト:moitr.cc
普通の静的ブログは「どう公開するか」という問題は解決しても、「どう証明するか」という問題までは解決できていない。記事が改ざんされていないか、あるバージョンがいつ登場したのか、サイトが閉鎖された後も元の内容を検証できるのか——こうしたことはすべて運営者とホスティングサービス任せになっている。
そこで生まれたのがこの仕組みだ。サイトの生成は Hexo、自動ビルドは GitHub Actions、ホームページと記事データの保存は Polygon PoS、そして通常の閲覧体験は GitHub Pages がそれぞれ担う。
最終的なアーキテクチャ
公開の流れは次のようにまとめられる。
ここには互いに独立した2つのコントラクトがある。
ページはこれまでどおり CDN か GitHub Pages から読み込むので、表示速度は普通の Hexo ブログと変わらない。ブロックチェーンは、公開され、検証可能で、こっそり書き換えられることのない公開記録を提供する役割を担う。
環境を準備する
ローカルには Node.js、pnpm、Git、それに Polygon ウォレットが必要だ。
依存関係をインストールし、Hexo を確認する。
本番の公開には Polygon メインネットを使う。チェーン ID は 137、Gas トークンは POL。公開専用のウォレットを別途作成するのが望ましい。主要な資産を保管しているウォレットをそのまま使ってはいけない。
まず記事のオンチェーン形式を設計する
記事は、Hexo が生成した HTML をそのままコントラクトに放り込むわけにはいかない。HTML にはテーマの構造やビルドメタデータ、スタイルの細部が混ざり込むので、テーマを少し調整するだけで全記事が新しいバージョンになってしまう。
最終的に私が使ったのは、以下の正規化 JSON だ。
content は普通の Markdown ではなく、次の処理を経た結果だ。
決定性はとても重要だ。同じ記事は、Windows でビルドしても Linux でビルドしても、同じバイト列にならなければならない。そうでなければ、GitHub Actions が実行されるたびに新しい内容だと誤判定され、Gas を毎回支払うことになる。
記事のコンテンツハッシュは、圧縮後の Base64 を直接計算するのではなく、正規化 Markdown を含む意味論的 JSON に対して keccak256 を計算する。こうすることで、ハッシュは記事の内容そのものを表し、特定の圧縮ツールのバージョンによる出力の細部を表すことはない。
画像について
ローカルの画像は実際のバイナリを読み取ってハッシュを計算できるが、リモート画像には見落とされがちな問題がある。ビルドマシンが安定してダウンロードできるとは限らないし、リモート側にはホットリンク防止やタイムアウト、内容の動的な変化もありうる。
現在の実装が外部リンク画像に対して計算しているのは「画像 URL 文字列の MD5」で、これが示せるのは記事がどのアドレスを参照しているかだけだ。そのアドレスが返す画像の内容は証明できないし、画像が永久に利用可能であることも保証できない。
厳密に画像を永久アーカイブしたいなら、次のような選択肢がある。
つまり、「記事の Markdown がオンチェーンに載った」ことと「記事が参照するすべてのリソースが永久に保存された」ことは別の話だ。
Solidity コントラクトの設計
記事コントラクトの中心となる構造はそれほど複雑ではない。
書き込み時は文字列 slug をそのまま使うのではなく、まず次を計算する。
コントラクトの publish 関数では、いくつかのチェックを済ませる必要がある。
1記事の上限は 24,576 バイトに設定した。ここで制限しているのは最終的な JSON の UTF-8 バイト数であって、Markdown ファイルのサイズではない。gzip は本文を小さくするが、Base64 がさらに約3分の1ほどサイズを増やすので、トランザクションを送る前に必ず最終的な payload を確認したい。
コントラクトは2つのハッシュを同時に保存する。
「永久」とは結局何を指すのか
現在の構造では、更新のたびに publications[slugHash].payload が上書きされる。そのためコントラクトの読み取りインターフェースは最新バージョンしか直接返せない。旧バージョンはブロックチェーンの履歴やトランザクションの calldata、イベントの中には残っているが、現在の mapping からは直接照会できない。
過去のバージョンをすべてコントラクトのメソッドから直接読み取れるようにしたいなら、次のように変えればいい。
代償はコントラクトストレージが増え続けることと、Gas が高くなることだ。個人ブログなら、最新の読めるバージョンを残しつつ、旧バージョンの証明はトランザクション履歴に頼るほうが現実的な妥協点になる。
CREATE2 でコントラクトアドレスを固定する
CI はどこかにローカル保存されたデプロイ結果に依存できないので、私は CREATE2 を使ってコントラクトアドレスを事前に計算した。
initCode にはコントラクトのバイトコードとコンストラクタ引数が入る。デプロイヤー、salt、コンパイル結果、owner が完全に一致していれば、最終的なアドレスも一致する。公開スクリプトはまず eth_getCode を呼び出す。アドレスにコードがなければデプロイし、コードがあればそのまま再利用する。
ここでの落とし穴は、公開用ウォレットを変えるとコントラクトアドレスも変わる という点だ。owner がコンストラクタ引数の一部だからだ。Solidity のバージョンを上げたり、最適化パラメータを変えたり、コントラクトコードや salt を調整したりしても、同じように新しいアドレスになる。移行計画がないまま、これらの値を気軽に変更してはいけない。
公開スクリプトはどうやって重複トランザクションを避けるか
記事の公開スクリプトは source/_posts を走査し、ファイル名が数字の slug と一致することを求める。
数字 slug は正の整数でなければならず、重複も許されない。これは Web ページのパーマリンクの一部であり、オンチェーン記事の論理主キーでもある。一度公開した後で元の記事を別の slug に移してはいけない。そうするとオンチェーンではまったく新しい記事と見なされる。
記事ごとに、スクリプトはまずオンチェーンの記録を読み取る。
つまり、CI のたびに全記事を走査できるが、トランザクションを送るのは新規追加か本当に変更された記事だけだ。変化のない記事については、公開時のブロックを手がかりに過去のイベントを照会し、トランザクションハッシュを復元して表示用メタデータを生成する。
送信前には estimateGas も実行し、gas limit のバッファを 20% 確保する。
残高チェックには、ネットワークが返す maxFeePerGas または gasPrice を使わなければならない。コントラクトの初回デプロイは通常、その後の公開より高くつき、完全な HTML や JSON を保存するコストはバイト数とともに目に見えて増える。
トップページになぜ個別の正規化が必要なのか
トップページに保存されるのは Hexo が生成した完全な HTML だが、ページ下部にはビルドハッシュとビルド時刻が含まれ、ページ内には前回公開時のブロック高とトランザクションハッシュも表示される。HTML をそのままハッシュ化すると、循環が生じてしまう。
解決方法は、contentHash を計算する前に2つの動的領域を取り除くことだ。
置換ロジックは、各領域がちょうど1回だけ現れることを要求すべきだ。テンプレート構造の変化を黙って無視するのは危険で、ある時テーマを変更したことで動的フィールドが再びハッシュに混入し、ビルドのたびに繰り返し支払う羽目になりかねない。
注意点として、コントラクトが実際に保存するのはそのビルドの完全な HTML であり、正規化は内容ハッシュの計算と重複排除にしか使われない。ビルド時刻だけが変わってもオンチェーンの古い HTML は上書きされない。これは想定どおりの動作だ。
GitHub Actions の二段階ビルドフロー
ワークフローは main ブランチだけを監視し、 contents: write 権限を与える。
手順は次のとおりだ。
中核となるコマンドは次のように書ける。
.onchain/articles.json は同じワークフローの2回目のビルドでしか使わないので、 .gitignore に追加すべきだ。本当に信頼できるデータソースはあくまで Polygon であり、JSON ファイルはページを生成するときのキャッシュにすぎない。
Secrets と権限の設定
リポジトリの Settings -> Secrets and variables -> Actions で設定する必要がある項目は次のとおり。
種類
名前
用途
Secret
`POLYGON_PRIVATE_KEY`
公開用ウォレットの秘密鍵
Variable
`POLYGON_RPC_URL`
任意の Polygon メインネット RPC
POLYGON_PRIVATE_KEY はウォレットの 32 バイトの秘密鍵で、通常は 64 桁の16進数文字列として表される。スクリプトは 0x プレフィックスの有無に対応しているが、ウォレットアドレスやニーモニックの名称、API Key を記入してはいけない。
秘密鍵は必ず Secret に置き、リポジトリのファイルや Actions Variable、ビルドログ、フロントエンドの JavaScript に置いてはいけない。GITHUB_TOKEN は GitHub Actions が自動で提供するので、手動で作成する必要はない。
公開用ウォレットには、次のことをおすすめする。
オンチェーン前後の検証
まずローカルでビルドと payload を検証する。
コントラクトのコンパイル、アドレス計算、記事のサイズだけをシミュレートしてもいい。
公開が完了したら、コントラクトから記事を読み取って解凍できる。
検証のときは、Web ページ上の文字だけを見てはいけない。少なくとも Polygonscan で次を確認しよう。
実際に踏んだ落とし穴
1. テストネットの Polygon は無制限に無料ではない
Amoy テストネットはフローの検証に向いているが、フォーセットには通常、受け取りの頻度と数量に制限がある。完全な HTML や記事 payload の calldata とストレージはどちらも高くつくので、初回デプロイに初回書き込みが加わると、その日に受け取ったテストトークンを簡単に超えてしまう。
テストのときはまず payload を短くし、「デプロイ」と「公開」を分けて検証し、トランザクションを送る前に gas estimate を出力しよう。メインネットに切り替えるときは、chain ID、RPC、ブロックエクスプローラーのアドレス、Gas トークンを同時に確認する必要があり、RPC URL だけを差し替えて済ませてはいけない。
2. ビルド時刻のせいでトップページが無限に繰り返しオンチェーンされる
HTML に現在時刻、Git commit、リアルタイムのブロック高、あるいはランダム値が含まれていれば、生の HTML ハッシュは安定しない。どのフィールドが内容を表し、どれが表示用メタデータにすぎないかをまず定義してから、厳密に正規化する必要がある。
3. RPC が不安定で CI がときどき失敗する
公共 RPC はレート制限されたり、タイムアウトしたり、バッチリクエストに対応していなかったりすることがある。記事公開ツールは不要な JSON-RPC batching を無効にし、ネットワークを明示的に設定し、ログで「RPC 失敗」「残高不足」「トランザクションのロールバック」「確認待ちのタイムアウト」を区別すべきだ。
失敗した後で無条件に再送してはいけない。まずオンチェーンの contentHash を読み取ろう。前のトランザクションはすでにブロードキャストに成功していて、CI がレシートを待っている途中で中断しただけかもしれないからだ。
参考リンク