commit e00b5ad8676abdf13bf5e048094a20bb21f28815 Author: Uncle Stretch Date: Tue Sep 1 13:11:06 2026 +0300 initial commit Signed-off-by: Uncle Stretch diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..276919c --- /dev/null +++ b/.gitignore @@ -0,0 +1,143 @@ +# Python virtual environment +venv/ +env/ +ENV/ +env.bak/ +venv.bak/ +.env/ +.venv/ + +# Python cache and compiled files +__pycache__/ +*.py[cod] +*$py.class +*.so +*.pyd +*.pyo +*.pyc +*.pyz +*.pyi + +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +share/python-wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST + +# Testing +.pytest_cache/ +.coverage +htmlcov/ +.tox/ +.mypy_cache/ +.dmypy.json +dmypy.json +.pytest_cache/ +coverage.xml +*.cover +*.log + +# Jupyter Notebooks +.ipynb_checkpoints/ +*.ipynb + +# Environment variables +.env +.env.local +.env.*.local + +# IDE and editor files +.vscode/ +.idea/ +*.swp +*.swo +*~ +.DS_Store +*.iml +.settings/ +.project +.classpath +.pydevproject + +# Logs and databases +logs/ +*.log +*.db +*.sqlite +*.sqlite3 + +# OS generated files +Thumbs.db +.DS_Store +.DS_Store? +._* +.Spotlight-V100 +.Trashes +ehthumbs.db +Desktop.ini + +# Backup files +*.bak +*.tmp +*.temp + +# Project specific (for your snapshot collector) +snapshots/*.json +!snapshots/.gitkeep # Optional: keep the directory but not the files + +# Output files +output/ +data/ +*.out + +# Security credentials +secrets.py +config.py +credentials.py +*.key +*.pem +*.crt + +# For web3 and blockchain projects +*.abi +*.bin +build/ +contracts/*.json +!contracts/abi.json + +# Type checking +.stubs/ + +# Profiling data +*.prof +*.gcda +*.gcno + +# Docker +.dockerignore +*.dockerfile + +# Terraform +*.tfstate +*.tfstate.* +.terraform/ + +# Node.js (if you have any) +node_modules/ +npm-debug.log* +yarn-debug.log* +yarn-error.log* diff --git a/README.md b/README.md new file mode 100644 index 0000000..7bb4a6e --- /dev/null +++ b/README.md @@ -0,0 +1,110 @@ +# Ghost Preclaim Snapshot Tool +The Ghost Preclaim Snapshot Tool is a comprehensive utility designed for the Envious NFT ecosystem, enabling seamless collection of NFT holdings snapshots and generation of cryptographically verifiable Merkle tree proofs. These proofs serve as the foundation for Ghostchain preclaim mechanisms, which are integral to the platform's governance framework. + +## Overview +This repository contains two Python scripts that work together to: + +1. Collect NFT ownership data and collateral balances from an EVM chain +2. Generate Merkle tree proofs for each EVM chain + +## Requirements +```bash +pip install web3 tqdm +``` + +## Usage + +### Collect Snapshot Data +The Ghost Preclaim Snapshot Tool provides a straightforward command-line interface for collecting NFT snapshot data across multiple blockchain networks. Before running the tool, ensure you have properly configured your environment with the necessary RPC endpoints and network access credentials. + +```bash +python finder.py --rpc [--delay 0.3] +``` + +Parameters: +```bash +--rpc: EVM JSON-RPC endpoint URL +--delay: (Optional) Delay between RPC requests in seconds (default: 0.3) +``` + +Example: + +```bash +python finder.py --rpc https://sepolia.infura.io/v3/YOUR_KEY +``` + +### Generate Merkle Proofs + +The Merkle proof generation module transforms raw snapshot data into cryptographically verifiable proofs that enable efficient and secure preclaim verification on the Ghostchain governance platform. This critical component ensures that only legitimate NFT holders can participate in governance decisions and claim their allocations. + +```bash +python forester.py --chain +``` + +Example: + +```bash +python forester.py --chain 11155111 +``` + +Output: + +```bash +[+] Reading data snapshot: snapshots\sepolia.json + +============================================================================================================ +=== FINAL MERKLE ROOT FOR [11155111]: 0x95eeb07a13173eaad41a98e2216f84171641a76474e9e9f4c3bc069e4d461e8c === +============================================================================================================ +[+] Total padded leaves: 128 +[+] Merkle tree size: 255 +[+] Proof size: 7 +[+] Total shares: 420000000000000000 + +[+] Proof are stored into: preclaims\11155111.json +``` + +## Supported Networks +* Sepolia Testnet +* Linea Sepolia Testnet +* BeraChain Testnet +* ZetaChain Testnet + +This tool support any EVM-compatible blockchain where the JML (John McAfee Legacy) and GMV (Ghost McAfee Vision) smart contracts are deployed. This universal compatibility ensures seamless operation across multiple networks without requiring chain-specific configuration. + +```bash +JML (NFT Collection): 0x91ba8A14D2CC851aBb69212c09f59e06e1e7f0a5 +GMV (ERC20 Collateral Token): 0x7EF911f8ef130F73D166468c0068753932357B17 +``` + +## Technical details +This section provides comprehensive technical specifications for developers, auditors, and advanced users who need to understand the underlying cryptographic mechanisms and data structures. + +### Merkle Tree Implementation +The Merkle tree serves as the cryptographic backbone for verifying requested claims without exposing the entire dataset on-chain. + +#### Leaf Node Construction +Each leaf node in the Merkle tree represents a unique NFT holding and is computed using the following formula: + +```bash +leaf = keccak256(token_id + address + balance) +``` + +Where components are: + +| Component | Description | Format | +|:-----------|:------------:|:------------| +| `token_id` | Unique NFT identifier within the contract | 32-byte unsigned integer (big-endian) | +| `address` | Token owner's wallet address | 20-byte EVM address | +| `balance` | GMV Collateral behind the NFT | 32-byte unsigned integer (big-endian) | + +#### Empty Leaf Handling +For tree padding to achieve power-of-two size, empty leaves are represented as: + +```bash +EMPTY_LEAF = Web3.keccak(b'') +# 0xc5d2460186f7233c927e7db2dcc703c0e500b653ca82273b7bfad8045d85a470 +``` + +## Use wisely + +Made with ❤️ for the ghosties all over the world diff --git a/finder.py b/finder.py new file mode 100644 index 0000000..8f8b5b9 --- /dev/null +++ b/finder.py @@ -0,0 +1,122 @@ +import os +import sys +import time +import json +import argparse +from tqdm import tqdm +from web3 import Web3 + +JML_ADDRESS = "0x91ba8A14D2CC851aBb69212c09f59e06e1e7f0a5" +GMV_ADDRESS = "0x7EF911f8ef130F73D166468c0068753932357B17" + +def parse_arguments(): + parser = argparse.ArgumentParser(description="EVM NFT Snapshot Collector for Ghostchain Preclaims") + parser.add_argument("--rpc", required=True, help="EVM JSON-RPC Node URL") + parser.add_argument("--delay", type=float, default=0.3, help="Delay between RPC requests in seconds (default: 0.3)") + return parser.parse_args() + + +def get_contract_instance(w3): + abi = [ + { + "inputs": [], + "name": "totalSupply", + "outputs": [{"internalType": "uint256", "name": "", "type": "uint256"}], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [{"internalType": "uint256", "name": "_tokenId", "type": "uint256"}], + "name": "ownerOf", + "outputs": [{"internalType": "address", "name": "owner", "type": "address"}], + "stateMutability": "view", + "type": "function" + }, + { + "inputs":[ + {"internalType": "uint256", "name": "", "type":"uint256"}, + {"internalType": "address", "name": "", "type":"address"} + ], + "name": "collateralBalances", + "outputs":[{"internalType": "uint256", "name":"", "type": "uint256"}], + "stateMutability": "view", + "type": "function" + } + ] + return w3.eth.contract(address=w3.to_checksum_address(JML_ADDRESS), abi=abi) + +def load_existing_snapshot(file_path): + if os.path.exists(file_path): + try: + with open(file_path, "r") as f: + data = json.load(f) + if isinstance(data, list) and len(data) > 0: + last_token_id = max(item["token_id"] for item in data) + print(f"[+] Snapshot exists. Last processed Token ID: {last_token_id}") + return data, last_token_id + 1 + except Exception as e: + print(f"[!] File not fount at {file_path}. Start from scratch.") + return [], 1 + +def save_snapshot(file_path, snapshot_data): + os.makedirs(os.path.dirname(file_path), exist_ok=True) + with open(file_path, "w") as f: + json.dump(snapshot_data, f, indent=2) + +def main(): + args = parse_arguments() + + w3 = Web3(Web3.HTTPProvider(args.rpc)) + if not w3.is_connected(): + print(f"[-] Could not connect to EVM RPC: {args.rpc}") + sys.exit(1) + + chain_id = w3.eth.chain_id + print(f"[+] Successfully connected to [{chain_id}] over {args.rpc}.") + + contract = get_contract_instance(w3) + total_supply = contract.functions.totalSupply().call() + print(f"[+] Current totalSupply: {total_supply}") + time.sleep(args.delay) + + output_file = os.path.join("snapshots", f"{chain_id}.json") + snapshot, start_token_id = load_existing_snapshot(output_file) + + if start_token_id >= total_supply: + print(f"[+] All preclaims are already collected.") + sys.exit(0) + + print(f"[+] Start loop over from Token ID #{start_token_id + 1} to #{total_supply}...") + + try: + current_index = len(snapshot) + for token_id in tqdm(range(start_token_id, total_supply), desc=f"Scanning {chain_id}"): + try: + owner = contract.functions.ownerOf(token_id).call() + time.sleep(args.delay) + shares = contract.functions.collateralBalances(token_id, GMV_ADDRESS).call() + time.sleep(args.delay) + + snapshot.append({ + "index": current_index, + "token_id": token_id, + "address": owner, + "shares": shares + }) + + current_index += 1 + except Exception as e: + print(f"[-] Error occured during on the Token ID {token_id}: {e}") + break + + except KeyboardInterrupt: + print("\n[!] Stopped. Saving data...") + except Exception as fatal_err: + print(f"\n[-] Critical error: {fatal_err}. Saving data...") + finally: + save_snapshot(output_file, snapshot) + print(f"[+] Saved to file: {output_file}") + print(f"[+] Chain [{chain_id}] has {len(snapshot)} records") + +if __name__ == "__main__": + main() diff --git a/forester.py b/forester.py new file mode 100644 index 0000000..70fb63c --- /dev/null +++ b/forester.py @@ -0,0 +1,148 @@ +import os +import json +import argparse +import sys +from eth_hash.auto import keccak + +def parse_arguments(): + parser = argparse.ArgumentParser(description="Dynamic Merkle Tree for Ghost Preclaims") + parser.add_argument( + "--network-id", + type=int, + required=True, + help="EVM numeric Network ID to mix into preimage (e.g., 11155111)" + ) + return parser.parse_args() + +def keccak256(data: bytes) -> bytes: + return keccak(data) + +EMPTY_HASH = keccak256(b'') + +def next_power_of_two(n: int) -> int: + if n <= 1: + return 1 + return 1 << (n - 1).bit_length() + +def generate_preimage(token_id, evm_address, shares, network_id) -> bytes: + token_bytes = token_id.to_bytes(32, byteorder='big') + shares_bytes = shares.to_bytes(32, byteorder='big') + + clean_addr = evm_address.replace('0x', '') + raw_addr_bytes = bytes.fromhex(clean_addr) + addr_bytes = b'\x00' * 12 + raw_addr_bytes + + network_bytes = network_id.to_bytes(32, byteorder='big') + + return token_bytes + shares_bytes + addr_bytes + network_bytes + +def generate_tree(raw_values, network_id): + max_index = max(item['index'] for item in raw_values) + num_of_leaves = max_index + 1 + padded_leaves = next_power_of_two(num_of_leaves) + total_capacity = (2 * padded_leaves) - 1 + + merkle_tree = [EMPTY_HASH] * total_capacity + + for item in raw_values: + idx = item['index'] + preimage = generate_preimage(item['token_id'], item['address'], item['shares'], network_id) + merkle_tree[idx] = keccak256(preimage) + + layer_start = 0 + current_layer_len = padded_leaves + write_ptr = padded_leaves + + while current_layer_len > 1: + for i in range(0, current_layer_len, 2): + layer_index = layer_start + i + left_bytes = merkle_tree[layer_index] + right_bytes = merkle_tree[layer_index | 1] + + combined = left_bytes + right_bytes + merkle_tree[write_ptr] = keccak256(combined) + write_ptr += 1 + + layer_start += current_layer_len + current_layer_len >>= 1 + + return merkle_tree, padded_leaves + +def get_merkle_proof(tree, index, padded_leaves): + proof = [] + current_idx = index + layer_start = 0 + current_layer_len = padded_leaves + + while current_layer_len > 1: + sibling_idx = current_idx ^ 1 + proof.append(tree[sibling_idx].hex()) + + local_pair_idx = (current_idx - layer_start) // 2 + next_layer_start = layer_start + current_layer_len + current_idx = next_layer_start + local_pair_idx + + layer_start = next_layer_start + current_layer_len >>= 1 + + return proof + +def main(): + args = parse_arguments() + network_id = args.network_id + + snapshot_file = os.path.join("snapshots", f"{network_id}.json") + + if not os.path.exists(snapshot_file): + print(f"[-] File not found: {snapshot_file}") + sys.exit(1) + + print(f"[+] Reading data snapshot: {snapshot_file}") + with open(snapshot_file, "r") as f: + snapshot_data = json.load(f) + + if not snapshot_data: + print("[-] Snapshot file is empty.") + sys.exit(1) + + total_shares = 0 + for data in snapshot_data: + total_shares += data["shares"] + + tree, padded_leaves = generate_tree(snapshot_data, args.network_id) + root_hash = tree[-1].hex() + + proof_len = 0 + output_packages = {} + for item in snapshot_data: + proof = get_merkle_proof(tree, item['index'], padded_leaves) + proof_len = len(proof) + + output_packages[item['address']] = { + "index": item["index"], + "token_id": item['token_id'], + "shares": item['shares'], + "merkle_proof": [f"0x{p}" for p in proof] + } + + message = f"=== FINAL MERKLE ROOT [{network_id}]: {root_hash} ===" + separator = "=" * len(message) + + print(f"\n{separator}") + print(message) + print(f"{separator}\n") + + print(f"[+] Total padded leaves: {padded_leaves}") + print(f"[+] Merkle tree size: {len(tree)}") + print(f"[+] Proof size: {proof_len}") + print(f"[+] Total shares: {total_shares}") + + output_file = os.path.join("preclaims", f"{network_id}.json") + os.makedirs(os.path.dirname(output_file), exist_ok=True) + with open(output_file, "w") as f: + json.dump(output_packages, f, indent=2) + + print(f"\n[+] Proof are stored into: {output_file}") + +if __name__ == "__main__": + main()