diff --git a/Cargo.lock b/Cargo.lock index 3c75f3aa..92c56ec0 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -335,6 +335,7 @@ dependencies = [ "dash-script", "dash-types", "hex-conservative", + "hex-literal", "libm", "rstest", "serde", diff --git a/pkgs/p2p_core/src/msg/addr.rs b/pkgs/p2p_core/src/msg/addr.rs index f4a2793b..8f7088dd 100644 --- a/pkgs/p2p_core/src/msg/addr.rs +++ b/pkgs/p2p_core/src/msg/addr.rs @@ -69,7 +69,7 @@ impl_p2p!(AddrV2Entry); impl fmt::Display for AddrV2Entry { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "{:?}:{}", self.addr.network, self.port) + write!(f, "{:?}:{}", self.addr.network(), self.port) } } diff --git a/pkgs/p2p_core/tests/addrv2.rs b/pkgs/p2p_core/tests/addrv2.rs index 73148b70..78d93d04 100644 --- a/pkgs/p2p_core/tests/addrv2.rs +++ b/pkgs/p2p_core/tests/addrv2.rs @@ -18,10 +18,7 @@ fn ipv4_entry(ip: [u8; 4], port: u16, time: u32) -> AddrV2Entry { AddrV2Entry { time, services: ServiceFlags(1), - addr: AddrV2 { - network: NetworkType::Ipv4, - addr: ip.to_vec(), - }, + addr: AddrV2::Ipv4(ip), port, } } @@ -98,26 +95,26 @@ fn addrv2_bip155_wire_vector() { assert_eq!(decoded.addrs.len(), 3); - let loopback: Vec = { - let mut v = vec![0u8; 15]; - v.push(1); + let loopback = { + let mut v = [0u8; 16]; + v[15] = 1; v }; assert_eq!(decoded.addrs[0].time, 0x4966bc61); assert_eq!(decoded.addrs[0].services, ServiceFlags(0)); - assert_eq!(decoded.addrs[0].addr.network, NetworkType::Ipv6); - assert_eq!(decoded.addrs[0].addr.addr, loopback); + assert_eq!(decoded.addrs[0].addr.network(), NetworkType::Ipv6); + assert_eq!(decoded.addrs[0].addr, AddrV2::Ipv6(loopback)); assert_eq!(decoded.addrs[0].port, 0); assert_eq!(decoded.addrs[1].time, 0x83766279); assert_eq!(decoded.addrs[1].services, ServiceFlags(1)); - assert_eq!(decoded.addrs[1].addr.addr, loopback); + assert_eq!(decoded.addrs[1].addr, AddrV2::Ipv6(loopback)); assert_eq!(decoded.addrs[1].port, 241); assert_eq!(decoded.addrs[2].time, 0xffffffff); assert_eq!(decoded.addrs[2].services, ServiceFlags(1024)); - assert_eq!(decoded.addrs[2].addr.addr, loopback); + assert_eq!(decoded.addrs[2].addr, AddrV2::Ipv6(loopback)); assert_eq!(decoded.addrs[2].port, 0xf1f2); assert_eq!(encode_to_vec(&decoded), bytes); @@ -167,61 +164,56 @@ fn addr_v1_wire_vector() { /// round-trip correctly. #[rstest] fn addrv2_all_bip155_network_types() { - let torv3: Vec = Vec::::from_hex("79bcc625184b05194975c28b66b66b0469f7f6556fb1ac3189a79b40dda32f1f") - .unwrap_or_else(|e| panic!("bad hex: {e}")); + let torv3_bytes: [u8; 32] = { + let v = Vec::::from_hex("79bcc625184b05194975c28b66b66b0469f7f6556fb1ac3189a79b40dda32f1f") + .unwrap_or_else(|e| panic!("bad hex: {e}")); + let mut arr = [0u8; 32]; + arr.copy_from_slice(&v); + arr + }; - let i2p: Vec = Vec::::from_hex("a2894dabaec08c0051a481a6dac88b64f98232ae42d4b6fd2fa81952dfe36a87") - .unwrap_or_else(|e| panic!("bad hex: {e}")); + let i2p_bytes: [u8; 32] = { + let v = Vec::::from_hex("a2894dabaec08c0051a481a6dac88b64f98232ae42d4b6fd2fa81952dfe36a87") + .unwrap_or_else(|e| panic!("bad hex: {e}")); + let mut arr = [0u8; 32]; + arr.copy_from_slice(&v); + arr + }; let original = AddrV2Msg { addrs: vec![ AddrV2Entry { time: 1_700_000_000, services: ServiceFlags(1), - addr: AddrV2 { - network: NetworkType::Ipv4, - addr: vec![1, 2, 3, 4], - }, + addr: AddrV2::Ipv4([1, 2, 3, 4]), port: 9999, }, AddrV2Entry { time: 1_700_000_001, services: ServiceFlags(1), - addr: AddrV2 { - network: NetworkType::Ipv6, - addr: vec![ - 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f, 0x10, - ], - }, + addr: AddrV2::Ipv6([ + 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f, 0x10, + ]), port: 9999, }, AddrV2Entry { time: 1_700_000_002, services: ServiceFlags(1), - addr: AddrV2 { - network: NetworkType::TorV3, - addr: torv3, - }, + addr: AddrV2::TorV3(torv3_bytes), port: 9999, }, AddrV2Entry { time: 1_700_000_003, services: ServiceFlags(1), - addr: AddrV2 { - network: NetworkType::I2P, - addr: i2p, - }, + addr: AddrV2::I2p(i2p_bytes), port: 9999, }, AddrV2Entry { time: 1_700_000_004, services: ServiceFlags(1), - addr: AddrV2 { - network: NetworkType::Cjdns, - addr: vec![ - 0xfc, 0x00, 0x00, 0x01, 0x00, 0x02, 0x00, 0x03, 0x00, 0x04, 0x00, 0x05, 0x00, 0x06, 0x00, 0x07, - ], - }, + addr: AddrV2::Cjdns([ + 0xfc, 0x00, 0x00, 0x01, 0x00, 0x02, 0x00, 0x03, 0x00, 0x04, 0x00, 0x05, 0x00, 0x06, 0x00, 0x07, + ]), port: 9999, }, ], diff --git a/pkgs/primitives/Cargo.toml b/pkgs/primitives/Cargo.toml index d12514a6..3908b791 100644 --- a/pkgs/primitives/Cargo.toml +++ b/pkgs/primitives/Cargo.toml @@ -56,6 +56,7 @@ serde = { version = "1", default-features = false, features = [ [dev-dependencies] dash-dev = { version = "0.0.0", path = "../dev", features = ["full"] } dash-pow = { version = "0.0.0", path = "../pow" } +hex-literal = "0.4" rstest = "0.25" serde = { version = "1", features = ["derive"] } serde_json = "1" diff --git a/pkgs/primitives/corpus/proregtx.json5 b/pkgs/primitives/corpus/proregtx.json5 index 14cb234f..cc67e99e 100644 --- a/pkgs/primitives/corpus/proregtx.json5 +++ b/pkgs/primitives/corpus/proregtx.json5 @@ -539,6 +539,104 @@ "platformHTTPPort": null, "vchSig": "1fbeb3d72e590341119141f2e56b00c99c43377ebc587b8b02984633d0814916c52b9a0dbf2d8eab115e87a6762981cf8c6b8a332807cc579eb00b871051a28b19" } + }, + // Synthetic ExtAddr from rpc_netinfo.py, Evonode with single IPv4 per purpose + "f137aa6f6e0a8895086f5eb18a3de18acf2cba5a1a598a13722a7d8cbdfb1261": { + "raw": "030001000103549432f45ab131b382207c2b8188e50e75af6baf17fe9c6e308ee57f660f7f010000006a47304402205058745ff7b9addc793390c58a5bd54bf7a0563c298eae6e166604f924af512102203b662d789e8852122ff122bd1753b75743a55e6b9e394c7cb2941dc8317cf656012102de469765cb9273617bc34cf5cbf1a0a919db9b14b3d9d7880289139b34e291d6feffffff01ce54f405000000001976a91469b366dadd041a9afb5a59b277f7164027c1285388ac00000000fd370103000100000003549432f45ab131b382207c2b8188e50e75af6baf17fe9c6e308ee57f660f7f00000000010300010101047f000001334501010101047f00000156b802010101047f00000156b9a8de9990c0a0676a95450ee59c6032c0222060dca326c971a901c71247043152a691a5a460c79ac9ff83fd876e3c1a6fa3b6734ecc72961438ddfc67049f941b3dfbedea19badc75f739727264b6848edef4e2f97ae303eb00001976a9142528448b4447518b5eafe16f574661c8e07bb18588ac3aaefd39eb12cdbcc1c40bc2d4ad000b4cafc9e0fa3e901e7571c9ff7c77ff72e5992efdc3d9e31931fb19590b33aaa1b90bb9584120c21b280fb2e8f72f9c46d7af4f78e50f6f6c31f0aeb080c6042879027a096a2b2f29047d0871e506e93c277e37e3ac52a2823cd205fe6051be5539b798d0f08f", + "details": { + "version": 3, + "mnType": 1, + "mode": 0, + "collateralHash": "7f0f667fe58e306e9cfe17af6baf750ee588812b7c2082b331b15af432945403", + "collateralIndex": 0, + "netInfo": { + "extended": { + "version": 1, + "entries": [ + [0, [{"service": {"addr": {"Ipv4": [127, 0, 0, 1]}, "port": 13125}}]], + [1, [{"service": {"addr": {"Ipv4": [127, 0, 0, 1]}, "port": 22200}}]], + [2, [{"service": {"addr": {"Ipv4": [127, 0, 0, 1]}, "port": 22201}}]] + ] + } + }, + "keyIdOwner": "a8de9990c0a0676a95450ee59c6032c0222060dc", + "pubKeyOperator": "a326c971a901c71247043152a691a5a460c79ac9ff83fd876e3c1a6fa3b6734ecc72961438ddfc67049f941b3dfbedea", + "keyIdVoting": "19badc75f739727264b6848edef4e2f97ae303eb", + "operatorReward": 0, + "scriptPayout": "76a9142528448b4447518b5eafe16f574661c8e07bb18588ac", + "inputsHash": "72ff777cffc971751e903efae0c9af4c0b00add4c20bc4c1bccd12eb39fdae3a", + "platformNodeId": "e5992efdc3d9e31931fb19590b33aaa1b90bb958", + "platformP2PPort": null, + "platformHTTPPort": null, + "vchSig": "20c21b280fb2e8f72f9c46d7af4f78e50f6f6c31f0aeb080c6042879027a096a2b2f29047d0871e506e93c277e37e3ac52a2823cd205fe6051be5539b798d0f08f" + } + }, + // Synthetic ExtAddr from rpc_netinfo.py, Evonode with dual IPv4+IPv6 per purpose + "a51324799403f291cef55feb42d68d4e226187b83faf5e44b38112dc3bfbf5dc": { + "raw": "0300010001cd3f92989da4770b690bb464f2faaa3dc21f7eed750922fd4a75ef821df9ddd3010000006a473044022034a3d407f02004877cdec6ab5481b9d871433f76b123a0852ecd42f68da6240f0220596b64b09edcbf0273c3ce8725a1ec2812b630fdafda49ea04f104e10fdbc9ab012102c28ef58e2f72d4785500e96965c1f8dcd86db1ed38fc87b00c8aae4cc225c547feffffff01c1c5f205000000001976a914d5b798dfb6a4fc4820ee449561e8871b3b11a98688ac00000000fd7601030001000000cd3f92989da4770b690bb464f2faaa3dc21f7eed750922fd4a75ef821df9ddd300000000010300020101047f000001334501021000000000000000000000000000000001334501020101047f00000256b80102100000000000000000000000000000000256b802020101047f00000356b90102100000000000000000000000000000000356b9fd553fc926287871a7dcd71968ad659026c093f795d86735440a8f1f650c1efae172fa21e5ea6cb5edb374e2cb6c3399184a5904f36a35d5969a01c9a15f49d3d4075b731441c5334f5decfbff85f3e60b1073ee2597c75000001976a9146fcb03df8b448707f27f4adb99bb1ddbf11a561a88ac2b4dff8a08c996556b81a270b7e337d6a86ecb6f1ac1702820b5348900797a8344442971737accd3a941166d9b3dc35f619ca10f4120c9582a842b570f95c68246ed563f992328354d3e0ee0d64bba7344278ca2ee93519761d07d315b0108bd4e95487c5e6fdc07970fdf9b528c747b1292cfbe25c7", + "details": { + "version": 3, + "mnType": 1, + "mode": 0, + "collateralHash": "d3ddf91d82ef754afd220975ed7e1fc23daafaf264b40b690b77a49d98923fcd", + "collateralIndex": 0, + "netInfo": { + "extended": { + "version": 1, + "entries": [ + [0, [ + {"service": {"addr": {"Ipv4": [127, 0, 0, 1]}, "port": 13125}}, + {"service": {"addr": {"Ipv6": [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1]}, "port": 13125}} + ]], + [1, [ + {"service": {"addr": {"Ipv4": [127, 0, 0, 2]}, "port": 22200}}, + {"service": {"addr": {"Ipv6": [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2]}, "port": 22200}} + ]], + [2, [ + {"service": {"addr": {"Ipv4": [127, 0, 0, 3]}, "port": 22201}}, + {"service": {"addr": {"Ipv6": [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 3]}, "port": 22201}} + ]] + ] + } + }, + "keyIdOwner": "fd553fc926287871a7dcd71968ad659026c093f7", + "pubKeyOperator": "95d86735440a8f1f650c1efae172fa21e5ea6cb5edb374e2cb6c3399184a5904f36a35d5969a01c9a15f49d3d4075b73", + "keyIdVoting": "1441c5334f5decfbff85f3e60b1073ee2597c750", + "operatorReward": 0, + "scriptPayout": "76a9146fcb03df8b448707f27f4adb99bb1ddbf11a561a88ac", + "inputsHash": "837a79008934b5202870c11a6fcb6ea8d637e3b770a2816b5596c9088aff4d2b", + "platformNodeId": "44442971737accd3a941166d9b3dc35f619ca10f", + "platformP2PPort": null, + "platformHTTPPort": null, + "vchSig": "20c9582a842b570f95c68246ed563f992328354d3e0ee0d64bba7344278ca2ee93519761d07d315b0108bd4e95487c5e6fdc07970fdf9b528c747b1292cfbe25c7" + } + }, + // Synthetic ExtAddr from rpc_netinfo.py, Evonode with blanked services (later updated with ProUpServTx) + "ecf23a8aee664a60783d5db8b0bdd4d345dd4f71f9c9dcf0bf500f855ea800c0": { + "raw": "0300010001b94ba65d1dde0f74f6fc8f4b78527820833f49c6cc58332206834b67429f84a4010000006a473044022039729c72931e81af2d055fb497ccd3b42bdf14544770f622316184b0de0cec2902206e0de7c5797fb73db6aa9ee50420f7adb7bb2821c368688fa1e59f5a5f5191e501210375add90cceec34273c5072e07679f827eb3cc64e16a545e38ff24a42fb924d8efeffffff017e58f405000000001976a91412035297dd4327017e0e5e8caf62f27f94aee03d88ac00000000fd1601030001000000b94ba65d1dde0f74f6fc8f4b78527820833f49c6cc58332206834b67429f84a400000000010015dde7c373a14ad16c091f409198b0f40f831b1f87dc4da5b388e0287ef12d48630e441e36baf164e6a2d24f986a4d0584144f879725722d0336c35c4ceaa36aeb607ae0f69375ea93a7fd20b3e2c0e24df9b3aa82613c9600001976a9144eb62894923a194f0f56ed20e37ba336b680264c88ac77a9fe84dd644ce20f0bd98282626bedd85fc87997e4b7f223e5229e25ae85ead769470503e89d55001d813fcbb8b3a770f97fe0411fbeb28ed0e786225ff0bed8b2eb86a3d95929737b25bd0083ecf0efeeabe74b9127a5436866bc2a38af290d64d05d8b77b8ee428337af47a4ca1cd97c3af59297", + "details": { + "version": 3, + "mnType": 1, + "mode": 0, + "collateralHash": "a4849f42674b8306223358ccc6493f83207852784b8ffcf6740fde1d5da64bb9", + "collateralIndex": 0, + "netInfo": { + "extended": { + "version": 1, + "entries": [] + } + }, + "keyIdOwner": "15dde7c373a14ad16c091f409198b0f40f831b1f", + "pubKeyOperator": "87dc4da5b388e0287ef12d48630e441e36baf164e6a2d24f986a4d0584144f879725722d0336c35c4ceaa36aeb607ae0", + "keyIdVoting": "f69375ea93a7fd20b3e2c0e24df9b3aa82613c96", + "operatorReward": 0, + "scriptPayout": "76a9144eb62894923a194f0f56ed20e37ba336b680264c88ac", + "inputsHash": "ea85ae259e22e523f2b7e49779c85fd8ed6b628282d90b0fe24c64dd84fea977", + "platformNodeId": "d769470503e89d55001d813fcbb8b3a770f97fe0", + "platformP2PPort": null, + "platformHTTPPort": null, + "vchSig": "1fbeb28ed0e786225ff0bed8b2eb86a3d95929737b25bd0083ecf0efeeabe74b9127a5436866bc2a38af290d64d05d8b77b8ee428337af47a4ca1cd97c3af59297" + } } } } diff --git a/pkgs/primitives/corpus/proupservtx.json5 b/pkgs/primitives/corpus/proupservtx.json5 index df1a03d8..a6f68021 100644 --- a/pkgs/primitives/corpus/proupservtx.json5 +++ b/pkgs/primitives/corpus/proupservtx.json5 @@ -419,6 +419,103 @@ "platformHTTPPort": 443, "sig": "84daa3b3afd35091f2db2837ce74b0392783dfe9f08787310ddbda953613846175ef67c5af77831d685fa44c2c3b43a8122a5b08898304830af8aa5766551d19a50fe202189f36dd2eddb242fcde98d515e183e6cfb414e4f1e87439f700da64" } + }, + // Synthetic ExtAddr from rpc_netinfo.py, Evonode with single IPv4 per purpose + "369881c12aa676d0a9829cdc849df0c75e9729296883cbbdc3822f1e267c6368": { + "raw": "03000200016112fbbd8c7d2a72138a591a5aba2ccf8ae13d8ab15e6f0895880a6e6faa37f1000000006a47304402205d99a789666ae397b46b2c7cc826ca72c15580cbdd6282c2eed2da58c234c3d70220369401f0c6a21c63fd4d72f9af4ddaed25da4cba6e24db3ca9cb7f7c73088227012102de469765cb9273617bc34cf5cbf1a0a919db9b14b3d9d7880289139b34e291d6feffffff012853f405000000001976a91469b366dadd041a9afb5a59b277f7164027c1285388ac00000000dc030001006112fbbd8c7d2a72138a591a5aba2ccf8ae13d8ab15e6f0895880a6e6faa37f1010300010101047f000001334501010101047f00000156cc02010101047f00000156cd000797eeb9810cb2132c24cd798f33934740d8f7ea33df74db8fde6046ffa82e77e5992efdc3d9e31931fb19590b33aaa1b90bb95882f472080ab9042136ac31bcc3fc0a4339cfb049d65aaa9815449b7f7a07a0413b5537dd8a4571e7813c7db282e55d8d19518e3ad5040646d95b1a721833d40e4199ac1e9c12df262aa54a5cdb958e2f7ca08f0624ef8655c88c83892c47ccee", + "details": { + "version": 3, + "mnType": 1, + "proTxHash": "f137aa6f6e0a8895086f5eb18a3de18acf2cba5a1a598a13722a7d8cbdfb1261", + "netInfo": { + "extended": { + "version": 1, + "entries": [ + [0, [{"service": {"addr": {"Ipv4": [127, 0, 0, 1]}, "port": 13125}}]], + [1, [{"service": {"addr": {"Ipv4": [127, 0, 0, 1]}, "port": 22220}}]], + [2, [{"service": {"addr": {"Ipv4": [127, 0, 0, 1]}, "port": 22221}}]] + ] + } + }, + "scriptOperatorPayout": "", + "inputsHash": "772ea8ff4660de8fdb74df33eaf7d8404793338f79cd242c13b20c81b9ee9707", + "platformNodeId": "e5992efdc3d9e31931fb19590b33aaa1b90bb958", + "platformP2PPort": null, + "platformHTTPPort": null, + "sig": "82f472080ab9042136ac31bcc3fc0a4339cfb049d65aaa9815449b7f7a07a0413b5537dd8a4571e7813c7db282e55d8d19518e3ad5040646d95b1a721833d40e4199ac1e9c12df262aa54a5cdb958e2f7ca08f0624ef8655c88c83892c47ccee" + } + }, + // Synthetic ExtAddr from rpc_netinfo.py, Evonode with dual IPv4+IPv6 per purpose + "639f00f22f730daa3fb0bb09e250d3f5c486987c4fb48da42804908c015a1ef6": { + "raw": "030002000168637c261e2f82c3bdcb83682929975ec7f09d84dc9c82a9d076a62ac1819836000000006a47304402204c195a1db8a2b57aae9827ca27e1680f454a5b2e7762508e79f07e6bf6128e28022051cd09a9c74f5240201b92d12a40a8db5a48c377cece7c5cf08605fc8d615dd3012102de469765cb9273617bc34cf5cbf1a0a919db9b14b3d9d7880289139b34e291d6feffffff014151f405000000001976a91469b366dadd041a9afb5a59b277f7164027c1285388ac00000000fd1b01030001006112fbbd8c7d2a72138a591a5aba2ccf8ae13d8ab15e6f0895880a6e6faa37f1010300020101047f000001334501021000000000000000000000000000000001334501020101047f00000256b80102100000000000000000000000000000000256b802020101047f00000356b90102100000000000000000000000000000000356b9001a8d1d53ba334ebf484153e512734a3a6ac8c8b155fc50bf8a51c42643f4c4e2e5992efdc3d9e31931fb19590b33aaa1b90bb9589792fe587c23c184602bc283e5f0e133b5a4cb3b9f8b00937947b7fd77968880dde692fc93de16bbcdf7bcfc20e5547b14ff016e2530cf6aa2f2d0e69af5bf6ffea3fcc8121dc0398485a23889607afb3f259396c875267d4013723aa3239c5e", + "details": { + "version": 3, + "mnType": 1, + "proTxHash": "f137aa6f6e0a8895086f5eb18a3de18acf2cba5a1a598a13722a7d8cbdfb1261", + "netInfo": { + "extended": { + "version": 1, + "entries": [ + [0, [ + {"service": {"addr": {"Ipv4": [127, 0, 0, 1]}, "port": 13125}}, + {"service": {"addr": {"Ipv6": [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1]}, "port": 13125}} + ]], + [1, [ + {"service": {"addr": {"Ipv4": [127, 0, 0, 2]}, "port": 22200}}, + {"service": {"addr": {"Ipv6": [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2]}, "port": 22200}} + ]], + [2, [ + {"service": {"addr": {"Ipv4": [127, 0, 0, 3]}, "port": 22201}}, + {"service": {"addr": {"Ipv6": [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 3]}, "port": 22201}} + ]] + ] + } + }, + "scriptOperatorPayout": "", + "inputsHash": "e2c4f44326c4518abf50fc55b1c8c86a3a4a7312e5534148bf4e33ba531d8d1a", + "platformNodeId": "e5992efdc3d9e31931fb19590b33aaa1b90bb958", + "platformP2PPort": null, + "platformHTTPPort": null, + "sig": "9792fe587c23c184602bc283e5f0e133b5a4cb3b9f8b00937947b7fd77968880dde692fc93de16bbcdf7bcfc20e5547b14ff016e2530cf6aa2f2d0e69af5bf6ffea3fcc8121dc0398485a23889607afb3f259396c875267d4013723aa3239c5e" + } + }, + // Synthetic ExtAddr from rpc_netinfo.py, Evonode with mixed address types (IPv4, TorV3, I2P, and domain) + "1c4cee0257db158902bfdd4441c88f86e1dd53617156cb53ff3cc92d6caeef43": { + "raw": "0300020001f61e5a018c900428a48db44f7c9886c4f5d350e209bbb03faa0d732ff2009f63000000006a473044022064d91d02be5a8036e7dfbff7c3dd7a89feab44a415343f30266f22979478741a022010fc521f27868718d623a3e8894213e9845145ff6452cb0a7c2ac7743c2e0ac8012102de469765cb9273617bc34cf5cbf1a0a919db9b14b3d9d7880289139b34e291d6feffffff01a34ef405000000001976a91469b366dadd041a9afb5a59b277f7164027c1285388ac00000000fdd201030001006112fbbd8c7d2a72138a591a5aba2ccf8ae13d8ab15e6f0895880a6e6faa37f1010300030101047f000001334501042053cd5648488c4707914182655b7664034e09e66f7e8cbf1084e654eb56c5bd883345010520170c56ce72a5a0e62306a3c7084318ee3a46355d17f67896a09c51efbe23fd71000001030101047f00000156b801042053cd5648488c4707914182655b7664034e09e66f7e8cbf1084e654eb56c5bd8856b8010520a0ce38ce2224d2cecaf9929388f73379259c0c27e0debdbd7ca4cd085b55e25a000002040101047f00000156b901042053cd5648488c4707914182655b7664034e09e66f7e8cbf1084e654eb56c5bd8856b9010520a2894dabaec08c0051a481a6dac88b64f98232ae42d4b6fd2fa81952dfe36a87000002147365727665722d312e6578616d706c652e636f6d56b900162c01625064b07b28a43af41fca4c886963690d0435c20ac23ce95e3c42b742e5992efdc3d9e31931fb19590b33aaa1b90bb958a962a6fba440cc328e9acb9c06d3a487d08b635008d7af5aba040a5ef53656f974ac492e235ab757504b709ba80b530007e92cea0aa4455dbe1eac9ed67f57d1ba46ed4e324b83d896832c05afc260970cdbbc9837c75cce573b25b45f1de799", + "details": { + "version": 3, + "mnType": 1, + "proTxHash": "f137aa6f6e0a8895086f5eb18a3de18acf2cba5a1a598a13722a7d8cbdfb1261", + "netInfo": { + "extended": { + "version": 1, + "entries": [ + [0, [ + {"service": {"addr": {"Ipv4": [127, 0, 0, 1]}, "port": 13125}}, + {"service": {"addr": {"TorV3": [83, 205, 86, 72, 72, 140, 71, 7, 145, 65, 130, 101, 91, 118, 100, 3, 78, 9, 230, 111, 126, 140, 191, 16, 132, 230, 84, 235, 86, 197, 189, 136]}, "port": 13125}}, + {"service": {"addr": {"I2p": [23, 12, 86, 206, 114, 165, 160, 230, 35, 6, 163, 199, 8, 67, 24, 238, 58, 70, 53, 93, 23, 246, 120, 150, 160, 156, 81, 239, 190, 35, 253, 113]}, "port": 0}} + ]], + [1, [ + {"service": {"addr": {"Ipv4": [127, 0, 0, 1]}, "port": 22200}}, + {"service": {"addr": {"TorV3": [83, 205, 86, 72, 72, 140, 71, 7, 145, 65, 130, 101, 91, 118, 100, 3, 78, 9, 230, 111, 126, 140, 191, 16, 132, 230, 84, 235, 86, 197, 189, 136]}, "port": 22200}}, + {"service": {"addr": {"I2p": [160, 206, 56, 206, 34, 36, 210, 206, 202, 249, 146, 147, 136, 247, 51, 121, 37, 156, 12, 39, 224, 222, 189, 189, 124, 164, 205, 8, 91, 85, 226, 90]}, "port": 0}} + ]], + [2, [ + {"service": {"addr": {"Ipv4": [127, 0, 0, 1]}, "port": 22201}}, + {"service": {"addr": {"TorV3": [83, 205, 86, 72, 72, 140, 71, 7, 145, 65, 130, 101, 91, 118, 100, 3, 78, 9, 230, 111, 126, 140, 191, 16, 132, 230, 84, 235, 86, 197, 189, 136]}, "port": 22201}}, + {"service": {"addr": {"I2p": [162, 137, 77, 171, 174, 192, 140, 0, 81, 164, 129, 166, 218, 200, 139, 100, 249, 130, 50, 174, 66, 212, 182, 253, 47, 168, 25, 82, 223, 227, 106, 135]}, "port": 0}}, + {"domain": {"name": "server-1.example.com", "port": 22201}} + ]] + ] + } + }, + "scriptOperatorPayout": "", + "inputsHash": "42b7423c5ee93cc20ac235040d696369884cca1ff43aa4287bb0645062012c16", + "platformNodeId": "e5992efdc3d9e31931fb19590b33aaa1b90bb958", + "platformP2PPort": null, + "platformHTTPPort": null, + "sig": "a962a6fba440cc328e9acb9c06d3a487d08b635008d7af5aba040a5ef53656f974ac492e235ab757504b709ba80b530007e92cea0aa4455dbe1eac9ed67f57d1ba46ed4e324b83d896832c05afc260970cdbbc9837c75cce573b25b45f1de799" + } } } } diff --git a/pkgs/primitives/src/lib.rs b/pkgs/primitives/src/lib.rs index f0b36c33..33659946 100644 --- a/pkgs/primitives/src/lib.rs +++ b/pkgs/primitives/src/lib.rs @@ -52,5 +52,6 @@ pub use transaction::{ OutPoint, Transaction, TxHash, TxIn, TxInvalid, TxOut, MAX_COINBASE_SCRIPT_SIZE, MAX_TX_EXTRA_PAYLOAD, }; pub use types::{ - AddrV1, AddrV2, ExtendedNetInfo, NetInfo, NetInfoEntry, NetInfoPurpose, NetworkType, ServiceV1, ServiceV2, + is_bad_port, AddrV1, AddrV2, NIEntry, NIEntryCode, NIError, NIPurpose, NITrait, NetAddr, NetAddrError, NetInfo, + NetInfoV1, NetInfoV2, NetworkType, ServiceV1, ServiceV2, }; diff --git a/pkgs/primitives/src/payload/mod.rs b/pkgs/primitives/src/payload/mod.rs index c3254e66..1711d348 100644 --- a/pkgs/primitives/src/payload/mod.rs +++ b/pkgs/primitives/src/payload/mod.rs @@ -20,7 +20,7 @@ mod proupservtx; mod quorum; use crate::prelude::*; -use crate::types::{NetInfoEntry, NetInfoPurpose}; +use crate::types::{NIError, NIPurpose, NITrait, NetInfoV2}; use dash_num::{make_hash, Hash256}; use dash_types::codec::{Checkable, NumCodec}; @@ -197,7 +197,10 @@ pub enum ProTxInvalid { /// `bad-protx-netinfo-empty` NetInfoEmpty, /// `bad-protx-netinfo-bad` - NetInfoInvalid, + NetInfoInvalid { + /// The underlying error. + error: NIError, + }, /// `bad-protx-payee-reuse` PayoutKeyReuse, /// `bad-protx-operator-reward` @@ -220,7 +223,7 @@ impl fmt::Display for ProTxInvalid { Self::BadPayoutScript => write!(f, "bad-protx-payee"), Self::NetInfoVersionMismatch => write!(f, "bad-protx-netinfo-version"), Self::NetInfoEmpty => write!(f, "bad-protx-netinfo-empty"), - Self::NetInfoInvalid => write!(f, "bad-protx-netinfo-bad"), + Self::NetInfoInvalid { error } => write!(f, "bad-protx-netinfo-bad: {error}"), Self::PayoutKeyReuse => write!(f, "bad-protx-payee-reuse"), Self::OperatorRewardTooHigh { reward } => write!(f, "bad-protx-operator-reward: {reward}"), Self::BadReason { reason } => write!(f, "bad-protx-reason: {reason}"), @@ -230,41 +233,26 @@ impl fmt::Display for ProTxInvalid { } /// Checks that an extended net info payload is trivially valid. -pub(crate) fn check_sptx_netinfo( - entries: &[(NetInfoPurpose, Vec)], - mn_type: MnType, - can_store_platform: bool, -) -> Option { - let has_core = entries - .iter() - .any(|(p, e)| *p == NetInfoPurpose::CoreP2p && !e.is_empty()); - if !has_core { +pub(crate) fn check_sptx_netinfo(ext: &NetInfoV2, version: u16, mn_type: MnType) -> Option { + if let Some(error) = ext.check() { + return Some(ProTxInvalid::NetInfoInvalid { error }); + } + if !ext.has_entries(NIPurpose::CoreP2p) { return Some(ProTxInvalid::NetInfoEmpty); } - - let has_platform_p2p = entries - .iter() - .any(|(p, e)| *p == NetInfoPurpose::PlatformP2p && !e.is_empty()); - let has_platform_https = entries - .iter() - .any(|(p, e)| *p == NetInfoPurpose::PlatformHttps && !e.is_empty()); - - if mn_type == MnType::Regular && (has_platform_p2p || has_platform_https) { - return Some(ProTxInvalid::NetInfoInvalid); + if mn_type == MnType::Regular + && (ext.has_entries(NIPurpose::PlatformP2p) || ext.has_entries(NIPurpose::PlatformHttps)) + { + return Some(ProTxInvalid::NetInfoInvalid { + error: NIError::Malformed, + }); } - - if can_store_platform && mn_type == MnType::Evo && (!has_platform_p2p || !has_platform_https) { + if version >= PROTX_VERSION_EXT_ADDR + && mn_type == MnType::Evo + && (!ext.has_entries(NIPurpose::PlatformP2p) || !ext.has_entries(NIPurpose::PlatformHttps)) + { return Some(ProTxInvalid::NetInfoEmpty); } - - for (_purpose, group) in entries { - for entry in group { - if matches!(entry, NetInfoEntry::Invalid) { - return Some(ProTxInvalid::NetInfoInvalid); - } - } - } - None } diff --git a/pkgs/primitives/src/payload/proregtx.rs b/pkgs/primitives/src/payload/proregtx.rs index 042f8dcc..71767e6f 100644 --- a/pkgs/primitives/src/payload/proregtx.rs +++ b/pkgs/primitives/src/payload/proregtx.rs @@ -13,7 +13,7 @@ use super::{ use crate::codec::impl_payload; use crate::prelude::*; use crate::script::{KeyId, Script}; -use crate::types::{ExtendedNetInfo, NetInfo, ServiceV1}; +use crate::types::{NITrait, NetInfo, NetInfoV1, NetInfoV2, ServiceV1}; use crate::TxHash; use dash_pkc::BlsPublicKeyBytes; @@ -102,10 +102,9 @@ impl BaseCodec for ProRegTx { let collateral_hash = TxHash::decode(data)?; let collateral_index = u32::decode(data)?; let net_info = if version >= 3 { - let raw: Vec = Vec::decode(data)?; - NetInfo::Extended(ExtendedNetInfo::decode(&mut &raw[..])?) + NetInfo::Extended(NetInfoV2::decode(data)?) } else { - NetInfo::Legacy(ServiceV1::decode(data)?) + NetInfo::Legacy(NetInfoV1(ServiceV1::decode(data)?)) }; let key_id_owner = KeyId::decode(data)?; let pub_key_operator = BlsPublicKeyBytes::decode(data)?; @@ -154,9 +153,7 @@ impl BaseCodec for ProRegTx { // guarantees the variant matches the version. if self.version >= 3 { if let NetInfo::Extended(ext) = &self.net_info { - let mut inner = Vec::new(); - ext.encode(&mut inner); - inner.encode(buf); + ext.encode(buf); } } else if let NetInfo::Legacy(svc) = &self.net_info { svc.encode(buf); @@ -213,12 +210,18 @@ impl Checkable for ProRegTx { return Some(ProTxInvalid::NetInfoVersionMismatch); } - if let NetInfo::Extended(ref ext) = self.net_info { - if ext.entries.is_empty() { - return Some(ProTxInvalid::NetInfoEmpty); - } - if let Some(e) = check_sptx_netinfo(&ext.entries, self.mn_type, self.version >= PROTX_VERSION_EXT_ADDR) { - return Some(e); + if !self.net_info.is_empty() { + match &self.net_info { + NetInfo::Legacy(addr) => { + if let Some(error) = addr.check() { + return Some(ProTxInvalid::NetInfoInvalid { error }); + } + } + NetInfo::Extended(addr) => { + if let Some(e) = check_sptx_netinfo(addr, self.version, self.mn_type) { + return Some(e); + } + } } } diff --git a/pkgs/primitives/src/payload/proupservtx.rs b/pkgs/primitives/src/payload/proupservtx.rs index 9e022034..e5731d7d 100644 --- a/pkgs/primitives/src/payload/proupservtx.rs +++ b/pkgs/primitives/src/payload/proupservtx.rs @@ -11,7 +11,7 @@ use super::{check_sptx_netinfo, InputsHash, MnType, ProTxInvalid, PROTX_VERSION_ use crate::codec::impl_payload; use crate::prelude::*; use crate::script::Script; -use crate::types::{ExtendedNetInfo, NetInfo, ServiceV1}; +use crate::types::{NITrait, NetInfo, NetInfoV1, NetInfoV2, ServiceV1}; use crate::TxHash; use dash_pkc::BlsSignatureBytes; @@ -66,10 +66,9 @@ impl BaseCodec for ProUpServTx { let pro_tx_hash = TxHash::decode(data)?; let net_info = if version >= 3 { - let raw: Vec = Vec::decode(data)?; - NetInfo::Extended(ExtendedNetInfo::decode(&mut &raw[..])?) + NetInfo::Extended(NetInfoV2::decode(data)?) } else { - NetInfo::Legacy(ServiceV1::decode(data)?) + NetInfo::Legacy(NetInfoV1(ServiceV1::decode(data)?)) }; let script_operator_payout = Script::decode(data)?; let inputs_hash = InputsHash::decode(data)?; @@ -108,9 +107,7 @@ impl BaseCodec for ProUpServTx { // guarantees the variant matches the version. if self.version >= 3 { if let NetInfo::Extended(ext) = &self.net_info { - let mut inner = Vec::new(); - ext.encode(&mut inner); - inner.encode(buf); + ext.encode(buf); } } else if let NetInfo::Legacy(svc) = &self.net_info { svc.encode(buf); @@ -150,17 +147,17 @@ impl Checkable for ProUpServTx { } match &self.net_info { - NetInfo::Extended(ext) => { - if ext.entries.is_empty() { + NetInfo::Legacy(addr) => { + if addr.is_empty() { return Some(ProTxInvalid::NetInfoEmpty); } - if let Some(e) = check_sptx_netinfo(&ext.entries, self.mn_type, self.version >= PROTX_VERSION_EXT_ADDR) { - return Some(e); + if let Some(error) = addr.check() { + return Some(ProTxInvalid::NetInfoInvalid { error }); } } - NetInfo::Legacy(svc) => { - if svc.addr.is_null() && svc.port == 0 { - return Some(ProTxInvalid::NetInfoEmpty); + NetInfo::Extended(addr) => { + if let Some(e) = check_sptx_netinfo(addr, self.version, self.mn_type) { + return Some(e); } } } diff --git a/pkgs/primitives/src/prelude.rs b/pkgs/primitives/src/prelude.rs index af703a4c..7c1a7e30 100644 --- a/pkgs/primitives/src/prelude.rs +++ b/pkgs/primitives/src/prelude.rs @@ -8,7 +8,8 @@ pub(crate) use alloc::collections::BTreeSet; pub(crate) use alloc::format; -pub(crate) use alloc::string::String; +pub(crate) use alloc::string::{String, ToString}; +pub(crate) use alloc::vec; pub(crate) use alloc::vec::Vec; // Shim for f64::round(), see rust-lang/rust#137578. diff --git a/pkgs/primitives/src/types/addrv1.rs b/pkgs/primitives/src/types/addrv1.rs index 1a88d8f6..73e8f4eb 100644 --- a/pkgs/primitives/src/types/addrv1.rs +++ b/pkgs/primitives/src/types/addrv1.rs @@ -6,16 +6,202 @@ //! Legacy ADDRv1 address and service types. +use super::addrv2::{AddrV2, ServiceV2}; +use super::netaddr::{NetAddr, NetAddrError, NetworkType}; use crate::prelude::*; -use dash_types::codec::{self, BaseCodec, DecodeError}; -use dash_types::impl_type; +use dash_types::codec::{self, BaseCodec, Checkable, DecodeError}; +use dash_types::{impl_bytes, impl_type, type_cvrt}; -dash_types::make_bytes! { - /// ADDRv1 IPv4-mapped IPv6 address (16 bytes). - AddrV1, 16 +use core::fmt; +use core::net::{Ipv4Addr, Ipv6Addr}; +use core::str::FromStr; + +/// First 12 bytes of an IPv4-mapped IPv6 address. +const IPV4_MAPPED_PREFIX: [u8; 12] = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0xff, 0xff]; + +/// ADDRv1 IPv4-mapped IPv6 address (16 bytes). +#[derive(Clone, Copy, Default, Eq, Hash, PartialEq)] +pub struct AddrV1(pub [u8; 16]); + +impl_bytes!(16, AddrV1); + +impl Checkable for AddrV1 { + type Error = NetAddrError; + + fn check(&self) -> Option { + if self.is_null() { + return Some(NetAddrError::BadRange { value: 0 }); + } + // IPv4-mapped null (::ffff:0.0.0.0) has a non-zero prefix + // so the all-zeros check above does not catch it. + if self.is_ipv4() && self.0[12..] == [0; 4] { + return Some(NetAddrError::BadRange { value: 0 }); + } + if self.is_ipv4() && self.0[12..] == [255; 4] { + return Some(NetAddrError::BadRange { value: 255 }); + } + if NetAddr::is_rfc3849(self) { + return Some(NetAddrError::BadRange { value: 0xb8 }); + } + None + } +} + +impl AddrV1 { + /// Returns the inner byte array. + pub const fn to_bytes(self) -> [u8; 16] { + self.0 + } + + /// Borrows the inner byte array. + pub const fn as_bytes(&self) -> &[u8; 16] { + &self.0 + } + + /// Returns `true` when every byte is zero. + pub fn is_null(&self) -> bool { + self.0.iter().all(|&b| b == 0) + } + + /// Returns `true` if the address is IPv4-mapped. + pub fn is_ipv4(&self) -> bool { + self.0[..12] == IPV4_MAPPED_PREFIX + } +} + +impl From for [u8; 16] { + fn from(val: AddrV1) -> Self { + val.0 + } +} + +impl AsRef<[u8]> for AddrV1 { + fn as_ref(&self) -> &[u8] { + &self.0 + } +} + +impl AsRef<[u8; 16]> for AddrV1 { + fn as_ref(&self) -> &[u8; 16] { + &self.0 + } +} + +impl fmt::Debug for AddrV1 { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "AddrV1(")?; + for byte in &self.0 { + write!(f, "{byte:02x}")?; + } + write!(f, ")") + } +} + +#[cfg(feature = "serde")] +impl ::serde::Serialize for AddrV1 { + fn serialize(&self, serializer: S) -> Result { + use dash_types::__private::hex_conservative::DisplayHex; + serializer.serialize_str(&self.0.to_lower_hex_string()) + } +} + +#[cfg(feature = "serde")] +impl<'de> ::serde::Deserialize<'de> for AddrV1 { + fn deserialize>(deserializer: D) -> Result { + let s = ::deserialize(deserializer)?; + <[u8; 16] as dash_types::__private::hex_conservative::FromHex>::from_hex(&s) + .map(Self) + .map_err(::serde::de::Error::custom) + } +} + +impl NetAddr for AddrV1 { + fn bytes(&self) -> &[u8] { + if self.is_ipv4() { + &self.0[12..] + } else { + &self.0 + } + } + + fn network(&self) -> NetworkType { + if self.is_ipv4() { + NetworkType::Ipv4 + } else { + NetworkType::Ipv6 + } + } + + fn is_ipv4(&self) -> bool { + self.is_ipv4() + } + + fn is_ipv6(&self) -> bool { + !self.is_ipv4() + } + + fn is_null(&self) -> bool { + self.is_null() + } } +impl fmt::Display for AddrV1 { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + if self.is_ipv4() { + let ip = Ipv4Addr::new(self.0[12], self.0[13], self.0[14], self.0[15]); + write!(f, "{ip}") + } else { + let ip = Ipv6Addr::from(self.0); + write!(f, "[{ip}]") + } + } +} + +impl FromStr for AddrV1 { + type Err = NetAddrError; + + fn from_str(s: &str) -> Result { + if let Some(inner) = s.strip_prefix('[').and_then(|r| r.strip_suffix(']')) { + let ip: Ipv6Addr = inner.parse().map_err(|_| NetAddrError::BadEncode { pos: 0 })?; + return Ok(Self(ip.octets())); + } + if s.ends_with(".onion") { + return Err(NetAddrError::AddrTooNew { + network: NetworkType::TorV3, + }); + } + if s.ends_with(".b32.i2p") { + return Err(NetAddrError::AddrTooNew { + network: NetworkType::I2p, + }); + } + if let Ok(ip) = s.parse::() { + let octets = ip.octets(); + let mut arr = [0u8; 16]; + arr[..12].copy_from_slice(&IPV4_MAPPED_PREFIX); + arr[12..].copy_from_slice(&octets); + return Ok(Self(arr)); + } + Err(NetAddrError::BadEncode { pos: 0 }) + } +} + +type_cvrt!(TryFrom for AddrV1, NetAddrError, |addr| { + match addr { + AddrV2::Ipv4(b) => { + let mut arr = [0u8; 16]; + arr[..12].copy_from_slice(&IPV4_MAPPED_PREFIX); + arr[12..].copy_from_slice(b); + Ok(Self(arr)) + } + AddrV2::Ipv6(b) => Ok(Self(*b)), + other => Err(NetAddrError::AddrTooNew { + network: other.network(), + }), + } +}); + /// Legacy network address (ADDRv1 format, 18 bytes). #[derive(Clone, Debug, Eq, Hash, PartialEq)] #[cfg_attr(feature = "serde", derive(::serde::Serialize, ::serde::Deserialize))] @@ -42,3 +228,240 @@ impl BaseCodec for ServiceV1 { buf.extend_from_slice(&self.port.to_be_bytes()); } } + +impl Checkable for ServiceV1 { + type Error = NetAddrError; + + fn check(&self) -> Option { + if self.port == 0 { + return Some(NetAddrError::BadPort { port: 0 }); + } + self.addr.check() + } +} + +impl fmt::Display for ServiceV1 { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{}:{}", self.addr, self.port) + } +} + +impl FromStr for ServiceV1 { + type Err = NetAddrError; + + fn from_str(s: &str) -> Result { + let (addr_str, port) = split_service_str(s)?; + let addr = AddrV1::from_str(addr_str)?; + Ok(Self { addr, port }) + } +} + +type_cvrt!(TryFrom for ServiceV1, NetAddrError, |v2| { + Ok(Self { + addr: AddrV1::try_from(&v2.addr)?, + port: v2.port, + }) +}); + +/// Splits a service string into `(addr, port)`. +/// +/// Handles both bracketed (`[addr]:port`) and plain (`addr:port`) forms. The +/// unbracketed path uses `rfind(':')`, so callers must never pass bare IPv6 +/// addresses without brackets. +/// +/// # Errors +/// +/// Returns `BadEncode` when the input cannot be split into a +/// valid address and port pair. +pub(super) fn split_service_str(s: &str) -> Result<(&str, u16), NetAddrError> { + if s.starts_with('[') { + let close = s.rfind(']').ok_or(NetAddrError::BadEncode { pos: 0 })?; + let addr_str = &s[..=close]; + let rest = &s[close + 1..]; + let port_str = rest.strip_prefix(':').ok_or(NetAddrError::BadEncode { pos: 0 })?; + let port: u16 = port_str.parse().map_err(|_| NetAddrError::BadEncode { pos: 0 })?; + return Ok((addr_str, port)); + } + let colon = s.rfind(':').ok_or(NetAddrError::BadEncode { pos: 0 })?; + let addr_str = &s[..colon]; + let port_str = &s[colon + 1..]; + let port: u16 = port_str.parse().map_err(|_| NetAddrError::BadEncode { pos: 0 })?; + Ok((addr_str, port)) +} + +#[cfg(test)] +#[expect(clippy::unwrap_used, reason = "test code")] +mod tests { + use super::*; + + use hex_literal::hex; + use rstest::rstest; + + #[rstest] + #[case::ipv4("1.2.3.4", hex!("00000000000000000000ffff01020304"))] + #[case::loopback("127.0.0.1", hex!("00000000000000000000ffff7f000001"))] + #[case::ipv6("[::1]", hex!("00000000000000000000000000000001"))] + #[case::ipv6_full("[2001:db8::1]", hex!("20010db8000000000000000000000001"))] + fn addr_roundtrip(#[case] s: &str, #[case] raw: [u8; 16]) { + let parsed: AddrV1 = s.parse().unwrap(); + assert_eq!(parsed, AddrV1(raw)); + assert_eq!(parsed.to_string(), s); + } + + #[rstest] + #[case::ipv4("1.2.3.4:8333")] + #[case::ipv6("[::1]:9999")] + fn service_roundtrip(#[case] s: &str) { + let parsed: ServiceV1 = s.parse().unwrap(); + assert_eq!(parsed.to_string(), s); + } + + #[rstest] + #[case::local("127.0.0.1", true)] + #[case::rfc1918("10.0.0.1", false)] + #[case::routable("1.2.3.4", false)] + #[case::ipv6_local("[::1]", true)] + fn is_local(#[case] s: &str, #[case] expected: bool) { + let addr: AddrV1 = s.parse().unwrap(); + assert_eq!(NetAddr::is_local(&addr), expected); + } + + #[rstest] + #[case::routable("1.2.3.4", true)] + #[case::local("127.0.0.1", false)] + #[case::private("10.0.0.1", false)] + fn is_routable(#[case] s: &str, #[case] expected: bool) { + let addr: AddrV1 = s.parse().unwrap(); + assert_eq!(NetAddr::is_routable(&addr), expected); + } + + #[rstest] + #[case::yes("10.0.0.1", true)] + #[case::no("8.8.8.8", false)] + fn is_rfc1918(#[case] s: &str, #[case] expected: bool) { + let addr: AddrV1 = s.parse().unwrap(); + assert_eq!(NetAddr::is_rfc1918(&addr), expected); + } + + #[rstest] + #[case::null([0u8; 16], Some(NetAddrError::BadRange { value: 0 }))] + #[case::ipv4_null( + hex!("00000000000000000000ffff00000000"), + Some(NetAddrError::BadRange { value: 0 }), + )] + #[case::broadcast( + hex!("00000000000000000000ffffffffffff"), + Some(NetAddrError::BadRange { value: 255 }), + )] + #[case::rfc3849( + hex!("20010db8000000000000000000000001"), + Some(NetAddrError::BadRange { value: 0xb8 }), + )] + #[case::valid(hex!("00000000000000000000ffff08080808"), None)] + fn check_addr(#[case] raw: [u8; 16], #[case] expected: Option) { + assert_eq!(AddrV1(raw).check(), expected); + } + + #[rstest] + #[case::zero_port("8.8.8.8", 0, Some(NetAddrError::BadPort { port: 0 }))] + #[case::null_addr([0u8; 16], 8333, Some(NetAddrError::BadRange { value: 0 }))] + #[case::valid("8.8.8.8", 8333, None)] + fn check_service( + #[case] input: impl Into, + #[case] port: u16, + #[case] expected: Option, + ) { + let addr = input.into().0; + assert_eq!(ServiceV1 { addr, port }.check(), expected); + } + + /// Helper for polymorphic test inputs in `check_service`. + struct CheckServiceInput(AddrV1); + + impl From<&str> for CheckServiceInput { + fn from(s: &str) -> Self { + Self(s.parse().unwrap()) + } + } + + impl From<[u8; 16]> for CheckServiceInput { + fn from(raw: [u8; 16]) -> Self { + Self(AddrV1(raw)) + } + } + + #[rstest] + #[case::ipv4("1.2.3.4", AddrV2::Ipv4([1, 2, 3, 4]))] + #[case::ipv6( + "[2001:db8::1]", + AddrV2::Ipv6(hex!("20010db8000000000000000000000001")), + )] + fn from_addrv1(#[case] s: &str, #[case] expected: AddrV2) { + let v1: AddrV1 = s.parse().unwrap(); + assert_eq!(AddrV2::from(v1), expected); + } + + #[rstest] + #[case::ipv4(AddrV2::Ipv4([1, 2, 3, 4]), "1.2.3.4")] + #[case::ipv6( + AddrV2::Ipv6(hex!("20010db8000000000000000000000001")), + "[2001:db8::1]", + )] + fn try_from_addrv2_ok(#[case] v2: AddrV2, #[case] s: &str) { + let v1 = AddrV1::try_from(v2).unwrap(); + assert_eq!(v1.to_string(), s); + } + + #[rstest] + #[case::tor(AddrV2::TorV3([1; 32]), NetworkType::TorV3)] + #[case::i2p(AddrV2::I2p([1; 32]), NetworkType::I2p)] + #[case::cjdns(AddrV2::Cjdns([0xfc; 16]), NetworkType::Cjdns)] + fn try_from_addrv2_fails(#[case] v2: AddrV2, #[case] net: NetworkType) { + let err = AddrV1::try_from(v2).unwrap_err(); + assert_eq!(err, NetAddrError::AddrTooNew { network: net }); + } + + #[rstest] + #[case::onion("pg6mmjiyjmcrsslvykfwnntlaru7p5svn6y2ymmju6nubxndf4pscryd.onion", NetworkType::TorV3)] + #[case::i2p("ukeu3k5oycgaauneqgtnvselmt4yemvoilkln7jpvamvfx7dnkdq.b32.i2p", NetworkType::I2p)] + fn from_str_rejects(#[case] s: &str, #[case] net: NetworkType) { + let err = s.parse::().unwrap_err(); + assert_eq!(err, NetAddrError::AddrTooNew { network: net }); + } + + #[rstest] + fn service_from_v1() { + let v1 = ServiceV1 { + addr: "1.2.3.4".parse().unwrap(), + port: 8333, + }; + let v2 = ServiceV2::from(v1); + assert_eq!(v2.addr, AddrV2::Ipv4([1, 2, 3, 4])); + assert_eq!(v2.port, 8333); + } + + #[rstest] + fn service_try_from_v2() { + let v2 = ServiceV2 { + addr: AddrV2::Ipv4([1, 2, 3, 4]), + port: 8333, + }; + let v1 = ServiceV1::try_from(v2).unwrap(); + assert_eq!(v1.to_string(), "1.2.3.4:8333"); + } + + #[rstest] + fn service_try_from_v2_tor_fails() { + let v2 = ServiceV2 { + addr: AddrV2::TorV3([1; 32]), + port: 8333, + }; + let err = ServiceV1::try_from(v2).unwrap_err(); + assert_eq!( + err, + NetAddrError::AddrTooNew { + network: NetworkType::TorV3, + } + ); + } +} diff --git a/pkgs/primitives/src/types/addrv2.rs b/pkgs/primitives/src/types/addrv2.rs index 09eef03d..86b3197c 100644 --- a/pkgs/primitives/src/types/addrv2.rs +++ b/pkgs/primitives/src/types/addrv2.rs @@ -6,124 +6,321 @@ //! BIP155 network address types (ADDRv2). +use super::addrv1::{AddrV1, ServiceV1}; +use super::netaddr::{NetAddr, NetAddrError, NetworkType}; +use super::util::{base16_dec, base16_enc, base32r_dec, base32r_enc}; use crate::prelude::*; -use dash_types::codec::{self, BaseCodec, DecodeError, NumCodec}; -use dash_types::{impl_num, impl_type}; +use bitcoin_hashes::sha3_256; +use dash_types::codec::{self, BaseCodec, Checkable, DecodeError, NumCodec}; +use dash_types::{impl_type, type_cvrt}; use core::fmt; +use core::net::{Ipv4Addr, Ipv6Addr}; +use core::str::FromStr; /// Maximum raw address length for any known BIP155 network type. const MAX_ADDR_LEN: usize = 512; -/// Network address type (BIP155). -#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)] -pub enum NetworkType { - /// IPv4. - Ipv4, - /// IPv6. - Ipv6, - /// Tor v3 hidden service. - TorV3, - /// I2P. - I2P, - /// CJDNS. - Cjdns, - /// Unknown network type. - Unknown(u8), +/// BIP155 network address. +#[derive(Clone, Debug, Eq, Hash, PartialEq)] +#[cfg_attr(feature = "serde", derive(::serde::Serialize, ::serde::Deserialize))] +pub enum AddrV2 { + /// IPv4 address (4 bytes). + Ipv4([u8; 4]), + /// IPv6 address (16 bytes). + Ipv6([u8; 16]), + /// Onion hidden service (32 bytes). + TorV3([u8; 32]), + /// I2P address (32 bytes). + I2p([u8; 32]), + /// CJDNS address (16 bytes). + Cjdns([u8; 16]), + /// Unknown network type with raw address bytes. + Unknown { + /// Wire network ID. + network: u8, + /// Raw address bytes. + addr: Vec, + }, } -impl NumCodec for NetworkType { - fn from_base(val: u8) -> Self { - match val { - 1 => Self::Ipv4, - 2 => Self::Ipv6, - 4 => Self::TorV3, - 5 => Self::I2P, - 6 => Self::Cjdns, - other => Self::Unknown(other), +impl BaseCodec for AddrV2 { + fn decode(data: &mut &[u8]) -> Result { + let net_byte = u8::decode(data)?; + let network = NetworkType::from_base(net_byte); + let len = codec::read_compact_size(data, MAX_ADDR_LEN)?; + if let Some(expected) = network.expected_len() { + if len != expected { + return Err(DecodeError::InvalidValue { + expected: expected as u64, + actual: len as u64, + }); + } + } + let raw = codec::read_bytes(data, len)?; + match network { + NetworkType::Ipv4 => { + let mut buf = [0u8; 4]; + buf.copy_from_slice(raw); + Ok(Self::Ipv4(buf)) + } + NetworkType::Ipv6 => { + let mut buf = [0u8; 16]; + buf.copy_from_slice(raw); + // BIP155: fc00::/8 is CJDNS, not generic IPv6. + if buf[0] == 0xfc { + Ok(Self::Cjdns(buf)) + } else { + Ok(Self::Ipv6(buf)) + } + } + NetworkType::TorV3 => { + let mut buf = [0u8; 32]; + buf.copy_from_slice(raw); + Ok(Self::TorV3(buf)) + } + NetworkType::I2p => { + let mut buf = [0u8; 32]; + buf.copy_from_slice(raw); + Ok(Self::I2p(buf)) + } + NetworkType::Cjdns => { + let mut buf = [0u8; 16]; + buf.copy_from_slice(raw); + Ok(Self::Cjdns(buf)) + } + NetworkType::Unknown(n) => Ok(Self::Unknown { + network: n, + addr: raw.to_vec(), + }), } } - fn to_base(&self) -> u8 { - match self { - Self::Ipv4 => 1, - Self::Ipv6 => 2, - Self::TorV3 => 4, - Self::I2P => 5, - Self::Cjdns => 6, - Self::Unknown(v) => *v, - } + fn encode(&self, buf: &mut Vec) { + self.network().to_base().encode(buf); + let bytes = self.bytes(); + codec::write_compact_size(bytes.len(), buf); + buf.extend_from_slice(bytes); } } -impl_num!(NetworkType, u8); +impl_type!(AddrV2); + +impl Checkable for AddrV2 { + type Error = NetAddrError; -impl NetworkType { - /// Expected byte length for a known network type, or `None` for unknown. - pub const fn expected_len(self) -> Option { + fn check(&self) -> Option { + if self.is_null() { + return Some(NetAddrError::BadRange { value: 0 }); + } match self { - Self::Ipv4 => Some(4), - Self::Ipv6 => Some(16), - Self::TorV3 => Some(32), - Self::I2P => Some(32), - Self::Cjdns => Some(16), - Self::Unknown(_) => None, + Self::Ipv4(b) => { + // broadcast address (255.255.255.255) + if *b == [255; 4] { + return Some(NetAddrError::BadRange { value: 255 }); + } + } + Self::Ipv6(b) => { + // fc00::/8 belongs in the Cjdns variant per BIP155. + if b[0] == 0xfc { + return Some(NetAddrError::BadRange { value: 0xfc }); + } + } + Self::Cjdns(b) => { + if b[0] != 0xfc { + return Some(NetAddrError::BadRange { value: b[0] }); + } + } + Self::Unknown { network, addr } => { + // Known network IDs must use their typed variant. + if NetworkType::from_base(*network).expected_len().is_some() { + return Some(NetAddrError::BadRange { value: *network }); + } + if addr.len() > MAX_ADDR_LEN { + return Some(NetAddrError::BadLen { + expected: MAX_ADDR_LEN, + actual: addr.len(), + }); + } + } + _ => {} + } + if self.is_rfc3849() { + return Some(NetAddrError::BadRange { value: 0xb8 }); } + None } } -impl fmt::Display for NetworkType { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { +impl AddrV2 { + /// Returns the BIP155 network type for this address. + pub fn network(&self) -> NetworkType { match self { - Self::Ipv4 => f.write_str("ipv4"), - Self::Ipv6 => f.write_str("ipv6"), - Self::TorV3 => f.write_str("torv3"), - Self::I2P => f.write_str("i2p"), - Self::Cjdns => f.write_str("cjdns"), - Self::Unknown(v) => write!(f, "unknown({v})"), + Self::Ipv4(_) => NetworkType::Ipv4, + Self::Ipv6(_) => NetworkType::Ipv6, + Self::TorV3(_) => NetworkType::TorV3, + Self::I2p(_) => NetworkType::I2p, + Self::Cjdns(_) => NetworkType::Cjdns, + Self::Unknown { network, .. } => NetworkType::Unknown(*network), } } -} -/// BIP155 network address. -#[derive(Clone, Debug, Eq, Hash, PartialEq)] -#[cfg_attr(feature = "serde", derive(::serde::Serialize, ::serde::Deserialize))] -pub struct AddrV2 { - /// Network transport type. - pub network: NetworkType, - /// Raw address bytes (length depends on network type). - pub addr: Vec, + /// Raw address bytes. + pub fn bytes(&self) -> &[u8] { + match self { + Self::Ipv4(b) => b, + Self::Ipv6(b) => b, + Self::TorV3(b) => b, + Self::I2p(b) => b, + Self::Cjdns(b) => b, + Self::Unknown { addr, .. } => addr, + } + } } -impl_type!(AddrV2); +impl NetAddr for AddrV2 { + fn bytes(&self) -> &[u8] { + self.bytes() + } -impl BaseCodec for AddrV2 { - fn decode(data: &mut &[u8]) -> Result { - let net_byte = u8::decode(data)?; - let network = NetworkType::from_base(net_byte); - let len = codec::read_compact_size(data, MAX_ADDR_LEN)?; - if let Some(expected) = network.expected_len() { - if len != expected { - return Err(DecodeError::InvalidValue { - expected: expected as u64, - actual: len as u64, - }); - } - } - let addr = codec::read_bytes(data, len)?.to_vec(); - Ok(Self { network, addr }) + fn network(&self) -> NetworkType { + self.network() } - fn encode(&self, buf: &mut Vec) { - self.network.to_base().encode(buf); - self.addr.encode(buf); + fn is_ipv4(&self) -> bool { + matches!(self, Self::Ipv4(_)) + } + + fn is_ipv6(&self) -> bool { + matches!(self, Self::Ipv6(_)) + } + + fn is_null(&self) -> bool { + self.bytes().iter().all(|&b| b == 0) + } + + fn is_tor(&self) -> bool { + matches!(self, Self::TorV3(_)) + } + + fn is_i2p(&self) -> bool { + matches!(self, Self::I2p(_)) + } + + fn is_cjdns(&self) -> bool { + matches!(self, Self::Cjdns(_)) } } impl fmt::Display for AddrV2 { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "{}", self.network) + match self { + Self::Ipv4(b) => { + let ip = Ipv4Addr::new(b[0], b[1], b[2], b[3]); + write!(f, "{ip}") + } + Self::Ipv6(b) | Self::Cjdns(b) => { + let ip = Ipv6Addr::from(*b); + write!(f, "[{ip}]") + } + Self::TorV3(pubkey) => { + const VERSION: u8 = 3; + let mut pre = [0u8; 48]; + pre[..15].copy_from_slice(b".onion checksum"); + pre[15..47].copy_from_slice(pubkey); + pre[47] = VERSION; + let hash = sha3_256::Hash::hash(&pre); + let cs = hash.to_byte_array(); + let mut buf = [0u8; 35]; + buf[..32].copy_from_slice(pubkey); + buf[32] = cs[0]; + buf[33] = cs[1]; + buf[34] = VERSION; + base32r_enc(&buf, f)?; + f.write_str(".onion") + } + Self::I2p(b) => { + base32r_enc(b, f)?; + f.write_str(".b32.i2p") + } + Self::Unknown { network, addr } => { + write!(f, "{}:", NetworkType::Unknown(*network))?; + base16_enc(addr, f) + } + } + } +} + +type_cvrt!(From for AddrV2, |v1| { + if v1.is_ipv4() { + let b = v1.as_bytes(); + Self::Ipv4([b[12], b[13], b[14], b[15]]) + } else { + Self::Ipv6(*v1.as_bytes()) + } +}); + +impl FromStr for AddrV2 { + type Err = NetAddrError; + + fn from_str(s: &str) -> Result { + if let Some(name) = s.strip_suffix(".onion") { + let mut buf = [0u8; 35]; + base32r_dec(name, &mut buf)?; + let version = buf[34]; + if version != 3 { + return Err(NetAddrError::BadVersion { version }); + } + let mut pubkey = [0u8; 32]; + pubkey.copy_from_slice(&buf[..32]); + // Verify SHA3-256 checksum. + let mut pre = [0u8; 48]; + pre[..15].copy_from_slice(b".onion checksum"); + pre[15..47].copy_from_slice(&pubkey); + pre[47] = version; + let hash = sha3_256::Hash::hash(&pre); + let cs = hash.to_byte_array(); + if buf[32] != cs[0] || buf[33] != cs[1] { + return Err(NetAddrError::BadChecksum { + expected: [cs[0], cs[1]], + actual: [buf[32], buf[33]], + }); + } + return Ok(Self::TorV3(pubkey)); + } + if let Some(name) = s.strip_suffix(".b32.i2p") { + let mut buf = [0u8; 32]; + base32r_dec(name, &mut buf)?; + return Ok(Self::I2p(buf)); + } + if let Some(rest) = s.strip_prefix("unknown(") { + let close = rest.find(')').ok_or(NetAddrError::BadEncode { pos: 0 })?; + let net_str = &rest[..close]; + let net: u8 = net_str.parse().map_err(|_| NetAddrError::BadEncode { pos: 0 })?; + let hex_str = rest + .get(close + 1..) + .and_then(|r| r.strip_prefix(':')) + .ok_or(NetAddrError::BadEncode { pos: close + 1 })?; + let addr = base16_dec(hex_str)?; + if addr.len() > MAX_ADDR_LEN { + return Err(NetAddrError::BadLen { + expected: MAX_ADDR_LEN, + actual: addr.len(), + }); + } + return Ok(Self::Unknown { network: net, addr }); + } + if let Some(inner) = s.strip_prefix('[').and_then(|r| r.strip_suffix(']')) { + let ip: Ipv6Addr = inner.parse().map_err(|_| NetAddrError::BadEncode { pos: 0 })?; + let octets = ip.octets(); + if octets[0] == 0xfc { + return Ok(Self::Cjdns(octets)); + } + return Ok(Self::Ipv6(octets)); + } + let ip: Ipv4Addr = s.parse().map_err(|_| NetAddrError::BadEncode { pos: 0 })?; + Ok(Self::Ipv4(ip.octets())) } } @@ -152,8 +349,212 @@ impl BaseCodec for ServiceV2 { } } +impl Checkable for ServiceV2 { + type Error = NetAddrError; + + fn check(&self) -> Option { + // I2P SAM 3.1 does not use ports; port must be exactly 0. + if self.addr.is_i2p() { + if self.port != 0 { + return Some(NetAddrError::BadPort { port: self.port }); + } + } else if self.port == 0 { + return Some(NetAddrError::BadPort { port: 0 }); + } + self.addr.check() + } +} + impl fmt::Display for ServiceV2 { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { write!(f, "{}:{}", self.addr, self.port) } } + +type_cvrt!(From for ServiceV2, |v1| { + Self { + addr: AddrV2::from(&v1.addr), + port: v1.port, + } +}); + +impl FromStr for ServiceV2 { + type Err = NetAddrError; + + fn from_str(s: &str) -> Result { + let (addr_str, port) = super::addrv1::split_service_str(s)?; + let addr = AddrV2::from_str(addr_str)?; + Ok(Self { addr, port }) + } +} + +#[cfg(test)] +#[expect(clippy::unwrap_used, reason = "test code")] +mod tests { + use super::*; + + use hex_literal::hex; + use rstest::rstest; + + #[rstest] + #[case::torv3_vec1( + AddrV2::TorV3(hex!("79bcc625184b05194975c28b66b66b0469f7f6556fb1ac3189a79b40dda32f1f")), + "pg6mmjiyjmcrsslvykfwnntlaru7p5svn6y2ymmju6nubxndf4pscryd.onion", + )] + #[case::torv3_vec2( + AddrV2::TorV3(hex!("53cd5648488c4707914182655b7664034e09e66f7e8cbf1084e654eb56c5bd88")), + "kpgvmscirrdqpekbqjsvw5teanhatztpp2gl6eee4zkowvwfxwenqaid.onion", + )] + #[case::i2p( + AddrV2::I2p(hex!("a2894dabaec08c0051a481a6dac88b64f98232ae42d4b6fd2fa81952dfe36a87")), + "ukeu3k5oycgaauneqgtnvselmt4yemvoilkln7jpvamvfx7dnkdq.b32.i2p", + )] + #[case::ipv4(AddrV2::Ipv4([1, 2, 3, 4]), "1.2.3.4")] + #[case::ipv6( + AddrV2::Ipv6(hex!("00000000000000000000000000000001")), + "[::1]", + )] + #[case::cjdns( + AddrV2::Cjdns(hex!("fc000000000000000000000000000001")), + "[fc00::1]", + )] + fn display(#[case] addr: AddrV2, #[case] expected: &str) { + assert_eq!(addr.to_string(), expected); + assert_eq!(expected.parse::().unwrap(), addr); + } + + #[rstest] + #[case::ipv4(AddrV2::Ipv4([1, 2, 3, 4]))] + #[case::ipv6(AddrV2::Ipv6(hex!("20010db8000000000000000000000001")))] + fn codec_roundtrip(#[case] addr: AddrV2) { + let mut buf = Vec::new(); + addr.encode(&mut buf); + let decoded = AddrV2::decode(&mut buf.as_slice()).unwrap(); + assert_eq!(decoded, addr); + } + + #[rstest] + fn roundtrip_service() { + let svc = ServiceV2 { + addr: AddrV2::Ipv4([10, 0, 0, 1]), + port: 9999, + }; + let mut buf = Vec::new(); + svc.encode(&mut buf); + let decoded = ServiceV2::decode(&mut buf.as_slice()).unwrap(); + assert_eq!(decoded, svc); + } + + #[rstest] + fn netaddr_classification() { + assert!(AddrV2::Ipv4([10, 0, 0, 1]).is_rfc1918()); + assert!(AddrV2::Ipv4([8, 8, 8, 8]).is_routable()); + assert!(!AddrV2::Ipv4([127, 0, 0, 1]).is_routable()); + assert!(AddrV2::Ipv4([127, 0, 0, 1]).is_local()); + assert!(AddrV2::Ipv4([0, 0, 0, 0]).is_null()); + assert!(AddrV2::TorV3([1; 32]).is_tor()); + assert!(AddrV2::I2p([1; 32]).is_i2p()); + assert!(AddrV2::Cjdns([0xfc; 16]).is_cjdns()); + assert!(AddrV2::TorV3([1; 32]).is_privacy_net()); + assert!(AddrV2::TorV3([1; 32]).is_routable()); + } + + #[rstest] + fn wire_compat_with_old_format() { + // Verify the wire encoding is identical to the old + // AddrV2 struct format: network byte + compact-size + // length + address bytes. + let addr = AddrV2::Ipv4([1, 2, 3, 4]); + let mut buf = Vec::new(); + addr.encode(&mut buf); + // 0x01 (ipv4) + 0x04 (length) + 01020304 + assert_eq!(buf, vec![0x01, 0x04, 1, 2, 3, 4]); + } + + #[rstest] + #[case::ipv4_null(AddrV2::Ipv4([0; 4]), Some(NetAddrError::BadRange { value: 0 }))] + #[case::ipv4_broadcast(AddrV2::Ipv4([255; 4]), Some(NetAddrError::BadRange { value: 255 }))] + #[case::ipv4_valid(AddrV2::Ipv4([8, 8, 8, 8]), None)] + #[case::ipv4_low(AddrV2::Ipv4([0, 1, 2, 3]), None)] + #[case::ipv4_high(AddrV2::Ipv4([240, 0, 0, 1]), None)] + #[case::ipv6_null(AddrV2::Ipv6([0; 16]), Some(NetAddrError::BadRange { value: 0 }))] + #[case::ipv6_rfc3849( + AddrV2::Ipv6(hex!("20010db8000000000000000000000001")), + Some(NetAddrError::BadRange { value: 0xb8 }), + )] + #[case::ipv6_valid(AddrV2::Ipv6(hex!("20010000000000000000000000000001")), None)] + #[case::cjdns_bad_prefix( + AddrV2::Cjdns(hex!("fd000000000000000000000000000001")), + Some(NetAddrError::BadRange { value: 0xfd }), + )] + #[case::cjdns_valid(AddrV2::Cjdns(hex!("fc000000000000000000000000000001")), None)] + #[case::ipv6_cjdns_range( + AddrV2::Ipv6(hex!("fc000000000000000000000000000001")), + Some(NetAddrError::BadRange { value: 0xfc }), + )] + #[case::unknown_known_id( + AddrV2::Unknown { network: 1, addr: vec![1, 2, 3, 4] }, + Some(NetAddrError::BadRange { value: 1 }), + )] + #[case::unknown_valid( + AddrV2::Unknown { network: 99, addr: vec![1, 2] }, + None, + )] + #[case::tor_valid(AddrV2::TorV3([1; 32]), None)] + #[case::i2p_valid(AddrV2::I2p([1; 32]), None)] + fn check_addr(#[case] addr: AddrV2, #[case] expected: Option) { + assert_eq!(addr.check(), expected); + } + + #[rstest] + #[case::zero_port(AddrV2::Ipv4([8, 8, 8, 8]), 0, Some(NetAddrError::BadPort { port: 0 }))] + #[case::delegates_to_addr( + AddrV2::Cjdns(hex!("fd000000000000000000000000000001")), + 8333, + Some(NetAddrError::BadRange { value: 0xfd }), + )] + #[case::delegates_null(AddrV2::Ipv4([0; 4]), 8333, Some(NetAddrError::BadRange { value: 0 }))] + #[case::valid(AddrV2::Ipv4([8, 8, 8, 8]), 8333, None)] + #[case::i2p_port_zero(AddrV2::I2p([1; 32]), 0, None)] + #[case::i2p_nonzero_port(AddrV2::I2p([1; 32]), 9999, Some(NetAddrError::BadPort { port: 9999 }))] + fn check_service(#[case] addr: AddrV2, #[case] port: u16, #[case] expected: Option) { + assert_eq!(ServiceV2 { addr, port }.check(), expected); + } + + #[rstest] + #[case::bad_ipv4("999.999.999.999")] + #[case::bad_bracket("[not-an-ip]")] + #[case::bad_onion("zzzz.onion")] + fn from_str_errors(#[case] s: &str) { + assert!(s.parse::().is_err()); + } + + #[rstest] + #[case::ipv4("1.2.3.4:8333")] + #[case::ipv6("[::1]:9999")] + #[case::cjdns("[fc00::1]:1234")] + #[case::tor("pg6mmjiyjmcrsslvykfwnntlaru7p5svn6y2ymmju6nubxndf4pscryd.onion:8333")] + #[case::i2p("ukeu3k5oycgaauneqgtnvselmt4yemvoilkln7jpvamvfx7dnkdq.b32.i2p:7654")] + fn service_from_str_roundtrip(#[case] s: &str) { + let parsed: ServiceV2 = s.parse().unwrap(); + assert_eq!(parsed.to_string(), s); + } + + #[rstest] + #[case::missing_separator("unknown(99)abcd")] + #[case::unclosed_paren("unknown(99abcd")] + fn from_str_unknown_bad_format(#[case] s: &str) { + assert!(s.parse::().is_err()); + } + + #[rstest] + fn unknown_from_str_roundtrip() { + let addr = AddrV2::Unknown { + network: 99, + addr: vec![0xab, 0xcd], + }; + let s = addr.to_string(); + let parsed: AddrV2 = s.parse().unwrap(); + assert_eq!(parsed, addr); + } +} diff --git a/pkgs/primitives/src/types/mod.rs b/pkgs/primitives/src/types/mod.rs index 7cd46a8b..9c949869 100644 --- a/pkgs/primitives/src/types/mod.rs +++ b/pkgs/primitives/src/types/mod.rs @@ -8,8 +8,11 @@ mod addrv1; mod addrv2; +mod netaddr; mod netinfo; +mod util; pub use addrv1::{AddrV1, ServiceV1}; -pub use addrv2::{AddrV2, NetworkType, ServiceV2}; -pub use netinfo::{ExtendedNetInfo, NetInfo, NetInfoEntry, NetInfoPurpose}; +pub use addrv2::{AddrV2, ServiceV2}; +pub use netaddr::{is_bad_port, NetAddr, NetAddrError, NetworkType}; +pub use netinfo::{NIEntry, NIEntryCode, NIError, NIPurpose, NITrait, NetInfo, NetInfoV1, NetInfoV2}; diff --git a/pkgs/primitives/src/types/netaddr.rs b/pkgs/primitives/src/types/netaddr.rs new file mode 100644 index 00000000..c8d46504 --- /dev/null +++ b/pkgs/primitives/src/types/netaddr.rs @@ -0,0 +1,656 @@ +// +// Copyright (c) 2026-present, The Dash Core developers +// SPDX-License-Identifier: MIT +// See the accompanying file LICENSE or https://opensource.org/license/MIT +// + +//! Network address types and classification. + +use dash_types::codec::NumCodec; +use dash_types::impl_num; + +use core::fmt; + +/// Network address type (BIP155). +#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)] +pub enum NetworkType { + /// IPv4. + Ipv4, + /// IPv6. + Ipv6, + /// Tor v3 hidden service. + TorV3, + /// I2P. + I2p, + /// CJDNS. + Cjdns, + /// Unknown network type. + Unknown(u8), +} + +impl NumCodec for NetworkType { + fn from_base(val: u8) -> Self { + match val { + 1 => Self::Ipv4, + 2 => Self::Ipv6, + 4 => Self::TorV3, + 5 => Self::I2p, + 6 => Self::Cjdns, + other => Self::Unknown(other), + } + } + + fn to_base(&self) -> u8 { + match self { + Self::Ipv4 => 1, + Self::Ipv6 => 2, + Self::TorV3 => 4, + Self::I2p => 5, + Self::Cjdns => 6, + Self::Unknown(v) => *v, + } + } +} + +impl_num!(NetworkType, u8); + +impl NetworkType { + /// Expected byte length for a known network type, or `None` + /// for unknown. + pub const fn expected_len(self) -> Option { + match self { + Self::Ipv4 => Some(4), + Self::Ipv6 => Some(16), + Self::TorV3 => Some(32), + Self::I2p => Some(32), + Self::Cjdns => Some(16), + Self::Unknown(_) => None, + } + } +} + +impl fmt::Display for NetworkType { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::Ipv4 => f.write_str("ipv4"), + Self::Ipv6 => f.write_str("ipv6"), + Self::TorV3 => f.write_str("torv3"), + Self::I2p => f.write_str("i2p"), + Self::Cjdns => f.write_str("cjdns"), + Self::Unknown(v) => write!(f, "unknown({v})"), + } + } +} + +/// Network address validation error. +#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)] +pub enum NetAddrError { + /// Numeric value outside structural bounds. + BadRange { + /// The out-of-range value. + value: u8, + }, + /// Invalid character in encoded address. + BadChar { + /// The offending byte. + byte: u8, + }, + /// Checksum mismatch. + BadChecksum { + /// Expected checksum bytes. + expected: [u8; 2], + /// Actual checksum bytes. + actual: [u8; 2], + }, + /// Encoding error at a byte position. + BadEncode { + /// Byte offset of the error. + pos: usize, + }, + /// Unexpected address length. + BadLen { + /// Expected byte count. + expected: usize, + /// Actual byte count. + actual: usize, + }, + /// Unsupported address version byte. + BadVersion { + /// The version byte. + version: u8, + }, + /// Port outside u16 range or zero. + BadPort { + /// The invalid port value. + port: u16, + }, + /// Address type not representable in target format. + AddrTooNew { + /// The incompatible network type. + network: NetworkType, + }, +} + +impl fmt::Display for NetAddrError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::BadRange { value } => { + write!(f, "value 0x{value:02x} out of range") + } + Self::BadChar { byte } => { + write!(f, "invalid character 0x{byte:02x}") + } + Self::BadChecksum { expected, actual } => { + write!( + f, + "checksum mismatch: expected {:02x}{:02x}, got {:02x}{:02x}", + expected[0], expected[1], actual[0], actual[1] + ) + } + Self::BadEncode { pos } => { + write!(f, "encoding error at position {pos}") + } + Self::BadLen { expected, actual } => { + write!(f, "expected {expected} bytes, got {actual}") + } + Self::BadVersion { version } => { + write!(f, "unsupported version {version}") + } + Self::BadPort { port } => { + write!(f, "invalid port {port}") + } + Self::AddrTooNew { network } => { + write!(f, "{network} address type not supported") + } + } + } +} + +#[cfg(feature = "std")] +impl std::error::Error for NetAddrError {} + +/// Returns `true` when the port is on the blocklist. +pub fn is_bad_port(port: u16) -> bool { + if (1..=1023).contains(&port) { + return true; + } + matches!( + port, + 1719 // h323gatestat + | 1720 // h323hostcall + | 1723 // pptp + | 2049 // nfs + | 3659 // apple-sasl / PasswordServer + | 4045 // lockd + | 5060 // sip + | 5061 // sips + | 6000 // X11 + | 6566 // sane-port + | 6665 // alternate IRC + | 6666 // alternate IRC + | 6667 // standard IRC + | 6668 // alternate IRC + | 6669 // alternate IRC + | 6697 // IRC + TLS + | 8332 // Bitcoin JSON-RPC + | 8333 // Bitcoin P2P + | 10080 // Amanda + | 18332 // Bitcoin testnet RPC + | 18333 // Bitcoin testnet P2P + ) +} + +/// Shared classification interface for network addresses. +/// +/// Both legacy (`AddrV1`) and modern (`AddrV2`) address types +/// implement this trait, providing uniform RFC classification. +pub trait NetAddr { + /// Raw address bytes (4 for IPv4, 16 for IPv6, etc.). + fn bytes(&self) -> &[u8]; + + /// BIP155 network type. + fn network(&self) -> NetworkType; + + /// Returns `true` for IPv4 addresses. + fn is_ipv4(&self) -> bool; + + /// Returns `true` for IPv6 addresses. + fn is_ipv6(&self) -> bool; + + /// Returns `true` when every address byte is zero. + fn is_null(&self) -> bool; + + /// Returns `true` for Tor v3 addresses. + fn is_tor(&self) -> bool { + false + } + + /// Returns `true` for I2P addresses. + fn is_i2p(&self) -> bool { + false + } + + /// Returns `true` for CJDNS addresses. + fn is_cjdns(&self) -> bool { + false + } + + /// Returns `true` for privacy networks (Tor, I2P, CJDNS). + fn is_privacy_net(&self) -> bool { + self.is_tor() || self.is_i2p() || self.is_cjdns() + } + + /// Returns `true` when the address fits in an ADDRv1 message. + fn is_v1_compatible(&self) -> bool { + self.is_ipv4() || self.is_ipv6() + } + + /// Returns `true` for loopback and link-local addresses. + fn is_local(&self) -> bool { + let b = self.bytes(); + if self.is_ipv4() && b.len() == 4 { + // 127.0.0.0/8 + if b[0] == 127 { + return true; + } + // RFC 3927 link-local 169.254.0.0/16 + if b[0] == 169 && b[1] == 254 { + return true; + } + return false; + } + if self.is_ipv6() && b.len() == 16 { + // ::1 + if b[..15] == [0; 15] && b[15] == 1 { + return true; + } + // fe80::/10 link-local + if b[0] == 0xfe && (b[1] & 0xc0) == 0x80 { + return true; + } + return false; + } + false + } + + /// RFC 1918: private IPv4 (10/8, 172.16/12, 192.168/16). + fn is_rfc1918(&self) -> bool { + if !self.is_ipv4() { + return false; + } + let b = self.bytes(); + if b.len() != 4 { + return false; + } + // 10.0.0.0/8 + if b[0] == 10 { + return true; + } + // 172.16.0.0/12 + if b[0] == 172 && (b[1] & 0xf0) == 16 { + return true; + } + // 192.168.0.0/16 + b[0] == 192 && b[1] == 168 + } + + /// RFC 2544: benchmarking (198.18.0.0/15). + fn is_rfc2544(&self) -> bool { + if !self.is_ipv4() { + return false; + } + let b = self.bytes(); + b.len() == 4 && b[0] == 198 && (b[1] == 18 || b[1] == 19) + } + + /// RFC 3849: documentation IPv6 (2001:db8::/32). + fn is_rfc3849(&self) -> bool { + if !self.is_ipv6() { + return false; + } + let b = self.bytes(); + b.len() == 16 && b[0] == 0x20 && b[1] == 0x01 && b[2] == 0x0d && b[3] == 0xb8 + } + + /// RFC 3927: link-local IPv4 (169.254.0.0/16). + fn is_rfc3927(&self) -> bool { + if !self.is_ipv4() { + return false; + } + let b = self.bytes(); + b.len() == 4 && b[0] == 169 && b[1] == 254 + } + + /// RFC 4193: unique-local IPv6 (fc00::/7). + fn is_rfc4193(&self) -> bool { + if !self.is_ipv6() { + return false; + } + let b = self.bytes(); + b.len() == 16 && (b[0] & 0xfe) == 0xfc + } + + /// RFC 4843: ORCHID IPv6 (2001:10::/28). + fn is_rfc4843(&self) -> bool { + if !self.is_ipv6() { + return false; + } + let b = self.bytes(); + b.len() == 16 && b[0] == 0x20 && b[1] == 0x01 && b[2] == 0x00 && (b[3] & 0xf0) == 0x10 + } + + /// RFC 6052: well-known prefix (64:ff9b::/96). + fn is_rfc6052(&self) -> bool { + if !self.is_ipv6() { + return false; + } + let b = self.bytes(); + b.len() == 16 && b[0] == 0x00 && b[1] == 0x64 && b[2] == 0xff && b[3] == 0x9b && b[4..12] == [0; 8] + } + + /// RFC 6145: IPv6 translation (::ffff:0:0:0/96). + fn is_rfc6145(&self) -> bool { + if !self.is_ipv6() { + return false; + } + let b = self.bytes(); + b.len() == 16 && b[0..8] == [0; 8] && b[8] == 0xff && b[9] == 0xff && b[10] == 0x00 && b[11] == 0x00 + } + + /// RFC 6598: carrier-grade NAT (100.64.0.0/10). + fn is_rfc6598(&self) -> bool { + if !self.is_ipv4() { + return false; + } + let b = self.bytes(); + b.len() == 4 && b[0] == 100 && (b[1] & 0xc0) == 64 + } + + /// Returns `true` when the address is globally routable. + fn is_routable(&self) -> bool { + if self.is_null() { + return false; + } + if self.is_local() { + return false; + } + if self.is_privacy_net() { + return true; + } + if self.is_ipv4() { + let b = self.bytes(); + if b.len() == 4 && (b[0] == 0 || b[0] >= 224) { + return false; + } + return !self.is_rfc1918() && !self.is_rfc2544() && !self.is_rfc3927() && !self.is_rfc6598(); + } + if self.is_ipv6() { + let b = self.bytes(); + // ff00::/8 multicast + if b.len() == 16 && b[0] == 0xff { + return false; + } + return !self.is_rfc3849() + && !self.is_rfc4193() + && !self.is_rfc4843() + && !self.is_rfc6052() + && !self.is_rfc6145(); + } + false + } +} + +#[cfg(test)] +mod tests { + use super::*; + + use rstest::rstest; + + /// Minimal test adapter: 4-byte IPv4 address. + struct Ipv4([u8; 4]); + + impl NetAddr for Ipv4 { + fn bytes(&self) -> &[u8] { + &self.0 + } + fn network(&self) -> NetworkType { + NetworkType::Ipv4 + } + fn is_ipv4(&self) -> bool { + true + } + fn is_ipv6(&self) -> bool { + false + } + fn is_null(&self) -> bool { + self.0 == [0; 4] + } + } + + /// Minimal test adapter: 16-byte IPv6 address. + struct Ipv6([u8; 16]); + + impl NetAddr for Ipv6 { + fn bytes(&self) -> &[u8] { + &self.0 + } + fn network(&self) -> NetworkType { + NetworkType::Ipv6 + } + fn is_ipv4(&self) -> bool { + false + } + fn is_ipv6(&self) -> bool { + true + } + fn is_null(&self) -> bool { + self.0 == [0; 16] + } + } + + fn v4(a: u8, b: u8, c: u8, d: u8) -> Ipv4 { + Ipv4([a, b, c, d]) + } + + fn v6(bytes: [u8; 16]) -> Ipv6 { + Ipv6(bytes) + } + + #[rstest] + #[case::rfc1918_10(v4(10, 0, 0, 1), true)] + #[case::rfc1918_172(v4(172, 31, 255, 255), true)] + #[case::rfc1918_192(v4(192, 168, 1, 1), true)] + #[case::rfc1918_public(v4(8, 8, 8, 8), false)] + fn rfc1918(#[case] addr: Ipv4, #[case] expected: bool) { + assert_eq!(addr.is_rfc1918(), expected); + } + + #[rstest] + #[case::rfc2544_lo(v4(198, 18, 0, 0), true)] + #[case::rfc2544_hi(v4(198, 19, 255, 255), true)] + #[case::rfc2544_below(v4(198, 17, 0, 0), false)] + fn rfc2544(#[case] addr: Ipv4, #[case] expected: bool) { + assert_eq!(addr.is_rfc2544(), expected); + } + + #[rstest] + fn rfc3849() { + let mut b = [0u8; 16]; + b[0] = 0x20; + b[1] = 0x01; + b[2] = 0x0d; + b[3] = 0xb8; + assert!(v6(b).is_rfc3849()); + } + + #[rstest] + #[case::rfc3927_yes(v4(169, 254, 1, 1), true)] + #[case::rfc3927_no(v4(169, 253, 1, 1), false)] + fn rfc3927(#[case] addr: Ipv4, #[case] expected: bool) { + assert_eq!(addr.is_rfc3927(), expected); + } + + #[rstest] + fn rfc4193() { + let mut b = [0u8; 16]; + b[0] = 0xfc; + assert!(v6(b).is_rfc4193()); + b[0] = 0xfd; + assert!(v6(b).is_rfc4193()); + b[0] = 0xfe; + assert!(!v6(b).is_rfc4193()); + } + + #[rstest] + fn rfc4843() { + let mut b = [0u8; 16]; + b[0] = 0x20; + b[1] = 0x01; + b[2] = 0x00; + b[3] = 0x10; + assert!(v6(b).is_rfc4843()); + b[3] = 0x1f; + assert!(v6(b).is_rfc4843()); + b[3] = 0x20; + assert!(!v6(b).is_rfc4843()); + } + + #[rstest] + fn rfc6052() { + let mut b = [0u8; 16]; + b[0] = 0x00; + b[1] = 0x64; + b[2] = 0xff; + b[3] = 0x9b; + assert!(v6(b).is_rfc6052()); + b[4] = 1; + assert!(!v6(b).is_rfc6052()); + } + + #[rstest] + fn rfc6145() { + let mut b = [0u8; 16]; + b[8] = 0xff; + b[9] = 0xff; + assert!(v6(b).is_rfc6145()); + b[0] = 1; + assert!(!v6(b).is_rfc6145()); + } + + #[rstest] + #[case::rfc6598_lo(v4(100, 64, 0, 0), true)] + #[case::rfc6598_hi(v4(100, 127, 255, 255), true)] + #[case::rfc6598_below(v4(100, 63, 0, 0), false)] + fn rfc6598(#[case] addr: Ipv4, #[case] expected: bool) { + assert_eq!(addr.is_rfc6598(), expected); + } + + #[rstest] + #[case::local_v4(v4(127, 0, 0, 1), true)] + #[case::local_link(v4(169, 254, 0, 1), true)] + #[case::local_public(v4(8, 8, 8, 8), false)] + fn local_v4(#[case] addr: Ipv4, #[case] expected: bool) { + assert_eq!(addr.is_local(), expected); + } + + #[rstest] + fn local_v6() { + let mut loopback = [0u8; 16]; + loopback[15] = 1; + assert!(v6(loopback).is_local()); + + let mut link_local = [0u8; 16]; + link_local[0] = 0xfe; + link_local[1] = 0x80; + assert!(v6(link_local).is_local()); + + let mut global = [0u8; 16]; + global[0] = 0x20; + global[1] = 0x01; + global[15] = 1; + assert!(!v6(global).is_local()); + } + + #[rstest] + #[case::routable_v4(v4(8, 8, 8, 8), true)] + #[case::not_routable_loopback(v4(127, 0, 0, 1), false)] + #[case::not_routable_private(v4(10, 0, 0, 1), false)] + #[case::not_routable_null(v4(0, 0, 0, 0), false)] + #[case::not_routable_multicast(v4(224, 0, 0, 1), false)] + #[case::not_routable_multicast_hi(v4(239, 255, 255, 255), false)] + fn routable_v4(#[case] addr: Ipv4, #[case] expected: bool) { + assert_eq!(addr.is_routable(), expected); + } + + #[rstest] + fn routable_v6() { + let mut global = [0u8; 16]; + global[0] = 0x20; + global[1] = 0x01; + global[15] = 1; + assert!(v6(global).is_routable()); + + let mut doc = [0u8; 16]; + doc[0] = 0x20; + doc[1] = 0x01; + doc[2] = 0x0d; + doc[3] = 0xb8; + doc[15] = 1; + assert!(!v6(doc).is_routable()); + + let mut mcast = [0u8; 16]; + mcast[0] = 0xff; + mcast[1] = 0x02; + mcast[15] = 1; + assert!(!v6(mcast).is_routable()); + } + + #[rstest] + fn null() { + assert!(v4(0, 0, 0, 0).is_null()); + assert!(!v4(1, 2, 3, 4).is_null()); + assert!(v6([0; 16]).is_null()); + let mut nonzero = [0u8; 16]; + nonzero[15] = 1; + assert!(!v6(nonzero).is_null()); + } + + #[rstest] + fn invariant_rfc1918_implies_ipv4() { + let addr = v4(10, 0, 0, 1); + if addr.is_rfc1918() { + assert!(addr.is_ipv4()); + } + } + + #[rstest] + fn invariant_rfc3849_implies_ipv6() { + let mut b = [0u8; 16]; + b[0] = 0x20; + b[1] = 0x01; + b[2] = 0x0d; + b[3] = 0xb8; + let addr = v6(b); + if addr.is_rfc3849() { + assert!(addr.is_ipv6()); + } + } + + #[rstest] + fn invariant_rfc4193_implies_ipv6() { + let mut b = [0u8; 16]; + b[0] = 0xfc; + let addr = v6(b); + if addr.is_rfc4193() { + assert!(addr.is_ipv6()); + } + } + + #[rstest] + fn invariant_local_implies_not_routable() { + let addr = v4(127, 0, 0, 1); + if addr.is_local() { + assert!(!addr.is_routable()); + } + } +} diff --git a/pkgs/primitives/src/types/netinfo.rs b/pkgs/primitives/src/types/netinfo.rs index 0e4f3db3..9d3fa545 100644 --- a/pkgs/primitives/src/types/netinfo.rs +++ b/pkgs/primitives/src/types/netinfo.rs @@ -4,24 +4,53 @@ // See the accompanying file LICENSE or https://opensource.org/license/MIT // -//! Extended network info types for v3+ provider transactions. +//! Network information types and trait. -use super::ServiceV1; +use super::netaddr::{is_bad_port, NetAddr}; +use super::{AddrV2, NetAddrError, ServiceV1, ServiceV2}; use crate::prelude::*; -use dash_types::codec::{self, BaseCodec, DecodeError, NumCodec}; +use dash_types::codec::{self, BaseCodec, Checkable, DecodeError, NumCodec}; use dash_types::{impl_num, impl_type}; use core::fmt; /// Maximum entries per purpose. -const MAX_ENTRIES: usize = 8; -/// Maximum number of purpose groups. -const MAX_PURPOSES: usize = 8; +const MAX_ENTRIES: usize = 4; +/// Maximum label length per RFC 1035. +const DOMAIN_LABEL_MAX: usize = 63; +/// Maximum FQDN length. +const DOMAIN_MAX: usize = 253; +/// Minimum FQDN length. +const DOMAIN_MIN: usize = 3; + +/// Reserved and privacy TLDs that must be rejected. +const TLDS_BAD: &[&str] = &[ + // ICANN resolution 2018.02.04.12 + ".mail", + // Infrastructure TLD + ".arpa", + // RFC 6761 + ".example", + ".invalid", + ".localhost", + ".test", + // RFC 6762 + ".local", + // RFC 6762, appendix G + ".corp", + ".home", + ".internal", + ".intranet", + ".lan", + ".private", +]; +/// Privacy-network TLDs that must be rejected. +const TLDS_PRIVACY: &[&str] = &[".i2p", ".onion"]; /// Purpose tag for an extended network info entry. #[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)] -pub enum NetInfoPurpose { +pub enum NIPurpose { /// Core P2P port. CoreP2p, /// Platform P2P port. @@ -32,7 +61,7 @@ pub enum NetInfoPurpose { Unknown(u8), } -impl NumCodec for NetInfoPurpose { +impl NumCodec for NIPurpose { fn from_base(val: u8) -> Self { match val { 0 => Self::CoreP2p, @@ -52,9 +81,9 @@ impl NumCodec for NetInfoPurpose { } } -impl_num!(NetInfoPurpose, u8); +impl_num!(NIPurpose, u8); -impl fmt::Display for NetInfoPurpose { +impl fmt::Display for NIPurpose { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { Self::CoreP2p => write!(f, "core_p2p"), @@ -65,13 +94,105 @@ impl fmt::Display for NetInfoPurpose { } } +/// Type tag for an extended network info entry. +#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)] +pub enum NIEntryCode { + /// BIP155 address + port. + Service, + /// Domain name + port. + Domain, + /// Unrecognized entry type code. + Unknown(u8), +} + +impl NumCodec for NIEntryCode { + fn from_base(val: u8) -> Self { + match val { + 0x01 => Self::Service, + 0x02 => Self::Domain, + other => Self::Unknown(other), + } + } + + fn to_base(&self) -> u8 { + match self { + Self::Service => 0x01, + Self::Domain => 0x02, + Self::Unknown(v) => *v, + } + } +} + +impl_num!(NIEntryCode, u8); + +impl fmt::Display for NIEntryCode { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::Service => write!(f, "service"), + Self::Domain => write!(f, "domain"), + Self::Unknown(v) => write!(f, "unknown({v})"), + } + } +} + +/// Network info validation error. +#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)] +pub enum NIError { + /// Address failed validation. + BadAddr { + /// The underlying address error. + error: NetAddrError, + }, + /// Port is zero or invalid for context. + BadPort { + /// The invalid port value. + port: u16, + }, + /// Entry or address type not valid for this purpose. + BadType { + /// The offending entry type byte. + entry_type: u8, + }, + /// Duplicate address:port within the structure. + Duplicate, + /// Too many entries or purpose groups. + MaxLimit { + /// Actual count. + count: usize, + /// Maximum allowed. + max: usize, + }, + /// Structural integrity violation. + Malformed, +} + +impl fmt::Display for NIError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::BadAddr { error } => write!(f, "invalid address: {error}"), + Self::BadPort { port } => write!(f, "invalid port {port}"), + Self::BadType { entry_type } => { + write!(f, "unsupported entry type {entry_type}") + } + Self::Duplicate => f.write_str("duplicate entry"), + Self::MaxLimit { count, max } => { + write!(f, "too many entries: {count} exceeds limit {max}") + } + Self::Malformed => f.write_str("malformed structure"), + } + } +} + +#[cfg(feature = "std")] +impl std::error::Error for NIError {} + /// A single network info entry within a purpose group. #[derive(Clone, Debug, Eq, Hash, PartialEq)] #[cfg_attr(feature = "serde", derive(::serde::Serialize, ::serde::Deserialize))] #[cfg_attr(feature = "serde", serde(rename_all = "camelCase"))] -pub enum NetInfoEntry { - /// ADDRv1-style IP + port. - Service(ServiceV1), +pub enum NIEntry { + /// BIP155 address + port. + Service(ServiceV2), /// Domain name + port. Domain { /// The domain name as raw bytes. @@ -80,8 +201,135 @@ pub enum NetInfoEntry { /// Network port (big-endian on wire). port: u16, }, - /// Invalid / placeholder entry. - Invalid, +} + +impl BaseCodec for NIEntry { + fn decode(data: &mut &[u8]) -> Result { + match NIEntryCode::from_base(u8::decode(data)?) { + NIEntryCode::Service => Ok(Self::Service(ServiceV2::decode(data)?)), + NIEntryCode::Domain => { + let name_len = codec::read_compact_size(data, data.len())?; + let name = codec::read_bytes(data, name_len)?.to_vec(); + let port = codec::read_u16_be(data)?; + Ok(Self::Domain { name, port }) + } + NIEntryCode::Unknown(t) => Err(DecodeError::InvalidValue { + expected: NIEntryCode::Service.to_base() as u64, + actual: u64::from(t), + }), + } + } + + fn encode(&self, buf: &mut Vec) { + match self { + Self::Service(svc) => { + NIEntryCode::Service.to_base().encode(buf); + svc.encode(buf); + } + Self::Domain { name, port } => { + NIEntryCode::Domain.to_base().encode(buf); + name.encode(buf); + buf.extend_from_slice(&port.to_be_bytes()); + } + } + } +} + +/// Validates a domain name per RFC 1035 consensus rules. +fn check_domain(name: &[u8]) -> Option { + let s = match core::str::from_utf8(name) { + Ok(s) => s, + Err(_) => return Some(NIError::Malformed), + }; + if s.len() < DOMAIN_MIN || s.len() > DOMAIN_MAX { + return Some(NIError::Malformed); + } + if s.bytes().any(|b| b.is_ascii_uppercase()) { + return Some(NIError::Malformed); + } + if !s.bytes().all(|b| b.is_ascii_alphanumeric() || b == b'.' || b == b'-') { + return Some(NIError::Malformed); + } + if s.as_bytes()[0] == b'.' || s.as_bytes()[s.len() - 1] == b'.' { + return Some(NIError::Malformed); + } + let mut label_count = 0usize; + for label in s.split('.') { + if label.is_empty() || label.len() > DOMAIN_LABEL_MAX { + return Some(NIError::Malformed); + } + if label.as_bytes()[0] == b'-' || label.as_bytes()[label.len() - 1] == b'-' { + return Some(NIError::Malformed); + } + label_count += 1; + } + if label_count < 2 { + return Some(NIError::Malformed); + } + // Reject reserved and privacy TLDs. + if TLDS_BAD.iter().chain(TLDS_PRIVACY.iter()).any(|tld| s.ends_with(tld)) { + return Some(NIError::Malformed); + } + // TLD must be purely alphabetic (ICANN guideline). + let last_label = s.rsplit('.').next().unwrap_or(""); + if !last_label.bytes().all(|b| b.is_ascii_lowercase()) { + return Some(NIError::Malformed); + } + None +} + +impl Checkable for NIEntry { + type Error = NIError; + + fn check(&self) -> Option { + match self { + Self::Service(svc) => { + if let Some(error) = svc.check() { + return Some(NIError::BadAddr { error }); + } + if !svc.addr.is_i2p() && is_bad_port(svc.port) { + return Some(NIError::BadPort { port: svc.port }); + } + None + } + Self::Domain { name, port } => { + if *port == 0 || (is_bad_port(*port) && *port != 443) { + return Some(NIError::BadPort { port: *port }); + } + check_domain(name) + } + } + } +} + +impl fmt::Display for NIEntry { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::Service(svc) => write!(f, "{svc}"), + Self::Domain { name, port } => { + let s = core::str::from_utf8(name).unwrap_or(""); + write!(f, "{s}:{port}") + } + } + } +} + +/// Interface for network information types. +pub trait NITrait: fmt::Display { + /// Returns entries, optionally filtered by purpose. + fn entries(&self, purpose: Option) -> impl Iterator + '_; + + /// Returns the primary service if available. + fn primary(&self) -> Option; + + /// Returns `true` when this value carries no addresses. + fn is_empty(&self) -> bool; + + /// Returns `true` if entries exist for the given purpose. + fn has_entries(&self, purpose: NIPurpose) -> bool; + + /// Returns `true` when this type can carry platform addresses. + fn stores_platform(&self) -> bool; } /// Extended network info for v3+ ProRegTx / ProUpServTx. @@ -91,36 +339,26 @@ pub enum NetInfoEntry { #[derive(Clone, Debug, Eq, Hash, PartialEq)] #[cfg_attr(feature = "serde", derive(::serde::Serialize, ::serde::Deserialize))] #[cfg_attr(feature = "serde", serde(rename_all = "camelCase"))] -pub struct ExtendedNetInfo { +pub struct NetInfoV2 { /// Format version. pub version: u8, /// Purpose-grouped entries. - pub entries: Vec<(NetInfoPurpose, Vec)>, + pub entries: Vec<(NIPurpose, Vec)>, } -impl_type!(ExtendedNetInfo); +impl_type!(NetInfoV2); -impl BaseCodec for ExtendedNetInfo { +impl BaseCodec for NetInfoV2 { fn decode(data: &mut &[u8]) -> Result { let version = u8::decode(data)?; - let purpose_count = codec::read_compact_size(data, MAX_PURPOSES)?; + let purpose_count = codec::read_compact_size(data, data.len())?; let mut entries = Vec::with_capacity(purpose_count); for _ in 0..purpose_count { - let purpose = NetInfoPurpose::from_base(u8::decode(data)?); - let entry_count = codec::read_compact_size(data, MAX_ENTRIES)?; + let purpose = NIPurpose::from_base(u8::decode(data)?); + let entry_count = codec::read_compact_size(data, data.len())?; let mut group = Vec::with_capacity(entry_count); for _ in 0..entry_count { - let entry_type = u8::decode(data)?; - let entry = match entry_type { - 0x01 => NetInfoEntry::Service(ServiceV1::decode(data)?), - 0x02 => { - let name: Vec = Vec::decode(data)?; - let port = codec::read_u16_be(data)?; - NetInfoEntry::Domain { name, port } - } - _ => NetInfoEntry::Invalid, - }; - group.push(entry); + group.push(NIEntry::decode(data)?); } entries.push((purpose, group)); } @@ -132,23 +370,223 @@ impl BaseCodec for ExtendedNetInfo { codec::write_compact_size(self.entries.len(), buf); for (purpose, group) in &self.entries { purpose.to_base().encode(buf); - let valid_count = group.iter().filter(|e| !matches!(e, NetInfoEntry::Invalid)).count(); - codec::write_compact_size(valid_count, buf); + codec::write_compact_size(group.len(), buf); for entry in group { - match entry { - NetInfoEntry::Service(svc) => { - 0x01u8.encode(buf); - svc.encode(buf); + entry.encode(buf); + } + } + } +} + +impl Checkable for NetInfoV2 { + type Error = NIError; + + fn check(&self) -> Option { + if self.version == 0 || self.version > Self::CURRENT_VERSION { + return Some(NIError::Malformed); + } + if self.entries.is_empty() { + return Some(NIError::Malformed); + } + // Duplicate purpose key detection. + for i in 0..self.entries.len() { + for j in (i + 1)..self.entries.len() { + if self.entries[i].0 == self.entries[j].0 { + return Some(NIError::Duplicate); + } + } + } + // addr:port duplicates across all entries + let all: Vec<&NIEntry> = self.entries.iter().flat_map(|(_, g)| g.iter()).collect(); + for i in 0..all.len() { + for j in (i + 1)..all.len() { + if all[i] == all[j] { + return Some(NIError::Duplicate); + } + } + } + for (purpose, group) in &self.entries { + if matches!(purpose, NIPurpose::Unknown(_)) { + return Some(NIError::Malformed); + } + if group.is_empty() { + return Some(NIError::Malformed); + } + if group.len() > MAX_ENTRIES { + return Some(NIError::MaxLimit { + count: group.len(), + max: MAX_ENTRIES, + }); + } + // addr-only duplicates within purpose group + for i in 0..group.len() { + for j in (i + 1)..group.len() { + if same_addr(&group[i], &group[j]) { + return Some(NIError::Duplicate); } - NetInfoEntry::Domain { name, port } => { - 0x02u8.encode(buf); - name.encode(buf); - buf.extend_from_slice(&port.to_be_bytes()); + } + } + for entry in group { + if let NIEntry::Service(svc) = entry { + if matches!(svc.addr, AddrV2::Unknown { .. }) { + return Some(NIError::BadType { + entry_type: svc.addr.network().to_base(), + }); } - NetInfoEntry::Invalid => {} } + if matches!(entry, NIEntry::Domain { .. }) && *purpose != NIPurpose::PlatformHttps { + return Some(NIError::BadType { entry_type: 0x02 }); + } + if let Some(e) = entry.check() { + return Some(e); + } + } + } + None + } +} + +impl NetInfoV2 { + /// Highest supported format version. + const CURRENT_VERSION: u8 = 1; +} + +impl fmt::Display for NetInfoV2 { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + if self.entries.is_empty() { + return f.write_str("NetInfoV2()"); + } + f.write_str("NetInfoV2(")?; + for (i, (purpose, group)) in self.entries.iter().enumerate() { + if i > 0 { + f.write_str(", ")?; + } + write!(f, "{purpose}=[")?; + for (j, entry) in group.iter().enumerate() { + if j > 0 { + f.write_str(", ")?; + } + write!(f, "{entry}")?; + } + f.write_str("]")?; + } + f.write_str(")") + } +} + +impl NITrait for NetInfoV2 { + fn entries(&self, purpose: Option) -> impl Iterator + '_ { + self + .entries + .iter() + .filter(move |(pp, _)| purpose.is_none() || purpose == Some(*pp)) + .flat_map(|(_, group)| group.iter().cloned()) + } + + fn primary(&self) -> Option { + self + .entries + .iter() + .find(|(p, e)| *p == NIPurpose::CoreP2p && !e.is_empty()) + .and_then(|(_, entries)| { + entries.iter().find_map(|e| match e { + NIEntry::Service(svc) => Some(svc.clone()), + _ => None, + }) + }) + } + + fn is_empty(&self) -> bool { + self.entries.iter().all(|(_, group)| group.is_empty()) + } + + fn has_entries(&self, purpose: NIPurpose) -> bool { + self.entries.iter().any(|(p, e)| *p == purpose && !e.is_empty()) + } + + fn stores_platform(&self) -> bool { + true + } +} + +/// Returns `true` when two entries share the same address, +/// ignoring port. +fn same_addr(a: &NIEntry, b: &NIEntry) -> bool { + match (a, b) { + (NIEntry::Service(sa), NIEntry::Service(sb)) => sa.addr == sb.addr, + (NIEntry::Domain { name: na, .. }, NIEntry::Domain { name: nb, .. }) => na == nb, + _ => false, + } +} + +/// Legacy network information wrapper. +#[derive(Clone, Debug, Eq, Hash, PartialEq)] +#[cfg_attr(feature = "serde", derive(::serde::Serialize, ::serde::Deserialize))] +#[cfg_attr(feature = "serde", serde(transparent))] +pub struct NetInfoV1(pub ServiceV1); + +impl_type!(NetInfoV1); + +impl BaseCodec for NetInfoV1 { + fn decode(data: &mut &[u8]) -> Result { + Ok(Self(ServiceV1::decode(data)?)) + } + + fn encode(&self, buf: &mut Vec) { + self.0.encode(buf); + } +} + +impl Checkable for NetInfoV1 { + type Error = NIError; + + fn check(&self) -> Option { + if let Some(error) = self.0.check() { + return Some(NIError::BadAddr { error }); + } + None + } +} + +impl fmt::Display for NetInfoV1 { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + if self.0.addr.is_null() && self.0.port == 0 { + return f.write_str("NetInfoV1()"); + } + write!(f, "NetInfoV1({})", self.0) + } +} + +impl NITrait for NetInfoV1 { + fn entries(&self, purpose: Option) -> impl Iterator + '_ { + let entry = if self.is_empty() { + None + } else { + match purpose { + None | Some(NIPurpose::CoreP2p) => Some(NIEntry::Service(ServiceV2::from(&self.0))), + Some(_) => None, } + }; + entry.into_iter() + } + + fn primary(&self) -> Option { + if self.is_empty() { + return None; } + Some(ServiceV2::from(&self.0)) + } + + fn is_empty(&self) -> bool { + self.0.addr.is_null() && self.0.port == 0 + } + + fn has_entries(&self, purpose: NIPurpose) -> bool { + purpose == NIPurpose::CoreP2p && !self.is_empty() + } + + fn stores_platform(&self) -> bool { + false } } @@ -157,8 +595,480 @@ impl BaseCodec for ExtendedNetInfo { #[cfg_attr(feature = "serde", derive(::serde::Serialize, ::serde::Deserialize))] #[cfg_attr(feature = "serde", serde(rename_all = "camelCase"))] pub enum NetInfo { - /// ADDRv1 ServiceV1 (18 bytes). - Legacy(ServiceV1), + /// ADDRv1 service (18 bytes). + Legacy(NetInfoV1), /// Extended format (v3+) with purpose-grouped entries. - Extended(ExtendedNetInfo), + Extended(NetInfoV2), +} + +impl Checkable for NetInfo { + type Error = NIError; + + fn check(&self) -> Option { + match self { + Self::Legacy(v1) => v1.check(), + Self::Extended(v2) => v2.check(), + } + } +} + +impl fmt::Display for NetInfo { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::Legacy(v1) => v1.fmt(f), + Self::Extended(v2) => v2.fmt(f), + } + } +} + +impl NITrait for NetInfo { + fn entries(&self, purpose: Option) -> impl Iterator + '_ { + let (a, b) = match self { + Self::Legacy(v1) => (Some(v1.entries(purpose)), None), + Self::Extended(v2) => (None, Some(v2.entries(purpose))), + }; + a.into_iter().flatten().chain(b.into_iter().flatten()) + } + + fn primary(&self) -> Option { + match self { + Self::Legacy(v1) => v1.primary(), + Self::Extended(v2) => v2.primary(), + } + } + + fn is_empty(&self) -> bool { + match self { + Self::Legacy(v1) => v1.is_empty(), + Self::Extended(v2) => v2.is_empty(), + } + } + + fn has_entries(&self, purpose: NIPurpose) -> bool { + match self { + Self::Legacy(v1) => v1.has_entries(purpose), + Self::Extended(v2) => v2.has_entries(purpose), + } + } + + fn stores_platform(&self) -> bool { + match self { + Self::Legacy(v1) => v1.stores_platform(), + Self::Extended(v2) => v2.stores_platform(), + } + } +} + +#[cfg(test)] +#[expect(clippy::unwrap_used, reason = "test code")] +mod tests { + use super::*; + use crate::types::{AddrV1, AddrV2}; + + use dash_types::codec::{BaseCodec, Checkable}; + use hex_literal::hex; + use rstest::rstest; + + #[rstest] + #[case::ipv4( + &hex!( + "01" // entry_type=Service + "01" // network=ipv4 + "04" // addr_len=4 + "01020304" // addr 1.2.3.4 + "270f" // port=9999 + ), + NIEntry::Service(ServiceV2 { addr: AddrV2::Ipv4([1, 2, 3, 4]), port: 9999 }), + )] + #[case::ipv6( + &hex!( + "01" // entry_type=Service + "02" // network=ipv6 + "10" // addr_len=16 + "00000000000000000000000000000001" // addr ::1 + "270f" // port=9999 + ), + NIEntry::Service(ServiceV2 { addr: AddrV2::Ipv6([0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1]), port: 9999 }), + )] + #[case::domain( + &hex!( + "02" // entry_type=Domain + "0b" // name_len=11 + "6578616d706c652e636f6d" // "example.com" + "01bb" // port=443 + ), + NIEntry::Domain { name: b"example.com".to_vec(), port: 443 }, + )] + fn nientry_roundtrip(#[case] wire: &[u8], #[case] expected: NIEntry) { + let decoded = NIEntry::decode(&mut &wire[..]).unwrap(); + assert_eq!(decoded, expected); + let mut buf = Vec::new(); + decoded.encode(&mut buf); + assert_eq!(buf, wire); + } + + #[rstest] + fn nientry_unknown_type_fails() { + let wire = hex!("ff"); + assert!(NIEntry::decode(&mut &wire[..]).is_err()); + } + + #[rstest] + #[case::single_ipv4( + &hex!( + "01" // version=1 + "01" // purpose_count=1 + "00" // purpose=CoreP2p + "01" // entry_count=1 + "01" // entry_type=Service + "0104 01020304" // ipv4 1.2.3.4 + "270f" // port=9999 + ), + NetInfoV2 { + version: 1, + entries: vec![( + NIPurpose::CoreP2p, + vec![NIEntry::Service(ServiceV2 { + addr: AddrV2::Ipv4([1, 2, 3, 4]), + port: 9999, + })], + )], + }, + )] + #[case::multi_purpose( + &hex!( + "01" // version=1 + "02" // purpose_count=2 + "00" // purpose=CoreP2p + "01" // entry_count=1 + "01" // entry_type=Service + "0104 c0a80001" // ipv4 192.168.0.1 + "238e" // port=9102 + "02" // purpose=PlatformHttps + "01" // entry_count=1 + "02" // entry_type=Domain + "0b6578616d706c652e636f6d" // "example.com" + "01bb" // port=443 + ), + NetInfoV2 { + version: 1, + entries: vec![ + ( + NIPurpose::CoreP2p, + vec![NIEntry::Service(ServiceV2 { + addr: AddrV2::Ipv4([192, 168, 0, 1]), + port: 9102, + })], + ), + ( + NIPurpose::PlatformHttps, + vec![NIEntry::Domain { + name: b"example.com".to_vec(), + port: 443, + }], + ), + ], + }, + )] + fn netinfov2_roundtrip(#[case] wire: &[u8], #[case] expected: NetInfoV2) { + let decoded = NetInfoV2::decode(&mut &wire[..]).unwrap(); + assert_eq!(decoded, expected); + let mut buf = Vec::new(); + decoded.encode(&mut buf); + assert_eq!(buf, wire); + } + + #[rstest] + #[case::ipv4_valid(AddrV2::Ipv4([1, 2, 3, 4]), 9999, None)] + #[case::ipv4_port_zero( + AddrV2::Ipv4([1, 2, 3, 4]), 0, + Some(NIError::BadAddr { error: NetAddrError::BadPort { port: 0 } }), + )] + #[case::ipv4_port_privileged(AddrV2::Ipv4([1, 2, 3, 4]), 22, Some(NIError::BadPort { port: 22 }))] + #[case::ipv4_port_named_bad(AddrV2::Ipv4([1, 2, 3, 4]), 8333, Some(NIError::BadPort { port: 8333 }))] + #[case::i2p_port_zero(AddrV2::I2p([1; 32]), 0, None)] + #[case::i2p_port_nonzero( + AddrV2::I2p([1; 32]), 9998, + Some(NIError::BadAddr { error: NetAddrError::BadPort { port: 9998 } }), + )] + #[case::tor_port_zero( + AddrV2::TorV3([1; 32]), 0, + Some(NIError::BadAddr { error: NetAddrError::BadPort { port: 0 } }), + )] + #[case::tor_port_valid(AddrV2::TorV3([1; 32]), 9998, None)] + #[case::cjdns_valid(AddrV2::Cjdns(hex!("fc000000000000000000000000000001")), 9998, None)] + fn check_entry_service(#[case] addr: AddrV2, #[case] port: u16, #[case] expected: Option) { + let entry = NIEntry::Service(ServiceV2 { addr, port }); + assert_eq!(entry.check(), expected); + } + + #[rstest] + // Port rules + #[case::valid(b"example.com", 443, None)] + #[case::bad_port_zero(b"example.com", 0, Some(NIError::BadPort { port: 0 }))] + #[case::bad_port_privileged(b"example.com", 80, Some(NIError::BadPort { port: 80 }))] + #[case::port_443_exception(b"example.com", 443, None)] + #[case::port_above_threshold(b"example.com", 9999, None)] + // RFC 1035 syntax + #[case::small_label(b"r.server-1.ab.cd", 443, None)] + #[case::numeric_label_rfc1123(b"9998.9example7.ab", 443, None)] + #[case::uppercase(b"Example.com", 443, Some(NIError::Malformed))] + #[case::too_short(b"ab", 443, Some(NIError::Malformed))] + #[case::dotless(b"localhost", 443, Some(NIError::Malformed))] + #[case::leading_dot(b".abc.com", 443, Some(NIError::Malformed))] + #[case::trailing_dot(b"abc.com.", 443, Some(NIError::Malformed))] + #[case::empty_label(b"a..b.com", 443, Some(NIError::Malformed))] + #[case::leading_hyphen(b"-example.com", 443, Some(NIError::Malformed))] + #[case::trailing_hyphen(b"a-.bc.de", 443, Some(NIError::Malformed))] + #[case::bad_char_apostrophe(b"it's.example.com", 443, Some(NIError::Malformed))] + #[case::bad_char_space(b"some host.example.com", 443, Some(NIError::Malformed))] + // TLD rules + #[case::tld_local(b"host.local", 443, Some(NIError::Malformed))] + #[case::tld_onion(b"hidden.onion", 443, Some(NIError::Malformed))] + #[case::tld_test(b"host.test", 443, Some(NIError::Malformed))] + #[case::tld_i2p(b"host.i2p", 443, Some(NIError::Malformed))] + #[case::tld_arpa(b"host.arpa", 443, Some(NIError::Malformed))] + #[case::tld_numeric(b"example.123", 443, Some(NIError::Malformed))] + fn check_entry_domain(#[case] name: &[u8], #[case] port: u16, #[case] expected: Option) { + let entry = NIEntry::Domain { + name: name.to_vec(), + port, + }; + assert_eq!(entry.check(), expected); + } + + #[rstest] + fn check_domain_length_limits() { + // 63-char label is valid + let label63 = "a".repeat(63); + let valid_long_label = format!("{label63}.com"); + assert_eq!(check_domain(valid_long_label.as_bytes()), None,); + // 64-char label exceeds per-label maximum + let label64 = "a".repeat(64); + let bad_label = format!("{label64}.com"); + assert_eq!(check_domain(bad_label.as_bytes()), Some(NIError::Malformed),); + // 253-char FQDN is at the maximum limit + let fqdn253 = format!( + "{}.{}.{}.{}.ab", + "a".repeat(63), + "b".repeat(63), + "c".repeat(63), + "d".repeat(58), + ); + assert_eq!(fqdn253.len(), 253); + assert_eq!(check_domain(fqdn253.as_bytes()), None); + // 254-char FQDN exceeds maximum + let fqdn254 = format!( + "{}.{}.{}.{}.abc", + "a".repeat(63), + "b".repeat(63), + "c".repeat(63), + "d".repeat(58), + ); + assert_eq!(fqdn254.len(), 254); + assert_eq!(check_domain(fqdn254.as_bytes()), Some(NIError::Malformed),); + } + + #[rstest] + #[case::valid("1.2.3.4", 9999, None)] + #[case::bad_port("1.2.3.4", 0, Some(NIError::BadAddr { error: NetAddrError::BadPort { port: 0 } }))] + #[case::null_addr_raw("[::0]", 9999, Some(NIError::BadAddr { error: NetAddrError::BadRange { value: 0 } }))] + #[case::ipv6_valid("[2001::1]", 9999, None)] + fn check_v1(#[case] addr_str: &str, #[case] port: u16, #[case] expected: Option) { + let addr: AddrV1 = addr_str.parse().unwrap(); + let v1 = NetInfoV1(ServiceV1 { addr, port }); + assert_eq!(v1.check(), expected); + } + + fn svc(addr: AddrV2, port: u16) -> NIEntry { + NIEntry::Service(ServiceV2 { addr, port }) + } + + fn dom(name: &[u8], port: u16) -> NIEntry { + NIEntry::Domain { + name: name.to_vec(), + port, + } + } + + fn valid_v2() -> NetInfoV2 { + NetInfoV2 { + version: 1, + entries: vec![(NIPurpose::CoreP2p, vec![svc(AddrV2::Ipv4([1, 2, 3, 4]), 9999)])], + } + } + + #[rstest] + #[case::zero(0, Some(NIError::Malformed))] + #[case::current(1, None)] + #[case::future(2, Some(NIError::Malformed))] + fn check_v2_version(#[case] version: u8, #[case] expected: Option) { + let mut v2 = valid_v2(); + v2.version = version; + assert_eq!(v2.check(), expected); + } + + #[rstest] + #[case::empty_group( + NIPurpose::CoreP2p, + vec![], + Some(NIError::Malformed), + )] + #[case::unknown_purpose( + NIPurpose::Unknown(99), + vec![svc(AddrV2::Ipv4([1, 2, 3, 4]), 9999)], + Some(NIError::Malformed), + )] + #[case::domain_wrong_purpose( + NIPurpose::CoreP2p, + vec![dom(b"example.com", 443)], + Some(NIError::BadType { entry_type: 0x02 }), + )] + #[case::unknown_network( + NIPurpose::CoreP2p, + vec![svc(AddrV2::Unknown { network: 99, addr: vec![1, 2] }, 9999)], + Some(NIError::BadType { entry_type: 99 }), + )] + #[case::delegates_entry_error( + NIPurpose::CoreP2p, + vec![svc(AddrV2::Ipv4([1, 2, 3, 4]), 0)], + Some(NIError::BadAddr { error: NetAddrError::BadPort { port: 0 } }), + )] + #[case::duplicate_addr_same_group( + NIPurpose::CoreP2p, + vec![svc(AddrV2::Ipv4([1, 2, 3, 4]), 9999), svc(AddrV2::Ipv4([1, 2, 3, 4]), 8888)], + Some(NIError::Duplicate), + )] + fn check_v2_group(#[case] purpose: NIPurpose, #[case] group: Vec, #[case] expected: Option) { + let v2 = NetInfoV2 { + version: 1, + entries: vec![(purpose, group)], + }; + assert_eq!(v2.check(), expected); + } + + #[rstest] + #[case::empty(vec![], Some(NIError::Malformed))] + #[case::duplicate_purpose_key( + vec![ + (NIPurpose::CoreP2p, vec![svc(AddrV2::Ipv4([10, 0, 0, 1]), 9999)]), + (NIPurpose::CoreP2p, vec![svc(AddrV2::Ipv4([10, 0, 0, 2]), 9999)]), + ], + Some(NIError::Duplicate), + )] + #[case::duplicate_addr_port_cross_group( + vec![ + (NIPurpose::CoreP2p, vec![svc(AddrV2::Ipv4([1, 2, 3, 4]), 9999)]), + (NIPurpose::PlatformP2p, vec![svc(AddrV2::Ipv4([1, 2, 3, 4]), 9999)]), + ], + Some(NIError::Duplicate), + )] + fn check_v2_structure(#[case] entries: Vec<(NIPurpose, Vec)>, #[case] expected: Option) { + let v2 = NetInfoV2 { version: 1, entries }; + assert_eq!(v2.check(), expected); + } + + #[rstest] + fn check_v2_too_many_entries() { + let v2 = NetInfoV2 { + version: 1, + entries: vec![( + NIPurpose::CoreP2p, + (0..MAX_ENTRIES + 1) + .map(|i| svc(AddrV2::Ipv4([10, 0, 0, i as u8 + 1]), 9999)) + .collect(), + )], + }; + assert_eq!( + v2.check(), + Some(NIError::MaxLimit { + count: MAX_ENTRIES + 1, + max: MAX_ENTRIES, + }) + ); + } + + #[rstest] + fn trait_v2_entries() { + let v2 = valid_v2(); + let all: Vec<_> = v2.entries(None).collect(); + assert_eq!(all.len(), 1); + let core: Vec<_> = v2.entries(Some(NIPurpose::CoreP2p)).collect(); + assert_eq!(core.len(), 1); + let plat: Vec<_> = v2.entries(Some(NIPurpose::PlatformP2p)).collect(); + assert!(plat.is_empty()); + } + + #[rstest] + fn trait_v2_primary() { + let v2 = valid_v2(); + let primary = v2.primary().unwrap(); + assert_eq!(primary.addr, AddrV2::Ipv4([1, 2, 3, 4])); + assert_eq!(primary.port, 9999); + } + + #[rstest] + fn trait_v2_empty() { + let empty = NetInfoV2 { + version: 1, + entries: vec![], + }; + assert!(empty.is_empty()); + assert!(!empty.has_entries(NIPurpose::CoreP2p)); + assert!(empty.primary().is_none()); + assert!(!valid_v2().is_empty()); + assert!(valid_v2().has_entries(NIPurpose::CoreP2p)); + assert!(valid_v2().stores_platform()); + } + + #[rstest] + fn trait_v1_entries() { + let v1 = NetInfoV1(ServiceV1 { + addr: "1.2.3.4".parse().unwrap(), + port: 9999, + }); + assert!(!v1.is_empty()); + assert!(v1.has_entries(NIPurpose::CoreP2p)); + assert!(!v1.has_entries(NIPurpose::PlatformP2p)); + assert!(!v1.stores_platform()); + let all: Vec<_> = v1.entries(None).collect(); + assert_eq!(all.len(), 1); + let plat: Vec<_> = v1.entries(Some(NIPurpose::PlatformP2p)).collect(); + assert!(plat.is_empty()); + assert!(v1.primary().is_some()); + } + + #[rstest] + fn trait_v1_empty() { + let empty = NetInfoV1(ServiceV1 { + addr: AddrV1::default(), + port: 0, + }); + assert!(empty.is_empty()); + assert!(!empty.has_entries(NIPurpose::CoreP2p)); + assert!(empty.primary().is_none()); + let all: Vec<_> = empty.entries(None).collect(); + assert!(all.is_empty()); + } + + #[rstest] + fn trait_dispatch_legacy() { + let v1 = NetInfo::Legacy(NetInfoV1(ServiceV1 { + addr: "1.2.3.4".parse().unwrap(), + port: 9999, + })); + assert!(!v1.is_empty()); + assert!(v1.has_entries(NIPurpose::CoreP2p)); + assert!(!v1.stores_platform()); + assert!(v1.primary().is_some()); + } + + #[rstest] + fn trait_dispatch_extended() { + let v2 = NetInfo::Extended(valid_v2()); + assert!(!v2.is_empty()); + assert!(v2.has_entries(NIPurpose::CoreP2p)); + assert!(v2.stores_platform()); + assert!(v2.primary().is_some()); + } } diff --git a/pkgs/primitives/src/types/util.rs b/pkgs/primitives/src/types/util.rs new file mode 100644 index 00000000..91b49f5e --- /dev/null +++ b/pkgs/primitives/src/types/util.rs @@ -0,0 +1,187 @@ +// +// Copyright (c) 2026-present, The Dash Core developers +// SPDX-License-Identifier: MIT +// See the accompanying file LICENSE or https://opensource.org/license/MIT +// + +//! Encoding helpers shared across address types. + +use super::netaddr::NetAddrError; +use crate::prelude::*; + +use core::fmt::{self, Write as _}; + +/// RFC 4648 base32 alphabet (lowercase). +const CHARSET_B32: &[u8; 32] = b"abcdefghijklmnopqrstuvwxyz234567"; + +/// Encodes `data` as RFC 4648 Base32 without padding. +pub(super) fn base32r_enc(data: &[u8], f: &mut fmt::Formatter<'_>) -> fmt::Result { + let mut bits: u32 = 0; + let mut n: u32 = 0; + for &b in data { + bits = (bits << 8) | u32::from(b); + n += 8; + while n >= 5 { + n -= 5; + let idx = ((bits >> n) & 0x1f) as usize; + f.write_char(char::from(CHARSET_B32[idx]))?; + } + } + if n > 0 { + let idx = ((bits << (5 - n)) & 0x1f) as usize; + f.write_char(char::from(CHARSET_B32[idx]))?; + } + Ok(()) +} + +/// Writes each byte as two lowercase hex digits. +pub(super) fn base16_enc(data: &[u8], f: &mut fmt::Formatter<'_>) -> fmt::Result { + for b in data { + write!(f, "{b:02x}")?; + } + Ok(()) +} + +/// Decodes an unpadded RFC 4648 Base32 string into `out`. +/// +/// # Errors +/// +/// Returns `BadChar` for non-base32 bytes, `BadLen` when the +/// decoded length does not match `out.len()`. +pub(super) fn base32r_dec(s: &str, out: &mut [u8]) -> Result<(), NetAddrError> { + let mut bits: u32 = 0; + let mut n: u32 = 0; + let mut pos = 0usize; + for &b in s.as_bytes() { + let val = match b { + b'a'..=b'z' => b - b'a', + b'2'..=b'7' => b - b'2' + 26, + _ => return Err(NetAddrError::BadChar { byte: b }), + }; + bits = (bits << 5) | u32::from(val); + n += 5; + if n >= 8 { + n -= 8; + if pos >= out.len() { + return Err(NetAddrError::BadLen { + expected: out.len(), + actual: pos + 1, + }); + } + out[pos] = (bits >> n) as u8; + pos += 1; + } + } + if pos != out.len() { + return Err(NetAddrError::BadLen { + expected: out.len(), + actual: pos, + }); + } + if n > 0 && (bits & ((1 << n) - 1)) != 0 { + return Err(NetAddrError::BadEncode { pos: s.len() - 1 }); + } + Ok(()) +} + +/// Returns the numeric value of a lowercase hex nibble. +fn hex_nibble(b: u8) -> Result { + match b { + b'0'..=b'9' => Ok(b - b'0'), + b'a'..=b'f' => Ok(b - b'a' + 10), + _ => Err(NetAddrError::BadChar { byte: b }), + } +} + +/// Decodes a lowercase hex string into a byte vector. +/// +/// # Errors +/// +/// Returns `BadChar` for non-hex bytes, `BadLen` for odd-length +/// input. +pub(super) fn base16_dec(s: &str) -> Result, NetAddrError> { + let bytes = s.as_bytes(); + if bytes.len() % 2 != 0 { + return Err(NetAddrError::BadLen { + expected: bytes.len() + 1, + actual: bytes.len(), + }); + } + let mut out = Vec::with_capacity(bytes.len() / 2); + for chunk in bytes.chunks(2) { + let hi = hex_nibble(chunk[0])?; + let lo = hex_nibble(chunk[1])?; + out.push((hi << 4) | lo); + } + Ok(out) +} + +#[cfg(test)] +#[expect(clippy::unwrap_used, reason = "test code")] +mod tests { + use super::*; + + use rstest::rstest; + + // RFC 4648, Section 10 test vectors (lowercase, no padding). + // https://datatracker.ietf.org/doc/html/rfc4648#section-10 + #[rstest] + #[case::f(b"f", "my")] + #[case::fo(b"fo", "mzxq")] + #[case::foo(b"foo", "mzxw6")] + #[case::foob(b"foob", "mzxw6yq")] + #[case::fooba(b"fooba", "mzxw6ytb")] + #[case::foobar(b"foobar", "mzxw6ytboi")] + fn base32_rfc4648(#[case] input: &[u8], #[case] expected: &str) { + let mut decoded = vec![0u8; input.len()]; + base32r_dec(expected, &mut decoded).unwrap(); + assert_eq!(decoded, input); + + struct Fmt<'a>(&'a [u8]); + impl fmt::Display for Fmt<'_> { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + base32r_enc(self.0, f) + } + } + assert_eq!(Fmt(input).to_string(), expected); + } + + // RFC 4648, Section 10 test vectors (lowercase). + #[rstest] + #[case::f(b"f", "66")] + #[case::fo(b"fo", "666f")] + #[case::foo(b"foo", "666f6f")] + #[case::foob(b"foob", "666f6f62")] + #[case::fooba(b"fooba", "666f6f6261")] + #[case::foobar(b"foobar", "666f6f626172")] + fn base16_rfc4648(#[case] input: &[u8], #[case] hex: &str) { + let decoded = base16_dec(hex).unwrap(); + assert_eq!(decoded, input); + } + + #[rstest] + fn base32_bad_char() { + let mut out = [0u8; 4]; + let err = base32r_dec("AAAA", &mut out).unwrap_err(); + assert_eq!(err, NetAddrError::BadChar { byte: b'A' }); + } + + #[rstest] + fn base32_wrong_length() { + let mut out = [0u8; 32]; + let err = base32r_dec("aa", &mut out).unwrap_err(); + assert!(matches!(err, NetAddrError::BadLen { .. })); + } + + #[rstest] + fn base32_trailing_bits() { + // "my" decodes to b"f" (0x66). "mz" would set trailing + // bits that don't map to a full byte -- must be rejected. + let mut out = [0u8; 1]; + assert!(base32r_dec("my", &mut out).is_ok()); + // 'm' = 12, 'z' = 25 -> bits = 12<<5|25 = 409 = 0b110011001 + // After extracting 8 bits (0x66), 1 trailing bit = 1 -> bad. + let err = base32r_dec("mz", &mut out).unwrap_err(); + assert!(matches!(err, NetAddrError::BadEncode { .. })); + } +} diff --git a/pkgs/types/src/lib.rs b/pkgs/types/src/lib.rs index 38e77d72..f5e1f976 100644 --- a/pkgs/types/src/lib.rs +++ b/pkgs/types/src/lib.rs @@ -30,3 +30,35 @@ pub mod __private { #[cfg(feature = "serde")] pub use hex_conservative; } + +/// Generates \`From\` + \`From<&T>\` (or \`TryFrom\` equivalents). +/// The closure body receives \`&$src\`; the owned impl delegates. +#[macro_export] +macro_rules! type_cvrt { + (From<$src:ty> for $dst:ty, |$v:ident| $body:expr) => { + impl From<&$src> for $dst { + fn from($v: &$src) -> Self { + $body + } + } + impl From<$src> for $dst { + fn from(v: $src) -> Self { + Self::from(&v) + } + } + }; + (TryFrom<$src:ty> for $dst:ty, $err:ty, |$v:ident| $body:expr) => { + impl TryFrom<&$src> for $dst { + type Error = $err; + fn try_from($v: &$src) -> Result { + $body + } + } + impl TryFrom<$src> for $dst { + type Error = $err; + fn try_from(v: $src) -> Result { + Self::try_from(&v) + } + } + }; +}