stacks-blockchain-api icon indicating copy to clipboard operation
stacks-blockchain-api copied to clipboard

Historic mining rewards are returned as part of STX balance, they shouldn't be.

Open 314159265359879 opened this issue 6 months ago • 1 comments

Describe the bug I believe there is a bug in the balances API. This address has about a 3 STX balance, but the API returns a balance of 81k STX, the balance returned by the API is equal to the (historic) mining rewards +the actual balance.

https://api.hiro.so/extended/v2/addresses/SP12QK2PQZXTWPF3K9ES0MA51JW17KXMBYG3W2V9M/balances/stx {"balance":"81284617344","total_miner_rewards_received":"81281147927","lock_tx_id":"","locked":"0","lock_height":0,"burnchain_lock_height":0,"burnchain_unlock_height":0}

To Reproduce Steps to reproduce the behavior:

  1. Go to https://api.hiro.so/extended/v2/addresses/SP12QK2PQZXTWPF3K9ES0MA51JW17KXMBYG3W2V9M/balances/stx
  2. See this in the API result "balance":"81284617344"
  3. Compare with actual balance on https://explorer.hiro.so/address/SP12QK2PQZXTWPF3K9ES0MA51JW17KXMBYG3W2V9M?chain=mainnet

Image

  1. See error, the balances do not match

Expected behavior I expect the API to return the same balance as displayed on the explorer. It appears that the historic mining rewards are included in the balance even when they are not present in the account.

Screenshots Complete API result {"balance":"81284617344","total_miner_rewards_received":"81281147927","lock_tx_id":"","locked":"0","lock_height":0,"burnchain_lock_height":0,"burnchain_unlock_height":0}

Another miner with 0.000~ STX balance https://api.hiro.so/extended/v2/addresses/SP3MCVE3HJP6T8QS4A9R0ETVJVGRZJA04MKJTPHG5/balances/stx {"balance":"2373249411583","total_miner_rewards_received":"2373249409914","lock_tx_id":"","locked":"0","lock_height":0,"burnchain_lock_height":0,"burnchain_unlock_height":0}

Console log n.a.

Desktop (please complete the following information): n.a.

Smartphone (please complete the following information): n.a.

314159265359879 avatar Jun 19 '25 07:06 314159265359879

I did some preliminary investigation to help the team. Please provide feedback on how I can improve this format in the future


Overview


GET /extended/v2/addresses/:principal/balances/stx sometimes returns an STX balance that is higher than the real on-chain amount. The number includes each historical miner reward twice, so any account that has ever mined shows an inflated balance.


Root Cause Analysis


The balance handler already pulls the current balance from ft_balances, which is continuously updated by the ingestion path (see updateMinerRewards in src/datastore/pg-write-store.ts).

Right after that, the handler queries getStxMinerRewardsAtBlock for the cumulative miner rewards, then adds that total to the balance:

// src/api/routes/v2/addresses.ts  (approx line 145)
const { totalMinerRewardsReceived } =
  await fastify.db.v2.getStxMinerRewardsAtBlock({ sql, stxAddress, blockHeight: chainTip.block_height });

stxBalance += totalMinerRewardsReceived;   // <-- duplicates rewards
``` :contentReference[oaicite:0]{index=0}  

Because ft_balances already contains every matured miner reward, the extra addition overstates the true balance by the sum of all rewards.


Proposed fix


  • Delete the line that mutates stxBalance with += totalMinerRewardsReceived.
    • File: src/api/routes/v2/addresses.ts
    • Block: inside the /:principal/balances/stx handler, right after the miner-reward query.

After the change the endpoint will:

  • Return balance exactly as recorded in ft_balances.
  • Still publish total_miner_rewards_received as a separate field for clients that need historical reward totals.

No other schema changes required.


Proposed Test Coverage


1. Baseline sanity

  • Setup

    • Build simple transfer-only blocks with TestBlockBuilder.
  • Steps

    • Query
      • GET /extended/v1/address/:addr/stx
      • GET /extended/v2/addresses/:addr/balances/stx
    • Compare the returned balances.
  • Expectation

    • Both endpoints return identical balances, confirming no behavioral regressions.

2. Mined-and-spent flow

  • Setup

    1. Block 1: credit a matured miner reward to addrA via addMinerReward.
    2. Block 2: spend the entire reward from addrA to addrB (fee 0).
    3. Apply both blocks with db.update.
  • Steps

    • Query the same two endpoints for addrA.
  • Expectation

    • balance"0" on both v1 and v2 responses.
    • total_miner_rewards_received equals the mined amount.

3. Pending outbound transaction

  • Setup

    • From the state above, craft a mempool transaction sending 1 STX from addrA.
  • Steps

    • Query
      • GET /extended/v2/addresses/:addrA/balances/stx?include_mempool=true
  • Expectation

    • balance remains "0".
    • estimated_balance reflects the pending debit ("-1" STX).

Other considerations


  • Backwards compatibility — Are there any downstream services that have silently compensated for this that will be in error when this is fixed?
  • Docs — The OpenAPI spec already documents total_miner_rewards_received; they do not appear to need an update.

alexthuth avatar Jun 19 '25 13:06 alexthuth

:tada: This issue has been resolved in version 8.11.2 :tada:

The release is available on:

Your semantic-release bot :package::rocket:

Thanks for the report @314159265359879 this should be fixed via https://github.com/hirosystems/stacks-blockchain-api/pull/2302

andresgalante avatar Jun 20 '25 16:06 andresgalante