デジタル保存とブロックチェーン

デジタル文化財を長期的に保存するためには、ファイルそのものだけでなく、そのファイルがいつ・誰によって作成され・どのような変更を経てきたかという**来歴情報(provenance)**を記録する必要があります。ブロックチェーンのトランザクション履歴は、この来歴情報の信頼性を保証する手段として活用できます。

本章では、デジタル保存の国際標準であるOAIS参照モデルとBagIt仕様を学び、ブロックチェーンのトランザクション情報を来歴メタデータとして含むAIPパッケージを生成する方法を実践します。

OAIS参照モデル

OAIS(Open Archival Information System)は、ISOで標準化されたデジタル保存の参照モデルです(ISO 14721:2012)。デジタルアーカイブで扱う情報パッケージを3つに分類しています。

パッケージ正式名称役割
SIPSubmission Information Package生産者からアーカイブへの提出用パッケージ
AIPArchival Information Packageアーカイブ内での長期保存用パッケージ
DIPDissemination Information Package利用者への配布用パッケージ

本章で生成するのはAIP(保存用パッケージ)です。AIPには以下の要素が含まれます。

  • Content Information: 保存対象のデジタルオブジェクト(画像、メタデータ等)
  • Preservation Description Information(PDI): 来歴(Provenance)、コンテキスト、参照、固定性の情報
  • Packaging Information: パッケージの構造に関する情報

BagIt仕様

BagIt(RFC 8493)は、米国議会図書館が開発したデジタルコンテンツのパッケージング仕様です。シンプルなディレクトリ構造とチェックサムファイルで、デジタルオブジェクトの完全性を保証します。

BagItの構造

my-babbmtdgaaaaa/ggngti-imatifa/imp.nenmertfsiatoxotfgavt.-eedebtss.anlxhtjtaota-panc2sg.ck5hjec6ash.2at5nix6nt.-ttxxt.j#####soBdnaagtIat/Key-Value
  • bagit.txt: BagIt-Version: 1.0Tag-File-Character-Encoding: UTF-8 の2行のみ
  • manifest-sha256.txt: data/ 配下の各ファイルのSHA-256ハッシュをリスト化
  • bag-info.txt: パッケージのメタデータ(作成日、送信元機関名など)を記述

ブロックチェーン来歴情報の取得

まず、NFTのミントおよび転送に関するトランザクション情報をブロックチェーンから取得するスクリプトを作成します。

// scripts/fetchProvenance.ts
import { ethers } from "hardhat";

interface ProvenanceEvent {
  eventType: string;
  transactionHash: string;
  blockNumber: number;
  timestamp: string;
  from: string;
  to: string;
  tokenId: number;
}

async function fetchProvenance(
  contractAddress: string,
  tokenId: number
): Promise<ProvenanceEvent[]> {
  const contract = await ethers.getContractAt(
    "CulturalNFT",
    contractAddress
  );

  const provenance: ProvenanceEvent[] = [];

  // Transfer イベントを取得(ERC721標準)
  const transferFilter = contract.filters.Transfer(
    null, null, tokenId
  );
  const transferEvents = await contract.queryFilter(transferFilter);

  for (const event of transferEvents) {
    const block = await event.getBlock();
    const args = (event as any).args;

    provenance.push({
      eventType:
        args.from === ethers.ZeroAddress ? "Mint" : "Transfer",
      transactionHash: event.transactionHash,
      blockNumber: event.blockNumber,
      timestamp: new Date(block.timestamp * 1000).toISOString(),
      from: args.from,
      to: args.to,
      tokenId: Number(args.tokenId),
    });
  }

  return provenance;
}

// 使用例
async function main() {
  const contractAddress = process.env.CONTRACT_ADDRESS!;
  const tokenId = 0;

  const provenance = await fetchProvenance(
    contractAddress,
    tokenId
  );

  console.log(JSON.stringify(provenance, null, 2));
}

main().catch(console.error);

PythonによるBagItパッケージ生成

BagItパッケージの生成にはPythonの bagit ライブラリを使用します。ブロックチェーンから取得した来歴情報をパッケージに含めます。

pip install bagit requests
# scripts/generate_aip.py
import bagit
import json
import hashlib
import os
import shutil
from datetime import datetime

def generate_aip(
    bag_path: str,
    image_path: str,
    nft_metadata: dict,
    blockchain_provenance: list,
    iiif_manifest_uri: str = None
):
    """
    BagIt準拠のAIPパッケージを生成する

    Args:
        bag_path: 出力先ディレクトリのパス
        image_path: 保存対象の画像ファイルパス
        nft_metadata: NFTメタデータ(JSON)
        blockchain_provenance: ブロックチェーン来歴情報のリスト
        iiif_manifest_uri: IIIFマニフェストURI(オプション)
    """

    # 1. ディレクトリ構造の作成
    data_dir = os.path.join(bag_path, "data")
    provenance_dir = os.path.join(data_dir, "provenance")
    os.makedirs(provenance_dir, exist_ok=True)

    # 2. 保存対象ファイルのコピー
    # 画像ファイル
    image_filename = os.path.basename(image_path)
    shutil.copy2(image_path, os.path.join(data_dir, image_filename))

    # NFTメタデータ
    metadata_path = os.path.join(data_dir, "nft-metadata.json")
    with open(metadata_path, "w", encoding="utf-8") as f:
        json.dump(nft_metadata, f, ensure_ascii=False, indent=2)

    # 3. ブロックチェーン来歴情報の保存
    provenance_path = os.path.join(
        provenance_dir, "blockchain-transactions.json"
    )
    provenance_data = {
        "source": "Ethereum Blockchain",
        "network": "sepolia",
        "exportedAt": datetime.utcnow().isoformat() + "Z",
        "events": blockchain_provenance,
    }
    with open(provenance_path, "w", encoding="utf-8") as f:
        json.dump(provenance_data, f, ensure_ascii=False, indent=2)

    # 4. IIIF情報の保存(存在する場合)
    if iiif_manifest_uri:
        iiif_info = {
            "manifestURI": iiif_manifest_uri,
            "standard": "IIIF Presentation API 3.0",
            "recordedAt": datetime.utcnow().isoformat() + "Z",
        }
        iiif_path = os.path.join(
            provenance_dir, "iiif-reference.json"
        )
        with open(iiif_path, "w", encoding="utf-8") as f:
            json.dump(iiif_info, f, ensure_ascii=False, indent=2)

    # 5. BagItパッケージの生成
    bag = bagit.make_bag(
        bag_path,
        checksums=["sha256"],
        bag_info={
            "Source-Organization": "Cultural Heritage Archive",
            "Organization-Address": "Tokyo, Japan",
            "Contact-Name": "Nakamura",
            "Bagging-Date": datetime.utcnow().strftime("%Y-%m-%d"),
            "External-Description":
                nft_metadata.get("description", ""),
            "External-Identifier":
                blockchain_provenance[0]["transactionHash"]
                if blockchain_provenance else "",
            "Bag-Group-Identifier": "cultural-heritage-nft",
            "Internal-Sender-Identifier":
                f"token-{blockchain_provenance[0]['tokenId']}"
                if blockchain_provenance else "",
        },
    )

    # 6. パッケージの検証
    if bag.is_valid():
        print(f"AIPパッケージを生成しました: {bag_path}")
        print(f"  ファイル数: {len(list(bag.payload_files()))}")
        print(f"  合計サイズ: {bag.info.get('Payload-Oxum', 'N/A')}")
    else:
        print("エラー: パッケージの検証に失敗しました")

    return bag


def validate_bag(bag_path: str) -> bool:
    """BagItパッケージの完全性を検証する"""
    try:
        bag = bagit.Bag(bag_path)
        bag.validate()
        print(f"検証成功: {bag_path}")
        return True
    except bagit.BagValidationError as e:
        print(f"検証失敗: {e}")
        return False


# 使用例
if __name__ == "__main__":
    # NFTメタデータ
    nft_metadata = {
        "name": "鳥獣戯画 甲巻(部分)",
        "description": "国宝・鳥獣人物戯画のデジタル複製",
        "image": "ipfs://QmYourImageHash",
        "attributes": [
            {"trait_type": "Institution", "value": "高山寺"},
            {"trait_type": "Period", "value": "平安時代後期"},
        ],
    }

    # ブロックチェーン来歴情報(fetchProvenanceの出力)
    blockchain_provenance = [
        {
            "eventType": "Mint",
            "transactionHash": "0xabc123...def456",
            "blockNumber": 12345678,
            "timestamp": "2024-01-15T10:30:00.000Z",
            "from": "0x0000000000000000000000000000000000000000",
            "to": "0x742d35Cc6634C0532925a3b844Bc9e7595f2bD18",
            "tokenId": 0,
        },
    ]

    # AIPパッケージの生成
    generate_aip(
        bag_path="./output/aip-choju-giga",
        image_path="./images/choju-giga.jpg",
        nft_metadata=nft_metadata,
        blockchain_provenance=blockchain_provenance,
        iiif_manifest_uri=(
            "https://example.org/iiif/choju-giga/manifest.json"
        ),
    )

    # 生成されたパッケージの検証
    validate_bag("./output/aip-choju-giga")

生成されるAIPパッケージの構造

aip-cbBTbSBEEBImad7dt135dhaaaaoaxxana148ea135aoggggugttgtnbe9fg135tjiI--rgee-ei25a0m246auttFicirrGrfcfb1a246/cnp-.-inennnrne36c2n246hfrgtVlf-gaaoas....i...otoixeeoO-llult....f...j-vgtr-.rD--p--....e...umebiasCtgaDI-Sss-enli/ihxatedIehddddtbbmgtaoioatnesednaaaaa-aaaiancfnri:cned2ttttsggngdck-:azrtne5aaaahi-iaaecrca2iitr6///atif.t/he1tt0pfi-.cnpp2.nejaaf.ei2tifIthfrr5tfsp.ie0ro4ieidxotoo6xotgjnr-n-oreetj-vv.t.-s-eE:0n:rnumeettsotnn1::t-ennxxhnrccC-0igtaattaaeou2xcfiann2n.dl0auigdcc5sjitbleaaee6asnuctr.t//.cogr1u:jabittn:a2rp.lixil3atgjoitoU.loscfnTH.-kok-sFe.hencr.-rdenhej8ier-afstfi0ieoa4tnrng5a-ee6gtnercA-aernn.cfsjhtasicovtneions.json

TypeScriptからPythonスクリプトを呼び出す

Hardhatのスクリプトから、来歴情報の取得とAIPパッケージ生成を一気通貫で実行するワークフローです。

// scripts/generateAIP.ts
import { ethers } from "hardhat";
import { execSync } from "child_process";
import * as fs from "fs";

async function main() {
  const contractAddress = process.env.CONTRACT_ADDRESS!;
  const tokenId = 0;

  // 1. ブロックチェーンから来歴情報を取得
  console.log("来歴情報を取得中...");
  const contract = await ethers.getContractAt(
    "CulturalNFT",
    contractAddress
  );

  const filter = contract.filters.Transfer(null, null, tokenId);
  const events = await contract.queryFilter(filter);

  const provenance = [];
  for (const event of events) {
    const block = await event.getBlock();
    const args = (event as any).args;
    provenance.push({
      eventType:
        args.from === ethers.ZeroAddress ? "Mint" : "Transfer",
      transactionHash: event.transactionHash,
      blockNumber: event.blockNumber,
      timestamp: new Date(block.timestamp * 1000).toISOString(),
      from: args.from,
      to: args.to,
      tokenId: Number(args.tokenId),
    });
  }

  // 2. 来歴情報をJSONファイルとして保存
  const provenancePath = "./tmp/provenance.json";
  fs.mkdirSync("./tmp", { recursive: true });
  fs.writeFileSync(
    provenancePath,
    JSON.stringify(provenance, null, 2)
  );

  // 3. Pythonスクリプトでパッケージ生成
  console.log("AIPパッケージを生成中...");
  execSync(
    `python scripts/generate_aip.py --provenance ${provenancePath}`,
    { stdio: "inherit" }
  );

  console.log("AIPパッケージの生成が完了しました。");
}

main().catch(console.error);

パッケージの長期保存

生成したAIPパッケージは、以下の場所に保存することを検討します。

保存先特徴
IPFS(Pinata)コンテンツアドレッシングによる完全性保証
AWS S3 Glacier低コストの長期保存ストレージ
機関リポジトリ大学・博物館の既存保存インフラ

BagItパッケージ全体をIPFSにアップロードすることで、パッケージのCID(Content Identifier)がオンチェーンのメタデータとリンクし、保存対象から来歴情報まで一貫した完全性検証が可能になります。

まとめ

本章では、OAIS参照モデルとBagIt仕様に基づくAIPパッケージの生成を実践しました。ブロックチェーンのトランザクション履歴を来歴メタデータとしてパッケージに含めることで、デジタル文化財の長期保存に必要な信頼性と完全性を確保しています。

次章では、Semantic Web技術(RDF/JSON-LD)を使って、ブロックチェーン上の来歴情報をより構造的に記述する方法を学びます。

関連記事