127 lines
4.6 KiB
Python
127 lines
4.6 KiB
Python
import secrets, hashlib, json, base58
|
|
from typing import Dict, Optional
|
|
from ecdsa import SECP256k1, SigningKey
|
|
from ecdsa.ellipticcurve import Point
|
|
|
|
NETWORK_CONFIG = {
|
|
'mainnet': {'hrp': 'bc', 'wif_prefix': b'\x80'},
|
|
'testnet': {'hrp': 'tb', 'wif_prefix': b'\xEF'},
|
|
'regtest': {'hrp': 'bcrt', 'wif_prefix': b'\xEF'},
|
|
}
|
|
|
|
_BECH32_CHARSET = "qpzry9x8gf2tvdw0s3jn54khce6mua7l"
|
|
|
|
def _bech32_polymod(values):
|
|
"""Calcola il polymod per bech32/bech32m"""
|
|
GEN = [0x3b6a57b2, 0x26508e6d, 0x1ea119fa, 0x3d4233dd, 0x2a1462b3]
|
|
chk = 1
|
|
for v in values:
|
|
b = (chk >> 25) & 0xff
|
|
chk = ((chk & 0x1ffffff) << 5) ^ v
|
|
for i in range(5):
|
|
chk ^= GEN[i] if ((b >> i) & 1) else 0
|
|
return chk
|
|
|
|
def _bech32_hrp_expand(hrp):
|
|
"""Espande l'HRP per il calcolo bech32"""
|
|
return [ord(x) >> 5 for x in hrp] + [0] + [ord(x) & 31 for x in hrp]
|
|
|
|
def _bech32_create_checksum(hrp, data, spec="bech32m"):
|
|
"""Crea il checksum per bech32/bech32m"""
|
|
const = 0x2bc830a3 if spec == "bech32m" else 1
|
|
values = _bech32_hrp_expand(hrp) + data
|
|
polymod = _bech32_polymod(values + [0,0,0,0,0,0]) ^ const
|
|
return [(polymod >> 5 * (5 - i)) & 31 for i in range(6)]
|
|
|
|
def bech32m_encode(hrp: str, data: list) -> str:
|
|
"""Codifica dati in formato bech32m"""
|
|
combined = data + _bech32_create_checksum(hrp, data, "bech32m")
|
|
return hrp + "1" + "".join([_BECH32_CHARSET[d] for d in combined])
|
|
|
|
def convertbits(data: bytes, frombits: int, tobits: int, pad: bool = True) -> Optional[list]:
|
|
"""Converte bit tra diverse basi"""
|
|
acc = 0; bits = 0; ret = []
|
|
maxv = (1 << tobits) - 1
|
|
max_acc = (1 << (frombits + tobits - 1)) - 1
|
|
for b in data:
|
|
if b < 0 or b >> frombits: return None
|
|
acc = ((acc << frombits) | b) & max_acc
|
|
bits += frombits
|
|
while bits >= tobits:
|
|
bits -= tobits
|
|
ret.append((acc >> bits) & maxv)
|
|
if pad:
|
|
if bits: ret.append((acc << (tobits - bits)) & maxv)
|
|
elif bits >= frombits or ((acc << (tobits - bits)) & maxv): return None
|
|
return ret
|
|
|
|
def tagged_hash(tag: str, msg: bytes) -> bytes:
|
|
"""Calcola tagged hash BIP340"""
|
|
tagh = hashlib.sha256(tag.encode()).digest()
|
|
return hashlib.sha256(tagh + tagh + msg).digest()
|
|
|
|
curve = SECP256k1
|
|
G: Point = curve.generator
|
|
n = curve.order
|
|
|
|
def point_from_sk(sk_bytes: bytes) -> Point:
|
|
"""Genera punto pubblico da chiave privata"""
|
|
sk = int.from_bytes(sk_bytes, 'big')
|
|
if not (1 <= sk < n): raise ValueError("Chiave privata fuori range")
|
|
return SigningKey.from_string(sk_bytes, curve=SECP256k1).verifying_key.pubkey.point
|
|
|
|
def xonly_bytes(P: Point) -> bytes:
|
|
"""Estrae coordinata x da punto (32 byte)"""
|
|
return int(P.x()).to_bytes(32, 'big')
|
|
|
|
def pubkey_tweak(P: Point, merkle_root: Optional[bytes] = None):
|
|
"""Applica tweak Taproot al punto pubblico"""
|
|
mr = b"" if merkle_root is None else merkle_root
|
|
t = int.from_bytes(tagged_hash("TapTweak", xonly_bytes(P) + mr), 'big') % n
|
|
if t == 0: raise ValueError("Tweak nullo, rigenera la chiave")
|
|
return P + t*G, t
|
|
|
|
def to_wif(privkey: bytes, wif_prefix: bytes) -> str:
|
|
"""Converte chiave privata in formato WIF"""
|
|
extended = wif_prefix + privkey + b'\x01'
|
|
checksum = hashlib.sha256(hashlib.sha256(extended).digest()).digest()[:4]
|
|
return base58.b58encode(extended + checksum).decode()
|
|
|
|
def generate_p2tr_address(network: str = 'mainnet') -> Dict[str, str]:
|
|
"""Genera indirizzo P2TR completo"""
|
|
cfg = NETWORK_CONFIG.get(network)
|
|
if not cfg: raise ValueError("Network non supportato (mainnet, testnet, regtest).")
|
|
|
|
sk = secrets.token_bytes(32)
|
|
P = point_from_sk(sk)
|
|
Q, t = pubkey_tweak(P, merkle_root=None)
|
|
prog = xonly_bytes(Q)
|
|
data = [1] + convertbits(prog, 8, 5, True)
|
|
address = bech32m_encode(cfg['hrp'], data)
|
|
wif = to_wif(sk, cfg['wif_prefix'])
|
|
|
|
return {
|
|
"network": network,
|
|
"script_type": "p2tr",
|
|
"private_key_hex": sk.hex(),
|
|
"private_key_wif": wif,
|
|
"internal_pubkey_x_hex": xonly_bytes(P).hex(),
|
|
"address": address
|
|
}
|
|
|
|
def main():
|
|
"""Funzione principale interattiva"""
|
|
net = input("Seleziona rete (mainnet/testnet/regtest): ").strip().lower()
|
|
try:
|
|
res = generate_p2tr_address(net)
|
|
print("\n--- Risultati P2TR ---")
|
|
for k, v in res.items(): print(f"{k}: {v}")
|
|
nome = input("\nNome file per salvare (senza estensione): ").strip() or "wallet_p2tr"
|
|
if not nome.endswith(".json"): nome += ".json"
|
|
with open(nome, "w") as f: json.dump(res, f, indent=4)
|
|
print(f"Salvato in {nome}")
|
|
except Exception as e: print("Errore:", e)
|
|
|
|
if __name__ == "__main__":
|
|
main()
|