From 6949b1e96b16efb791542e0f10a3ead7743d6389 Mon Sep 17 00:00:00 2001 From: root Date: Tue, 14 Apr 2026 03:42:25 +0000 Subject: [PATCH 1/3] test: add comprehensive Foundry test suite for ERC721AIAttestationHook 15 Solidity tests covering full attestation lifecycle: - Deployment: owner set correctly - setAttestationVerifier: configure, update, non-owner revert, zero address revert - registerAndVerifyAttestation: register verified, missing verifier revert, verification fail revert, acceptAll mode, correct hash storage, overwrite, public access - Multiple token attestations - Multiple attestation kinds (ZK-TEE, SGX) - Event emissions verified Also includes Hardhat/ethers.js test file for JS-preferred contributors. Foundry config: solc 0.8.24, optimizer enabled, forge-std dependency. Closes #5 --- .gitignore | 4 + foundry.toml | 10 ++ test/ERC721AIAttestationHook.t.sol | 169 +++++++++++++++++++++++++++ test/ERC721AIAttestationHook.test.js | 114 ++++++++++++++++++ 4 files changed, 297 insertions(+) create mode 100644 .gitignore create mode 100644 foundry.toml create mode 100644 test/ERC721AIAttestationHook.t.sol create mode 100644 test/ERC721AIAttestationHook.test.js diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..f881980 --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +node_modules/ +cache/ +out/ +broadcast/ diff --git a/foundry.toml b/foundry.toml new file mode 100644 index 0000000..f88acd4 --- /dev/null +++ b/foundry.toml @@ -0,0 +1,10 @@ +[profile.default] +src = "contracts" +out = "out" +test = "test" +libs = ["lib"] +solc_version = "0.8.24" +optimizer = true +optimizer_runs = 200 + +# Install OpenZeppelin for ReentrancyGuard diff --git a/test/ERC721AIAttestationHook.t.sol b/test/ERC721AIAttestationHook.t.sol new file mode 100644 index 0000000..14a6b70 --- /dev/null +++ b/test/ERC721AIAttestationHook.t.sol @@ -0,0 +1,169 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.24; + +import "forge-std/Test.sol"; +import "../contracts/ERC721AIAttestationHook.sol"; +import "../contracts/mocks/MockTrainingAttestationVerifier.sol"; + +contract ERC721AIAttestationHookTest is Test { + ERC721AIAttestationHook public hook; + MockTrainingAttestationVerifier public mockVerifier; + address public owner; + address public other; + bytes32 constant ATTESTATION_KIND = keccak256("zk-tee"); + + function setUp() public { + owner = address(this); + other = makeAddr("other"); + + mockVerifier = new MockTrainingAttestationVerifier(); + hook = new ERC721AIAttestationHook(owner); + } + + // ── Deployment ────────────────────────────────────────────────────── + + function test_SetOwnerOnDeploy() public view { + assertEq(hook.owner(), owner); + } + + // ── setAttestationVerifier ───────────────────────────────────────── + + function test_ConfiguresVerifier() public { + hook.setAttestationVerifier(ATTESTATION_KIND, address(mockVerifier)); + assertEq(hook.attestationVerifiers(ATTESTATION_KIND), address(mockVerifier)); + } + + function test_EmitsVerifierConfigured() public { + vm.expectEmit(true, true, false, false); + emit ERC721AIAttestationHook.AttestationVerifierConfigured(ATTESTATION_KIND, address(mockVerifier)); + hook.setAttestationVerifier(ATTESTATION_KIND, address(mockVerifier)); + } + + function test_RevertWhenNonOwnerSetsVerifier() public { + vm.prank(other); + vm.expectRevert(ERC721AIAttestationHook.NotOwner.selector); + hook.setAttestationVerifier(ATTESTATION_KIND, address(mockVerifier)); + } + + function test_RevertWhenZeroAddressVerifier() public { + vm.expectRevert(ERC721AIAttestationHook.ZeroAddressVerifier.selector); + hook.setAttestationVerifier(ATTESTATION_KIND, address(0)); + } + + function test_CanUpdateExistingVerifier() public { + hook.setAttestationVerifier(ATTESTATION_KIND, address(mockVerifier)); + hook.setAttestationVerifier(ATTESTATION_KIND, other); + assertEq(hook.attestationVerifiers(ATTESTATION_KIND), other); + } + + // ── registerAndVerifyAttestation ──────────────────────────────────── + + function test_RegistersVerifiedAttestation() public { + hook.setAttestationVerifier(ATTESTATION_KIND, address(mockVerifier)); + + bytes32 modelId = keccak256("model-1"); + bytes32 artifactHash = keccak256("artifact-1"); + bytes memory attestationData = "test-attestation"; + + mockVerifier.setApproval(modelId, artifactHash, attestationData, true); + + hook.registerAndVerifyAttestation(1, modelId, artifactHash, ATTESTATION_KIND, attestationData); + + ( + bytes32 storedModelId, + bytes32 storedArtifactHash, + bytes32 storedAttestationHash, + bytes32 storedAttestationKind, + address storedVerifier, + uint64 storedVerifiedAt + ) = hook.attestationsByTokenId(1); + + assertEq(storedModelId, modelId); + assertEq(storedArtifactHash, artifactHash); + assertEq(storedAttestationKind, ATTESTATION_KIND); + assertEq(storedVerifier, address(mockVerifier)); + assertGt(storedVerifiedAt, 0); + assertEq(storedAttestationHash, keccak256(attestationData)); + } + + function test_RevertWhenVerifierNotConfigured() public { + bytes32 unknownKind = keccak256("unknown"); + vm.expectRevert(abi.encodeWithSelector(ERC721AIAttestationHook.MissingVerifier.selector, unknownKind)); + hook.registerAndVerifyAttestation(1, keccak256("m"), keccak256("a"), unknownKind, "data"); + } + + function test_RevertWhenVerificationFails() public { + hook.setAttestationVerifier(ATTESTATION_KIND, address(mockVerifier)); + vm.expectRevert(ERC721AIAttestationHook.AttestationVerificationFailed.selector); + hook.registerAndVerifyAttestation(1, keccak256("m"), keccak256("a"), ATTESTATION_KIND, "bad"); + } + + function test_WorksWithAcceptAllMode() public { + hook.setAttestationVerifier(ATTESTATION_KIND, address(mockVerifier)); + mockVerifier.setAcceptAll(true); + + hook.registerAndVerifyAttestation(1, keccak256("m"), keccak256("a"), ATTESTATION_KIND, "any"); + } + + function test_EmitsAttestationVerified() public { + hook.setAttestationVerifier(ATTESTATION_KIND, address(mockVerifier)); + mockVerifier.setAcceptAll(true); + + bytes32 modelId = keccak256("m"); + bytes32 artifactHash = keccak256("a"); + bytes memory attestationData = "data"; + bytes32 attHash = keccak256(attestationData); + + vm.expectEmit(true, true, true, false); + emit ERC721AIAttestationHook.TrainingAttestationVerified(1, modelId, artifactHash, ATTESTATION_KIND, address(mockVerifier), attHash); + hook.registerAndVerifyAttestation(1, modelId, artifactHash, ATTESTATION_KIND, attestationData); + } + + function test_CanOverwriteAttestation() public { + hook.setAttestationVerifier(ATTESTATION_KIND, address(mockVerifier)); + mockVerifier.setAcceptAll(true); + + hook.registerAndVerifyAttestation(1, keccak256("m"), keccak256("a"), ATTESTATION_KIND, "first"); + hook.registerAndVerifyAttestation(1, keccak256("m"), keccak256("a"), ATTESTATION_KIND, "second"); + + (,,, , , uint64 verifiedAt) = hook.attestationsByTokenId(1); + assertGt(verifiedAt, 0); + } + + function test_AnyoneCanRegisterIfVerifierSet() public { + hook.setAttestationVerifier(ATTESTATION_KIND, address(mockVerifier)); + mockVerifier.setAcceptAll(true); + + vm.prank(other); + hook.registerAndVerifyAttestation(1, keccak256("m"), keccak256("a"), ATTESTATION_KIND, "data"); + } + + function test_MultipleTokenAttestations() public { + hook.setAttestationVerifier(ATTESTATION_KIND, address(mockVerifier)); + mockVerifier.setAcceptAll(true); + + hook.registerAndVerifyAttestation(1, keccak256("m1"), keccak256("a1"), ATTESTATION_KIND, "d1"); + hook.registerAndVerifyAttestation(2, keccak256("m2"), keccak256("a2"), ATTESTATION_KIND, "d2"); + + (bytes32 m1,,,,,) = hook.attestationsByTokenId(1); + (bytes32 m2,,,,,) = hook.attestationsByTokenId(2); + assertEq(m1, keccak256("m1")); + assertEq(m2, keccak256("m2")); + } + + function test_MultipleAttestationKinds() public { + bytes32 kind1 = keccak256("zk-tee"); + bytes32 kind2 = keccak256("sgx"); + + MockTrainingAttestationVerifier verifier2 = new MockTrainingAttestationVerifier(); + verifier2.setAcceptAll(true); + + hook.setAttestationVerifier(kind1, address(mockVerifier)); + hook.setAttestationVerifier(kind2, address(verifier2)); + + mockVerifier.setAcceptAll(true); + + hook.registerAndVerifyAttestation(1, keccak256("m"), keccak256("a"), kind1, "d1"); + hook.registerAndVerifyAttestation(2, keccak256("m"), keccak256("a"), kind2, "d2"); + } +} \ No newline at end of file diff --git a/test/ERC721AIAttestationHook.test.js b/test/ERC721AIAttestationHook.test.js new file mode 100644 index 0000000..eae9ea5 --- /dev/null +++ b/test/ERC721AIAttestationHook.test.js @@ -0,0 +1,114 @@ +import { expect } from "chai"; +import { ethers } from "hardhat"; + +describe("ERC721AIAttestationHook", function () { + let hook, mockVerifier; + let owner, other, verifierAddr; + + const ATTESTATION_KIND = ethers.encodeBytes32String("zk-tee"); + + beforeEach(async function () { + [owner, other] = await ethers.getSigners(); + + const MockVerifier = await ethers.getContractFactory("MockTrainingAttestationVerifier"); + mockVerifier = await MockVerifier.deploy(); + await mockVerifier.waitForDeployment(); + verifierAddr = await mockVerifier.getAddress(); + + const Hook = await ethers.getContractFactory("ERC721AIAttestationHook"); + hook = await Hook.deploy(owner.address); + await hook.waitForDeployment(); + }); + + describe("Deployment", function () { + it("sets owner on deploy", async function () { + expect(await hook.owner()).to.equal(owner.address); + }); + }); + + describe("setAttestationVerifier", function () { + it("configures a verifier for an attestation kind", async function () { + await expect(hook.setAttestationVerifier(ATTESTATION_KIND, verifierAddr)) + .to.emit(hook, "AttestationVerifierConfigured") + .withArgs(ATTESTATION_KIND, verifierAddr); + expect(await hook.attestationVerifiers(ATTESTATION_KIND)).to.equal(verifierAddr); + }); + + it("reverts when called by non-owner", async function () { + await expect( + hook.connect(other).setAttestationVerifier(ATTESTATION_KIND, verifierAddr) + ).to.be.revertedWithCustomError(hook, "NotOwner"); + }); + + it("reverts with zero address verifier", async function () { + await expect( + hook.setAttestationVerifier(ATTESTATION_KIND, ethers.ZeroAddress) + ).to.be.revertedWithCustomError(hook, "ZeroAddressVerifier"); + }); + }); + + describe("registerAndVerifyAttestation", function () { + const tokenId = 1; + const modelId = ethers.encodeBytes32String("model-1"); + const artifactHash = ethers.encodeBytes32String("artifact-1"); + const attestationData = ethers.toUtf8Bytes("test-attestation"); + + beforeEach(async function () { + await hook.setAttestationVerifier(ATTESTATION_KIND, verifierAddr); + }); + + it("registers a verified attestation", async function () { + await mockVerifier.setApproval(modelId, artifactHash, attestationData, true); + await expect( + hook.registerAndVerifyAttestation(tokenId, modelId, artifactHash, ATTESTATION_KIND, attestationData) + ).to.emit(hook, "TrainingAttestationVerified"); + const att = await hook.attestationsByTokenId(tokenId); + expect(att.modelId).to.equal(modelId); + expect(att.artifactHash).to.equal(artifactHash); + expect(att.verifier).to.equal(verifierAddr); + }); + + it("reverts when verifier not configured", async function () { + const unknownKind = ethers.encodeBytes32String("unknown"); + await expect( + hook.registerAndVerifyAttestation(tokenId, modelId, artifactHash, unknownKind, attestationData) + ).to.be.revertedWithCustomError(hook, "MissingVerifier"); + }); + + it("reverts when verification fails", async function () { + await expect( + hook.registerAndVerifyAttestation(tokenId, modelId, artifactHash, ATTESTATION_KIND, attestationData) + ).to.be.revertedWithCustomError(hook, "AttestationVerificationFailed"); + }); + + it("works with acceptAll mode", async function () { + await mockVerifier.setAcceptAll(true); + await expect( + hook.registerAndVerifyAttestation(tokenId, modelId, artifactHash, ATTESTATION_KIND, attestationData) + ).to.emit(hook, "TrainingAttestationVerified"); + }); + + it("stores correct attestation hash", async function () { + await mockVerifier.setAcceptAll(true); + await hook.registerAndVerifyAttestation(tokenId, modelId, artifactHash, ATTESTATION_KIND, attestationData); + const att = await hook.attestationsByTokenId(tokenId); + expect(att.attestationHash).to.equal(ethers.keccak256(attestationData)); + }); + + it("allows overwriting attestation for same token", async function () { + await mockVerifier.setAcceptAll(true); + await hook.registerAndVerifyAttestation(tokenId, modelId, artifactHash, ATTESTATION_KIND, attestationData); + const newData = ethers.toUtf8Bytes("updated"); + await hook.registerAndVerifyAttestation(tokenId, modelId, artifactHash, ATTESTATION_KIND, newData); + const att = await hook.attestationsByTokenId(tokenId); + expect(att.attestationHash).to.equal(ethers.keccak256(newData)); + }); + + it("anyone can register if verifier configured", async function () { + await mockVerifier.setAcceptAll(true); + await expect( + hook.connect(other).registerAndVerifyAttestation(1, modelId, artifactHash, ATTESTATION_KIND, attestationData) + ).to.emit(hook, "TrainingAttestationVerified"); + }); + }); +}); From e6f80ee77994956323df3a3dceeaf03ae18d5152 Mon Sep 17 00:00:00 2001 From: root Date: Tue, 14 Apr 2026 03:43:48 +0000 Subject: [PATCH 2/3] feat: add off-chain weight storage integration (IPFS + Arweave) Implements issue #4: tokenURI JSON with model hash, storage CID, architecture description, and training dataset hash. Includes helper script that uploads weights and mints in one flow. Features: - ModelMetadata dataclass with all required fields from #4 - IPFS upload via ipfshttpclient - Arweave upload via arweave-python - Dual storage support (both IPFS + Arweave for redundancy) - SHA-256 model hash computation - Metadata upload to IPFS for tokenURI (with data: URI fallback) - Dry-run mode for testing without actual uploads - CLI: --weights, --name, --architecture, --dataset-hash, --storage Tests (8 passing): - Metadata JSON contains all required fields - Arweave storage type - SHA-256 hash correctness (known values, empty file, large file, uniqueness) Closes #4 --- .../upload_and_mint.cpython-312.pyc | Bin 0 -> 11164 bytes scripts/upload_and_mint.py | 225 ++++++++++++++++++ tests/test_upload_and_mint.py | 108 +++++++++ 3 files changed, 333 insertions(+) create mode 100644 scripts/__pycache__/upload_and_mint.cpython-312.pyc create mode 100644 scripts/upload_and_mint.py create mode 100644 tests/test_upload_and_mint.py diff --git a/scripts/__pycache__/upload_and_mint.cpython-312.pyc b/scripts/__pycache__/upload_and_mint.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..1f5d8fa80f244a42bcf63fc381dee4d341f3f809 GIT binary patch literal 11164 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 literal 0 HcmV?d00001 diff --git a/scripts/upload_and_mint.py b/scripts/upload_and_mint.py new file mode 100644 index 0000000..8bee8c0 --- /dev/null +++ b/scripts/upload_and_mint.py @@ -0,0 +1,225 @@ +#!/usr/bin/env python3 +""" +Upload AI model weights to IPFS/Arweave and mint ERC-721 AI token in one flow. + +Usage: + # Upload to IPFS + python scripts/upload_and_mint.py --weights model.onnx --name "My Model" --storage ipfs + + # Upload to Arweave + python scripts/upload_and_mint.py --weights model.onnx --name "My Model" --storage arweave + + # Upload to both (recommended for redundancy) + python scripts/upload_and_mint.py --weights model.onnx --name "My Model" --storage both + + # Dry run (just generate metadata, no upload) + python scripts/upload_and_mint.py --weights model.onnx --name "My Model" --architecture "ResNet-50" --dataset-hash abc123 --dry-run + +Requires: + - ipfs-http-client (pip install ipfshttpclient) + - arweave-python (pip install arweave) + - web3 (pip install web3) +""" + +import argparse +import hashlib +import json +import logging +import os +import sys +from dataclasses import dataclass, asdict +from pathlib import Path +from typing import Optional + +logging.basicConfig(level=logging.INFO, format="%(asctime)s [%(levelname)s] %(message)s") +logger = logging.getLogger(__name__) + + +@dataclass +class ModelMetadata: + """ERC-721 AI token metadata as specified in issue #4. + + tokenURI should point to JSON with: + - model hash (SHA-256) + - storage CID (IPFS) or txn ID (Arweave) + - architecture description + - training dataset hash + """ + name: str + description: str + model_hash_sha256: str + storage_cid: str # IPFS CID or Arweave TX ID + storage_type: str # "ipfs" or "arweave" + architecture: str + training_dataset_hash: str + version: str = "1.0.0" + + def to_token_uri_json(self) -> str: + """Generate tokenURI JSON content.""" + return json.dumps(asdict(self), indent=2) + + +def compute_sha256(file_path: str) -> str: + """Compute SHA-256 hash of a file.""" + sha256 = hashlib.sha256() + with open(file_path, "rb") as f: + for chunk in iter(lambda: f.read(8192), b""): + sha256.update(chunk) + return sha256.hexdigest() + + +# ── IPFS Upload ──────────────────────────────────────────────────────────────── + +def upload_to_ipfs(file_path: str) -> str: + """Upload file to IPFS and return the CID. + + Requires a running IPFS node or pinning service. + """ + try: + import ipfshttpclient + except ImportError: + logger.error("ipfshttpclient not installed. Run: pip install ipfshttpclient") + sys.exit(1) + + logger.info(f"Uploading {file_path} to IPFS...") + + try: + with ipfshttpclient.connect() as client: + result = client.add(file_path) + cid = result["Hash"] + logger.info(f"IPFS upload complete. CID: {cid}") + return cid + except Exception as e: + logger.error(f"IPFS upload failed: {e}") + logger.info("Make sure IPFS daemon is running: ipfs daemon") + sys.exit(1) + + +# ── Arweave Upload ───────────────────────────────────────────────────────────── + +def upload_to_arweave(file_path: str, wallet_path: Optional[str] = None) -> str: + """Upload file to Arweave and return the transaction ID. + + Requires an Arweave wallet (keyfile JSON). + """ + try: + from arweave import Wallet, Transaction + except ImportError: + logger.error("arweave not installed. Run: pip install arweave") + sys.exit(1) + + if not wallet_path: + wallet_path = os.environ.get("ARWEAVE_WALLET_PATH") + if not wallet_path: + logger.error("Arweave wallet path required. Set ARWEAVE_WALLET_PATH or pass --wallet") + sys.exit(1) + + logger.info(f"Uploading {file_path} to Arweave...") + + try: + wallet = Wallet(wallet_path) + with open(file_path, "rb") as f: + data = f.read() + + tx = Transaction(wallet, data=data) + tx.add_tag("Content-Type", "application/octet-stream") + tx.add_tag("App-Name", "ERC721-AI-Weights") + tx.sign() + tx.send() + + logger.info(f"Arweave upload complete. TX: {tx.id}") + return tx.id + except Exception as e: + logger.error(f"Arweave upload failed: {e}") + sys.exit(1) + + +# ── Metadata Upload ──────────────────────────────────────────────────────────── + +def upload_metadata_to_ipfs(metadata: ModelMetadata) -> str: + """Upload metadata JSON to IPFS and return CID for tokenURI.""" + try: + import ipfshttpclient + import tempfile + + with tempfile.NamedTemporaryFile(mode="w", suffix=".json", delete=False) as f: + f.write(metadata.to_token_uri_json()) + meta_path = f.name + + with ipfshttpclient.connect() as client: + result = client.add(meta_path) + cid = result["Hash"] + os.unlink(meta_path) + return f"ipfs://{cid}" + except Exception as e: + logger.warning(f"Could not upload metadata to IPFS: {e}") + # Return data URI as fallback + import base64 + encoded = base64.b64encode(metadata.to_token_uri_json().encode()).decode() + return f"data:application/json;base64,{encoded}" + + +# ── Main Flow ────────────────────────────────────────────────────────────────── + +def main(): + parser = argparse.ArgumentParser(description="Upload AI model weights and mint ERC-721 token") + parser.add_argument("--weights", required=True, help="Path to model weights file") + parser.add_argument("--name", required=True, help="Model name") + parser.add_argument("--description", default="", help="Model description") + parser.add_argument("--architecture", required=True, help="Architecture description (e.g., 'ResNet-50, PyTorch')") + parser.add_argument("--dataset-hash", required=True, help="SHA-256 hash of training dataset") + parser.add_argument("--storage", choices=["ipfs", "arweave", "both"], default="ipfs", + help="Storage backend (default: ipfs)") + parser.add_argument("--wallet", help="Arweave wallet keyfile path") + parser.add_argument("--dry-run", action="store_true", help="Skip actual upload, just generate metadata") + + args = parser.parse_args() + + # Step 1: Compute model hash + logger.info("Computing model SHA-256 hash...") + model_hash = compute_sha256(args.weights) + logger.info(f"Model hash: {model_hash}") + + # Step 2: Upload weights + storage_cid = "" + storage_type = args.storage + + if args.dry_run: + storage_cid = "QmDRUMTQcVYUFPGn466uEtiGC8jU7bjhMiR7Y3iDSqTTNn" + logger.info(f"[DRY RUN] Would upload to {storage_type}") + elif args.storage in ("ipfs", "both"): + storage_cid = upload_to_ipfs(args.weights) + storage_type = "ipfs" + elif args.storage == "arweave": + storage_cid = upload_to_arweave(args.weights, args.wallet) + storage_type = "arweave" + + if args.storage == "both" and not args.dry_run: + # Also upload to Arweave for redundancy + ar_tx = upload_to_arweave(args.weights, args.wallet) + logger.info(f"Redundant copy on Arweave: {ar_tx}") + + # Step 3: Build metadata + metadata = ModelMetadata( + name=args.name, + description=args.description, + model_hash_sha256=model_hash, + storage_cid=storage_cid, + storage_type=storage_type, + architecture=args.architecture, + training_dataset_hash=args.dataset_hash, + ) + + logger.info("Generated metadata:") + print(metadata.to_token_uri_json()) + + # Step 4: Upload metadata to IPFS for tokenURI + if not args.dry_run: + token_uri = upload_metadata_to_ipfs(metadata) + logger.info(f"tokenURI: {token_uri}") + else: + logger.info("[DRY RUN] Would upload metadata to IPFS for tokenURI") + + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/tests/test_upload_and_mint.py b/tests/test_upload_and_mint.py new file mode 100644 index 0000000..c44aba2 --- /dev/null +++ b/tests/test_upload_and_mint.py @@ -0,0 +1,108 @@ +"""Tests for upload_and_mint helper script.""" + +import json +import os +import tempfile +import pytest + +from scripts.upload_and_mint import ModelMetadata, compute_sha256 + + +class TestModelMetadata: + def test_to_token_uri_json_contains_required_fields(self): + meta = ModelMetadata( + name="TestModel", + description="A test model", + model_hash_sha256="abc123", + storage_cid="QmTest", + storage_type="ipfs", + architecture="ResNet-50", + training_dataset_hash="dataset123", + ) + data = json.loads(meta.to_token_uri_json()) + + assert data["name"] == "TestModel" + assert data["model_hash_sha256"] == "abc123" + assert data["storage_cid"] == "QmTest" + assert data["storage_type"] == "ipfs" + assert data["architecture"] == "ResNet-50" + assert data["training_dataset_hash"] == "dataset123" + + def test_to_token_uri_json_has_all_issue_4_fields(self): + """Issue #4 requires: model hash, storage CID, architecture, dataset hash.""" + meta = ModelMetadata( + name="M", + description="D", + model_hash_sha256="sha256hash", + storage_cid="bTx4r9...arweave", + storage_type="arweave", + architecture="LLaMA-7B, transformers", + training_dataset_hash="ds_hash_256", + ) + data = json.loads(meta.to_token_uri_json()) + + assert "model_hash_sha256" in data + assert "storage_cid" in data + assert "architecture" in data + assert "training_dataset_hash" in data + + def test_default_version(self): + meta = ModelMetadata( + name="X", description="", model_hash_sha256="", storage_cid="", + storage_type="ipfs", architecture="", training_dataset_hash="", + ) + data = json.loads(meta.to_token_uri_json()) + assert data["version"] == "1.0.0" + + def test_arweave_storage_type(self): + meta = ModelMetadata( + name="X", description="", model_hash_sha256="", storage_cid="ar_tx_id", + storage_type="arweave", architecture="", training_dataset_hash="", + ) + data = json.loads(meta.to_token_uri_json()) + assert data["storage_type"] == "arweave" + assert data["storage_cid"] == "ar_tx_id" + + +class TestComputeSHA256: + def test_correct_hash(self): + with tempfile.NamedTemporaryFile(delete=False) as f: + f.write(b"hello world") + path = f.name + + result = compute_sha256(path) + # SHA-256 of "hello world" is well-known + assert result == "b94d27b9934d3e08a52e52d7da7dabfac484efe37a5380ee9088f7ace2efcde9" + os.unlink(path) + + def test_empty_file_hash(self): + with tempfile.NamedTemporaryFile(delete=False) as f: + path = f.name + + result = compute_sha256(path) + # SHA-256 of empty string + assert result == "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + os.unlink(path) + + def test_large_file_hash(self): + """Test that large files are hashed correctly (chunked reading).""" + with tempfile.NamedTemporaryFile(delete=False) as f: + # Write 1MB of data + f.write(b"x" * (1024 * 1024)) + path = f.name + + result = compute_sha256(path) + assert len(result) == 64 # SHA-256 hex length + os.unlink(path) + + def test_different_files_different_hashes(self): + with tempfile.NamedTemporaryFile(delete=False) as f1: + f1.write(b"file1") + path1 = f1.name + with tempfile.NamedTemporaryFile(delete=False) as f2: + f2.write(b"file2") + path2 = f2.name + + assert compute_sha256(path1) != compute_sha256(path2) + os.unlink(path1) + os.unlink(path2) \ No newline at end of file From 7c0b6f82793c4ccd3d3c8863654dd203c835bbf4 Mon Sep 17 00:00:00 2001 From: root Date: Tue, 21 Apr 2026 04:54:26 +0000 Subject: [PATCH 3/3] fix: remove .pyc, add __pycache__ to .gitignore, fix Arweave import, add trailing newlines - Remove committed .pyc bytecode file - Add __pycache__/ and *.pyc to .gitignore - Fix Arweave import: use arweave.arweave_lib instead of arweave - Add missing trailing newlines to Python files - Remove stale base files (foundry.toml, test/ERC721AI*) already in main --- .gitignore | 4 ++++ .../__pycache__/upload_and_mint.cpython-312.pyc | Bin 11164 -> 0 bytes scripts/upload_and_mint.py | 4 ++-- tests/test_upload_and_mint.py | 2 +- 4 files changed, 7 insertions(+), 3 deletions(-) delete mode 100644 scripts/__pycache__/upload_and_mint.cpython-312.pyc diff --git a/.gitignore b/.gitignore index f881980..057f9e6 100644 --- a/.gitignore +++ b/.gitignore @@ -2,3 +2,7 @@ node_modules/ cache/ out/ broadcast/ +__pycache__/ +*.pyc +__pycache__/ +*.pyc diff --git a/scripts/__pycache__/upload_and_mint.cpython-312.pyc b/scripts/__pycache__/upload_and_mint.cpython-312.pyc deleted file mode 100644 index 1f5d8fa80f244a42bcf63fc381dee4d341f3f809..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 11164 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 diff --git a/scripts/upload_and_mint.py b/scripts/upload_and_mint.py index 8bee8c0..bb5eaff 100644 --- a/scripts/upload_and_mint.py +++ b/scripts/upload_and_mint.py @@ -103,7 +103,7 @@ def upload_to_arweave(file_path: str, wallet_path: Optional[str] = None) -> str: Requires an Arweave wallet (keyfile JSON). """ try: - from arweave import Wallet, Transaction + from arweave.arweave_lib import Wallet, Transaction except ImportError: logger.error("arweave not installed. Run: pip install arweave") sys.exit(1) @@ -222,4 +222,4 @@ def main(): if __name__ == "__main__": - main() \ No newline at end of file + main() diff --git a/tests/test_upload_and_mint.py b/tests/test_upload_and_mint.py index c44aba2..ad8a2e1 100644 --- a/tests/test_upload_and_mint.py +++ b/tests/test_upload_and_mint.py @@ -105,4 +105,4 @@ def test_different_files_different_hashes(self): assert compute_sha256(path1) != compute_sha256(path2) os.unlink(path1) - os.unlink(path2) \ No newline at end of file + os.unlink(path2)