Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

signed overflow in coinstats index #26362

Open
ajtowns opened this issue Oct 21, 2022 · 8 comments · May be fixed by #26426
Open

signed overflow in coinstats index #26362

ajtowns opened this issue Oct 21, 2022 · 8 comments · May be fixed by #26426

Comments

@ajtowns
Copy link
Contributor

ajtowns commented Oct 21, 2022

CAmount m_total_prevout_spent_amount{0};
CAmount m_total_new_outputs_ex_coinbase_amount{0};
CAmount m_total_coinbase_amount{0};

//! Total cumulative amount of prevouts spent up to and including this block
CAmount total_prevout_spent_amount{0};
//! Total cumulative amount of outputs created up to and including this block
CAmount total_new_outputs_ex_coinbase_amount{0};
//! Total cumulative amount of coinbase outputs up to and including this block
CAmount total_coinbase_amount{0};

The values:

  • m_total_prevout_spent_amount
  • m_total_new_outputs_ex_coinbase_amount
  • m_total_coinbase_amount

are all subject to overflowing an int64_t (ie, CAmount). This is visible in practice as of signet block 112516; with total_prevout_spent rolling over to -92230771076.81494784 (from from 92232270378.54920553 BTC at block 112515). With debug builds, this will cause a crash due to the -ftrapv compiler option.

Making these just be per-block totals instead of cumulative totals should fix this in practice. I think that would require a full index rebuild, though?

Even then I think it would still be technically possible to overflow m_block_prevout_spent_amount or m_total_new_outputs_ex_coinbase_amount with a single block: you can fit over 15,000 65-byte transactions spending to padding DROP TRUE in a block, and if you start with a utxo containing 6.15M BTC and chain it through 15,000 txs, then that will overflow a signed 64 bit counter. That's not reproducible on regtest, though, since the halving schedule there limits the total supply to ~15,000 regtest-BTC, which means you need about 6.15M transactions to trigger the same overflow.

Since we don't actually expose the cumulative totals; I observed the values above by patching gettxoutsetinfo in rpc/blockchain to add:

/*line ~877*/    {RPCResult::Type::STR_AMOUNT, "total_prevout_spent", "Total amount of all prevouts spent up to this block"},
/*line ~981*/    block_info.pushKV("total_prevout_spent", ValueFromAmount(stats.total_prevout_spent_amount));
@ajtowns
Copy link
Contributor Author

ajtowns commented Oct 21, 2022

cc @fjahr

@mzumsande
Copy link
Contributor

This can also be reproduced with the undefined sanitizer.
Keeping running totals but using a user-defined datatype that is larger than int64_t could be an alternative fix. Though that would also require the index to be rebuilt - I can't think of a way to fix this without a breaking change to the index.

@maflcko
Copy link
Member

maflcko commented Oct 22, 2022

Is it important to report this number? Why not remove it from the RPC result?

@fjahr
Copy link
Contributor

fjahr commented Oct 23, 2022

Since we don't actually expose the cumulative totals

FWIW, they would be possible to get the cumulative numbers with #19792. The reason to use them rather than the per block values, was some of the following review feedback:

I haven't gotten much feedback on use of the index lately and the use of the cumulative numbers was only in combination with not-yet-merged #19792 so I think removing the cumulative numbers should not be causing too many complaints. Pinging @PierreRochard

Is it important to report this number? Why not remove it from the RPC result?

IIRC users that were interested in auditing the coin supply in detail were very interested in this, so I would much rather like to fix it than remove it.

I can't think of a way to fix this without a breaking change to the index.

Me neither as of now but I had some ideas on "gracefully" upgrading indices because there were so many ideas floating around for coinstatsindex two years ago. Will check if that is valuable in any way.

@maflcko
Copy link
Member

maflcko commented Oct 24, 2022

IIRC users that were interested in auditing the coin supply in detail were very interested in this, so I would much rather like to fix it than remove it.

Wouldn't it be easier to audit the supply if the equivalent metrics are reported: fee, subsidy, unspendable, unclaimed. At least per-block those should all be less than MAX_MONEY.

@ajtowns
Copy link
Contributor Author

ajtowns commented Oct 24, 2022

I don't think a total/per-block cumulative prevout_spent_amount is very interesting for auditing purposes -- it's easy to produce large values just by cycling funds to yourself. I think the interesting identities are sum(coinbase-fees) + sum(unclaimed) = sum(subsidy) = spendable + unspendable and each of those values and each of the sums are bounded by MAX_MONEY (though sum(coinbase) and sum(fees) independently would be unbounded).

Note that total_out in getblockstats has the same edge-case. Adding it was suggested at #10757 (comment)

@fjahr fjahr linked a pull request Oct 31, 2022 that will close this issue
@fjahr
Copy link
Contributor

fjahr commented Oct 31, 2022

I have a draft for changing the cumulative values to be per block. So far I have not gotten any feedback but I think this should be ok to change as the feedback in the original PR that I linked above has not making it sound like a hard requirement to have cumulative values. I will clean up the PR and test it more thoroughly in the next couple of days but I tested it on signet already and should fix the issue there.

For the approach to upgrade: my current thinking is that it is probably best to build the new version of the index in a new folder and give the user a notice in the logs that they can delete the old index folder. While many users may have this useless data on their disk for a while because they ignore the logs, it seems like the cleanest approach to me. Of course the old index folder can also be deleted automatically but I am not sure this is desirable. I am thinking of users that may jump between different versions of core, maybe the latest release and master. When switching between versions before and after the upgrade, this approach should produce the least side effects. Worst case I think they delete the old index after the reading of the log message but then when they switch back to an older version they see the old index being rebuilt. But if the new index is built in the same folder, it would be much worse and the index would be corrupted leading to very confusing issues for these users. I am sure something could be done to mitigate that but it seems like much more complex solution that is not absolutely necessary.

Happy to hear your feedback, especially on the approach to upgrade!

@luke-jr
Copy link
Member

luke-jr commented Nov 5, 2022

Upgrade approach looks okay to me for this, but maybe the index format should be rethought so new fields can be added without repeating it again.

Also I'm wondering if maybe there should be a blockhash<->id mapping shared by all the indexes...

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging a pull request may close this issue.

5 participants