-
Notifications
You must be signed in to change notification settings - Fork 56
fix(swift-sdk): decode DIP-0018 platform addresses in address queries #4021
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -44,9 +44,9 @@ public struct PlatformAddressInfo: Sendable, Equatable, Codable { | |||||||||||||||||||||||||||||||||||||||
| /// Convert address bytes to bech32m string (requires network parameter) | ||||||||||||||||||||||||||||||||||||||||
| /// Format: [type_byte][20_byte_hash] | ||||||||||||||||||||||||||||||||||||||||
| public func toBech32m(network: Network) -> String? { | ||||||||||||||||||||||||||||||||||||||||
| guard addressBytes.count == 21 else { return nil } | ||||||||||||||||||||||||||||||||||||||||
| let hrp = network == .mainnet ? "dashevo" : "tdashevo" | ||||||||||||||||||||||||||||||||||||||||
| return Bech32m.encode(hrp: hrp, data: addressBytes) | ||||||||||||||||||||||||||||||||||||||||
| guard let payload = Bech32m.bech32mPayload(fromStorageBytes: addressBytes) else { return nil } | ||||||||||||||||||||||||||||||||||||||||
| let hrp = Bech32m.platformHrp(mainnet: network == .mainnet) | ||||||||||||||||||||||||||||||||||||||||
| return Bech32m.encode(hrp: hrp, data: payload) | ||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
|
|
@@ -180,9 +180,9 @@ public struct AddressBalanceChange: Sendable, Equatable { | |||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
| /// Convert address bytes to bech32m string | ||||||||||||||||||||||||||||||||||||||||
| public func toBech32m(network: Network) -> String? { | ||||||||||||||||||||||||||||||||||||||||
| guard addressBytes.count == 21 else { return nil } | ||||||||||||||||||||||||||||||||||||||||
| let hrp = network == .mainnet ? "dashevo" : "tdashevo" | ||||||||||||||||||||||||||||||||||||||||
| return Bech32m.encode(hrp: hrp, data: addressBytes) | ||||||||||||||||||||||||||||||||||||||||
| guard let payload = Bech32m.bech32mPayload(fromStorageBytes: addressBytes) else { return nil } | ||||||||||||||||||||||||||||||||||||||||
| let hrp = Bech32m.platformHrp(mainnet: network == .mainnet) | ||||||||||||||||||||||||||||||||||||||||
| return Bech32m.encode(hrp: hrp, data: payload) | ||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
|
|
@@ -264,9 +264,9 @@ public struct CompactedAddressChange: Sendable, Equatable { | |||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
| /// Convert address bytes to bech32m string | ||||||||||||||||||||||||||||||||||||||||
| public func toBech32m(network: Network) -> String? { | ||||||||||||||||||||||||||||||||||||||||
| guard addressBytes.count == 21 else { return nil } | ||||||||||||||||||||||||||||||||||||||||
| let hrp = network == .mainnet ? "dashevo" : "tdashevo" | ||||||||||||||||||||||||||||||||||||||||
| return Bech32m.encode(hrp: hrp, data: addressBytes) | ||||||||||||||||||||||||||||||||||||||||
| guard let payload = Bech32m.bech32mPayload(fromStorageBytes: addressBytes) else { return nil } | ||||||||||||||||||||||||||||||||||||||||
| let hrp = Bech32m.platformHrp(mainnet: network == .mainnet) | ||||||||||||||||||||||||||||||||||||||||
| return Bech32m.encode(hrp: hrp, data: payload) | ||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
|
|
@@ -317,6 +317,68 @@ public enum Bech32m { | |||||||||||||||||||||||||||||||||||||||
| return map | ||||||||||||||||||||||||||||||||||||||||
| }() | ||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
| /// DIP-0018 human-readable part for mainnet Platform addresses. | ||||||||||||||||||||||||||||||||||||||||
| public static let platformHrpMainnet = "dash" | ||||||||||||||||||||||||||||||||||||||||
| /// DIP-0018 human-readable part for testnet/devnet/regtest Platform addresses. | ||||||||||||||||||||||||||||||||||||||||
| public static let platformHrpTestnet = "tdash" | ||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
| /// HRP for the given network per DIP-0018 (mainnet = "dash", all others = "tdash"). | ||||||||||||||||||||||||||||||||||||||||
| public static func platformHrp(mainnet: Bool) -> String { | ||||||||||||||||||||||||||||||||||||||||
| mainnet ? platformHrpMainnet : platformHrpTestnet | ||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
| /// Whether a string looks like a bech32m Platform address (starts with the | ||||||||||||||||||||||||||||||||||||||||
| /// mainnet or testnet HRP + separator). Detection only — `decode` still | ||||||||||||||||||||||||||||||||||||||||
| /// validates the checksum, HRP, and 21-byte length. A 42-char hex address | ||||||||||||||||||||||||||||||||||||||||
| /// can never match, since neither HRP prefix is valid hex. | ||||||||||||||||||||||||||||||||||||||||
| public static func looksLikePlatformAddress(_ address: String) -> Bool { | ||||||||||||||||||||||||||||||||||||||||
| let lower = address.lowercased() | ||||||||||||||||||||||||||||||||||||||||
| return lower.hasPrefix(platformHrpMainnet + "1") | ||||||||||||||||||||||||||||||||||||||||
| || lower.hasPrefix(platformHrpTestnet + "1") | ||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
| // Platform addresses have two distinct 21-byte serializations that differ | ||||||||||||||||||||||||||||||||||||||||
| // only in the leading type byte (per DIP-0018, mirrored in rs-dpp | ||||||||||||||||||||||||||||||||||||||||
| // `PlatformAddress`): | ||||||||||||||||||||||||||||||||||||||||
| // • bech32m payload — user-facing: 0xb0 = P2PKH, 0x80 = P2SH | ||||||||||||||||||||||||||||||||||||||||
| // • storage / FFI — bincode `PlatformAddress`: 0x00 = P2PKH, 0x01 = P2SH | ||||||||||||||||||||||||||||||||||||||||
| // The FFI (`PlatformAddress::from_bytes`) consumes and (`address_fetch_info`) | ||||||||||||||||||||||||||||||||||||||||
| // returns the storage form; only bech32m strings carry the user-facing byte. | ||||||||||||||||||||||||||||||||||||||||
| static let p2pkhBech32mTypeByte: UInt8 = 0xb0 | ||||||||||||||||||||||||||||||||||||||||
| static let p2shBech32mTypeByte: UInt8 = 0x80 | ||||||||||||||||||||||||||||||||||||||||
| static let p2pkhStorageTypeByte: UInt8 = 0x00 | ||||||||||||||||||||||||||||||||||||||||
| static let p2shStorageTypeByte: UInt8 = 0x01 | ||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
| /// Convert a decoded bech32m payload (`[0xb0|0x80] || 20-byte hash`) to the | ||||||||||||||||||||||||||||||||||||||||
| /// storage/FFI form (`[0x00|0x01] || hash`) that the FFI expects. Returns | ||||||||||||||||||||||||||||||||||||||||
| /// nil for a bad length or an unknown type byte. | ||||||||||||||||||||||||||||||||||||||||
| public static func storageBytes(fromBech32mPayload data: Data) -> Data? { | ||||||||||||||||||||||||||||||||||||||||
| let bytes = [UInt8](data) | ||||||||||||||||||||||||||||||||||||||||
| guard bytes.count == 21 else { return nil } | ||||||||||||||||||||||||||||||||||||||||
| let mapped: UInt8 | ||||||||||||||||||||||||||||||||||||||||
| switch bytes[0] { | ||||||||||||||||||||||||||||||||||||||||
| case p2pkhBech32mTypeByte: mapped = p2pkhStorageTypeByte | ||||||||||||||||||||||||||||||||||||||||
| case p2shBech32mTypeByte: mapped = p2shStorageTypeByte | ||||||||||||||||||||||||||||||||||||||||
| default: return nil | ||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||
| return Data([mapped] + bytes[1...]) | ||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
| /// Inverse of `storageBytes(fromBech32mPayload:)`: convert the storage/FFI | ||||||||||||||||||||||||||||||||||||||||
| /// form (`[0x00|0x01] || hash`) to a bech32m payload (`[0xb0|0x80] || hash`) | ||||||||||||||||||||||||||||||||||||||||
| /// for encoding. Returns nil for a bad length or an unknown type byte. | ||||||||||||||||||||||||||||||||||||||||
| public static func bech32mPayload(fromStorageBytes data: Data) -> Data? { | ||||||||||||||||||||||||||||||||||||||||
| let bytes = [UInt8](data) | ||||||||||||||||||||||||||||||||||||||||
| guard bytes.count == 21 else { return nil } | ||||||||||||||||||||||||||||||||||||||||
| let mapped: UInt8 | ||||||||||||||||||||||||||||||||||||||||
| switch bytes[0] { | ||||||||||||||||||||||||||||||||||||||||
| case p2pkhStorageTypeByte: mapped = p2pkhBech32mTypeByte | ||||||||||||||||||||||||||||||||||||||||
| case p2shStorageTypeByte: mapped = p2shBech32mTypeByte | ||||||||||||||||||||||||||||||||||||||||
| default: return nil | ||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||
| return Data([mapped] + bytes[1...]) | ||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
| /// Decode result containing HRP and data | ||||||||||||||||||||||||||||||||||||||||
| public struct DecodeResult { | ||||||||||||||||||||||||||||||||||||||||
| public let hrp: String | ||||||||||||||||||||||||||||||||||||||||
|
|
@@ -373,8 +435,8 @@ public enum Bech32m { | |||||||||||||||||||||||||||||||||||||||
| guard let result = decode(address) else { | ||||||||||||||||||||||||||||||||||||||||
| return false | ||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||
| // Valid Platform addresses have dashevo or tdashevo HRP and 21 bytes of data | ||||||||||||||||||||||||||||||||||||||||
| let validHrp = result.hrp == "dashevo" || result.hrp == "tdashevo" | ||||||||||||||||||||||||||||||||||||||||
| // Valid Platform addresses have the DIP-0018 dash/tdash HRP and 21 bytes of data | ||||||||||||||||||||||||||||||||||||||||
| let validHrp = result.hrp == platformHrpMainnet || result.hrp == platformHrpTestnet | ||||||||||||||||||||||||||||||||||||||||
| let validLength = result.data.count == 21 | ||||||||||||||||||||||||||||||||||||||||
| return validHrp && validLength | ||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
435
to
442
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🟡 Suggestion: The validator now checks the canonical HRP and the 21-byte length, but does not verify that the payload's leading byte is one of the DIP-0018 Platform type bytes (
Suggested change
source: ['codex'] |
||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🟡 Suggestion: Bech32m fetch APIs accept non-
dash/tdashHRPsgetInfo(bech32mAddress:)decodes any bech32m string and only validates the 21-byte payload / type byte before querying the FFI. A caller passing a bech32m string with an arbitrary HRP but a coincidentally-valid[0xb0|0x80] || 20-byte hashpayload is accepted and queried as a Platform address, even though the Rust side (PlatformAddress::from_bech32m_string) andAddressTransformer.parseBech32mAddressboth reject non-dash/tdashHRPs. Apply the same HRP check here and ingetInfos(bech32mAddresses:)at line 222 for API consistency with the rest of this PR.source: ['codex']