diff options
| author | bitflash-maintainer <noreply@bitflash.invalid> | 2026-09-05 22:46:52 +0000 |
|---|---|---|
| committer | bitflash-maintainer <noreply@bitflash.invalid> | 2026-09-05 22:46:52 +0000 |
| commit | ceae3d076b5ad11ee84729ae43f14e9312896646 (patch) | |
| tree | 7e9132aa871fdc01cb95268d2f5ea3bb11853ddf /src/btfaddr.cpp | |
| parent | be63e83d79e7e00e6b8ca889a70e6a19886aa1d8 (diff) | |
| parent | 66e0b9158d050c46b1ff2509faeee3f4573cac1c (diff) | |
Bring main onto the line this project actually ships
main had drifted to a history with no ancestor in common with the tree we
build and deploy. It sat at be63e83 (2026-08-09) with 28 commits; the shipped
line, deploy/soak-2026-08-21, had 383. The two shared nothing -- git
merge-base returns no result at all -- so anyone cloning this repository
landed on a default branch that was neither current nor related to the
running node.
The 28 commits on main were audited before this merge: every one is already
present in the tree by content, including the two that are hardest to see
because they were rewritten afterwards -- the payout reorg-safety work
(PayoutBlockStillOnChain, since evolved to re-check on every retry rather
than once at maturity) and the nostr relay certificate verification
(SSL_set1_host, now with a Windows root-store path). Nothing on main is lost
by this merge.
Recorded as a merge rather than a force-push: main only ever moves forward,
its old history stays reachable as the first parent, and this is revertible.
The tree is taken wholesale from the merged branch, so main's content is now
exactly what was built and tested.
What this brings onto main:
- compactblock: BIP152-style compact block relay ported from upstream 1.2.23,
primitives and unit test only, with the RAND_bytes failure path closed and
the reconstruction allocation bounded.
- upstream/main 17741d9: isolated testnet parameters and genesis, optional
2 MB large pages for RandomX. Mainnet genesis verified byte-identical
through upstream's BuildGenesisBlock() refactor.
- deploy: every image built on bookworm so binaries run on Debian 12, and the
container build now honours the RandomX and libsecp256k1 pins it had been
silently ignoring since before 1.2.15.
Gates at this tree: 15/15 selftests, compactblock's 17 checks, consensus
constants unchanged against e4f898c apart from the pre-existing
MAX_BLOCK_SIZE_GEN, mainnet genesis intact, money-log check ok.
Diffstat (limited to 'src/btfaddr.cpp')
| -rw-r--r-- | src/btfaddr.cpp | 114 |
1 files changed, 107 insertions, 7 deletions
diff --git a/src/btfaddr.cpp b/src/btfaddr.cpp index 6edcc91..502220e 100644 --- a/src/btfaddr.cpp +++ b/src/btfaddr.cpp | |||
| @@ -19,6 +19,8 @@ | |||
| 19 | #include <cstring> | 19 | #include <cstring> |
| 20 | #include <algorithm> | 20 | #include <algorithm> |
| 21 | #include <cctype> | 21 | #include <cctype> |
| 22 | #include <cerrno> | ||
| 23 | #include <cstdlib> | ||
| 22 | 24 | ||
| 23 | using json = nlohmann::json; | 25 | using json = nlohmann::json; |
| 24 | 26 | ||
| @@ -166,10 +168,47 @@ static bool HexDecode(const std::string& s, unsigned char* out, size_t n) | |||
| 166 | return true; | 168 | return true; |
| 167 | } | 169 | } |
| 168 | 170 | ||
| 171 | bool NormalizeOnionEndpoint(const std::string& in, std::string& out) | ||
| 172 | { | ||
| 173 | out.clear(); | ||
| 174 | std::string s = ToLowerTrim(in); | ||
| 175 | size_t colon = s.rfind(':'); | ||
| 176 | if (colon == std::string::npos || colon == 0 || colon + 1 >= s.size()) | ||
| 177 | return false; | ||
| 178 | |||
| 179 | std::string host = s.substr(0, colon); | ||
| 180 | std::string port = s.substr(colon + 1); | ||
| 181 | while (!host.empty() && host.back() == '.') host.pop_back(); | ||
| 182 | static const char* suffix = ".onion"; | ||
| 183 | const size_t suffixLen = strlen(suffix); | ||
| 184 | if (host.size() != 56 + suffixLen) | ||
| 185 | return false; | ||
| 186 | if (host.compare(host.size() - suffixLen, suffixLen, suffix) != 0) | ||
| 187 | return false; | ||
| 188 | for (size_t i = 0; i < 56; i++) | ||
| 189 | { | ||
| 190 | char c = host[i]; | ||
| 191 | if (!((c >= 'a' && c <= 'z') || (c >= '2' && c <= '7'))) | ||
| 192 | return false; | ||
| 193 | } | ||
| 194 | for (size_t i = 0; i < port.size(); i++) | ||
| 195 | if (!isdigit((unsigned char)port[i])) | ||
| 196 | return false; | ||
| 197 | |||
| 198 | errno = 0; | ||
| 199 | char* end = NULL; | ||
| 200 | long nPort = strtol(port.c_str(), &end, 10); | ||
| 201 | if (errno == ERANGE || end == NULL || *end != '\0' || nPort <= 0 || nPort > 65535) | ||
| 202 | return false; | ||
| 203 | |||
| 204 | out = host + ":" + port; | ||
| 205 | return true; | ||
| 206 | } | ||
| 207 | |||
| 169 | // Digest signed over the descriptor fields (domain-separated). | 208 | // Digest signed over the descriptor fields (domain-separated). |
| 170 | static void DescriptorDigest(const unsigned char pubkey[32], const std::string& enc, | 209 | static void DescriptorDigestV2(const unsigned char pubkey[32], const std::string& enc, |
| 171 | const std::string& meeting_node, uint64_t created, | 210 | const std::string& meeting_node, uint64_t created, |
| 172 | unsigned char out[32]) | 211 | unsigned char out[32]) |
| 173 | { | 212 | { |
| 174 | static const char* DS = ".btf-descriptor-v2"; | 213 | static const char* DS = ".btf-descriptor-v2"; |
| 175 | SHA256_CTX ctx; | 214 | SHA256_CTX ctx; |
| @@ -184,9 +223,27 @@ static void DescriptorDigest(const unsigned char pubkey[32], const std::string& | |||
| 184 | SHA256_Final(out, &ctx); | 223 | SHA256_Final(out, &ctx); |
| 185 | } | 224 | } |
| 186 | 225 | ||
| 226 | static void DescriptorDigestV3(const unsigned char pubkey[32], const std::string& enc, | ||
| 227 | const std::string& meeting_node, const std::string& onion, | ||
| 228 | uint64_t created, unsigned char out[32]) | ||
| 229 | { | ||
| 230 | static const char* DS = ".btf-descriptor-v3-onion"; | ||
| 231 | SHA256_CTX ctx; | ||
| 232 | SHA256_Init(&ctx); | ||
| 233 | SHA256_Update(&ctx, (const unsigned char*)DS, strlen(DS)); | ||
| 234 | SHA256_Update(&ctx, pubkey, 32); | ||
| 235 | SHA256_Update(&ctx, (const unsigned char*)enc.data(), enc.size()); | ||
| 236 | SHA256_Update(&ctx, (const unsigned char*)meeting_node.data(), meeting_node.size()); | ||
| 237 | SHA256_Update(&ctx, (const unsigned char*)onion.data(), onion.size()); | ||
| 238 | unsigned char be[8]; | ||
| 239 | for (int i = 0; i < 8; i++) be[i] = (unsigned char)((created >> (8 * (7 - i))) & 0xff); | ||
| 240 | SHA256_Update(&ctx, be, 8); | ||
| 241 | SHA256_Final(out, &ctx); | ||
| 242 | } | ||
| 243 | |||
| 187 | std::string SignDescriptor(void* ctxv, const unsigned char seckey[32], | 244 | std::string SignDescriptor(void* ctxv, const unsigned char seckey[32], |
| 188 | const std::string& enc, const std::string& meeting_node, | 245 | const std::string& enc, const std::string& meeting_node, |
| 189 | uint64_t created) | 246 | uint64_t created, const std::string& onion) |
| 190 | { | 247 | { |
| 191 | secp256k1_context* ctx = (secp256k1_context*)ctxv; | 248 | secp256k1_context* ctx = (secp256k1_context*)ctxv; |
| 192 | secp256k1_keypair kp; | 249 | secp256k1_keypair kp; |
| @@ -197,17 +254,28 @@ std::string SignDescriptor(void* ctxv, const unsigned char seckey[32], | |||
| 197 | secp256k1_xonly_pubkey_serialize(ctx, pubkey, &xpub); | 254 | secp256k1_xonly_pubkey_serialize(ctx, pubkey, &xpub); |
| 198 | 255 | ||
| 199 | unsigned char digest[32]; | 256 | unsigned char digest[32]; |
| 200 | DescriptorDigest(pubkey, enc, meeting_node, created, digest); | 257 | DescriptorDigestV2(pubkey, enc, meeting_node, created, digest); |
| 201 | 258 | ||
| 202 | unsigned char sig[64]; | 259 | unsigned char sig[64]; |
| 203 | // Deterministic signature (NULL aux_rand), matching Itzal's no-aux-rand signing. | 260 | // Deterministic signature (NULL aux_rand), matching Itzal's no-aux-rand signing. |
| 204 | if (!secp256k1_schnorrsig_sign32(ctx, sig, digest, &kp, NULL)) return std::string(); | 261 | if (!secp256k1_schnorrsig_sign32(ctx, sig, digest, &kp, NULL)) return std::string(); |
| 205 | 262 | ||
| 206 | json v = json::object(); | 263 | json v = json::object(); |
| 207 | v["v"] = 2; | 264 | v["v"] = onion.empty() ? 2 : 3; |
| 208 | v["pubkey"] = HexEncode(pubkey, 32); | 265 | v["pubkey"] = HexEncode(pubkey, 32); |
| 209 | v["enc"] = enc; | 266 | v["enc"] = enc; |
| 210 | v["meeting_node"] = meeting_node; | 267 | v["meeting_node"] = meeting_node; |
| 268 | if (!onion.empty()) | ||
| 269 | { | ||
| 270 | std::string onionNorm; | ||
| 271 | if (!NormalizeOnionEndpoint(onion, onionNorm)) return std::string(); | ||
| 272 | unsigned char digest3[32]; | ||
| 273 | DescriptorDigestV3(pubkey, enc, meeting_node, onionNorm, created, digest3); | ||
| 274 | unsigned char sig3[64]; | ||
| 275 | if (!secp256k1_schnorrsig_sign32(ctx, sig3, digest3, &kp, NULL)) return std::string(); | ||
| 276 | v["onion"] = onionNorm; | ||
| 277 | v["sig3"] = HexEncode(sig3, 64); | ||
| 278 | } | ||
| 211 | v["created"] = created; | 279 | v["created"] = created; |
| 212 | v["sig"] = HexEncode(sig, 64); | 280 | v["sig"] = HexEncode(sig, 64); |
| 213 | return v.dump(); | 281 | return v.dump(); |
| @@ -242,15 +310,47 @@ bool VerifyDescriptor(void* ctxv, const std::string& jsonStr, | |||
| 242 | if (!HexDecode(v["sig"].get<std::string>(), sig, 64)) return false; | 310 | if (!HexDecode(v["sig"].get<std::string>(), sig, 64)) return false; |
| 243 | 311 | ||
| 244 | unsigned char digest[32]; | 312 | unsigned char digest[32]; |
| 245 | DescriptorDigest(expect_pubkey, enc, meeting_node, created, digest); | 313 | DescriptorDigestV2(expect_pubkey, enc, meeting_node, created, digest); |
| 246 | 314 | ||
| 247 | secp256k1_xonly_pubkey xpub; | 315 | secp256k1_xonly_pubkey xpub; |
| 248 | if (!secp256k1_xonly_pubkey_parse(ctx, &xpub, expect_pubkey)) return false; | 316 | if (!secp256k1_xonly_pubkey_parse(ctx, &xpub, expect_pubkey)) return false; |
| 249 | if (!secp256k1_schnorrsig_verify(ctx, sig, digest, 32, &xpub)) return false; | 317 | if (!secp256k1_schnorrsig_verify(ctx, sig, digest, 32, &xpub)) return false; |
| 250 | 318 | ||
| 319 | std::string onion; | ||
| 320 | if (v.contains("onion")) | ||
| 321 | { | ||
| 322 | // A descriptor that carries an onion endpoint must prove it. The | ||
| 323 | // endpoint is covered by sig3 and only sig3, so any failure here -- | ||
| 324 | // missing sig3, bad hex, unnormalised endpoint, bad signature -- | ||
| 325 | // means the descriptor was tampered with, and a tampered | ||
| 326 | // descriptor is rejected outright. Upstream dropped just the onion | ||
| 327 | // and accepted the rest, which let any relay convert an onion | ||
| 328 | // descriptor into a rendezvous-only one by deleting a field. | ||
| 329 | // | ||
| 330 | // Residual, by design: removing BOTH "onion" and "sig3" still | ||
| 331 | // verifies, because the result is indistinguishable from a peer | ||
| 332 | // that never had an onion endpoint. Closing that requires binding | ||
| 333 | // the onion into the primary sig (the V2 digest), which every | ||
| 334 | // upstream 1.2.18/1.2.19 verifier would reject -- an upstream | ||
| 335 | // protocol change, not something this fork can do unilaterally. | ||
| 336 | if (!v["onion"].is_string()) return false; | ||
| 337 | if (!v.contains("sig3") || !v["sig3"].is_string()) return false; | ||
| 338 | std::string onionNorm; | ||
| 339 | std::string onionRaw = v["onion"].get<std::string>(); | ||
| 340 | if (!NormalizeOnionEndpoint(onionRaw, onionNorm) || onionNorm != onionRaw) | ||
| 341 | return false; | ||
| 342 | unsigned char sig3[64]; | ||
| 343 | unsigned char digest3[32]; | ||
| 344 | DescriptorDigestV3(expect_pubkey, enc, meeting_node, onionNorm, created, digest3); | ||
| 345 | if (!HexDecode(v["sig3"].get<std::string>(), sig3, 64)) return false; | ||
| 346 | if (!secp256k1_schnorrsig_verify(ctx, sig3, digest3, 32, &xpub)) return false; | ||
| 347 | onion = onionNorm; | ||
| 348 | } | ||
| 349 | |||
| 251 | memcpy(out.pubkey, expect_pubkey, 32); | 350 | memcpy(out.pubkey, expect_pubkey, 32); |
| 252 | out.enc = enc; | 351 | out.enc = enc; |
| 253 | out.meeting_node = meeting_node; | 352 | out.meeting_node = meeting_node; |
| 353 | out.onion = onion; | ||
| 254 | out.created = created; | 354 | out.created = created; |
| 255 | return true; | 355 | return true; |
| 256 | } | 356 | } |