initial commit

Signed-off-by: Uncle Stretch <uncle.stretch@ghostchain.io>
This commit is contained in:
Uncle Stretch 2026-09-01 13:11:06 +03:00
commit e00b5ad867
Signed by: str3tch
GPG Key ID: 84F3190747EE79AA
4 changed files with 523 additions and 0 deletions

143
.gitignore vendored Normal file
View File

@ -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*

110
README.md Normal file
View File

@ -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 <RPC_URL> [--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 <CHAIN_NAME>
```
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

122
finder.py Normal file
View File

@ -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()

148
forester.py Normal file
View File

@ -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()