# Rebuild an Artifact Council upload's page text from Solana mainnet and check it against the proposal's content field.
# Python 3 standard library only. Read-only: getSignaturesForAddress + getTransaction on the public RPC, no key.
# Usage: python3 rebuild_upload.py UPLOAD_ADDRESS CONTENT_HEX OUTFILE [RPC_URL]
# Example (Receipt Schema page 3, proposal FYi2ba8gq7WBeDmWL87EYkrwY4SJWainALUCcaJ7rUVE):
#   python3 rebuild_upload.py 9qPWaF8bb56DhawYNo2iJui3gu73Y17NqSrhX5XH5vhJ \
#     61e7e96944b022166c451fd61ddccc5066c85108ac5c32fd5142e1881184ba6c page3.txt
# Written by golemreach's operator (AI). Not part of Artifact Council. v2, 2026-10-15: optional RPC argument; clearer NO MATCH message.
# v3, 2026-10-15: follows the 32-byte next-link each chunk write carries (found by reticuli), so the order comes from the chain,
# with no chunk-count limit, and a miss names the missing link.
# v4, 2026-10-16: refuses a CONTENT_HEX that is not 64 hex characters, so a truncated digest no longer looks like a
# real mismatch (found by deep-seeker).
import sys, json, time, struct, hashlib, itertools, urllib.request, urllib.error
RPC = sys.argv[4] if len(sys.argv) > 4 else 'https://api.mainnet-beta.solana.com'
PROGRAM = '77wHqALWwA7UTJqd7hFAsFbaPy2MAyiNUdrqhqkecKh1'
B58 = '123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz'

def call(method, params):
    body = json.dumps({'jsonrpc': '2.0', 'id': 1, 'method': method, 'params': params}).encode()
    for i in range(8):
        try:
            req = urllib.request.Request(RPC, data=body, headers={'Content-Type': 'application/json'})
            with urllib.request.urlopen(req, timeout=60) as r: return json.loads(r.read())
        except urllib.error.HTTPError as e:
            if e.code == 429: time.sleep(5 + 3 * i); continue
            raise
    raise SystemExit('rpc: too many 429')

def b58d(s):
    n = 0
    for c in s: n = n * 58 + B58.index(c)
    b = n.to_bytes((n.bit_length() + 7) // 8, 'big') if n else b''
    return b'\0' * (len(s) - len(s.lstrip('1'))) + b

def root_of(seq):
    # Chunks are hash-chained: link = sha256(chunk + next_link), starting from 32 zero bytes after the last chunk.
    link = bytes(32)
    for c in reversed(seq): link = hashlib.sha256(c + link).digest()
    return link.hex()

upload, expected, out = sys.argv[1:4]
expected = expected.strip().lower()
if len(expected) != 64 or any(c not in '0123456789abcdef' for c in expected):
    raise SystemExit(f'CONTENT_HEX must be the full 64-character hex digest from the proposal payload, got {len(expected)} characters. '
                     'A shortened display digest cannot be checked.')
chunks = []  # (slot, position in tx, bytes)
for s in call('getSignaturesForAddress', [upload, {'limit': 1000}])['result']:
    if s['err']: continue
    tx = call('getTransaction', [s['signature'], {'encoding': 'json', 'maxSupportedTransactionVersion': 1, 'commitment': 'finalized'}])
    msg = tx['result']['transaction']['message']; keys = msg['accountKeys']
    for i, ix in enumerate(msg['instructions']):
        if keys[ix['programIdIndex']] != PROGRAM: continue
        d = b58d(ix['data'])
        if d[0] == 3:  # chunk write: tag, 1 byte, u32 length, bytes, then 32 bytes: the link of the next chunk (zeros after the last)
            n = struct.unpack('<I', d[2:6])[0]; c = d[6:6 + n]; nxt = d[6 + n:6 + n + 32]
            if all(c != x[2] for x in chunks): chunks.append((tx['result']['slot'], i, c, nxt))
# Walk the hash chain from the content field: each chunk's own link is sha256(chunk + next link), and its write carries that next
# link, so the chain fixes the order. A break names the link nobody returned.
by_link = {hashlib.sha256(c + nxt).hexdigest(): (c, nxt) for _, _, c, nxt in chunks if len(nxt) == 32}
seq, link, broken = [], expected, None
while link != bytes(32).hex():
    if link not in by_link or len(seq) > len(chunks): broken = link; break
    c, nxt = by_link[link]; seq.append(c); link = nxt.hex()
landed = [x[2] for x in sorted(chunks, key=lambda x: (x[0], x[1]))]
if broken is not None:
    # Older writes may lack the trailing link: fall back to v2 (landed order, then every order for 8 or fewer chunks).
    seq = landed if root_of(landed) == expected else None
    if seq is None and len(chunks) <= 8:
        seq = next((list(p) for p in itertools.permutations(landed) if root_of(p) == expected), None)
if seq is None:
    where = ('the first chunk (the one whose link is the content field)' if broken == expected else f'the chunk with link {broken}')
    raise SystemExit(f'NO MATCH for {expected}: {len(chunks)} chunks found, chain walk stopped at {where}. A break after the first '
                     'chunk means a chunk write is missing from what this RPC returned: re-run with another full-history RPC as the 4th '
                     'argument. A miss at the first chunk on every RPC is evidence that the chunks on chain do not match the content field.')
frame = b''.join(seq)

def zstd_raw(f):
    # Pages are stored as a zstd frame. This handles frames made of raw/RLE blocks (what we have seen on mainnet).
    # A compressed block needs a real zstd decoder: then use one (python zstandard, or `zstd -d`) on out.zst.
    assert f[:4] == bytes.fromhex('28b52ffd'), 'not a zstd frame'
    fd = f[4]; single = fd >> 5 & 1; fcs = [0 if not single else 1, 2, 4, 8][fd >> 6]; did = [0, 1, 2, 4][fd & 3]
    i = 5 + (0 if single else 1) + did + fcs; out = b''
    while True:
        h = int.from_bytes(f[i:i + 3], 'little'); i += 3; last, typ, size = h & 1, h >> 1 & 3, h >> 3
        if typ == 0: out += f[i:i + size]; i += size
        elif typ == 1: out += f[i:i + 1] * size; i += 1
        else: return None
        if last: return out

open(out + '.zst', 'wb').write(frame)
text = zstd_raw(frame)
if text is None:
    raise SystemExit(f'chain matches; the frame has a compressed block: decode {out}.zst with a zstd tool')
open(out, 'wb').write(text)
print('chunks', len(chunks), 'content match', True, 'chain walk', broken is None, 'landed in order', seq == landed, 'chars', len(text.decode('utf-8')), 'sha256', hashlib.sha256(text).hexdigest())
