Post
Topic
Board Bitcoin Discussion
Merits 8 from 3 users
Re: Bitcoin puzzle transaction ~32 BTC prize to who solves it
by
fixedpaul
on 22/04/2025, 17:14:54 UTC
⭐ Merited by Bram24732 (4) ,kTimesG (3) ,Cricktor (1)
That's exactly what you described. Leaving "less likely" ranges for later because you think it's less likely to be there.
The script that you sent shows that it does not change anything, both methods are equally fast on average.

Okay, I'll return to my field again. I hope this time it makes you happy.


Unfortunately, what you did doesn't work as you think. Let me explain.

First of all, the correct metric to look at, as many have already explained to you, is the average number of checks, not the number of wins. But okay, you say, "Well, in the end the prefix method wins more often, so in most cases I find the solution earlier" – This is incorrect, but we'll get to that.

I modified your code in the preciseSearch function.

Code:
const preciseSearch = (block, prefix, targetHash, order) => {
  const randomHash = generateH160(Math.floor(Math.random() * 999999));
  const prefixHash = randomHash.slice(0, prefix);  //generate a random hash prefix for each block

  let checks = 0, ranges = [];
  for (const idx of order) {
    let start = idx * block,
        end = start + block,
        foundPrefix = false;
    for (let num = start; num < end; num++) {
      checks++;
      const hash = generateH160(num);
      if (hash === targetHash) return { checks, found: true };
      if (!foundPrefix && hash.startsWith(prefixHash)) {
        foundPrefix = true;
        ranges.push({ start: num + 1, end });
        break;
      }
    }
  }
  for (const { start, end } of ranges) {
    for (let num = end - 1; num >= start; num--) {
      checks++;
      if (generateH160(num) === targetHash) return { checks, found: true };
    }
  }
  return { checks, found: false };
};

Now, the search in the block is simply "paused" when any random prefix is found—not the targetAddress's prefix, but any randomly chosen prefix for each block. If the prefix method somehow actually worked, I would expect a different result. Instead, I get exactly the same result: almost identical number of checks, but the prefix method wins many more times!

Code:
=== FINAL RESULTS ===
Wins:
Sequential: 158
Precise: 230
Ties: 12

Why does this happen? The answer is that the prefix method doesn't make sense—you're just performing a flawed analysis.

The mistake lies in comparing the two methods while keeping the same block search order. That is, you're forcing both methods to iterate through the blocks in exactly the same order. This introduces a kind of bias in the statistics: the distribution of the number of checks becomes highly variable, causing the "precise" method to win more often, but when it loses, it loses badly, and the average number of checks stays about the same.

So I'm attaching my version of the code, where it's possible to freely switch whether the two methods share the same order or not, by changing the variable SHARED_BLOCK_ORDER.

Code:
const { performance } = require("perf_hooks");
const crypto = require("crypto");

const TOTAL_SIZE = 100_000,
      RANGE_SIZE = 5_000,
      PREFIX_LENGTH = 3,
      SIMULATIONS = 1000,
      SHARED_BLOCK_ORDER = true; // << Set this to true or false

console.log(`
=== Configuration ===
Total numbers: ${TOTAL_SIZE.toLocaleString()}
Block size: ${RANGE_SIZE.toLocaleString()}
Prefix: ${PREFIX_LENGTH} characters (16^${PREFIX_LENGTH} combinations)
Simulations: ${SIMULATIONS}
Shared block order: ${SHARED_BLOCK_ORDER}
`);

const generateH160 = (data) =>
  crypto.createHash("ripemd160").update(data.toString()).digest("hex");

const shuffledRange = (n) => {
  const arr = [...Array(n + 1).keys()];
  for (let i = arr.length - 1; i > 0; i--) {
    const j = Math.floor(Math.random() * (i + 1));
    [arr[i], arr[j]] = [arr[j], arr[i]];
  }
  return arr;
};

const sequentialSearch = (block, targetHash, order) => {
  let checks = 0;
  for (const idx of order) {
    for (let num = idx * block; num < (idx + 1) * block; num++) {
      checks++;
      if (generateH160(num) === targetHash) return { checks, found: true };
    }
  }
  return { checks, found: false };
};

const preciseSearch = (block, prefix, targetHash, order) => {
  const prefixHash = targetHash.slice(0, prefix);
  let checks = 0, ranges = [];
  for (const idx of order) {
    let start = idx * block,
        end = start + block,
        foundPrefix = false;
    for (let num = start; num < end; num++) {
      checks++;
      const hash = generateH160(num);
      if (hash === targetHash) return { checks, found: true };
      if (!foundPrefix && hash.startsWith(prefixHash)) {
        foundPrefix = true;
        ranges.push({ start: num + 1, end });
        break;
      }
    }
  }
  for (const { start, end } of ranges) {
    for (let num = end - 1; num >= start; num--) {
      checks++;
      if (generateH160(num) === targetHash) return { checks, found: true };
    }
  }
  return { checks, found: false };
};

const compareMethods = async () => {
  const results = { sequential: { wins: 0 }, precise: { wins: 0 }, ties: 0 };

  const blocks = Math.floor(TOTAL_SIZE / RANGE_SIZE);

  for (let i = 0; i < SIMULATIONS; i++) {
    const targetNum = Math.floor(Math.random() * TOTAL_SIZE),
          targetHash = generateH160(targetNum);

    const orderSeq = shuffledRange(blocks - 1);
    const orderPre = SHARED_BLOCK_ORDER ? [...orderSeq] : shuffledRange(blocks - 1);

    const seqResult = sequentialSearch(RANGE_SIZE, targetHash, orderSeq);
    const preResult = preciseSearch(RANGE_SIZE, PREFIX_LENGTH, targetHash, orderPre);

    if (seqResult.checks < preResult.checks) {
      results.sequential.wins++;
    } else if (seqResult.checks > preResult.checks) {
      results.precise.wins++;
    } else {
      results.ties++;
    }

    console.log(`Simulation ${i + 1}: Sequential = ${seqResult.checks} | Precise = ${preResult.checks}`);
  }

  console.log(`
=== FINAL RESULTS ===
Wins:
Sequential: ${results.sequential.wins}
Precise: ${results.precise.wins}
Ties: ${results.ties}
`);
};

compareMethods();


What you get by setting SHARED_BLOCK_ORDER = true is exactly what you get with your original code. Setting it to false changes the result, precisely because we're introducing true randomness and making a genuinely fair comparison between the two methods.

Code:
=== Configuration ===
Total numbers: 100.000
Block size: 5.000
Prefix: 3 characters (16^3 combinations)
Simulations: 1000
Shared block order: false

=== FINAL RESULTS ===
Wins:
Sequential: 499
Precise: 500
Ties: 1

Total Checks:
Sequential: 49169108
Prefix: 48831019

Do you understand where the fallacy in your analysis lies? In case it's still not clear, I’ve prepared an even simpler example to remove all doubt.

Let’s now try to find a better method to guess a number from 1 to 1000, chosen randomly with a uniform distribution. The numbers from 1 to 1000 are divided into 20 blocks of 50 numbers each.

The "sequential" method searches sequentially through each block, with the blocks ordered randomly.

The "magic" method searches each block too, but does something different: it only checks the first 20 numbers of each block, leaving the remaining 30 for a second pass—similar to what the prefix method does.

Here as well, I leave the option to choose whether both methods should search the blocks in the same order or not (SHARED_BLOCK_ORDER).

Let’s look at a Python script that simulates this, printing the number of wins and ties for both methods, along with the total number of checks.

Code:
import random

# Number of simulations
NUM_SIMULATIONS = 10000

# Block settings
BLOCK_SIZE = 50
NUM_BLOCKS = 20
PARTIAL_SCAN = 20  # Magic method: check first 20 numbers in block

# Shared block order flag (1 = same for both methods, 0 = independent)
SHARED_BLOCK_ORDER = 1

# Stats counters
sequential_wins = 0
magic_wins = 0
ties = 0
total_sequential_checks = 0
total_magic_checks = 0

# Run simulations
for _ in range(NUM_SIMULATIONS):
    winning_number = random.randint(1, 1000)

    # Create blocks
    blocks = [list(range(i * BLOCK_SIZE + 1, (i + 1) * BLOCK_SIZE + 1)) for i in range(NUM_BLOCKS)]

    # Generate block orders
    block_order_seq = list(range(NUM_BLOCKS))
    block_order_magic = list(range(NUM_BLOCKS))
    random.shuffle(block_order_seq)
    if SHARED_BLOCK_ORDER:
        block_order_magic = block_order_seq.copy()
    else:
        random.shuffle(block_order_magic)

    # === Sequential Method ===
    sequential_checks = 0
    found_seq = False

    for block_idx in block_order_seq:
        block = blocks[block_idx]
        for number in block:
            sequential_checks += 1
            if number == winning_number:
                found_seq = True
                break
        if found_seq:
            break

    # === Magic Method ===
    magic_checks = 0
    found_magic = False

    # First pass: check first 20 numbers of each block
    for block_idx in block_order_magic:
        block = blocks[block_idx]
        for number in block[:PARTIAL_SCAN]:
            magic_checks += 1
            if number == winning_number:
                found_magic = True
                break
        if found_magic:
            break

    # Second pass: check 50 to 21 (reversed)
    if not found_magic:
        for block_idx in reversed(block_order_magic):
            block = blocks[block_idx]
            for number in reversed(block[PARTIAL_SCAN:]):
                magic_checks += 1
                if number == winning_number:
                    found_magic = True
                    break
            if found_magic:
                break

    # === Update stats ===
    total_sequential_checks += sequential_checks
    total_magic_checks += magic_checks

    if sequential_checks < magic_checks:
        sequential_wins += 1
    elif magic_checks < sequential_checks:
        magic_wins += 1
    else:
        ties += 1

# === Final Output ===
print("Results after", NUM_SIMULATIONS, "simulations:")
print("Sequential wins:", sequential_wins)
print("Magic wins:", magic_wins)
print("Ties:", ties)
print("Total sequential checks:", total_sequential_checks)
print("Total magic checks:", total_magic_checks)



Results with SHARED_BLOCK_ORDER = 1:

Code:
Results after 10000 simulations:
Sequential wins: 3704
Magic wins: 6098
Ties: 198
Total sequential checks: 5037847
Total magic checks: 5038341

OMG!!! Did I just find a better method to search for a random number in a uniform distribution? OBVIOUSLY NOT. It's just a flawed way of conducting this analysis that introduces a bias in the distribution of checks during the simulations.
Now let’s see what happens with SHARED_BLOCK_ORDER = 0:

Code:
Results after 10000 simulations:
Sequential wins: 4980
Magic wins: 4976
Ties: 44
Total sequential checks: 5013645
Total magic checks: 5004263

Well, luckily statistics still works. I hope it was appreciated that I took the time to explain to everyone why we were seeing those results. And I hope this general obsession with the prefix method—or any other so-called magical method—will soon be extinguished by logic and your ability to reason and think critically.

I'm sorry, but there is no better method to guess a random number. The ripemd160 function doesn’t somehow “understand” that it should generate more likely prefixes if I input the hash of public keys derived from nearby private keys. That would require consciousness, and it would need to be able to reverse sha256 and secp256k1—which I very much doubt it can do.