Burn-Block Timing

Why sBTC Pay measures time in Bitcoin burn blocks, not Stacks blocks, and why that matters after the Nakamoto upgrade.

Every time-sensitive piece of the contract — invoice expiry, refund window, subscription interval — is counted in burn blocks (Bitcoin blocks), not Stacks blocks. This page explains why, and why it matters specifically after the Stacks Nakamoto upgrade.

Two clocks, very different speeds

Stacks has two block heights you can reference from a Clarity contract:

  • stacks-block-height — increments every Stacks block. Post-Nakamoto: roughly every 5 seconds.
  • burn-block-height — increments every Bitcoin block. Roughly every 10 minutes on mainnet, 5 minutes on testnet.

Before Nakamoto, these were tightly coupled (one Stacks block per Bitcoin block). After Nakamoto, Stacks blocks are fast — ~120× faster than Bitcoin blocks. That speed is great for dApp UX, but it's a trap for anything that measures calendar time.

The trap

Imagine a subscription with a "monthly" billing interval. You might naively write:

"A month is 30 × 24 × 60 / 5 = 8,640 Stacks blocks" (at 5 seconds per block)

Then you use stacks-block-height in your contract. Looks fine. Ships to mainnet. Works for a few months. Then, during a period of congestion or slow block production, the Stacks block time temporarily drops to, say, 3 seconds — and suddenly your "monthly" subscription bills every 18 days. Or, in an idle period when Stacks blocks slow to 8 seconds, it bills every 48 days.

Stacks block time is variable. Calendar time is not. Measuring calendar time in Stacks blocks is always wrong, and after Nakamoto the error is large.

The fix: count in Bitcoin blocks

Bitcoin's block time is tightly regulated by its difficulty adjustment. Over any meaningful window, Bitcoin blocks land at ~10-minute intervals. That makes burn-block-height a reliable clock for calendar-time logic.

So the contract defines:

  • Daily: 144 burn blocks (24 × 60 / 10)
  • Weekly: 1,008 burn blocks
  • Monthly: 4,320 burn blocks (30 days)

These are approximate — Bitcoin blocks aren't exactly 10 minutes — but the error is small and bounded. Over a year, block-time drift is typically under 1–2%.

Testnet is faster

Stacks testnet anchors to Bitcoin testnet, which targets 5-minute blocks. So on testnet, "daily" is 288 burn blocks, not 144. The frontend and contract read the network at runtime and use the right constant. If you're building a widget that embeds an interval, let sBTC Pay handle the conversion — don't hard-code block counts in your URL unless you know what you're doing.

Where burn blocks are used

Every time reference in the sBTC Pay contract uses burn blocks:

  • Invoice creation: created-at = current burn block
  • Invoice expiry: expires-at = created-at + expires-in-blocks
  • Subscription creation: next-payment-at = current burn block (due immediately)
  • Subscription interval: next-payment-at = last-payment-at + interval-blocks
  • Refund window: refund-deadline = first-payment-at + refund-window-blocks

What this means for integrators

If you ever need to compute "when will this be due?" in your own code, use the same approach:

  1. Read the relevant block height field (in burn blocks)
  2. Convert to wall-clock time using the network's average burn-block time (~600 sec mainnet, ~300 sec testnet)

The frontend already does this for you — dashboard and customer portal dates are derived exactly this way.

Don't convert with stacks-block-height

If you see code that converts time by multiplying stacks-block-height by 5 seconds, it's wrong. It will drift whenever network conditions change. Always use burn-block-height for calendar-time math.