diff --git a/src/Core/Chain/ChainProfile.cs b/src/Core/Chain/ChainProfile.cs new file mode 100644 index 0000000..ae4f908 --- /dev/null +++ b/src/Core/Chain/ChainProfile.cs @@ -0,0 +1,134 @@ +namespace PalladiumWallet.Core.Chain; + +/// +/// Tipo di rete supportato (selettore unico per tutto il wallet, blueprint §3). +/// +public enum NetKind +{ + Mainnet, + Testnet, + Regtest, +} + +/// +/// Tipo di script/indirizzo supportato (blueprint §4.3). +/// +public enum ScriptKind +{ + /// P2PKH legacy. + Legacy, + + /// P2SH-P2WPKH (segwit wrapped). + WrappedSegwit, + + /// P2WPKH (native segwit) — default consigliato. + NativeSegwit, + + /// P2SH-P2WSH (multisig wrapped). + WrappedSegwitMultisig, + + /// P2WSH (multisig native). + NativeSegwitMultisig, +} + +/// +/// Coppia di header di versione BIP32 (4 byte big-endian) per serializzare +/// xprv/xpub e varianti SLIP-132 (y/z/Y/Z) — blueprint §3. +/// +public readonly record struct ExtKeyHeaders(uint Private, uint Public) +{ + /// Header privato come 4 byte big-endian (da anteporre al payload BIP32). + public byte[] PrivateBytes() => ToBytes(Private); + + /// Header pubblico come 4 byte big-endian. + public byte[] PublicBytes() => ToBytes(Public); + + internal static byte[] ToBytes(uint header) => + [ + (byte)(header >> 24), + (byte)(header >> 16), + (byte)(header >> 8), + (byte)header, + ]; +} + +/// +/// Server di indicizzazione di bootstrap: host + porta TCP + porta SSL (blueprint §3/§9). +/// +public readonly record struct ServerEndpoint(string Host, int TcpPort, int SslPort); + +/// +/// Checkpoint di catena: ancora la fiducia quando la verifica PoW è disattivata +/// (blueprint §7.3). Target serializzato come "bits" compatti. +/// +public readonly record struct Checkpoint(int Height, string BlockHash, uint Bits); + +/// +/// Profilo di rete/catena (blueprint §3): tutte le costanti specifiche della catena, +/// centralizzate in un solo punto. Nessun magic number altrove nel codice. +/// +public sealed record ChainProfile +{ + public required NetKind Kind { get; init; } + + /// Nome rete, usato anche come sottocartella dati. + public required string NetName { get; init; } + + /// Simbolo dell'unità (es. PLM). + public required string CoinUnit { get; init; } + + /// Prefisso WIF delle chiavi private. + public required byte WifPrefix { get; init; } + + /// Byte di versione indirizzi P2PKH. + public required byte AddrP2pkh { get; init; } + + /// Byte di versione indirizzi P2SH. + public required byte AddrP2sh { get; init; } + + /// Human-readable part bech32/bech32m. + public required string SegwitHrp { get; init; } + + /// HRP fatture BOLT11 (Lightning, fase successiva — §11). + public required string Bolt11Hrp { get; init; } + + /// Hash del blocco genesi (hex, byte order da explorer). + public required string GenesisHash { get; init; } + + /// Porta TCP del server di indicizzazione (non è la porta P2P del nodo). + public required int DefaultTcpPort { get; init; } + + /// Porta SSL del server di indicizzazione. + public required int DefaultSslPort { get; init; } + + /// Porta P2P del nodo — solo informativa: il wallet SPV non la usa. + public required int NodeP2pPort { get; init; } + + /// Coin type SLIP-0044 nei derivation path (convenzione wallet). + public required int Bip44CoinType { get; init; } + + /// Schema URI pagamenti BIP21 (senza i due punti). + public required string UriScheme { get; init; } + + /// Block explorer di default. + public required string ExplorerUrl { get; init; } + + /// + /// La catena usa LWMA (retargeting per-blocco, 2 minuti): un client SPV non può + /// ricalcolarla, quindi la verifica bits/target va saltata e la fiducia ancorata + /// ai checkpoint (§3/§7). Per la catena di riferimento è sempre true. + /// + public required bool SkipPowValidation { get; init; } + + /// Tempo di blocco target in secondi (LWMA v2: 120s). + public required int BlockTimeSeconds { get; init; } + + /// Header BIP32/SLIP-132 per ciascun tipo di script. + public required IReadOnlyDictionary ExtKeyHeaders { get; init; } + + /// Server di indicizzazione per il primo contatto (bootstrap). + public required IReadOnlyList BootstrapServers { get; init; } + + /// Checkpoint hardcoded, aggiornati a ogni release (§7.3). + public required IReadOnlyList Checkpoints { get; init; } +} diff --git a/src/Core/Chain/ChainProfiles.cs b/src/Core/Chain/ChainProfiles.cs new file mode 100644 index 0000000..825c3dc --- /dev/null +++ b/src/Core/Chain/ChainProfiles.cs @@ -0,0 +1,90 @@ +namespace PalladiumWallet.Core.Chain; + +/// +/// I tre profili di rete del wallet (blueprint §3). Valori mainnet verificati +/// contro il sorgente del nodo (chainparams.cpp / pow.cpp) secondo il blueprint. +/// +public static class ChainProfiles +{ + public static ChainProfile Mainnet { get; } = new() + { + Kind = NetKind.Mainnet, + NetName = "mainnet", + CoinUnit = "PLM", + WifPrefix = 0x80, + AddrP2pkh = 55, // indirizzi che iniziano con 'P' + AddrP2sh = 5, // indirizzi che iniziano con '3' + SegwitHrp = "plm", + Bolt11Hrp = "plm", + // La mainnet riusa la genesi di Bitcoin (blueprint §3). + GenesisHash = "000000000019d6689c085ae165831e934ff763ae46a2a6c172b3f1b60a8ce26f", + DefaultTcpPort = 50001, + DefaultSslPort = 50002, + NodeP2pPort = 2333, + Bip44CoinType = 746, + UriScheme = "palladium", + ExplorerUrl = "https://explorer.palladium-coin.com/", + SkipPowValidation = true, + BlockTimeSeconds = 120, + ExtKeyHeaders = new Dictionary + { + [ScriptKind.Legacy] = new(0x0488ade4, 0x0488b21e), // xprv / xpub + [ScriptKind.WrappedSegwit] = new(0x049d7878, 0x049d7cb2), // yprv / ypub + [ScriptKind.WrappedSegwitMultisig] = new(0x0295b005, 0x0295b43f), // Yprv / Ypub + [ScriptKind.NativeSegwit] = new(0x04b2430c, 0x04b24746), // zprv / zpub + [ScriptKind.NativeSegwitMultisig] = new(0x02aa7a99, 0x02aa7ed3), // Zprv / Zpub + }, + // TODO: popolare con i server reali prima della release (host:tcp:ssl). + BootstrapServers = [], + // TODO: popolare con [hash, bits] reali della catena (§7.3) prima della release. + Checkpoints = [], + }; + + public static ChainProfile Testnet { get; } = Mainnet with + { + Kind = NetKind.Testnet, + NetName = "testnet", + WifPrefix = 0xff, + AddrP2pkh = 127, + AddrP2sh = 115, + SegwitHrp = "tplm", + Bolt11Hrp = "tplm", + // TODO: verificare contro chainparams.cpp del nodo (il blueprint non la riporta; + // qui si assume la genesi di Bitcoin testnet3, da confermare). + GenesisHash = "000000000933ea01ad0ee984209779baaec3ced90fa3f408719526f8d77f4943", + NodeP2pPort = 12333, + Bip44CoinType = 1, + ExtKeyHeaders = new Dictionary + { + // tprv/tpub dal blueprint; le varianti SLIP-132 testnet (u/v/U/V) sono + // i valori standard Electrum. + [ScriptKind.Legacy] = new(0x04358394, 0x043587cf), // tprv / tpub + [ScriptKind.WrappedSegwit] = new(0x044a4e28, 0x044a5262), // uprv / upub + [ScriptKind.WrappedSegwitMultisig] = new(0x024285b5, 0x024289ef), // Uprv / Upub + [ScriptKind.NativeSegwit] = new(0x045f18bc, 0x045f1cf6), // vprv / vpub + [ScriptKind.NativeSegwitMultisig] = new(0x02575048, 0x02575483), // Vprv / Vpub + }, + BootstrapServers = [], + Checkpoints = [], + }; + + public static ChainProfile Regtest { get; } = Testnet with + { + Kind = NetKind.Regtest, + NetName = "regtest", + SegwitHrp = "rplm", + Bolt11Hrp = "rplm", + // TODO: verificare contro chainparams.cpp del nodo (assunta genesi regtest Bitcoin). + GenesisHash = "0f9188f13cb7b2c71f2a335e3a4fc328bf5beb436012afca590b1a11466e2206", + NodeP2pPort = 28444, + Checkpoints = [], + }; + + public static ChainProfile For(NetKind kind) => kind switch + { + NetKind.Mainnet => Mainnet, + NetKind.Testnet => Testnet, + NetKind.Regtest => Regtest, + _ => throw new ArgumentOutOfRangeException(nameof(kind)), + }; +} diff --git a/src/Core/Chain/PalladiumNetworks.cs b/src/Core/Chain/PalladiumNetworks.cs new file mode 100644 index 0000000..c88eb97 --- /dev/null +++ b/src/Core/Chain/PalladiumNetworks.cs @@ -0,0 +1,125 @@ +using NBitcoin; +using NBitcoin.DataEncoders; + +namespace PalladiumWallet.Core.Chain; + +/// +/// Costruisce e registra le NBitcoin di Palladium a partire dai +/// (blueprint §19.3). Tutto il resto del codice ottiene la +/// rete da qui, mai da costanti sparse. +/// +/// +/// Identità della moneta per NBitcoin: raggruppa le tre reti sotto il codice PLM. +/// I getter sono lazy, quindi nessuna ricorsione durante la costruzione. +/// +public sealed class PalladiumNetworkSet : INetworkSet +{ + public static readonly PalladiumNetworkSet Instance = new(); + + private PalladiumNetworkSet() { } + + public string CryptoCode => "PLM"; + public Network Mainnet => PalladiumNetworks.Mainnet; + public Network Testnet => PalladiumNetworks.Testnet; + public Network Regtest => PalladiumNetworks.Regtest; + + public Network GetNetwork(ChainName chainName) + { + if (chainName == ChainName.Mainnet) return Mainnet; + if (chainName == ChainName.Testnet) return Testnet; + if (chainName == ChainName.Regtest) return Regtest; + throw new ArgumentOutOfRangeException(nameof(chainName)); + } +} + +public static class PalladiumNetworks +{ + // Blocco genesi di Bitcoin (la mainnet Palladium lo riusa, blueprint §3). + private const string MainGenesisHex = + "0100000000000000000000000000000000000000000000000000000000000000000000003ba3edfd7a7b12b27ac72c3e67768f617fc81bc3888a51323a9fb8aa4b1e5e4a29ab5f49ffff001d1dac2b7c" + + Coinbase; + + // Genesi testnet3/regtest di Bitcoin: stessa coinbase, cambiano time/bits/nonce. + // TODO: confermare contro chainparams.cpp del nodo (vedi ChainProfiles). + private const string TestGenesisHex = + "0100000000000000000000000000000000000000000000000000000000000000000000003ba3edfd7a7b12b27ac72c3e67768f617fc81bc3888a51323a9fb8aa4b1e5e4adae5494dffff001d1aa4ae18" + + Coinbase; + + private const string RegtestGenesisHex = + "0100000000000000000000000000000000000000000000000000000000000000000000003ba3edfd7a7b12b27ac72c3e67768f617fc81bc3888a51323a9fb8aa4b1e5e4adae5494dffff7f2002000000" + + Coinbase; + + private const string Coinbase = + "0101000000010000000000000000000000000000000000000000000000000000000000000000ffffffff4d04ffff001d0104455468652054696d65732030332f4a616e2f32303039204368616e63656c6c6f72206f6e206272696e6b206f66207365636f6e64206261696c6f757420666f722062616e6b73ffffffff0100f2052a01000000434104678afdb0fe5548271967f1a67130b7105cd6a828e03909a67962e0ea1f61deb649f6bc3f4cef38c4f35504e51ec112de5c384df7ba0b8d578a4c702b6bf11d5fac00000000"; + + private static readonly Lazy _mainnet = new(() => Build(ChainProfiles.Mainnet, MainGenesisHex, 0x504c4d4du)); + private static readonly Lazy _testnet = new(() => Build(ChainProfiles.Testnet, TestGenesisHex, 0x504c4d54u)); + private static readonly Lazy _regtest = new(() => Build(ChainProfiles.Regtest, RegtestGenesisHex, 0x504c4d52u)); + + public static Network Mainnet => _mainnet.Value; + public static Network Testnet => _testnet.Value; + public static Network Regtest => _regtest.Value; + + public static Network For(NetKind kind) => kind switch + { + NetKind.Mainnet => Mainnet, + NetKind.Testnet => Testnet, + NetKind.Regtest => Regtest, + _ => throw new ArgumentOutOfRangeException(nameof(kind)), + }; + + private static Network Build(ChainProfile p, string genesisHex, uint magic) + { + var legacy = p.ExtKeyHeaders[ScriptKind.Legacy]; + + var builder = new NetworkBuilder() + .SetName($"plm-{p.NetName}") + .SetNetworkSet(PalladiumNetworkSet.Instance) + .SetChainName(p.Kind switch + { + NetKind.Mainnet => ChainName.Mainnet, + NetKind.Testnet => ChainName.Testnet, + _ => ChainName.Regtest, + }) + // Magic P2P solo segnaposto: il wallet SPV non apre mai connessioni P2P + // (parla solo col server di indicizzazione, §3). Serve a NBitcoin per + // distinguere le reti registrate. + .SetMagic(magic) + .SetPort(p.NodeP2pPort) + .SetRPCPort(p.NodeP2pPort + 1) + .SetGenesis(genesisHex) + .SetBase58Bytes(Base58Type.PUBKEY_ADDRESS, [p.AddrP2pkh]) + .SetBase58Bytes(Base58Type.SCRIPT_ADDRESS, [p.AddrP2sh]) + .SetBase58Bytes(Base58Type.SECRET_KEY, [p.WifPrefix]) + // NBitcoin gestisce una sola coppia di header BIP32 per rete: quella + // standard xprv/xpub. Le varianti SLIP-132 (y/z/Y/Z) si serializzano + // a mano con ChainProfile.ExtKeyHeaders quando servono. + .SetBase58Bytes(Base58Type.EXT_SECRET_KEY, legacy.PrivateBytes()) + .SetBase58Bytes(Base58Type.EXT_PUBLIC_KEY, legacy.PublicBytes()) + .SetBech32(Bech32Type.WITNESS_PUBKEY_ADDRESS, Encoders.Bech32(p.SegwitHrp)) + .SetBech32(Bech32Type.WITNESS_SCRIPT_ADDRESS, Encoders.Bech32(p.SegwitHrp)) + .SetBech32(Bech32Type.TAPROOT_ADDRESS, Encoders.Bech32(p.SegwitHrp)) + .SetUriScheme(p.UriScheme) + .SetConsensus(new Consensus + { + // Valori usati solo dalle API NBitcoin che li richiedono: la validazione + // header reale è custom (Core/Spv) con skip PoW + checkpoint (§7). + PowLimit = new Target(new uint256("00000000ffffffffffffffffffffffffffffffffffffffffffffffffffffffff")), + PowTargetSpacing = TimeSpan.FromSeconds(p.BlockTimeSeconds), + PowTargetTimespan = TimeSpan.FromDays(14), + PowAllowMinDifficultyBlocks = p.Kind != NetKind.Mainnet, + PowNoRetargeting = p.Kind == NetKind.Regtest, + SubsidyHalvingInterval = 210000, + MajorityEnforceBlockUpgrade = 750, + MajorityRejectBlockOutdated = 950, + MajorityWindow = 1000, + MinimumChainWork = uint256.Zero, + CoinbaseMaturity = 100, + SupportSegwit = true, + SupportTaproot = true, + ConsensusFactory = new ConsensusFactory(), + }); + + return builder.BuildAndRegister(); + } +}