| 1 | import CryptoKit |
| 2 | import XCTest |
| 3 | @testable import LChatCore |
| 4 | |
| 5 | // 🤖 The hashcash solver mirrors home's pow.go: SHA-256(challenge ":" nonce) |
| 6 | // must clear the difficulty's leading zero bits. These pin the two pure |
| 7 | // pieces (bit counting, solve/verify) against the same cases as the Go tests, |
| 8 | // so the native solver and the server can't drift. |
| 9 | final class PowSolverTests: XCTestCase { |
| 10 | func testLeadingZeroBits() { |
| 11 | XCTAssertEqual(HomeClient.leadingZeroBits([0x80]), 0) |
| 12 | XCTAssertEqual(HomeClient.leadingZeroBits([0x01]), 7) |
| 13 | XCTAssertEqual(HomeClient.leadingZeroBits([0x00, 0xFF]), 8) |
| 14 | XCTAssertEqual(HomeClient.leadingZeroBits([0x00, 0x0F]), 12) |
| 15 | XCTAssertEqual(HomeClient.leadingZeroBits([0x00, 0x00]), 16) |
| 16 | } |
| 17 | |
| 18 | func testSolveAndVerify() { |
| 19 | // 8 bits ≈ 256 hashes — instant. Grind exactly like the client does, |
| 20 | // then check the found nonce verifies against an independent digest. |
| 21 | let challenge = "test-challenge" |
| 22 | var nonce = 0 |
| 23 | while !HomeClient.powSolves(challenge: challenge, nonce: String(nonce), bits: 8) { |
| 24 | nonce += 1 |
| 25 | XCTAssertLessThan(nonce, 1_000_000, "8-bit solution should be found quickly") |
| 26 | } |
| 27 | let digest = Array(SHA256.hash(data: Data((challenge + ":" + String(nonce)).utf8))) |
| 28 | XCTAssertGreaterThanOrEqual(HomeClient.leadingZeroBits(digest), 8) |
| 29 | // Bits are a floor: a 8-bit solution rarely clears 32. |
| 30 | XCTAssertFalse(HomeClient.powSolves(challenge: challenge, nonce: "definitely-not-it", bits: 32)) |
| 31 | // A solution is bound to its challenge text, not portable. |
| 32 | XCTAssertTrue(HomeClient.powSolves(challenge: "anything", nonce: "0", bits: 0)) |
| 33 | } |
| 34 | } |
| 35 | |