hirosystems/stacks-blockchain-api

total_stacked_amount is not accurate

sbond14 opened this issue · 7 comments

Describe the bug
When querying the API endpoint https://api.hiro.so/extended/v2/pox/cycles/93, the returned field total_stacked_amount is not correct. It should match the value returned by a node's pox endpoint (ip:port/v2/pox | jq .current_cycle)

To Reproduce

  1. On a machine with a running follower node, run curl -s 0.0.0.0:20443/v2/pox | jq .current_cycle
  2. The result should be something like:
    {
    "id": 93,
    "min_threshold_ustx": 110000000000,
    "stacked_ustx": 421891523809040,
    "is_pox_active": true
    }
  3. Run 'curl https://api.hiro.so/extended/v2/pox/cycles/93 | jq'
  4. The result is:
    {
    "block_height": 165921,
    "index_block_hash": "0x27c464574930b0f6eddef653ec314f8ead17fdeef719ec130479f215e7d1b7d0",
    "cycle_number": 93,
    "total_weight": 3817,
    "total_stacked_amount": "421741523809040",
    "total_signers": 39
    }

Expected behavior
The fields 'stacked_ustx' and 'total_stacked_amount' should be identical, representing the total amount of stacked STX in the cycle.

@tippenein are you still interested in taking a look at this?

@janniks is it possible this is fixed by stacks-network/stacks-core#5227 ?

Yes, I believe so. I am check what the exact queries are now. This could be fixable with a migrate and additional check, or we wait until the patch is live on the node. After checking the total amount code, it seems we get this from .poxSetSigners (from CoreNodeBlockMessage reward_set signers).

Ok, figured it out:

The node /v2/pox doesn't show the anchored reward set (which is selected at the beginning of the prepare phase). Rather it shows the latest value of the pox contract state. So all the transactions which lock/stack in the prepare phase, aren't actually stacking (since they didn't make it into the state), but they are locked.

I hunted down the original event observer payload for this. And the sum is the same as the 'extended/v2/pox/cycles/93' total. No issues found on the API side.

Ref: Transaction, which skews the total for this specific example. (But this will often show different results, e.g. now again for cycle 94)


Here's a test to check based on different block height tips:

test('read total stacked from pox contract', async () => {
  const network = stacksNetwork();
  const alice = getAccount(ENV.PRIVATE_KEYS[0]);
  const client = new StackingClient('', network);
  const poxInfo = await client.getPoxInfo();

  const config = new Configuration({
    basePath: ENV.STACKS_API,
  });
  const api = new SmartContractsApi(config);

  const cycle = 93;
  const tip = 165966;

  const [contractAddress, contractName] = poxInfo.contract_id.split('.');

  const blockTip = await getStacksBlock(tip);
  const rawWithTip = await api.callReadOnlyFunction({
    contractAddress,
    contractName,
    functionName: 'get-total-ustx-stacked',
    readOnlyFunctionArgs: {
      arguments: [bytesToHex(Cl.serialize(Cl.uint(cycle)))],
      sender: alice.address,
    },
    tip: blockTip.index_block_hash.replace('0x', ''),
  });
  console.log(Cl.deserialize(rawWithTip.result as string));

  const blockTipBefore = await getStacksBlock(tip - 1);
  const rawBeforeTip = await api.callReadOnlyFunction({
    contractAddress,
    contractName,
    functionName: 'get-total-ustx-stacked',
    readOnlyFunctionArgs: {
      arguments: [bytesToHex(Cl.serialize(Cl.uint(cycle)))],
      sender: alice.address,
    },
    tip: blockTipBefore.index_block_hash.replace('0x', ''),
  });
  console.log(Cl.deserialize(rawBeforeTip.result as string));
});

@janniks so the API is showing the actual active stacked amount? And the node endpoint is including some newly stacked STX that is not yet active?

Sort of, yeah. The v2/pox endpoint shows the "current" stacked/locked amount (for the current cycle). But this number can be higher than the real/actual amount stacked. This is because cycles are "prepared" at the end of their previous cycle. The node chooses a point in time to "set the reward set in stone" (this sort of "locks-in" the total amount, the one that is sent to the API); however the prepare phase still has blocks remaining, which can't impact the next cycle anymore (because the reward set is set already); additional stacking transactions in these prepare phase blocks can lock stacks, but can't stack them for the next cycle. Unfortunately, the node shows the "latest" state, rather than the set in stone state. So, in this case the API shows the more accurate value.

Ok, thank you for the clarification! I'll be sure to update my metrics to use the API value instead