Add script for verifying block rewards and fees not claimed by miners.

Co-authored-by: Jack Grigg <jack@z.cash>
Co-authored-by: Daira Hopwood <daira@jacaranda.org>
This commit is contained in:
Kris Nuttycombe 2023-01-20 12:13:50 -07:00
parent 2fd52ada51
commit 810c191216
7 changed files with 30676 additions and 0 deletions

2
.gitignore vendored
View File

@ -130,3 +130,5 @@ src/fuzzing/*/output
src/fuzz.cpp
.updatecheck-token
.env
poetry.lock

View File

@ -0,0 +1,29 @@
[tool.poetry]
name = "zcash-metrics"
version = "0.1.0"
description = "Zcash Metrics"
authors = [
"Jack Grigg <jack@electriccoin.co>",
"Daira Hopwood <daira@jacaranda.org>",
"Kris Nuttycombe <kris@nutty.land>",
]
license = "MIT OR Apache-2.0"
readme = "README.md"
homepage = "https://github.com/zcash/zcash/"
repository = "https://github.com/zcash/zcash/"
documentation = "https://github.com/zcash/zcash/"
classifiers = [
"Private :: Do Not Upload",
]
packages = [
{ include = "supply_check" }
]
[tool.poetry.dependencies]
python = "^3.7"
slick-bitcoinrpc = "0.1.4"
progressbar2 = "4.2.0"
[tool.poetry.scripts]
supply-check = "supply_check:main"

View File

@ -0,0 +1,79 @@
#!/usr/bin/env python3
import os
import progressbar
from slickrpc.rpc import Proxy
from .deltas_mainnet import MainnetSupplyDeltas
from .theoretical import Network, MAINNET
COIN=100000000
# Returns the theoretical supply, the total block rewards claimed,
# and the block at the given height.
#
# - `zcashd` a slickrpc.rpc.Proxy that can be used to access zcashd
# - `deltas` a SupplyDeltas object tracking the deltas observed
# - `height` the block height to consider
# - `flag` Either `1` or `2`. This flag will be provided to the
# getblock RPC call to indicate how much data should be returned.
# When `1` is provided, data for transactions in the block will
# be limited to transaction identifiers; when `2` is provided
# full transaction data will be returned for each transaction in
# the block.
def TheoreticalAndEmpirical(zcashd, deltas, height, flag):
theoreticalSupply = Network(MAINNET).SupplyAfterHeight(height)
block = zcashd.getblock(str(height), flag)
measuredSupply = block['chainSupply']['chainValueZat']
empiricalMaximum = measuredSupply + deltas.DeviationAtHeight(height)
return (theoreticalSupply, empiricalMaximum, block)
# Returns `True` if the theoretical supply matches the empirically
# determined supply after accounting for known deltas over the specified
# range, or `False` if it was not possible to determine a miner address
# for a block containing unclaimed fee and/or block reward amounts.
#
# - `startHeight` The block height at which to begin checking
# - `endHeight` The end of the block height range to check (inclusive)
def Bisect(bar, zcashd, deltas, startHeight, endHeight):
assert startHeight <= endHeight
bar.update(startHeight)
flag = 2 if startHeight == endHeight else 1
(theoretical, empirical, block) = TheoreticalAndEmpirical(zcashd, deltas, endHeight, flag)
if theoretical == empirical:
return True
elif startHeight == endHeight:
return deltas.SaveMismatch(block, theoretical, empirical)
else:
midpoint = (startHeight + endHeight) // 2
return (Bisect(bar, zcashd, deltas, startHeight, midpoint) and
Bisect(bar, zcashd, deltas, midpoint + 1, endHeight))
def main():
zcashd = Proxy('http://{rpc_user}:{rpc_pass}@{rpc_host}:{rpc_port}'.format(
rpc_user=os.environ['ZCASHD_RPC_USER'],
rpc_pass=os.environ['ZCASHD_RPC_PASS'],
rpc_host=os.environ['ZCASHD_RPC_HOST'],
rpc_port=os.environ['ZCASHD_RPC_PORT'],
))
latestHeight = zcashd.getblockchaininfo()['blocks']
deltas = MainnetSupplyDeltas(zcashd)
(theoretical, empirical, block) = TheoreticalAndEmpirical(zcashd, deltas, latestHeight, 1)
if theoretical != empirical:
with progressbar.ProgressBar(max_value = latestHeight, redirect_stdout = True) as bar:
try:
Bisect(bar, zcashd, deltas, 0, latestHeight)
except KeyboardInterrupt:
pass
deltas.PrintDeltas()
print("Block height: {}".format(latestHeight))
print("Chain total value: {} ZEC".format(block['chainSupply']['chainValueZat'] / COIN))
print("Theoretical maximum supply: {} ZEC".format(theoretical / COIN))
print("Blocks with unclaimed balance: {}".format(deltas.delta_count))
print("Unclaimed total: {} ZEC".format(deltas.delta_total / COIN))
if __name__ == "__main__":
main()

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,104 @@
fr_addrs = set([
# FR addresses
"t3Vz22vK5z2LcKEdg16Yv4FFneEL1zg9ojd",
"t3cL9AucCajm3HXDhb5jBnJK2vapVoXsop3",
"t3fqvkzrrNaMcamkQMwAyHRjfDdM2xQvDTR",
"t3TgZ9ZT2CTSK44AnUPi6qeNaHa2eC7pUyF",
"t3SpkcPQPfuRYHsP5vz3Pv86PgKo5m9KVmx",
"t3Xt4oQMRPagwbpQqkgAViQgtST4VoSWR6S",
"t3ayBkZ4w6kKXynwoHZFUSSgXRKtogTXNgb",
"t3adJBQuaa21u7NxbR8YMzp3km3TbSZ4MGB",
"t3K4aLYagSSBySdrfAGGeUd5H9z5Qvz88t2",
"t3RYnsc5nhEvKiva3ZPhfRSk7eyh1CrA6Rk",
"t3Ut4KUq2ZSMTPNE67pBU5LqYCi2q36KpXQ",
"t3ZnCNAvgu6CSyHm1vWtrx3aiN98dSAGpnD",
"t3fB9cB3eSYim64BS9xfwAHQUKLgQQroBDG",
"t3cwZfKNNj2vXMAHBQeewm6pXhKFdhk18kD",
"t3YcoujXfspWy7rbNUsGKxFEWZqNstGpeG4",
"t3bLvCLigc6rbNrUTS5NwkgyVrZcZumTRa4",
"t3VvHWa7r3oy67YtU4LZKGCWa2J6eGHvShi",
"t3eF9X6X2dSo7MCvTjfZEzwWrVzquxRLNeY",
"t3esCNwwmcyc8i9qQfyTbYhTqmYXZ9AwK3X",
"t3M4jN7hYE2e27yLsuQPPjuVek81WV3VbBj",
"t3gGWxdC67CYNoBbPjNvrrWLAWxPqZLxrVY",
"t3LTWeoxeWPbmdkUD3NWBquk4WkazhFBmvU",
"t3P5KKX97gXYFSaSjJPiruQEX84yF5z3Tjq",
"t3f3T3nCWsEpzmD35VK62JgQfFig74dV8C9",
"t3Rqonuzz7afkF7156ZA4vi4iimRSEn41hj",
"t3fJZ5jYsyxDtvNrWBeoMbvJaQCj4JJgbgX",
"t3Pnbg7XjP7FGPBUuz75H65aczphHgkpoJW",
"t3WeKQDxCijL5X7rwFem1MTL9ZwVJkUFhpF",
"t3Y9FNi26J7UtAUC4moaETLbMo8KS1Be6ME",
"t3aNRLLsL2y8xcjPheZZwFy3Pcv7CsTwBec",
"t3gQDEavk5VzAAHK8TrQu2BWDLxEiF1unBm",
"t3Rbykhx1TUFrgXrmBYrAJe2STxRKFL7G9r",
"t3aaW4aTdP7a8d1VTE1Bod2yhbeggHgMajR",
"t3YEiAa6uEjXwFL2v5ztU1fn3yKgzMQqNyo",
"t3g1yUUwt2PbmDvMDevTCPWUcbDatL2iQGP",
"t3dPWnep6YqGPuY1CecgbeZrY9iUwH8Yd4z",
"t3QRZXHDPh2hwU46iQs2776kRuuWfwFp4dV",
"t3enhACRxi1ZD7e8ePomVGKn7wp7N9fFJ3r",
"t3PkLgT71TnF112nSwBToXsD77yNbx2gJJY",
"t3LQtHUDoe7ZhhvddRv4vnaoNAhCr2f4oFN",
"t3fNcdBUbycvbCtsD2n9q3LuxG7jVPvFB8L",
"t3dKojUU2EMjs28nHV84TvkVEUDu1M1FaEx",
"t3aKH6NiWN1ofGd8c19rZiqgYpkJ3n679ME",
"t3MEXDF9Wsi63KwpPuQdD6by32Mw2bNTbEa",
"t3WDhPfik343yNmPTqtkZAoQZeqA83K7Y3f",
"t3PSn5TbMMAEw7Eu36DYctFezRzpX1hzf3M",
"t3R3Y5vnBLrEn8L6wFjPjBLnxSUQsKnmFpv",
"t3Pcm737EsVkGTbhsu2NekKtJeG92mvYyoN",
# ECC funding stream addresses
"t3LmX1cxWPPPqL4TZHx42HU3U5ghbFjRiif",
"t3Toxk1vJQ6UjWQ42tUJz2rV2feUWkpbTDs",
"t3ZBdBe4iokmsjdhMuwkxEdqMCFN16YxKe6",
"t3ZuaJziLM8xZ32rjDUzVjVtyYdDSz8GLWB",
"t3bAtYWa4bi8VrtvqySxnbr5uqcG9czQGTZ",
"t3dktADfb5Rmxncpe1HS5BRS5Gcj7MZWYBi",
"t3hgskquvKKoCtvxw86yN7q8bzwRxNgUZmc",
"t3R1VrLzwcxAZzkX4mX3KGbWpNsgtYtMntj",
"t3ff6fhemqPMVujD3AQurxRxTdvS1pPSaa2",
"t3cEUQFG3KYnFG6qYhPxSNgGi3HDjUPwC3J",
"t3WR9F5U4QvUFqqx9zFmwT6xFqduqRRXnaa",
"t3PYc1LWngrdUrJJbHkYPCKvJuvJjcm85Ch",
"t3bgkjiUeatWNkhxY3cWyLbTxKksAfk561R",
"t3Z5rrR8zahxUpZ8itmCKhMSfxiKjUp5Dk5",
"t3PU1j7YW3fJ67jUbkGhSRto8qK2qXCUiW3",
"t3S3yaT7EwNLaFZCamfsxxKwamQW2aRGEkh",
"t3eutXKJ9tEaPSxZpmowhzKhPfJvmtwTEZK",
"t3gbTb7brxLdVVghSPSd3ycGxzHbUpukeDm",
"t3UCKW2LrHFqPMQFEbZn6FpjqnhAAbfpMYR",
"t3NyHsrnYbqaySoQqEQRyTWkjvM2PLkU7Uu",
"t3QEFL6acxuZwiXtW3YvV6njDVGjJ1qeaRo",
"t3PdBRr2S1XTDzrV8bnZkXF3SJcrzHWe1wj",
"t3ZWyRPpWRo23pKxTLtWsnfEKeq9T4XPxKM",
"t3he6QytKCTydhpztykFsSsb9PmBT5JBZLi",
"t3VWxWDsLb2TURNEP6tA1ZSeQzUmPKFNxRY",
"t3NmWLvZkbciNAipauzsFRMxoZGqmtJksbz",
"t3cKr4YxVPvPBG1mCvzaoTTdBNokohsRJ8n",
"t3T3smGZn6BoSFXWWXa1RaoQdcyaFjMfuYK",
"t3gkDUe9Gm4GGpjMk86TiJZqhztBVMiUSSA",
"t3eretuBeBXFHe5jAqeSpUS1cpxVh51fAeb",
"t3dN8g9zi2UGJdixGe9txeSxeofLS9t3yFQ",
"t3S799pq9sYBFwccRecoTJ3SvQXRHPrHqvx",
"t3fhYnv1S5dXwau7GED3c1XErzt4n4vDxmf",
"t3cmE3vsBc5xfDJKXXZdpydCPSdZqt6AcNi",
"t3h5fPdjJVHaH4HwynYDM5BB3J7uQaoUwKi",
"t3Ma35c68BgRX8sdLDJ6WR1PCrKiWHG4Da9",
"t3LokMKPL1J8rkJZvVpfuH7dLu6oUWqZKQK",
"t3WFFGbEbhJWnASZxVLw2iTJBZfJGGX73mM",
"t3L8GLEsUn4QHNaRYcX3EGyXmQ8kjpT1zTa",
"t3PgfByBhaBSkH8uq4nYJ9ZBX4NhGCJBVYm",
"t3WecsqKDhWXD4JAgBVcnaCC2itzyNZhJrv",
"t3ZG9cSfopnsMQupKW5v9sTotjcP5P6RTbn",
"t3hC1Ywb5zDwUYYV8LwhvF5rZ6m49jxXSG5",
"t3VgMqDL15ZcyQDeqBsBW3W6rzfftrWP2yB",
"t3LC94Y6BwLoDtBoK2NuewaEbnko1zvR9rm",
"t3cWCUZJR3GtALaTcatrrpNJ3MGbMFVLRwQ",
"t3YYF4rPLVxDcF9hHFsXyc5Yq1TFfbojCY6",
"t3XHAGxRP2FNfhAjxGjxbrQPYtQQjc3RCQD",
# ZF funding stream addresses
"t3dvVE3SQEi7kqNzwrfNePxZ1d4hUyztBA1",
# MG funding stream addresses
"t3XyYW8yBFRuMnfvm5KLGFbEVz25kckZXym",
])

View File

@ -0,0 +1,73 @@
import pprint
class SupplyDeltas:
def __init__(self, zcashd, fr_addrs, miner_deltas, flush_interval = 500):
self.zcashd = zcashd
self.fr_addrs = fr_addrs
self.miner_deltas = miner_deltas
self.delta_count = 0
self.delta_total = 0
self.delta_cache = []
self.flush_interval = flush_interval
deltas_flat = [pair for deltas in miner_deltas.values() for pair in deltas]
for (deltaHeight, delta) in sorted(deltas_flat):
self.AddSupplyDelta(deltaHeight, delta)
def AddSupplyDelta(self, deltaHeight, delta):
assert len(self.delta_cache) == 0 or deltaHeight > self.delta_cache[-1][0]
self.delta_count += 1
self.delta_total += delta
self.delta_cache.append((deltaHeight, self.delta_total))
def DeviationAtHeight(self, height):
# search for the cached entry at the maximum deltaHeight <= height
def binary_search(start, end, max_found):
if start >= end:
if max_found:
return max_found[1]
else:
return 0
else:
mid = (start + end) // 2
item = self.delta_cache[mid]
if item[0] <= height:
return binary_search(mid+1, end, item)
else:
return binary_search(start, mid, max_found)
return binary_search(0, len(self.delta_cache), None)
def SaveMismatch(self, block, theoretical, empirical):
height = block['height']
coinbase_tx = block['tx'][0]
delta = theoretical - empirical
print('Mismatch at height {}: {} != {} ({}) miner_deltas: {}'.format(
height,
theoretical,
empirical,
delta,
len(self.miner_deltas),
))
# if delta ever goes negative, we will halt
if delta >= 0:
miner_addrs = set([addr for out in coinbase_tx['vout'] for addr in out['scriptPubKey'].get('addresses', [])]) - self.fr_addrs
if len(miner_addrs) > 0:
self.miner_deltas.setdefault(",".join(sorted(miner_addrs)), []).append((height, delta))
self.AddSupplyDelta(height, delta)
if self.delta_count % 500 == 0:
with open("delta_cache.{}.out".format(self.delta_count), 'w', encoding="utf8") as f:
pprint.pprint(self.miner_deltas, stream = f, indent = 4)
return True
pprint.pprint(coinbase_tx['vout'], indent = 4)
return False
def PrintDeltas(self):
with open("delta_cache.out", 'w', encoding="utf8") as f:
pprint.pprint(self.miner_deltas, stream = f, indent = 4)

View File

@ -0,0 +1,67 @@
def exact_div(x, y):
assert x % y == 0
return x // y
# floor(u/x + v/y)
def div2(u, x, v, y):
return (u*y + v*x) // (x*y)
TESTNET = 0
MAINNET = 1
class Network:
# <https://zips.z.cash/protocol/protocol.pdf#constants>
def __init__(self, network):
self.BlossomActivationHeight = 653600 if network == MAINNET else 584000
SlowStartInterval = 20000
MaxBlockSubsidy = 1250000000 # 12.5 ZEC
PreBlossomHalvingInterval = 840000
PreBlossomPoWTargetSpacing = 150
PostBlossomPoWTargetSpacing = 75
# <https://zips.z.cash/protocol/protocol.pdf#diffadjustment>
def IsBlossomActivated(self, height):
return height >= self.BlossomActivationHeight
BlossomPoWTargetSpacingRatio = exact_div(PreBlossomPoWTargetSpacing, PostBlossomPoWTargetSpacing)
# no need for floor since this is necessarily an integer
PostBlossomHalvingInterval = PreBlossomHalvingInterval * BlossomPoWTargetSpacingRatio
# <https://zips.z.cash/protocol/protocol.pdf#subsidies>
SlowStartShift = exact_div(SlowStartInterval, 2)
SlowStartRate = exact_div(MaxBlockSubsidy, SlowStartInterval)
SupplyCache = []
def Halving(self, height):
if height < self.SlowStartShift:
return 0
elif not self.IsBlossomActivated(height):
return (height - self.SlowStartShift) // self.PreBlossomHalvingInterval
else:
return div2(self.BlossomActivationHeight - self.SlowStartShift, self.PreBlossomHalvingInterval,
height - self.BlossomActivationHeight, self.PostBlossomHalvingInterval)
def BlockSubsidy(self, height):
if height < self.SlowStartShift:
return self.SlowStartRate * height
elif self.SlowStartShift <= height and height < self.SlowStartInterval:
return self.SlowStartRate * (height + 1)
if self.SlowStartInterval <= height and not self.IsBlossomActivated(height):
return self.MaxBlockSubsidy // (1 << self.Halving(height))
else:
return self.MaxBlockSubsidy // (self.BlossomPoWTargetSpacingRatio << self.Halving(height))
def SupplyAfterHeight(self, height):
cacheLen = len(self.SupplyCache)
if cacheLen > height:
return self.SupplyCache[height]
else:
cur = 0 if cacheLen == 0 else self.SupplyCache[-1]
for h in range(cacheLen, height + 1):
cur += self.BlockSubsidy(h)
self.SupplyCache.append(cur)
return self.SupplyCache[-1]