feat(chain): network profiles and consensus constants
This commit is contained in:
@@ -0,0 +1,134 @@
|
|||||||
|
namespace PalladiumWallet.Core.Chain;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Tipo di rete supportato (selettore unico per tutto il wallet, blueprint §3).
|
||||||
|
/// </summary>
|
||||||
|
public enum NetKind
|
||||||
|
{
|
||||||
|
Mainnet,
|
||||||
|
Testnet,
|
||||||
|
Regtest,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Tipo di script/indirizzo supportato (blueprint §4.3).
|
||||||
|
/// </summary>
|
||||||
|
public enum ScriptKind
|
||||||
|
{
|
||||||
|
/// <summary>P2PKH legacy.</summary>
|
||||||
|
Legacy,
|
||||||
|
|
||||||
|
/// <summary>P2SH-P2WPKH (segwit wrapped).</summary>
|
||||||
|
WrappedSegwit,
|
||||||
|
|
||||||
|
/// <summary>P2WPKH (native segwit) — default consigliato.</summary>
|
||||||
|
NativeSegwit,
|
||||||
|
|
||||||
|
/// <summary>P2SH-P2WSH (multisig wrapped).</summary>
|
||||||
|
WrappedSegwitMultisig,
|
||||||
|
|
||||||
|
/// <summary>P2WSH (multisig native).</summary>
|
||||||
|
NativeSegwitMultisig,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Coppia di header di versione BIP32 (4 byte big-endian) per serializzare
|
||||||
|
/// xprv/xpub e varianti SLIP-132 (y/z/Y/Z) — blueprint §3.
|
||||||
|
/// </summary>
|
||||||
|
public readonly record struct ExtKeyHeaders(uint Private, uint Public)
|
||||||
|
{
|
||||||
|
/// <summary>Header privato come 4 byte big-endian (da anteporre al payload BIP32).</summary>
|
||||||
|
public byte[] PrivateBytes() => ToBytes(Private);
|
||||||
|
|
||||||
|
/// <summary>Header pubblico come 4 byte big-endian.</summary>
|
||||||
|
public byte[] PublicBytes() => ToBytes(Public);
|
||||||
|
|
||||||
|
internal static byte[] ToBytes(uint header) =>
|
||||||
|
[
|
||||||
|
(byte)(header >> 24),
|
||||||
|
(byte)(header >> 16),
|
||||||
|
(byte)(header >> 8),
|
||||||
|
(byte)header,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Server di indicizzazione di bootstrap: host + porta TCP + porta SSL (blueprint §3/§9).
|
||||||
|
/// </summary>
|
||||||
|
public readonly record struct ServerEndpoint(string Host, int TcpPort, int SslPort);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Checkpoint di catena: ancora la fiducia quando la verifica PoW è disattivata
|
||||||
|
/// (blueprint §7.3). Target serializzato come "bits" compatti.
|
||||||
|
/// </summary>
|
||||||
|
public readonly record struct Checkpoint(int Height, string BlockHash, uint Bits);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Profilo di rete/catena (blueprint §3): tutte le costanti specifiche della catena,
|
||||||
|
/// centralizzate in un solo punto. Nessun magic number altrove nel codice.
|
||||||
|
/// </summary>
|
||||||
|
public sealed record ChainProfile
|
||||||
|
{
|
||||||
|
public required NetKind Kind { get; init; }
|
||||||
|
|
||||||
|
/// <summary>Nome rete, usato anche come sottocartella dati.</summary>
|
||||||
|
public required string NetName { get; init; }
|
||||||
|
|
||||||
|
/// <summary>Simbolo dell'unità (es. PLM).</summary>
|
||||||
|
public required string CoinUnit { get; init; }
|
||||||
|
|
||||||
|
/// <summary>Prefisso WIF delle chiavi private.</summary>
|
||||||
|
public required byte WifPrefix { get; init; }
|
||||||
|
|
||||||
|
/// <summary>Byte di versione indirizzi P2PKH.</summary>
|
||||||
|
public required byte AddrP2pkh { get; init; }
|
||||||
|
|
||||||
|
/// <summary>Byte di versione indirizzi P2SH.</summary>
|
||||||
|
public required byte AddrP2sh { get; init; }
|
||||||
|
|
||||||
|
/// <summary>Human-readable part bech32/bech32m.</summary>
|
||||||
|
public required string SegwitHrp { get; init; }
|
||||||
|
|
||||||
|
/// <summary>HRP fatture BOLT11 (Lightning, fase successiva — §11).</summary>
|
||||||
|
public required string Bolt11Hrp { get; init; }
|
||||||
|
|
||||||
|
/// <summary>Hash del blocco genesi (hex, byte order da explorer).</summary>
|
||||||
|
public required string GenesisHash { get; init; }
|
||||||
|
|
||||||
|
/// <summary>Porta TCP del server di indicizzazione (non è la porta P2P del nodo).</summary>
|
||||||
|
public required int DefaultTcpPort { get; init; }
|
||||||
|
|
||||||
|
/// <summary>Porta SSL del server di indicizzazione.</summary>
|
||||||
|
public required int DefaultSslPort { get; init; }
|
||||||
|
|
||||||
|
/// <summary>Porta P2P del nodo — solo informativa: il wallet SPV non la usa.</summary>
|
||||||
|
public required int NodeP2pPort { get; init; }
|
||||||
|
|
||||||
|
/// <summary>Coin type SLIP-0044 nei derivation path (convenzione wallet).</summary>
|
||||||
|
public required int Bip44CoinType { get; init; }
|
||||||
|
|
||||||
|
/// <summary>Schema URI pagamenti BIP21 (senza i due punti).</summary>
|
||||||
|
public required string UriScheme { get; init; }
|
||||||
|
|
||||||
|
/// <summary>Block explorer di default.</summary>
|
||||||
|
public required string ExplorerUrl { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 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.
|
||||||
|
/// </summary>
|
||||||
|
public required bool SkipPowValidation { get; init; }
|
||||||
|
|
||||||
|
/// <summary>Tempo di blocco target in secondi (LWMA v2: 120s).</summary>
|
||||||
|
public required int BlockTimeSeconds { get; init; }
|
||||||
|
|
||||||
|
/// <summary>Header BIP32/SLIP-132 per ciascun tipo di script.</summary>
|
||||||
|
public required IReadOnlyDictionary<ScriptKind, ExtKeyHeaders> ExtKeyHeaders { get; init; }
|
||||||
|
|
||||||
|
/// <summary>Server di indicizzazione per il primo contatto (bootstrap).</summary>
|
||||||
|
public required IReadOnlyList<ServerEndpoint> BootstrapServers { get; init; }
|
||||||
|
|
||||||
|
/// <summary>Checkpoint hardcoded, aggiornati a ogni release (§7.3).</summary>
|
||||||
|
public required IReadOnlyList<Checkpoint> Checkpoints { get; init; }
|
||||||
|
}
|
||||||
@@ -0,0 +1,90 @@
|
|||||||
|
namespace PalladiumWallet.Core.Chain;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// I tre profili di rete del wallet (blueprint §3). Valori mainnet verificati
|
||||||
|
/// contro il sorgente del nodo (chainparams.cpp / pow.cpp) secondo il blueprint.
|
||||||
|
/// </summary>
|
||||||
|
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, ExtKeyHeaders>
|
||||||
|
{
|
||||||
|
[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<ScriptKind, ExtKeyHeaders>
|
||||||
|
{
|
||||||
|
// 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)),
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -0,0 +1,125 @@
|
|||||||
|
using NBitcoin;
|
||||||
|
using NBitcoin.DataEncoders;
|
||||||
|
|
||||||
|
namespace PalladiumWallet.Core.Chain;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Costruisce e registra le <see cref="Network"/> NBitcoin di Palladium a partire dai
|
||||||
|
/// <see cref="ChainProfiles"/> (blueprint §19.3). Tutto il resto del codice ottiene la
|
||||||
|
/// rete da qui, mai da costanti sparse.
|
||||||
|
/// </summary>
|
||||||
|
/// <summary>
|
||||||
|
/// Identità della moneta per NBitcoin: raggruppa le tre reti sotto il codice PLM.
|
||||||
|
/// I getter sono lazy, quindi nessuna ricorsione durante la costruzione.
|
||||||
|
/// </summary>
|
||||||
|
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<Network> _mainnet = new(() => Build(ChainProfiles.Mainnet, MainGenesisHex, 0x504c4d4du));
|
||||||
|
private static readonly Lazy<Network> _testnet = new(() => Build(ChainProfiles.Testnet, TestGenesisHex, 0x504c4d54u));
|
||||||
|
private static readonly Lazy<Network> _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();
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user