claim takeovers

This commit is contained in:
Jack Robison 2021-05-20 13:31:40 -04:00 committed by Victor Shyba
parent 6aa124592d
commit e678df86e0
6 changed files with 855 additions and 776 deletions

View file

@ -4,23 +4,24 @@ import typing
from bisect import bisect_right from bisect import bisect_right
from struct import pack, unpack from struct import pack, unpack
from concurrent.futures.thread import ThreadPoolExecutor from concurrent.futures.thread import ThreadPoolExecutor
from typing import Optional, List, Tuple from typing import Optional, List, Tuple, Set, DefaultDict, Dict
from prometheus_client import Gauge, Histogram from prometheus_client import Gauge, Histogram
from collections import defaultdict from collections import defaultdict
import lbry import lbry
from lbry.schema.claim import Claim from lbry.schema.claim import Claim
from lbry.wallet.transaction import OutputScript, Output from lbry.wallet.transaction import OutputScript, Output
from lbry.wallet.server.tx import Tx from lbry.wallet.server.tx import Tx, TxOutput, TxInput
from lbry.wallet.server.daemon import DaemonError from lbry.wallet.server.daemon import DaemonError
from lbry.wallet.server.hash import hash_to_hex_str, HASHX_LEN from lbry.wallet.server.hash import hash_to_hex_str, HASHX_LEN
from lbry.wallet.server.util import chunks, class_logger from lbry.wallet.server.util import chunks, class_logger
from lbry.crypto.hash import hash160 from lbry.crypto.hash import hash160
from lbry.wallet.server.leveldb import FlushData from lbry.wallet.server.leveldb import FlushData
from lbry.wallet.server.db import DB_PREFIXES from lbry.wallet.server.db import DB_PREFIXES
from lbry.wallet.server.db.claimtrie import StagedClaimtrieItem, StagedClaimtrieSupport, get_expiration_height from lbry.wallet.server.db.claimtrie import StagedClaimtrieItem, StagedClaimtrieSupport
from lbry.wallet.server.db.claimtrie import get_takeover_name_ops, get_force_activate_ops, get_delay_for_name from lbry.wallet.server.db.claimtrie import get_takeover_name_ops, StagedActivation
from lbry.wallet.server.db.prefixes import PendingClaimActivationPrefixRow, Prefixes from lbry.wallet.server.db.claimtrie import get_remove_name_ops
from lbry.wallet.server.db.revertable import RevertablePut from lbry.wallet.server.db.prefixes import ACTIVATED_SUPPORT_TXO_TYPE, ACTIVATED_CLAIM_TXO_TYPE
from lbry.wallet.server.db.prefixes import PendingActivationKey, PendingActivationValue
from lbry.wallet.server.udp import StatusServer from lbry.wallet.server.udp import StatusServer
if typing.TYPE_CHECKING: if typing.TYPE_CHECKING:
from lbry.wallet.server.leveldb import LevelDB from lbry.wallet.server.leveldb import LevelDB
@ -204,13 +205,19 @@ class BlockProcessor:
self.search_cache = {} self.search_cache = {}
self.history_cache = {} self.history_cache = {}
self.status_server = StatusServer() self.status_server = StatusServer()
self.effective_amount_changes = defaultdict(list)
self.pending_claims: typing.Dict[Tuple[int, int], StagedClaimtrieItem] = {} self.pending_claims: typing.Dict[Tuple[int, int], StagedClaimtrieItem] = {}
self.pending_claim_txos: typing.Dict[bytes, Tuple[int, int]] = {} self.pending_claim_txos: typing.Dict[bytes, Tuple[int, int]] = {}
self.pending_supports = defaultdict(set) self.pending_supports = defaultdict(list)
self.pending_support_txos = {} self.pending_support_txos = {}
self.pending_abandon = set()
self.staged_pending_abandoned = {} self.pending_removed_support = defaultdict(lambda: defaultdict(list))
self.staged_pending_abandoned: Dict[bytes, StagedClaimtrieItem] = {}
self.removed_active_support = defaultdict(list)
self.staged_activated_support = defaultdict(list)
self.staged_activated_claim = {}
self.pending_activated = defaultdict(lambda: defaultdict(list))
async def run_in_thread_with_lock(self, func, *args): async def run_in_thread_with_lock(self, func, *args):
# Run in a thread to prevent blocking. Shielded so that # Run in a thread to prevent blocking. Shielded so that
@ -241,6 +248,7 @@ class BlockProcessor:
try: try:
for block in blocks: for block in blocks:
await self.run_in_thread_with_lock(self.advance_block, block) await self.run_in_thread_with_lock(self.advance_block, block)
print("******************\n")
except: except:
self.logger.exception("advance blocks failed") self.logger.exception("advance blocks failed")
raise raise
@ -363,7 +371,6 @@ class BlockProcessor:
return start, count return start, count
# - Flushing # - Flushing
def flush_data(self): def flush_data(self):
"""The data for a flush. The lock must be taken.""" """The data for a flush. The lock must be taken."""
@ -386,461 +393,448 @@ class BlockProcessor:
await self.flush(True) await self.flush(True)
self.next_cache_check = time.perf_counter() + 30 self.next_cache_check = time.perf_counter() + 30
def check_cache_size(self): def _add_claim_or_update(self, height: int, txo: 'Output', tx_hash: bytes, tx_num: int, nout: int,
"""Flush a cache if it gets too big.""" spent_claims: typing.Dict[bytes, typing.Tuple[int, int, str]]) -> List['RevertableOp']:
# Good average estimates based on traversal of subobjects and
# requesting size from Python (see deep_getsizeof).
one_MB = 1000*1000
utxo_cache_size = len(self.utxo_cache) * 205
db_deletes_size = len(self.db_deletes) * 57
hist_cache_size = len(self.db.hist_unflushed) * 180 + self.db.hist_unflushed_count * 4
# Roughly ntxs * 32 + nblocks * 42
tx_hash_size = ((self.tx_count - self.db.fs_tx_count) * 32
+ (self.height - self.db.fs_height) * 42)
utxo_MB = (db_deletes_size + utxo_cache_size) // one_MB
hist_MB = (hist_cache_size + tx_hash_size) // one_MB
self.logger.info('our height: {:,d} daemon: {:,d} '
'UTXOs {:,d}MB hist {:,d}MB'
.format(self.height, self.daemon.cached_height(),
utxo_MB, hist_MB))
# Flush history if it takes up over 20% of cache memory.
# Flush UTXOs once they take up 80% of cache memory.
cache_MB = self.env.cache_MB
if utxo_MB + hist_MB >= cache_MB or hist_MB >= cache_MB // 5:
return utxo_MB >= cache_MB * 4 // 5
return None
def _add_claim_or_update(self, height: int, txo, script, tx_hash: bytes, idx: int, tx_count: int, txout,
spent_claims: typing.Dict[bytes, typing.Tuple[int, int, str]],
zero_delay_claims: typing.Dict[Tuple[str, bytes], Tuple[int, int]]) -> List['RevertableOp']:
try: try:
claim_name = txo.normalized_name claim_name = txo.normalized_name
except UnicodeDecodeError: except UnicodeDecodeError:
claim_name = ''.join(chr(c) for c in txo.script.values['claim_name']) claim_name = ''.join(chr(c) for c in txo.script.values['claim_name'])
if script.is_claim_name: if txo.script.is_claim_name:
claim_hash = hash160(tx_hash + pack('>I', idx))[::-1] claim_hash = hash160(tx_hash + pack('>I', nout))[::-1]
# print(f"\tnew lbry://{claim_name}#{claim_hash.hex()} ({tx_count} {txout.value})") print(f"\tnew lbry://{claim_name}#{claim_hash.hex()} ({tx_num} {txo.amount})")
else: else:
claim_hash = txo.claim_hash[::-1] claim_hash = txo.claim_hash[::-1]
print(f"\tupdate lbry://{claim_name}#{claim_hash.hex()} ({tx_num} {txo.amount})")
signing_channel_hash = None
channel_claims_count = 0
activation_delay = self.db.get_activation_delay(claim_hash, claim_name)
if activation_delay == 0:
zero_delay_claims[(claim_name, claim_hash)] = tx_count, idx
# else:
# print("delay activation ", claim_name, activation_delay, height)
activation_height = activation_delay + height
try: try:
signable = txo.signable signable = txo.signable
except: # google.protobuf.message.DecodeError: Could not parse JSON. except: # google.protobuf.message.DecodeError: Could not parse JSON.
signable = None signable = None
ops = []
signing_channel_hash = None
if signable and signable.signing_channel_hash: if signable and signable.signing_channel_hash:
signing_channel_hash = txo.signable.signing_channel_hash[::-1] signing_channel_hash = txo.signable.signing_channel_hash[::-1]
# if signing_channel_hash in self.pending_claim_txos: if txo.script.is_claim_name:
# pending_channel = self.pending_claims[self.pending_claim_txos[signing_channel_hash]] root_tx_num, root_idx = tx_num, nout
# channel_claims_count = pending_channel.
channel_claims_count = self.db.get_claims_in_channel_count(signing_channel_hash) + 1
if script.is_claim_name:
support_amount = 0
root_tx_num, root_idx = tx_count, idx
else: else:
if claim_hash not in spent_claims: if claim_hash not in spent_claims:
print(f"\tthis is a wonky tx, contains unlinked claim update {claim_hash.hex()}") print(f"\tthis is a wonky tx, contains unlinked claim update {claim_hash.hex()}")
return [] return []
support_amount = self.db.get_support_amount(claim_hash)
(prev_tx_num, prev_idx, _) = spent_claims.pop(claim_hash) (prev_tx_num, prev_idx, _) = spent_claims.pop(claim_hash)
# print(f"\tupdate lbry://{claim_name}#{claim_hash.hex()} {tx_hash[::-1].hex()} {txout.value}") print(f"\tupdate lbry://{claim_name}#{claim_hash.hex()} {tx_hash[::-1].hex()} {txo.amount}")
if (prev_tx_num, prev_idx) in self.pending_claims: if (prev_tx_num, prev_idx) in self.pending_claims:
previous_claim = self.pending_claims.pop((prev_tx_num, prev_idx)) previous_claim = self.pending_claims.pop((prev_tx_num, prev_idx))
root_tx_num = previous_claim.root_claim_tx_num root_tx_num, root_idx = previous_claim.root_claim_tx_num, previous_claim.root_claim_tx_position
root_idx = previous_claim.root_claim_tx_position
# prev_amount = previous_claim.amount
else: else:
k, v = self.db.get_root_claim_txo_and_current_amount( k, v = self.db.get_claim_txo(
claim_hash claim_hash
) )
root_tx_num = v.root_tx_num root_tx_num, root_idx = v.root_tx_num, v.root_position
root_idx = v.root_position activation = self.db.get_activation(prev_tx_num, prev_idx)
prev_amount = v.amount ops.extend(
StagedActivation(
ACTIVATED_CLAIM_TXO_TYPE, claim_hash, prev_tx_num, prev_idx, activation, claim_name, v.amount
).get_remove_activate_ops()
)
pending = StagedClaimtrieItem( pending = StagedClaimtrieItem(
claim_name, claim_hash, txout.value, support_amount + txout.value, claim_name, claim_hash, txo.amount, self.coin.get_expiration_height(height), tx_num, nout, root_tx_num,
activation_height, get_expiration_height(height), tx_count, idx, root_tx_num, root_idx, root_idx, signing_channel_hash
signing_channel_hash, channel_claims_count
) )
self.pending_claims[(tx_num, nout)] = pending
self.pending_claims[(tx_count, idx)] = pending self.pending_claim_txos[claim_hash] = (tx_num, nout)
self.pending_claim_txos[claim_hash] = (tx_count, idx) ops.extend(pending.get_add_claim_utxo_ops())
self.effective_amount_changes[claim_hash].append(txout.value)
return pending.get_add_claim_utxo_ops()
def _add_support(self, height, txo, txout, idx, tx_count,
zero_delay_claims: typing.Dict[Tuple[str, bytes], Tuple[int, int]]) -> List['RevertableOp']:
supported_claim_hash = txo.claim_hash[::-1]
claim_info = self.db.get_root_claim_txo_and_current_amount(
supported_claim_hash
)
controlling_claim = None
supported_tx_num = supported_position = supported_activation_height = supported_name = None
if claim_info:
k, v = claim_info
supported_name = v.name
supported_tx_num = k.tx_num
supported_position = k.position
supported_activation_height = v.activation
controlling_claim = self.db.get_controlling_claim(v.name)
if supported_claim_hash in self.effective_amount_changes:
# print(f"\tsupport claim {supported_claim_hash.hex()} {txout.value}")
self.effective_amount_changes[supported_claim_hash].append(txout.value)
self.pending_supports[supported_claim_hash].add((tx_count, idx))
self.pending_support_txos[(tx_count, idx)] = supported_claim_hash, txout.value
return StagedClaimtrieSupport(
supported_claim_hash, tx_count, idx, txout.value
).get_add_support_utxo_ops()
elif supported_claim_hash not in self.pending_claims and supported_claim_hash not in self.pending_abandon:
# print(f"\tsupport claim {supported_claim_hash.hex()} {txout.value}")
ops = []
if claim_info:
starting_amount = self.db.get_effective_amount(supported_claim_hash)
if supported_claim_hash not in self.effective_amount_changes:
self.effective_amount_changes[supported_claim_hash].append(starting_amount)
self.effective_amount_changes[supported_claim_hash].append(txout.value)
supported_amount = self._get_pending_effective_amount(supported_claim_hash)
if controlling_claim and supported_claim_hash != controlling_claim.claim_hash:
if supported_amount + txo.amount > self._get_pending_effective_amount(controlling_claim.claim_hash):
# takeover could happen
if (supported_name, supported_claim_hash) not in zero_delay_claims:
takeover_delay = get_delay_for_name(height - supported_activation_height)
if takeover_delay == 0:
zero_delay_claims[(supported_name, supported_claim_hash)] = (
supported_tx_num, supported_position
)
else:
ops.append(
RevertablePut(
*Prefixes.pending_activation.pack_item(
height + takeover_delay, supported_tx_num, supported_position,
supported_claim_hash, supported_name
)
)
)
self.pending_supports[supported_claim_hash].add((tx_count, idx))
self.pending_support_txos[(tx_count, idx)] = supported_claim_hash, txout.value
# print(f"\tsupport claim {supported_claim_hash.hex()} {starting_amount}+{txout.value}={starting_amount + txout.value}")
ops.extend(StagedClaimtrieSupport(
supported_claim_hash, tx_count, idx, txout.value
).get_add_support_utxo_ops())
return ops return ops
else:
print(f"\tthis is a wonky tx, contains unlinked support for non existent {supported_claim_hash.hex()}") def _add_support(self, txo: 'Output', tx_num: int, nout: int) -> List['RevertableOp']:
supported_claim_hash = txo.claim_hash[::-1]
self.pending_supports[supported_claim_hash].append((tx_num, nout))
self.pending_support_txos[(tx_num, nout)] = supported_claim_hash, txo.amount
print(f"\tsupport claim {supported_claim_hash.hex()} +{txo.amount}")
return StagedClaimtrieSupport(
supported_claim_hash, tx_num, nout, txo.amount
).get_add_support_utxo_ops()
def _add_claim_or_support(self, height: int, tx_hash: bytes, tx_num: int, nout: int, txo: 'Output',
spent_claims: typing.Dict[bytes, Tuple[int, int, str]]) -> List['RevertableOp']:
if txo.script.is_claim_name or txo.script.is_update_claim:
return self._add_claim_or_update(height, txo, tx_hash, tx_num, nout, spent_claims)
elif txo.script.is_support_claim or txo.script.is_support_claim_data:
return self._add_support(txo, tx_num, nout)
return [] return []
def _add_claim_or_support(self, height: int, tx_hash: bytes, tx_count: int, idx: int, txo, txout, script, def _spend_support_txo(self, txin):
spent_claims: typing.Dict[bytes, Tuple[int, int, str]],
zero_delay_claims: typing.Dict[Tuple[str, bytes], Tuple[int, int]]) -> List['RevertableOp']:
if script.is_claim_name or script.is_update_claim:
return self._add_claim_or_update(height, txo, script, tx_hash, idx, tx_count, txout, spent_claims,
zero_delay_claims)
elif script.is_support_claim or script.is_support_claim_data:
return self._add_support(height, txo, txout, idx, tx_count, zero_delay_claims)
return []
def _remove_support(self, txin, zero_delay_claims):
txin_num = self.db.transaction_num_mapping[txin.prev_hash] txin_num = self.db.transaction_num_mapping[txin.prev_hash]
supported_name = None
if (txin_num, txin.prev_idx) in self.pending_support_txos: if (txin_num, txin.prev_idx) in self.pending_support_txos:
spent_support, support_amount = self.pending_support_txos.pop((txin_num, txin.prev_idx)) spent_support, support_amount = self.pending_support_txos.pop((txin_num, txin.prev_idx))
supported_name = self._get_pending_claim_name(spent_support)
self.pending_supports[spent_support].remove((txin_num, txin.prev_idx)) self.pending_supports[spent_support].remove((txin_num, txin.prev_idx))
else:
spent_support, support_amount = self.db.get_supported_claim_from_txo(txin_num, txin.prev_idx)
if spent_support:
supported_name = self._get_pending_claim_name(spent_support) supported_name = self._get_pending_claim_name(spent_support)
print(f"\tspent support for lbry://{supported_name}#{spent_support.hex()}")
if spent_support and support_amount is not None and spent_support not in self.pending_abandon: self.pending_removed_support[supported_name][spent_support].append((txin_num, txin.prev_idx))
controlling = self.db.get_controlling_claim(supported_name)
if controlling:
bid_queue = {
claim_hash: self._get_pending_effective_amount(claim_hash)
for claim_hash in self.db.get_claims_for_name(supported_name)
if claim_hash not in self.pending_abandon
}
bid_queue[spent_support] -= support_amount
sorted_claims = sorted(
list(bid_queue.keys()), key=lambda claim_hash: bid_queue[claim_hash], reverse=True
)
if controlling.claim_hash == spent_support and sorted_claims.index(controlling.claim_hash) > 0:
print("takeover due to abandoned support")
# print(f"\tspent support for {spent_support.hex()} -{support_amount} ({txin_num}, {txin.prev_idx}) {supported_name}")
if spent_support not in self.effective_amount_changes:
assert spent_support not in self.pending_claims
prev_effective_amount = self.db.get_effective_amount(spent_support)
self.effective_amount_changes[spent_support].append(prev_effective_amount)
self.effective_amount_changes[spent_support].append(-support_amount)
return StagedClaimtrieSupport( return StagedClaimtrieSupport(
spent_support, txin_num, txin.prev_idx, support_amount spent_support, txin_num, txin.prev_idx, support_amount
).get_spend_support_txo_ops() ).get_spend_support_txo_ops()
spent_support, support_amount = self.db.get_supported_claim_from_txo(txin_num, txin.prev_idx)
if spent_support:
supported_name = self._get_pending_claim_name(spent_support)
self.pending_removed_support[supported_name][spent_support].append((txin_num, txin.prev_idx))
activation = self.db.get_activation(txin_num, txin.prev_idx, is_support=True)
self.removed_active_support[spent_support].append(support_amount)
print(f"\tspent support for lbry://{supported_name}#{spent_support.hex()} activation:{activation} {support_amount}")
return StagedClaimtrieSupport(
spent_support, txin_num, txin.prev_idx, support_amount
).get_spend_support_txo_ops() + StagedActivation(
ACTIVATED_SUPPORT_TXO_TYPE, spent_support, txin_num, txin.prev_idx, activation, supported_name,
support_amount
).get_remove_activate_ops()
return [] return []
def _remove_claim(self, txin, spent_claims, zero_delay_claims): def _spend_claim_txo(self, txin: TxInput, spent_claims: Dict[bytes, Tuple[int, int, str]]):
txin_num = self.db.transaction_num_mapping[txin.prev_hash] txin_num = self.db.transaction_num_mapping[txin.prev_hash]
if (txin_num, txin.prev_idx) in self.pending_claims: if (txin_num, txin.prev_idx) in self.pending_claims:
spent = self.pending_claims[(txin_num, txin.prev_idx)] spent = self.pending_claims[(txin_num, txin.prev_idx)]
name = spent.name
spent_claims[spent.claim_hash] = (txin_num, txin.prev_idx, name)
# print(f"spend lbry://{name}#{spent.claim_hash.hex()}")
else: else:
spent_claim_hash_and_name = self.db.claim_hash_and_name_from_txo( spent_claim_hash_and_name = self.db.get_claim_from_txo(
txin_num, txin.prev_idx txin_num, txin.prev_idx
) )
if not spent_claim_hash_and_name: # txo is not a claim if not spent_claim_hash_and_name: # txo is not a claim
return [] return []
prev_claim_hash = spent_claim_hash_and_name.claim_hash claim_hash = spent_claim_hash_and_name.claim_hash
signing_hash = self.db.get_channel_for_claim(claim_hash)
prev_signing_hash = self.db.get_channel_for_claim(prev_claim_hash) k, v = self.db.get_claim_txo(claim_hash)
prev_claims_in_channel_count = None
if prev_signing_hash:
prev_claims_in_channel_count = self.db.get_claims_in_channel_count(
prev_signing_hash
)
prev_effective_amount = self.db.get_effective_amount(
prev_claim_hash
)
k, v = self.db.get_root_claim_txo_and_current_amount(prev_claim_hash)
claim_root_tx_num = v.root_tx_num
claim_root_idx = v.root_position
prev_amount = v.amount
name = v.name
tx_num = k.tx_num
position = k.position
activation_height = v.activation
height = bisect_right(self.db.tx_counts, tx_num)
spent = StagedClaimtrieItem( spent = StagedClaimtrieItem(
name, prev_claim_hash, prev_amount, prev_effective_amount, v.name, claim_hash, v.amount,
activation_height, get_expiration_height(height), txin_num, txin.prev_idx, claim_root_tx_num, self.coin.get_expiration_height(bisect_right(self.db.tx_counts, txin_num)),
claim_root_idx, prev_signing_hash, prev_claims_in_channel_count txin_num, txin.prev_idx, v.root_tx_num, v.root_position, signing_hash
) )
spent_claims[prev_claim_hash] = (txin_num, txin.prev_idx, name) spent_claims[spent.claim_hash] = (spent.tx_num, spent.position, spent.name)
# print(f"spend lbry://{spent_claims[prev_claim_hash][2]}#{prev_claim_hash.hex()}") print(f"\tspend lbry://{spent.name}#{spent.claim_hash.hex()}")
if spent.claim_hash not in self.effective_amount_changes:
self.effective_amount_changes[spent.claim_hash].append(spent.effective_amount)
self.effective_amount_changes[spent.claim_hash].append(-spent.amount)
if (name, spent.claim_hash) in zero_delay_claims:
zero_delay_claims.pop((name, spent.claim_hash))
return spent.get_spend_claim_txo_ops() return spent.get_spend_claim_txo_ops()
def _remove_claim_or_support(self, txin, spent_claims, zero_delay_claims): def _spend_claim_or_support_txo(self, txin, spent_claims):
spend_claim_ops = self._remove_claim(txin, spent_claims, zero_delay_claims) spend_claim_ops = self._spend_claim_txo(txin, spent_claims)
if spend_claim_ops: if spend_claim_ops:
return spend_claim_ops return spend_claim_ops
return self._remove_support(txin, zero_delay_claims) return self._spend_support_txo(txin)
def _abandon(self, spent_claims) -> typing.Tuple[List['RevertableOp'], typing.Set[str]]: def _abandon_claim(self, claim_hash, tx_num, nout, name) -> List['RevertableOp']:
if (tx_num, nout) in self.pending_claims:
pending = self.pending_claims.pop((tx_num, nout))
self.staged_pending_abandoned[pending.claim_hash] = pending
claim_root_tx_num, claim_root_idx = pending.root_claim_tx_num, pending.root_claim_tx_position
prev_amount, prev_signing_hash = pending.amount, pending.signing_hash
expiration = self.coin.get_expiration_height(self.height)
else:
k, v = self.db.get_claim_txo(
claim_hash
)
claim_root_tx_num, claim_root_idx, prev_amount = v.root_tx_num, v.root_position, v.amount
prev_signing_hash = self.db.get_channel_for_claim(claim_hash)
expiration = self.coin.get_expiration_height(bisect_right(self.db.tx_counts, tx_num))
self.staged_pending_abandoned[claim_hash] = staged = StagedClaimtrieItem(
name, claim_hash, prev_amount, expiration, tx_num, nout, claim_root_tx_num,
claim_root_idx, prev_signing_hash
)
self.pending_supports[claim_hash].clear()
self.pending_supports.pop(claim_hash)
return staged.get_abandon_ops(self.db.db)
def _abandon(self, spent_claims) -> List['RevertableOp']:
# Handle abandoned claims # Handle abandoned claims
ops = [] ops = []
controlling_claims = {} for abandoned_claim_hash, (tx_num, nout, name) in spent_claims.items():
need_takeover = set() print(f"\tabandon lbry://{name}#{abandoned_claim_hash.hex()} {tx_num} {nout}")
ops.extend(self._abandon_claim(abandoned_claim_hash, tx_num, nout, name))
return ops
for abandoned_claim_hash, (prev_tx_num, prev_idx, name) in spent_claims.items(): def _expire_claims(self, height: int):
# print(f"\tabandon lbry://{name}#{abandoned_claim_hash.hex()} {prev_tx_num} {prev_idx}")
if (prev_tx_num, prev_idx) in self.pending_claims:
pending = self.pending_claims.pop((prev_tx_num, prev_idx))
self.staged_pending_abandoned[pending.claim_hash] = pending
claim_root_tx_num = pending.root_claim_tx_num
claim_root_idx = pending.root_claim_tx_position
prev_amount = pending.amount
prev_signing_hash = pending.signing_hash
prev_effective_amount = pending.effective_amount
prev_claims_in_channel_count = pending.claims_in_channel_count
else:
k, v = self.db.get_root_claim_txo_and_current_amount(
abandoned_claim_hash
)
claim_root_tx_num = v.root_tx_num
claim_root_idx = v.root_position
prev_amount = v.amount
prev_signing_hash = self.db.get_channel_for_claim(abandoned_claim_hash)
prev_claims_in_channel_count = None
if prev_signing_hash:
prev_claims_in_channel_count = self.db.get_claims_in_channel_count(
prev_signing_hash
)
prev_effective_amount = self.db.get_effective_amount(
abandoned_claim_hash
)
if name not in controlling_claims:
controlling_claims[name] = self.db.get_controlling_claim(name)
controlling = controlling_claims[name]
if controlling and controlling.claim_hash == abandoned_claim_hash:
need_takeover.add(name)
# print("needs takeover")
for (support_tx_num, support_tx_idx) in self.pending_supports[abandoned_claim_hash]:
_, support_amount = self.pending_support_txos.pop((support_tx_num, support_tx_idx))
ops.extend(
StagedClaimtrieSupport(
abandoned_claim_hash, support_tx_num, support_tx_idx, support_amount
).get_spend_support_txo_ops()
)
# print(f"\tremove pending support for abandoned lbry://{name}#{abandoned_claim_hash.hex()} {support_tx_num} {support_tx_idx}")
self.pending_supports[abandoned_claim_hash].clear()
self.pending_supports.pop(abandoned_claim_hash)
for (support_tx_num, support_tx_idx, support_amount) in self.db.get_supports(abandoned_claim_hash):
ops.extend(
StagedClaimtrieSupport(
abandoned_claim_hash, support_tx_num, support_tx_idx, support_amount
).get_spend_support_txo_ops()
)
# print(f"\tremove support for abandoned lbry://{name}#{abandoned_claim_hash.hex()} {support_tx_num} {support_tx_idx}")
height = bisect_right(self.db.tx_counts, prev_tx_num)
activation_height = 0
if abandoned_claim_hash in self.effective_amount_changes:
# print("pop")
self.effective_amount_changes.pop(abandoned_claim_hash)
self.pending_abandon.add(abandoned_claim_hash)
# print(f"\tabandoned lbry://{name}#{abandoned_claim_hash.hex()}, {len(need_takeover)} names need takeovers")
ops.extend(
StagedClaimtrieItem(
name, abandoned_claim_hash, prev_amount, prev_effective_amount,
activation_height, get_expiration_height(height), prev_tx_num, prev_idx, claim_root_tx_num,
claim_root_idx, prev_signing_hash, prev_claims_in_channel_count
).get_abandon_ops(self.db.db)
)
return ops, need_takeover
def _expire_claims(self, height: int, zero_delay_claims):
expired = self.db.get_expired_by_height(height) expired = self.db.get_expired_by_height(height)
spent_claims = {} spent_claims = {}
ops = [] ops = []
names_needing_takeover = set()
for expired_claim_hash, (tx_num, position, name, txi) in expired.items(): for expired_claim_hash, (tx_num, position, name, txi) in expired.items():
if (tx_num, position) not in self.pending_claims: if (tx_num, position) not in self.pending_claims:
ops.extend(self._remove_claim(txi, spent_claims, zero_delay_claims)) ops.extend(self._spend_claim_txo(txi, spent_claims))
if expired: if expired:
# do this to follow the same content claim removing pathway as if a claim (possible channel) was abandoned # do this to follow the same content claim removing pathway as if a claim (possible channel) was abandoned
abandon_ops, _names_needing_takeover = self._abandon(spent_claims)
if abandon_ops:
ops.extend(abandon_ops)
names_needing_takeover.update(_names_needing_takeover)
ops.extend(self._abandon(spent_claims)) ops.extend(self._abandon(spent_claims))
return ops, names_needing_takeover return ops
def _get_pending_claim_amount(self, claim_hash: bytes) -> int: def _get_pending_claim_amount(self, name: str, claim_hash: bytes) -> int:
if claim_hash in self.pending_claim_txos: if (name, claim_hash) in self.staged_activated_claim:
return self.pending_claims[self.pending_claim_txos[claim_hash]].amount return self.staged_activated_claim[(name, claim_hash)]
return self.db.get_claim_amount(claim_hash) return self.db._get_active_amount(claim_hash, ACTIVATED_CLAIM_TXO_TYPE, self.height + 1)
def _get_pending_claim_name(self, claim_hash: bytes) -> str: def _get_pending_claim_name(self, claim_hash: bytes) -> Optional[str]:
assert claim_hash is not None assert claim_hash is not None
if claim_hash in self.pending_claims: if claim_hash in self.pending_claims:
return self.pending_claims[claim_hash].name return self.pending_claims[claim_hash].name
claim = self.db.get_claim_from_txo(claim_hash) claim_info = self.db.get_claim_txo(claim_hash)
return claim.name if claim_info:
return claim_info[1].name
def _get_pending_effective_amount(self, claim_hash: bytes) -> int: def _get_pending_supported_amount(self, claim_hash: bytes) -> int:
claim_amount = self._get_pending_claim_amount(claim_hash) or 0 support_amount = self.db._get_active_amount(claim_hash, ACTIVATED_SUPPORT_TXO_TYPE, self.height + 1) or 0
support_amount = self.db.get_support_amount(claim_hash) or 0 amount = support_amount + sum(
return claim_amount + support_amount + sum( self.staged_activated_support.get(claim_hash, [])
self.pending_support_txos[support_txnum, support_n][1] )
for (support_txnum, support_n) in self.pending_supports.get(claim_hash, []) if claim_hash in self.removed_active_support:
) # TODO: subtract pending spend supports return amount - sum(self.removed_active_support[claim_hash])
return amount
def _get_name_takeover_ops(self, height: int, name: str, def _get_pending_effective_amount(self, name: str, claim_hash: bytes) -> int:
activated_claims: typing.Set[bytes]) -> List['RevertableOp']: claim_amount = self._get_pending_claim_amount(name, claim_hash)
controlling = self.db.get_controlling_claim(name) support_amount = self._get_pending_supported_amount(claim_hash)
if not controlling or controlling.claim_hash in self.pending_abandon: return claim_amount + support_amount
# print("no controlling claim for ", name)
bid_queue = { def _get_takeover_ops(self, height: int) -> List['RevertableOp']:
claim_hash: self._get_pending_effective_amount(claim_hash) for claim_hash in activated_claims
}
winning_claim = max(bid_queue, key=lambda k: bid_queue[k])
if winning_claim in self.pending_claim_txos:
s = self.pending_claims[self.pending_claim_txos[winning_claim]]
else:
s = self.db.make_staged_claim_item(winning_claim)
ops = [] ops = []
if s.activation_height > height:
ops.extend(get_force_activate_ops(
name, s.tx_num, s.position, s.claim_hash, s.root_claim_tx_num, s.root_claim_tx_position,
s.amount, s.effective_amount, s.activation_height, height
))
ops.extend(get_takeover_name_ops(name, winning_claim, height))
return ops
else:
# print(f"current controlling claim for {name}#{controlling.claim_hash.hex()}")
controlling_effective_amount = self._get_pending_effective_amount(controlling.claim_hash)
bid_queue = {
claim_hash: self._get_pending_effective_amount(claim_hash) for claim_hash in activated_claims
}
highest_newly_activated = max(bid_queue, key=lambda k: bid_queue[k])
if bid_queue[highest_newly_activated] > controlling_effective_amount:
# print(f"takeover controlling claim for {name}#{controlling.claim_hash.hex()}")
return get_takeover_name_ops(name, highest_newly_activated, height, controlling)
print(bid_queue[highest_newly_activated], controlling_effective_amount)
# print("no takeover")
return []
def _get_takeover_ops(self, height: int, zero_delay_claims) -> List['RevertableOp']:
ops = []
pending = defaultdict(set)
# get non delayed takeovers for new names
for (name, claim_hash) in zero_delay_claims:
if claim_hash not in self.pending_abandon:
pending[name].add(claim_hash)
# print("zero delay activate", name, claim_hash.hex())
# get takeovers from claims activated at this block # get takeovers from claims activated at this block
for activated in self.db.get_activated_claims_at_height(height): activated_at_height = self.db.get_activated_at_height(height)
if activated.claim_hash not in self.pending_abandon: controlling_claims = {}
pending[activated.name].add(activated.claim_hash) abandoned_need_takeover = []
# print("delayed activate") abandoned_support_check_need_takeover = defaultdict(list)
# get takeovers from supports for controlling claims being abandoned def get_controlling(_name):
for abandoned_claim_hash in self.pending_abandon: if _name not in controlling_claims:
if abandoned_claim_hash in self.staged_pending_abandoned: _controlling = self.db.get_controlling_claim(_name)
abandoned = self.staged_pending_abandoned[abandoned_claim_hash] controlling_claims[_name] = _controlling
controlling = self.db.get_controlling_claim(abandoned.name)
if controlling and controlling.claim_hash == abandoned_claim_hash and abandoned.name not in pending:
pending[abandoned.name].update(self.db.get_claims_for_name(abandoned.name))
else: else:
k, v = self.db.get_root_claim_txo_and_current_amount(abandoned_claim_hash) _controlling = controlling_claims[_name]
controlling_claim = self.db.get_controlling_claim(v.name) return _controlling
if controlling_claim and abandoned_claim_hash == controlling_claim.claim_hash and v.name not in pending:
pending[v.name].update(self.db.get_claims_for_name(v.name))
# print("check abandoned winning")
# determine names needing takeover/deletion due to controlling claims being abandoned
# and add ops to deactivate abandoned claims
for claim_hash, staged in self.staged_pending_abandoned.items():
controlling = get_controlling(staged.name)
if controlling and controlling.claim_hash == claim_hash:
abandoned_need_takeover.append(staged.name)
print(f"\t{staged.name} needs takeover")
activation = self.db.get_activation(staged.tx_num, staged.position)
if activation > 0:
# removed queued future activation from the db
ops.extend(
StagedActivation(
ACTIVATED_CLAIM_TXO_TYPE, staged.claim_hash, staged.tx_num, staged.position,
activation, staged.name, staged.amount
).get_remove_activate_ops()
)
else:
# it hadn't yet been activated, db returns -1 for non-existent txos
pass
# build set of controlling claims that had activated supports spent to check them for takeovers later
for claim_hash, amounts in self.removed_active_support.items():
name = self._get_pending_claim_name(claim_hash)
controlling = get_controlling(name)
if controlling and controlling.claim_hash == claim_hash and name not in abandoned_need_takeover:
abandoned_support_check_need_takeover[(name, claim_hash)].extend(amounts)
# get takeovers from controlling claims being abandoned # prepare to activate or delay activation of the pending claims being added this block
for (tx_num, nout), staged in self.pending_claims.items():
controlling = get_controlling(staged.name)
delay = 0
if not controlling or staged.claim_hash == controlling.claim_hash or \
controlling.claim_hash in abandoned_need_takeover:
pass
else:
controlling_effective_amount = self._get_pending_effective_amount(staged.name, controlling.claim_hash)
amount = self._get_pending_effective_amount(staged.name, staged.claim_hash)
delay = 0
# if this is an OP_CLAIM or the amount appears to trigger a takeover, delay
if not staged.is_update or (amount > controlling_effective_amount):
delay = self.coin.get_delay_for_name(height - controlling.height)
ops.extend(
StagedActivation(
ACTIVATED_CLAIM_TXO_TYPE, staged.claim_hash, staged.tx_num, staged.position,
height + delay, staged.name, staged.amount
).get_activate_ops()
)
if delay == 0: # if delay was 0 it needs to be considered for takeovers
activated_at_height[PendingActivationValue(staged.claim_hash, staged.name)].append(
PendingActivationKey(height, ACTIVATED_CLAIM_TXO_TYPE, tx_num, nout)
)
# and the supports
for (tx_num, nout), (claim_hash, amount) in self.pending_support_txos.items():
if claim_hash in self.staged_pending_abandoned:
continue
elif claim_hash in self.pending_claim_txos:
name = self.pending_claims[self.pending_claim_txos[claim_hash]].name
is_update = self.pending_claims[self.pending_claim_txos[claim_hash]].is_update
else:
k, v = self.db.get_claim_txo(claim_hash)
name = v.name
is_update = (v.root_tx_num, v.root_position) != (k.tx_num, k.position)
controlling = get_controlling(name)
delay = 0
if not controlling or claim_hash == controlling.claim_hash:
pass
elif not is_update or self._get_pending_effective_amount(staged.name,
claim_hash) > self._get_pending_effective_amount(staged.name, controlling.claim_hash):
delay = self.coin.get_delay_for_name(height - controlling.height)
if delay == 0:
activated_at_height[PendingActivationValue(claim_hash, name)].append(
PendingActivationKey(height + delay, ACTIVATED_SUPPORT_TXO_TYPE, tx_num, nout)
)
ops.extend(
StagedActivation(
ACTIVATED_SUPPORT_TXO_TYPE, claim_hash, tx_num, nout,
height + delay, name, amount
).get_activate_ops()
)
# add the activation/delayed-activation ops
for activated, activated_txos in activated_at_height.items():
controlling = get_controlling(activated.name)
if activated.claim_hash in self.staged_pending_abandoned:
continue
reactivate = False
if not controlling or controlling.claim_hash == activated.claim_hash:
# there is no delay for claims to a name without a controlling value or to the controlling value
reactivate = True
for activated_txo in activated_txos:
if activated_txo.is_support and (activated_txo.tx_num, activated_txo.position) in \
self.pending_removed_support[activated.name][activated.claim_hash]:
print("\tskip activate support for pending abandoned claim")
continue
if activated_txo.is_claim:
txo_type = ACTIVATED_CLAIM_TXO_TYPE
txo_tup = (activated_txo.tx_num, activated_txo.position)
if txo_tup in self.pending_claims:
amount = self.pending_claims[txo_tup].amount
else:
amount = self.db.get_claim_txo_amount(
activated.claim_hash, activated_txo.tx_num, activated_txo.position
)
self.staged_activated_claim[(activated.name, activated.claim_hash)] = amount
else:
txo_type = ACTIVATED_SUPPORT_TXO_TYPE
txo_tup = (activated_txo.tx_num, activated_txo.position)
if txo_tup in self.pending_support_txos:
amount = self.pending_support_txos[txo_tup][1]
else:
amount = self.db.get_support_txo_amount(
activated.claim_hash, activated_txo.tx_num, activated_txo.position
)
self.staged_activated_support[activated.claim_hash].append(amount)
self.pending_activated[activated.name][activated.claim_hash].append((activated_txo, amount))
print(f"\tactivate {'support' if txo_type == ACTIVATED_SUPPORT_TXO_TYPE else 'claim'} "
f"lbry://{activated.name}#{activated.claim_hash.hex()} @ {activated_txo.height}")
if reactivate:
ops.extend(
StagedActivation(
txo_type, activated.claim_hash, activated_txo.tx_num, activated_txo.position,
activated_txo.height, activated.name, amount
).get_activate_ops()
)
# go through claims where the controlling claim or supports to the controlling claim have been abandoned
# check if takeovers are needed or if the name node is now empty
need_reactivate_if_takes_over = {}
for need_takeover in abandoned_need_takeover:
existing = self.db.get_claim_txos_for_name(need_takeover)
has_candidate = False
# add existing claims to the queue for the takeover
# track that we need to reactivate these if one of them becomes controlling
for candidate_claim_hash, (tx_num, nout) in existing.items():
if candidate_claim_hash in self.staged_pending_abandoned:
continue
has_candidate = True
existing_activation = self.db.get_activation(tx_num, nout)
activate_key = PendingActivationKey(
existing_activation, ACTIVATED_CLAIM_TXO_TYPE, tx_num, nout
)
self.pending_activated[need_takeover][candidate_claim_hash].append((
activate_key, self.db.get_claim_txo_amount(candidate_claim_hash, tx_num, nout)
))
need_reactivate_if_takes_over[(need_takeover, candidate_claim_hash)] = activate_key
print(f"\tcandidate to takeover abandoned controlling claim for lbry://{need_takeover} - "
f"{activate_key.tx_num}:{activate_key.position} {activate_key.is_claim}")
if not has_candidate:
# remove name takeover entry, the name is now unclaimed
controlling = get_controlling(need_takeover)
ops.extend(get_remove_name_ops(need_takeover, controlling.claim_hash, controlling.height))
# process takeovers from the combined newly added and previously scheduled claims
checked_names = set()
for name, activated in self.pending_activated.items():
checked_names.add(name)
if name in abandoned_need_takeover:
print(f'\tabandoned {name} need takeover')
controlling = controlling_claims[name]
amounts = {
claim_hash: self._get_pending_effective_amount(name, claim_hash)
for claim_hash in activated.keys() if claim_hash not in self.staged_pending_abandoned
}
if controlling and controlling.claim_hash not in self.staged_pending_abandoned:
amounts[controlling.claim_hash] = self._get_pending_effective_amount(name, controlling.claim_hash)
winning = max(amounts, key=lambda x: amounts[x])
if not controlling or (winning != controlling.claim_hash and name in abandoned_need_takeover) or ((winning != controlling.claim_hash) and
(amounts[winning] > amounts[controlling.claim_hash])):
if (name, winning) in need_reactivate_if_takes_over:
previous_pending_activate = need_reactivate_if_takes_over[(name, winning)]
amount = self.db.get_claim_txo_amount(
winning, previous_pending_activate.tx_num, previous_pending_activate.position
)
if winning in self.pending_claim_txos:
tx_num, position = self.pending_claim_txos[winning]
amount = self.pending_claims[(tx_num, position)].amount
else:
tx_num, position = previous_pending_activate.tx_num, previous_pending_activate.position
if previous_pending_activate.height > height:
# the claim had a pending activation in the future, move it to now
ops.extend(
StagedActivation(
ACTIVATED_CLAIM_TXO_TYPE, winning, tx_num,
position, previous_pending_activate.height, name, amount
).get_remove_activate_ops()
)
ops.extend(
StagedActivation(
ACTIVATED_CLAIM_TXO_TYPE, winning, tx_num,
position, height, name, amount
).get_activate_ops()
)
ops.extend(get_takeover_name_ops(name, winning, height))
else:
ops.extend(get_takeover_name_ops(name, winning, height))
elif winning == controlling.claim_hash:
print("\tstill winning")
pass
else:
print("\tno takeover")
pass
# handle remaining takeovers from abandoned supports
for (name, claim_hash), amounts in abandoned_support_check_need_takeover.items():
if name in checked_names:
continue
checked_names.add(name)
controlling = get_controlling(name)
amounts = {
claim_hash: self._get_pending_effective_amount(name, claim_hash)
for claim_hash in self.db.get_claims_for_name(name) if claim_hash not in self.staged_pending_abandoned
}
if controlling and controlling.claim_hash not in self.staged_pending_abandoned:
amounts[controlling.claim_hash] = self._get_pending_effective_amount(name, controlling.claim_hash)
winning = max(amounts, key=lambda x: amounts[x])
if (controlling and winning != controlling.claim_hash) or (not controlling and winning):
print(f"\ttakeover from abandoned support {controlling.claim_hash.hex()} -> {winning.hex()}")
ops.extend(get_takeover_name_ops(name, winning, height))
for name, activated_claims in pending.items():
ops.extend(self._get_name_takeover_ops(height, name, activated_claims))
return ops return ops
def advance_block(self, block): def advance_block(self, block):
# print("advance ", height)
height = self.height + 1 height = self.height + 1
print("advance ", height)
txs: List[Tuple[Tx, bytes]] = block.transactions txs: List[Tuple[Tx, bytes]] = block.transactions
block_hash = self.coin.header_hash(block.header) block_hash = self.coin.header_hash(block.header)
@ -881,34 +875,32 @@ class BlockProcessor:
undo_info_append(cache_value) undo_info_append(cache_value)
append_hashX(cache_value[:-12]) append_hashX(cache_value[:-12])
spend_claim_or_support_ops = self._remove_claim_or_support(txin, spent_claims, zero_delay_claims) spend_claim_or_support_ops = self._spend_claim_or_support_txo(txin, spent_claims)
if spend_claim_or_support_ops: if spend_claim_or_support_ops:
claimtrie_stash_extend(spend_claim_or_support_ops) claimtrie_stash_extend(spend_claim_or_support_ops)
# Add the new UTXOs # Add the new UTXOs
for idx, txout in enumerate(tx.outputs): for nout, txout in enumerate(tx.outputs):
# Get the hashX. Ignore unspendable outputs # Get the hashX. Ignore unspendable outputs
hashX = hashX_from_script(txout.pk_script) hashX = hashX_from_script(txout.pk_script)
if hashX: if hashX:
append_hashX(hashX) append_hashX(hashX)
put_utxo(tx_hash + pack('<H', idx), hashX + tx_numb + pack('<Q', txout.value)) put_utxo(tx_hash + pack('<H', nout), hashX + tx_numb + pack('<Q', txout.value))
# add claim/support txo # add claim/support txo
script = OutputScript(txout.pk_script) script = OutputScript(txout.pk_script)
script.parse() script.parse()
txo = Output(txout.value, script)
claim_or_support_ops = self._add_claim_or_support( claim_or_support_ops = self._add_claim_or_support(
height, tx_hash, tx_count, idx, txo, txout, script, spent_claims, zero_delay_claims height, tx_hash, tx_count, nout, Output(txout.value, script), spent_claims
) )
if claim_or_support_ops: if claim_or_support_ops:
claimtrie_stash_extend(claim_or_support_ops) claimtrie_stash_extend(claim_or_support_ops)
# Handle abandoned claims # Handle abandoned claims
abandon_ops, abandoned_controlling_need_takeover = self._abandon(spent_claims) abandon_ops = self._abandon(spent_claims)
if abandon_ops: if abandon_ops:
claimtrie_stash_extend(abandon_ops) claimtrie_stash_extend(abandon_ops)
abandoned_or_expired_controlling.update(abandoned_controlling_need_takeover)
append_hashX_by_tx(hashXs) append_hashX_by_tx(hashXs)
update_touched(hashXs) update_touched(hashXs)
@ -917,14 +909,13 @@ class BlockProcessor:
tx_count += 1 tx_count += 1
# handle expired claims # handle expired claims
expired_ops, expired_need_takeover = self._expire_claims(height, zero_delay_claims) expired_ops = self._expire_claims(height)
if expired_ops: if expired_ops:
# print(f"************\nexpire claims at block {height}\n************") print(f"************\nexpire claims at block {height}\n************")
abandoned_or_expired_controlling.update(expired_need_takeover)
claimtrie_stash_extend(expired_ops) claimtrie_stash_extend(expired_ops)
# activate claims and process takeovers # activate claims and process takeovers
takeover_ops = self._get_takeover_ops(height, zero_delay_claims) takeover_ops = self._get_takeover_ops(height)
if takeover_ops: if takeover_ops:
claimtrie_stash_extend(takeover_ops) claimtrie_stash_extend(takeover_ops)
@ -939,13 +930,6 @@ class BlockProcessor:
self.tx_count = tx_count self.tx_count = tx_count
self.db.tx_counts.append(self.tx_count) self.db.tx_counts.append(self.tx_count)
for touched_claim_hash, amount_changes in self.effective_amount_changes.items():
new_effective_amount = sum(amount_changes)
assert new_effective_amount >= 0, f'{new_effective_amount}, {touched_claim_hash.hex()}'
claimtrie_stash.extend(
self.db.get_update_effective_amount_ops(touched_claim_hash, new_effective_amount)
)
undo_claims = b''.join(op.invert().pack() for op in claimtrie_stash) undo_claims = b''.join(op.invert().pack() for op in claimtrie_stash)
self.claimtrie_stash.extend(claimtrie_stash) self.claimtrie_stash.extend(claimtrie_stash)
# print("%i undo bytes for %i (%i claimtrie stash ops)" % (len(undo_claims), height, len(claimtrie_stash))) # print("%i undo bytes for %i (%i claimtrie stash ops)" % (len(undo_claims), height, len(claimtrie_stash)))
@ -961,14 +945,18 @@ class BlockProcessor:
self.db.flush_dbs(self.flush_data()) self.db.flush_dbs(self.flush_data())
self.effective_amount_changes.clear() # self.effective_amount_changes.clear()
self.pending_claims.clear() self.pending_claims.clear()
self.pending_claim_txos.clear() self.pending_claim_txos.clear()
self.pending_supports.clear() self.pending_supports.clear()
self.pending_support_txos.clear() self.pending_support_txos.clear()
self.pending_abandon.clear() self.pending_removed_support.clear()
self.staged_pending_abandoned.clear() self.staged_pending_abandoned.clear()
self.removed_active_support.clear()
self.staged_activated_support.clear()
self.staged_activated_claim.clear()
self.pending_activated.clear()
for cache in self.search_cache.values(): for cache in self.search_cache.values():
cache.clear() cache.clear()

View file

@ -12,11 +12,13 @@ class DB_PREFIXES(enum.Enum):
channel_to_claim = b'J' channel_to_claim = b'J'
claim_short_id_prefix = b'F' claim_short_id_prefix = b'F'
claim_effective_amount_prefix = b'D' # claim_effective_amount_prefix = b'D'
claim_expiration = b'O' claim_expiration = b'O'
claim_takeover = b'P' claim_takeover = b'P'
pending_activation = b'Q' pending_activation = b'Q'
activated_claim_and_support = b'R'
active_amount = b'S'
undo_claimtrie = b'M' undo_claimtrie = b'M'

View file

@ -45,40 +45,52 @@ class StagedClaimtrieSupport(typing.NamedTuple):
return self._get_add_remove_support_utxo_ops(add=False) return self._get_add_remove_support_utxo_ops(add=False)
def get_update_effective_amount_ops(name: str, new_effective_amount: int, prev_effective_amount: int, tx_num: int, class StagedActivation(typing.NamedTuple):
position: int, root_tx_num: int, root_position: int, claim_hash: bytes, txo_type: int
activation_height: int, prev_activation_height: int, claim_hash: bytes
signing_hash: Optional[bytes] = None, tx_num: int
claims_in_channel_count: Optional[int] = None): position: int
assert root_position != root_tx_num, f"{tx_num} {position} {root_tx_num} {root_tx_num}" activation_height: int
ops = [ name: str
RevertableDelete( amount: int
*Prefixes.claim_effective_amount.pack_item(
name, prev_effective_amount, tx_num, position, claim_hash, root_tx_num, root_position, def _get_add_remove_activate_ops(self, add=True):
prev_activation_height op = RevertablePut if add else RevertableDelete
print(f"\t{'add' if add else 'remove'} {self.txo_type}, {self.tx_num}, {self.position}, activation={self.activation_height}, {self.name}")
return [
op(
*Prefixes.activated.pack_item(
self.txo_type, self.tx_num, self.position, self.activation_height, self.claim_hash, self.name
) )
), ),
RevertablePut( op(
*Prefixes.claim_effective_amount.pack_item( *Prefixes.pending_activation.pack_item(
name, new_effective_amount, tx_num, position, claim_hash, root_tx_num, root_position, self.activation_height, self.txo_type, self.tx_num, self.position,
activation_height self.claim_hash, self.name
)
),
op(
*Prefixes.active_amount.pack_item(
self.claim_hash, self.txo_type, self.activation_height, self.tx_num, self.position, self.amount
) )
) )
] ]
if signing_hash:
ops.extend([ def get_activate_ops(self) -> typing.List[RevertableOp]:
return self._get_add_remove_activate_ops(add=True)
def get_remove_activate_ops(self) -> typing.List[RevertableOp]:
return self._get_add_remove_activate_ops(add=False)
def get_remove_name_ops(name: str, claim_hash: bytes, height: int) -> typing.List[RevertableDelete]:
return [
RevertableDelete( RevertableDelete(
*Prefixes.channel_to_claim.pack_item( *Prefixes.claim_takeover.pack_item(
signing_hash, name, prev_effective_amount, tx_num, position, claim_hash, claims_in_channel_count name, claim_hash, height
)
),
RevertablePut(
*Prefixes.channel_to_claim.pack_item(
signing_hash, name, new_effective_amount, tx_num, position, claim_hash, claims_in_channel_count
) )
) )
]) ]
return ops
def get_takeover_name_ops(name: str, claim_hash: bytes, takeover_height: int, def get_takeover_name_ops(name: str, claim_hash: bytes, takeover_height: int,
@ -107,76 +119,16 @@ def get_takeover_name_ops(name: str, claim_hash: bytes, takeover_height: int,
] ]
def get_force_activate_ops(name: str, tx_num: int, position: int, claim_hash: bytes, root_claim_tx_num: int,
root_claim_tx_position: int, amount: int, effective_amount: int,
prev_activation_height: int, new_activation_height: int):
return [
# delete previous
RevertableDelete(
*Prefixes.claim_effective_amount.pack_item(
name, effective_amount, tx_num, position, claim_hash,
root_claim_tx_num, root_claim_tx_position, prev_activation_height
)
),
RevertableDelete(
*Prefixes.claim_to_txo.pack_item(
claim_hash, tx_num, position, root_claim_tx_num, root_claim_tx_position,
amount, prev_activation_height, name
)
),
RevertableDelete(
*Prefixes.claim_short_id.pack_item(
name, claim_hash, root_claim_tx_num, root_claim_tx_position, tx_num,
position, prev_activation_height
)
),
RevertableDelete(
*Prefixes.pending_activation.pack_item(
prev_activation_height, tx_num, position, claim_hash, name
)
),
# insert new
RevertablePut(
*Prefixes.claim_effective_amount.pack_item(
name, effective_amount, tx_num, position, claim_hash,
root_claim_tx_num, root_claim_tx_position, new_activation_height
)
),
RevertablePut(
*Prefixes.claim_to_txo.pack_item(
claim_hash, tx_num, position, root_claim_tx_num, root_claim_tx_position,
amount, new_activation_height, name
)
),
RevertablePut(
*Prefixes.claim_short_id.pack_item(
name, claim_hash, root_claim_tx_num, root_claim_tx_position, tx_num,
position, new_activation_height
)
),
RevertablePut(
*Prefixes.pending_activation.pack_item(
new_activation_height, tx_num, position, claim_hash, name
)
)
]
class StagedClaimtrieItem(typing.NamedTuple): class StagedClaimtrieItem(typing.NamedTuple):
name: str name: str
claim_hash: bytes claim_hash: bytes
amount: int amount: int
effective_amount: int
activation_height: int
expiration_height: int expiration_height: int
tx_num: int tx_num: int
position: int position: int
root_claim_tx_num: int root_claim_tx_num: int
root_claim_tx_position: int root_claim_tx_position: int
signing_hash: Optional[bytes] signing_hash: Optional[bytes]
claims_in_channel_count: Optional[int]
@property @property
def is_update(self) -> bool: def is_update(self) -> bool:
@ -191,25 +143,11 @@ class StagedClaimtrieItem(typing.NamedTuple):
""" """
op = RevertablePut if add else RevertableDelete op = RevertablePut if add else RevertableDelete
ops = [ ops = [
# url resolution by effective amount
op(
*Prefixes.claim_effective_amount.pack_item(
self.name, self.effective_amount, self.tx_num, self.position, self.claim_hash,
self.root_claim_tx_num, self.root_claim_tx_position, self.activation_height
)
),
# claim tip by claim hash # claim tip by claim hash
op( op(
*Prefixes.claim_to_txo.pack_item( *Prefixes.claim_to_txo.pack_item(
self.claim_hash, self.tx_num, self.position, self.root_claim_tx_num, self.root_claim_tx_position, self.claim_hash, self.tx_num, self.position, self.root_claim_tx_num, self.root_claim_tx_position,
self.amount, self.activation_height, self.name self.amount, self.name
)
),
# short url resolution
op(
*Prefixes.claim_short_id.pack_item(
self.name, self.claim_hash, self.root_claim_tx_num, self.root_claim_tx_position, self.tx_num,
self.position, self.activation_height
) )
), ),
# claim hash by txo # claim hash by txo
@ -223,15 +161,16 @@ class StagedClaimtrieItem(typing.NamedTuple):
self.name self.name
) )
), ),
# claim activation # short url resolution
op( op(
*Prefixes.pending_activation.pack_item( *Prefixes.claim_short_id.pack_item(
self.activation_height, self.tx_num, self.position, self.claim_hash, self.name self.name, self.claim_hash, self.root_claim_tx_num, self.root_claim_tx_position, self.tx_num,
self.position
) )
) )
] ]
if self.signing_hash and self.claims_in_channel_count is not None:
# claims_in_channel_count can be none if the channel doesnt exist if self.signing_hash:
ops.extend([ ops.extend([
# channel by stream # channel by stream
op( op(
@ -240,8 +179,7 @@ class StagedClaimtrieItem(typing.NamedTuple):
# stream by channel # stream by channel
op( op(
*Prefixes.channel_to_claim.pack_item( *Prefixes.channel_to_claim.pack_item(
self.signing_hash, self.name, self.effective_amount, self.tx_num, self.position, self.signing_hash, self.name, self.tx_num, self.position, self.claim_hash
self.claim_hash, self.claims_in_channel_count
) )
) )
]) ])
@ -267,5 +205,4 @@ class StagedClaimtrieItem(typing.NamedTuple):
) )
delete_claim_ops = delete_prefix(db, DB_PREFIXES.claim_to_txo.value + self.claim_hash) delete_claim_ops = delete_prefix(db, DB_PREFIXES.claim_to_txo.value + self.claim_hash)
delete_supports_ops = delete_prefix(db, DB_PREFIXES.claim_to_support.value + self.claim_hash) delete_supports_ops = delete_prefix(db, DB_PREFIXES.claim_to_support.value + self.claim_hash)
invalidate_channel_ops = self.get_invalidate_channel_ops(db) return delete_short_id_ops + delete_claim_ops + delete_supports_ops + self.get_invalidate_channel_ops(db)
return delete_short_id_ops + delete_claim_ops + delete_supports_ops + invalidate_channel_ops

View file

@ -3,6 +3,10 @@ import struct
from lbry.wallet.server.db import DB_PREFIXES from lbry.wallet.server.db import DB_PREFIXES
ACTIVATED_CLAIM_TXO_TYPE = 1
ACTIVATED_SUPPORT_TXO_TYPE = 2
def length_encoded_name(name: str) -> bytes: def length_encoded_name(name: str) -> bytes:
encoded = name.encode('utf-8') encoded = name.encode('utf-8')
return len(encoded).to_bytes(2, byteorder='big') + encoded return len(encoded).to_bytes(2, byteorder='big') + encoded
@ -12,6 +16,11 @@ class PrefixRow:
prefix: bytes prefix: bytes
key_struct: struct.Struct key_struct: struct.Struct
value_struct: struct.Struct value_struct: struct.Struct
key_part_lambdas = []
@classmethod
def pack_partial_key(cls, *args) -> bytes:
return cls.prefix + cls.key_part_lambdas[len(args)](*args)
@classmethod @classmethod
def pack_key(cls, *args) -> bytes: def pack_key(cls, *args) -> bytes:
@ -35,20 +44,6 @@ class PrefixRow:
return cls.unpack_key(key), cls.unpack_value(value) return cls.unpack_key(key), cls.unpack_value(value)
class EffectiveAmountKey(typing.NamedTuple):
name: str
effective_amount: int
tx_num: int
position: int
class EffectiveAmountValue(typing.NamedTuple):
claim_hash: bytes
root_tx_num: int
root_position: int
activation: int
class ClaimToTXOKey(typing.NamedTuple): class ClaimToTXOKey(typing.NamedTuple):
claim_hash: bytes claim_hash: bytes
tx_num: int tx_num: int
@ -59,7 +54,7 @@ class ClaimToTXOValue(typing.NamedTuple):
root_tx_num: int root_tx_num: int
root_position: int root_position: int
amount: int amount: int
activation: int # activation: int
name: str name: str
@ -83,7 +78,6 @@ class ClaimShortIDKey(typing.NamedTuple):
class ClaimShortIDValue(typing.NamedTuple): class ClaimShortIDValue(typing.NamedTuple):
tx_num: int tx_num: int
position: int position: int
activation: int
class ClaimToChannelKey(typing.NamedTuple): class ClaimToChannelKey(typing.NamedTuple):
@ -97,14 +91,12 @@ class ClaimToChannelValue(typing.NamedTuple):
class ChannelToClaimKey(typing.NamedTuple): class ChannelToClaimKey(typing.NamedTuple):
signing_hash: bytes signing_hash: bytes
name: str name: str
effective_amount: int
tx_num: int tx_num: int
position: int position: int
class ChannelToClaimValue(typing.NamedTuple): class ChannelToClaimValue(typing.NamedTuple):
claim_hash: bytes claim_hash: bytes
claims_in_channel: int
class ClaimToSupportKey(typing.NamedTuple): class ClaimToSupportKey(typing.NamedTuple):
@ -148,55 +140,92 @@ class ClaimTakeoverValue(typing.NamedTuple):
class PendingActivationKey(typing.NamedTuple): class PendingActivationKey(typing.NamedTuple):
height: int height: int
txo_type: int
tx_num: int tx_num: int
position: int position: int
@property
def is_support(self) -> bool:
return self.txo_type == ACTIVATED_SUPPORT_TXO_TYPE
@property
def is_claim(self) -> bool:
return self.txo_type == ACTIVATED_CLAIM_TXO_TYPE
class PendingActivationValue(typing.NamedTuple): class PendingActivationValue(typing.NamedTuple):
claim_hash: bytes claim_hash: bytes
name: str name: str
class EffectiveAmountPrefixRow(PrefixRow): class ActivationKey(typing.NamedTuple):
prefix = DB_PREFIXES.claim_effective_amount_prefix.value txo_type: int
key_struct = struct.Struct(b'>QLH') tx_num: int
value_struct = struct.Struct(b'>20sLHL') position: int
class ActivationValue(typing.NamedTuple):
height: int
claim_hash: bytes
name: str
class ActiveAmountKey(typing.NamedTuple):
claim_hash: bytes
txo_type: int
activation_height: int
tx_num: int
position: int
class ActiveAmountValue(typing.NamedTuple):
amount: int
class ActiveAmountPrefixRow(PrefixRow):
prefix = DB_PREFIXES.active_amount.value
key_struct = struct.Struct(b'>20sBLLH')
value_struct = struct.Struct(b'>Q')
key_part_lambdas = [
lambda: b'',
struct.Struct(b'>20s').pack,
struct.Struct(b'>20sB').pack,
struct.Struct(b'>20sBL').pack,
struct.Struct(b'>20sBLL').pack,
struct.Struct(b'>20sBLLH').pack
]
@classmethod @classmethod
def pack_key(cls, name: str, effective_amount: int, tx_num: int, position: int): def pack_key(cls, claim_hash: bytes, txo_type: int, activation_height: int, tx_num: int, position: int):
return cls.prefix + length_encoded_name(name) + cls.key_struct.pack( return super().pack_key(claim_hash, txo_type, activation_height, tx_num, position)
0xffffffffffffffff - effective_amount, tx_num, position
)
@classmethod @classmethod
def unpack_key(cls, key: bytes) -> EffectiveAmountKey: def unpack_key(cls, key: bytes) -> ActiveAmountKey:
assert key[:1] == cls.prefix return ActiveAmountKey(*super().unpack_key(key))
name_len = int.from_bytes(key[1:3], byteorder='big')
name = key[3:3 + name_len].decode()
ones_comp_effective_amount, tx_num, position = cls.key_struct.unpack(key[3 + name_len:])
return EffectiveAmountKey(
name, 0xffffffffffffffff - ones_comp_effective_amount, tx_num, position
)
@classmethod @classmethod
def unpack_value(cls, data: bytes) -> EffectiveAmountValue: def unpack_value(cls, data: bytes) -> ActiveAmountValue:
return EffectiveAmountValue(*super().unpack_value(data)) return ActiveAmountValue(*super().unpack_value(data))
@classmethod @classmethod
def pack_value(cls, claim_hash: bytes, root_tx_num: int, root_position: int, activation: int) -> bytes: def pack_value(cls, amount: int) -> bytes:
return super().pack_value(claim_hash, root_tx_num, root_position, activation) return cls.value_struct.pack(amount)
@classmethod @classmethod
def pack_item(cls, name: str, effective_amount: int, tx_num: int, position: int, claim_hash: bytes, def pack_item(cls, claim_hash: bytes, txo_type: int, activation_height: int, tx_num: int, position: int, amount: int):
root_tx_num: int, root_position: int, activation: int): return cls.pack_key(claim_hash, txo_type, activation_height, tx_num, position), cls.pack_value(amount)
return cls.pack_key(name, effective_amount, tx_num, position), \
cls.pack_value(claim_hash, root_tx_num, root_position, activation)
class ClaimToTXOPrefixRow(PrefixRow): class ClaimToTXOPrefixRow(PrefixRow):
prefix = DB_PREFIXES.claim_to_txo.value prefix = DB_PREFIXES.claim_to_txo.value
key_struct = struct.Struct(b'>20sLH') key_struct = struct.Struct(b'>20sLH')
value_struct = struct.Struct(b'>LHQL') value_struct = struct.Struct(b'>LHQ')
key_part_lambdas = [
lambda: b'',
struct.Struct(b'>20s').pack,
struct.Struct(b'>20sL').pack,
struct.Struct(b'>20sLH').pack
]
@classmethod @classmethod
def pack_key(cls, claim_hash: bytes, tx_num: int, position: int): def pack_key(cls, claim_hash: bytes, tx_num: int, position: int):
@ -214,20 +243,20 @@ class ClaimToTXOPrefixRow(PrefixRow):
@classmethod @classmethod
def unpack_value(cls, data: bytes) -> ClaimToTXOValue: def unpack_value(cls, data: bytes) -> ClaimToTXOValue:
root_tx_num, root_position, amount, activation = cls.value_struct.unpack(data[:18]) root_tx_num, root_position, amount = cls.value_struct.unpack(data[:14])
name_len = int.from_bytes(data[18:20], byteorder='big') name_len = int.from_bytes(data[14:16], byteorder='big')
name = data[20:20 + name_len].decode() name = data[16:16 + name_len].decode()
return ClaimToTXOValue(root_tx_num, root_position, amount, activation, name) return ClaimToTXOValue(root_tx_num, root_position, amount, name)
@classmethod @classmethod
def pack_value(cls, root_tx_num: int, root_position: int, amount: int, activation: int, name: str) -> bytes: def pack_value(cls, root_tx_num: int, root_position: int, amount: int, name: str) -> bytes:
return cls.value_struct.pack(root_tx_num, root_position, amount, activation) + length_encoded_name(name) return cls.value_struct.pack(root_tx_num, root_position, amount) + length_encoded_name(name)
@classmethod @classmethod
def pack_item(cls, claim_hash: bytes, tx_num: int, position: int, root_tx_num: int, root_position: int, def pack_item(cls, claim_hash: bytes, tx_num: int, position: int, root_tx_num: int, root_position: int,
amount: int, activation: int, name: str): amount: int, name: str):
return cls.pack_key(claim_hash, tx_num, position), \ return cls.pack_key(claim_hash, tx_num, position), \
cls.pack_value(root_tx_num, root_position, amount, activation, name) cls.pack_value(root_tx_num, root_position, amount, name)
class TXOToClaimPrefixRow(PrefixRow): class TXOToClaimPrefixRow(PrefixRow):
@ -260,18 +289,32 @@ class TXOToClaimPrefixRow(PrefixRow):
cls.pack_value(claim_hash, name) cls.pack_value(claim_hash, name)
def shortid_key_helper(struct_fmt):
packer = struct.Struct(struct_fmt).pack
def wrapper(name, *args):
return length_encoded_name(name) + packer(*args)
return wrapper
class ClaimShortIDPrefixRow(PrefixRow): class ClaimShortIDPrefixRow(PrefixRow):
prefix = DB_PREFIXES.claim_short_id_prefix.value prefix = DB_PREFIXES.claim_short_id_prefix.value
key_struct = struct.Struct(b'>20sLH') key_struct = struct.Struct(b'>20sLH')
value_struct = struct.Struct(b'>LHL') value_struct = struct.Struct(b'>LH')
key_part_lambdas = [
lambda: b'',
length_encoded_name,
shortid_key_helper(b'>20s'),
shortid_key_helper(b'>20sL'),
shortid_key_helper(b'>20sLH'),
]
@classmethod @classmethod
def pack_key(cls, name: str, claim_hash: bytes, root_tx_num: int, root_position: int): def pack_key(cls, name: str, claim_hash: bytes, root_tx_num: int, root_position: int):
return cls.prefix + length_encoded_name(name) + cls.key_struct.pack(claim_hash, root_tx_num, root_position) return cls.prefix + length_encoded_name(name) + cls.key_struct.pack(claim_hash, root_tx_num, root_position)
@classmethod @classmethod
def pack_value(cls, tx_num: int, position: int, activation: int): def pack_value(cls, tx_num: int, position: int):
return super().pack_value(tx_num, position, activation) return super().pack_value(tx_num, position)
@classmethod @classmethod
def unpack_key(cls, key: bytes) -> ClaimShortIDKey: def unpack_key(cls, key: bytes) -> ClaimShortIDKey:
@ -286,9 +329,9 @@ class ClaimShortIDPrefixRow(PrefixRow):
@classmethod @classmethod
def pack_item(cls, name: str, claim_hash: bytes, root_tx_num: int, root_position: int, def pack_item(cls, name: str, claim_hash: bytes, root_tx_num: int, root_position: int,
tx_num: int, position: int, activation: int): tx_num: int, position: int):
return cls.pack_key(name, claim_hash, root_tx_num, root_position), \ return cls.pack_key(name, claim_hash, root_tx_num, root_position), \
cls.pack_value(tx_num, position, activation) cls.pack_value(tx_num, position)
class ClaimToChannelPrefixRow(PrefixRow): class ClaimToChannelPrefixRow(PrefixRow):
@ -317,15 +360,33 @@ class ClaimToChannelPrefixRow(PrefixRow):
return cls.pack_key(claim_hash), cls.pack_value(signing_hash) return cls.pack_key(claim_hash), cls.pack_value(signing_hash)
def channel_to_claim_helper(struct_fmt):
packer = struct.Struct(struct_fmt).pack
def wrapper(signing_hash: bytes, name: str, *args):
return signing_hash + length_encoded_name(name) + packer(*args)
return wrapper
class ChannelToClaimPrefixRow(PrefixRow): class ChannelToClaimPrefixRow(PrefixRow):
prefix = DB_PREFIXES.channel_to_claim.value prefix = DB_PREFIXES.channel_to_claim.value
key_struct = struct.Struct(b'>QLH') key_struct = struct.Struct(b'>LH')
value_struct = struct.Struct(b'>20sL') value_struct = struct.Struct(b'>20s')
key_part_lambdas = [
lambda: b'',
struct.Struct(b'>20s').pack,
channel_to_claim_helper(b''),
channel_to_claim_helper(b'>s'),
channel_to_claim_helper(b'>L'),
channel_to_claim_helper(b'>LH'),
]
@classmethod @classmethod
def pack_key(cls, signing_hash: bytes, name: str, effective_amount: int, tx_num: int, position: int): def pack_key(cls, signing_hash: bytes, name: str, tx_num: int, position: int):
return cls.prefix + signing_hash + length_encoded_name(name) + cls.key_struct.pack( return cls.prefix + signing_hash + length_encoded_name(name) + cls.key_struct.pack(
0xffffffffffffffff - effective_amount, tx_num, position tx_num, position
) )
@classmethod @classmethod
@ -334,24 +395,24 @@ class ChannelToClaimPrefixRow(PrefixRow):
signing_hash = key[1:21] signing_hash = key[1:21]
name_len = int.from_bytes(key[21:23], byteorder='big') name_len = int.from_bytes(key[21:23], byteorder='big')
name = key[23:23 + name_len].decode() name = key[23:23 + name_len].decode()
ones_comp_effective_amount, tx_num, position = cls.key_struct.unpack(key[23 + name_len:]) tx_num, position = cls.key_struct.unpack(key[23 + name_len:])
return ChannelToClaimKey( return ChannelToClaimKey(
signing_hash, name, 0xffffffffffffffff - ones_comp_effective_amount, tx_num, position signing_hash, name, tx_num, position
) )
@classmethod @classmethod
def pack_value(cls, claim_hash: bytes, claims_in_channel: int) -> bytes: def pack_value(cls, claim_hash: bytes) -> bytes:
return super().pack_value(claim_hash, claims_in_channel) return super().pack_value(claim_hash)
@classmethod @classmethod
def unpack_value(cls, data: bytes) -> ChannelToClaimValue: def unpack_value(cls, data: bytes) -> ChannelToClaimValue:
return ChannelToClaimValue(*cls.value_struct.unpack(data)) return ChannelToClaimValue(*cls.value_struct.unpack(data))
@classmethod @classmethod
def pack_item(cls, signing_hash: bytes, name: str, effective_amount: int, tx_num: int, position: int, def pack_item(cls, signing_hash: bytes, name: str, tx_num: int, position: int,
claim_hash: bytes, claims_in_channel: int): claim_hash: bytes):
return cls.pack_key(signing_hash, name, effective_amount, tx_num, position), \ return cls.pack_key(signing_hash, name, tx_num, position), \
cls.pack_value(claim_hash, claims_in_channel) cls.pack_value(claim_hash)
class ClaimToSupportPrefixRow(PrefixRow): class ClaimToSupportPrefixRow(PrefixRow):
@ -412,6 +473,12 @@ class ClaimExpirationPrefixRow(PrefixRow):
prefix = DB_PREFIXES.claim_expiration.value prefix = DB_PREFIXES.claim_expiration.value
key_struct = struct.Struct(b'>LLH') key_struct = struct.Struct(b'>LLH')
value_struct = struct.Struct(b'>20s') value_struct = struct.Struct(b'>20s')
key_part_lambdas = [
lambda: b'',
struct.Struct(b'>L').pack,
struct.Struct(b'>LL').pack,
struct.Struct(b'>LLH').pack,
]
@classmethod @classmethod
def pack_key(cls, expiration: int, tx_num: int, position: int) -> bytes: def pack_key(cls, expiration: int, tx_num: int, position: int) -> bytes:
@ -469,13 +536,20 @@ class ClaimTakeoverPrefixRow(PrefixRow):
return cls.pack_key(name), cls.pack_value(claim_hash, takeover_height) return cls.pack_key(name), cls.pack_value(claim_hash, takeover_height)
class PendingClaimActivationPrefixRow(PrefixRow): class PendingActivationPrefixRow(PrefixRow):
prefix = DB_PREFIXES.pending_activation.value prefix = DB_PREFIXES.pending_activation.value
key_struct = struct.Struct(b'>LLH') key_struct = struct.Struct(b'>LBLH')
key_part_lambdas = [
lambda: b'',
struct.Struct(b'>L').pack,
struct.Struct(b'>LB').pack,
struct.Struct(b'>LBL').pack,
struct.Struct(b'>LBLH').pack
]
@classmethod @classmethod
def pack_key(cls, height: int, tx_num: int, position: int): def pack_key(cls, height: int, txo_type: int, tx_num: int, position: int):
return super().pack_key(height, tx_num, position) return super().pack_key(height, txo_type, tx_num, position)
@classmethod @classmethod
def unpack_key(cls, key: bytes) -> PendingActivationKey: def unpack_key(cls, key: bytes) -> PendingActivationKey:
@ -493,11 +567,47 @@ class PendingClaimActivationPrefixRow(PrefixRow):
return PendingActivationValue(claim_hash, name) return PendingActivationValue(claim_hash, name)
@classmethod @classmethod
def pack_item(cls, height: int, tx_num: int, position: int, claim_hash: bytes, name: str): def pack_item(cls, height: int, txo_type: int, tx_num: int, position: int, claim_hash: bytes, name: str):
return cls.pack_key(height, tx_num, position), \ return cls.pack_key(height, txo_type, tx_num, position), \
cls.pack_value(claim_hash, name) cls.pack_value(claim_hash, name)
class ActivatedPrefixRow(PrefixRow):
prefix = DB_PREFIXES.activated_claim_and_support.value
key_struct = struct.Struct(b'>BLH')
value_struct = struct.Struct(b'>L20s')
key_part_lambdas = [
lambda: b'',
struct.Struct(b'>B').pack,
struct.Struct(b'>BL').pack,
struct.Struct(b'>BLH').pack
]
@classmethod
def pack_key(cls, txo_type: int, tx_num: int, position: int):
return super().pack_key(txo_type, tx_num, position)
@classmethod
def unpack_key(cls, key: bytes) -> ActivationKey:
return ActivationKey(*super().unpack_key(key))
@classmethod
def pack_value(cls, height: int, claim_hash: bytes, name: str) -> bytes:
return cls.value_struct.pack(height, claim_hash) + length_encoded_name(name)
@classmethod
def unpack_value(cls, data: bytes) -> ActivationValue:
height, claim_hash = cls.value_struct.unpack(data[:24])
name_len = int.from_bytes(data[24:26], byteorder='big')
name = data[26:26 + name_len].decode()
return ActivationValue(height, claim_hash, name)
@classmethod
def pack_item(cls, txo_type: int, tx_num: int, position: int, height: int, claim_hash: bytes, name: str):
return cls.pack_key(txo_type, tx_num, position), \
cls.pack_value(height, claim_hash, name)
class Prefixes: class Prefixes:
claim_to_support = ClaimToSupportPrefixRow claim_to_support = ClaimToSupportPrefixRow
support_to_claim = SupportToClaimPrefixRow support_to_claim = SupportToClaimPrefixRow
@ -509,10 +619,11 @@ class Prefixes:
channel_to_claim = ChannelToClaimPrefixRow channel_to_claim = ChannelToClaimPrefixRow
claim_short_id = ClaimShortIDPrefixRow claim_short_id = ClaimShortIDPrefixRow
claim_effective_amount = EffectiveAmountPrefixRow
claim_expiration = ClaimExpirationPrefixRow claim_expiration = ClaimExpirationPrefixRow
claim_takeover = ClaimTakeoverPrefixRow claim_takeover = ClaimTakeoverPrefixRow
pending_activation = PendingClaimActivationPrefixRow pending_activation = PendingActivationPrefixRow
activated = ActivatedPrefixRow
active_amount = ActiveAmountPrefixRow
# undo_claimtrie = b'M' # undo_claimtrie = b'M'

View file

@ -18,7 +18,7 @@ import struct
import attr import attr
import zlib import zlib
import base64 import base64
from typing import Optional, Iterable from typing import Optional, Iterable, Tuple, DefaultDict, Set, Dict, List
from functools import partial from functools import partial
from asyncio import sleep from asyncio import sleep
from bisect import bisect_right, bisect_left from bisect import bisect_right, bisect_left
@ -37,8 +37,9 @@ from lbry.wallet.server.storage import db_class
from lbry.wallet.server.db.revertable import RevertablePut, RevertableDelete, RevertableOp, delete_prefix from lbry.wallet.server.db.revertable import RevertablePut, RevertableDelete, RevertableOp, delete_prefix
from lbry.wallet.server.db import DB_PREFIXES from lbry.wallet.server.db import DB_PREFIXES
from lbry.wallet.server.db.prefixes import Prefixes, PendingActivationValue, ClaimTakeoverValue, ClaimToTXOValue from lbry.wallet.server.db.prefixes import Prefixes, PendingActivationValue, ClaimTakeoverValue, ClaimToTXOValue
from lbry.wallet.server.db.claimtrie import StagedClaimtrieItem, get_update_effective_amount_ops, length_encoded_name from lbry.wallet.server.db.prefixes import ACTIVATED_CLAIM_TXO_TYPE, ACTIVATED_SUPPORT_TXO_TYPE
from lbry.wallet.server.db.claimtrie import get_expiration_height, get_delay_for_name from lbry.wallet.server.db.prefixes import PendingActivationKey, ClaimToTXOKey, TXOToClaimValue
from lbry.wallet.server.db.claimtrie import StagedClaimtrieItem, length_encoded_name
from lbry.wallet.server.db.elasticsearch import SearchIndex from lbry.wallet.server.db.elasticsearch import SearchIndex
@ -56,17 +57,6 @@ TXO_STRUCT_unpack = TXO_STRUCT.unpack
TXO_STRUCT_pack = TXO_STRUCT.pack TXO_STRUCT_pack = TXO_STRUCT.pack
HISTORY_PREFIX = b'A'
TX_PREFIX = b'B'
BLOCK_HASH_PREFIX = b'C'
HEADER_PREFIX = b'H'
TX_NUM_PREFIX = b'N'
TX_COUNT_PREFIX = b'T'
UNDO_PREFIX = b'U'
TX_HASH_PREFIX = b'X'
HASHX_UTXO_PREFIX = b'h'
UTXO_PREFIX = b'u'
HASHX_HISTORY_PREFIX = b'x'
@attr.s(slots=True) @attr.s(slots=True)
@ -188,12 +178,22 @@ class LevelDB:
# Search index # Search index
self.search_index = SearchIndex(self.env.es_index_prefix, self.env.database_query_timeout) self.search_index = SearchIndex(self.env.es_index_prefix, self.env.database_query_timeout)
def claim_hash_and_name_from_txo(self, tx_num: int, tx_idx: int): def get_claim_from_txo(self, tx_num: int, tx_idx: int) -> Optional[TXOToClaimValue]:
claim_hash_and_name = self.db.get(Prefixes.txo_to_claim.pack_key(tx_num, tx_idx)) claim_hash_and_name = self.db.get(Prefixes.txo_to_claim.pack_key(tx_num, tx_idx))
if not claim_hash_and_name: if not claim_hash_and_name:
return return
return Prefixes.txo_to_claim.unpack_value(claim_hash_and_name) return Prefixes.txo_to_claim.unpack_value(claim_hash_and_name)
def get_activation(self, tx_num, position, is_support=False) -> int:
activation = self.db.get(
Prefixes.activated.pack_key(
ACTIVATED_SUPPORT_TXO_TYPE if is_support else ACTIVATED_CLAIM_TXO_TYPE, tx_num, position
)
)
if activation:
return Prefixes.activated.unpack_value(activation).height
return -1
def get_supported_claim_from_txo(self, tx_num: int, position: int) -> typing.Tuple[Optional[bytes], Optional[int]]: def get_supported_claim_from_txo(self, tx_num: int, position: int) -> typing.Tuple[Optional[bytes], Optional[int]]:
key = Prefixes.support_to_claim.pack_key(tx_num, position) key = Prefixes.support_to_claim.pack_key(tx_num, position)
supported_claim_hash = self.db.get(key) supported_claim_hash = self.db.get(key)
@ -228,7 +228,7 @@ class LevelDB:
created_height = bisect_right(self.tx_counts, root_tx_num) created_height = bisect_right(self.tx_counts, root_tx_num)
last_take_over_height = controlling_claim.height last_take_over_height = controlling_claim.height
expiration_height = get_expiration_height(height) expiration_height = self.coin.get_expiration_height(height)
support_amount = self.get_support_amount(claim_hash) support_amount = self.get_support_amount(claim_hash)
claim_amount = self.get_claim_txo_amount(claim_hash, tx_num, position) claim_amount = self.get_claim_txo_amount(claim_hash, tx_num, position)
@ -239,7 +239,7 @@ class LevelDB:
short_url = f'{name}#{claim_hash.hex()}' short_url = f'{name}#{claim_hash.hex()}'
canonical_url = short_url canonical_url = short_url
if channel_hash: if channel_hash:
channel_vals = self.get_root_claim_txo_and_current_amount(channel_hash) channel_vals = self.get_claim_txo(channel_hash)
if channel_vals: if channel_vals:
channel_name = channel_vals[1].name channel_name = channel_vals[1].name
claims_in_channel = self.get_claims_in_channel_count(channel_hash) claims_in_channel = self.get_claims_in_channel_count(channel_hash)
@ -260,11 +260,13 @@ class LevelDB:
:param claim_id: partial or complete claim id :param claim_id: partial or complete claim id
:param amount_order: '$<value>' suffix to a url, defaults to 1 (winning) if no claim id modifier is provided :param amount_order: '$<value>' suffix to a url, defaults to 1 (winning) if no claim id modifier is provided
""" """
if not amount_order and not claim_id: if (not amount_order and not claim_id) or amount_order == 1:
# winning resolution # winning resolution
controlling = self.get_controlling_claim(normalized_name) controlling = self.get_controlling_claim(normalized_name)
if not controlling: if not controlling:
print("none controlling")
return return
print("resolved controlling", controlling.claim_hash.hex())
return self._fs_get_claim_by_hash(controlling.claim_hash) return self._fs_get_claim_by_hash(controlling.claim_hash)
encoded_name = length_encoded_name(normalized_name) encoded_name = length_encoded_name(normalized_name)
@ -279,7 +281,7 @@ class LevelDB:
claim_txo = Prefixes.claim_short_id.unpack_value(v) claim_txo = Prefixes.claim_short_id.unpack_value(v)
return self._prepare_resolve_result( return self._prepare_resolve_result(
claim_txo.tx_num, claim_txo.position, key.claim_hash, key.name, key.root_tx_num, claim_txo.tx_num, claim_txo.position, key.claim_hash, key.name, key.root_tx_num,
key.root_position, claim_txo.activation key.root_position, self.get_activation(claim_txo.tx_num, claim_txo.position)
) )
return return
@ -302,8 +304,9 @@ class LevelDB:
for k, v in self.db.iterator(prefix=prefix): for k, v in self.db.iterator(prefix=prefix):
key = Prefixes.channel_to_claim.unpack_key(k) key = Prefixes.channel_to_claim.unpack_key(k)
stream = Prefixes.channel_to_claim.unpack_value(v) stream = Prefixes.channel_to_claim.unpack_value(v)
if not candidates or candidates[-1][-1] == key.effective_amount: effective_amount = self.get_effective_amount(stream.claim_hash)
candidates.append((stream.claim_hash, key.tx_num, key.position, key.effective_amount)) if not candidates or candidates[-1][-1] == effective_amount:
candidates.append((stream.claim_hash, key.tx_num, key.position, effective_amount))
else: else:
break break
if not candidates: if not candidates:
@ -347,12 +350,13 @@ class LevelDB:
return await asyncio.get_event_loop().run_in_executor(self.executor, self._fs_resolve, url) return await asyncio.get_event_loop().run_in_executor(self.executor, self._fs_resolve, url)
def _fs_get_claim_by_hash(self, claim_hash): def _fs_get_claim_by_hash(self, claim_hash):
for k, v in self.db.iterator(prefix=DB_PREFIXES.claim_to_txo.value + claim_hash): for k, v in self.db.iterator(prefix=Prefixes.claim_to_txo.pack_partial_key(claim_hash)):
unpacked_k = Prefixes.claim_to_txo.unpack_key(k) unpacked_k = Prefixes.claim_to_txo.unpack_key(k)
unpacked_v = Prefixes.claim_to_txo.unpack_value(v) unpacked_v = Prefixes.claim_to_txo.unpack_value(v)
activation_height = self.get_activation(unpacked_k.tx_num, unpacked_k.position)
return self._prepare_resolve_result( return self._prepare_resolve_result(
unpacked_k.tx_num, unpacked_k.position, unpacked_k.claim_hash, unpacked_v.name, unpacked_k.tx_num, unpacked_k.position, unpacked_k.claim_hash, unpacked_v.name,
unpacked_v.root_tx_num, unpacked_v.root_position, unpacked_v.activation unpacked_v.root_tx_num, unpacked_v.root_position, activation_height
) )
async def fs_getclaimbyid(self, claim_id): async def fs_getclaimbyid(self, claim_id):
@ -360,19 +364,8 @@ class LevelDB:
self.executor, self._fs_get_claim_by_hash, bytes.fromhex(claim_id) self.executor, self._fs_get_claim_by_hash, bytes.fromhex(claim_id)
) )
def claim_exists(self, claim_hash: bytes):
for _ in self.db.iterator(prefix=DB_PREFIXES.claim_to_txo.value + claim_hash, include_value=False):
return True
return False
def get_root_claim_txo_and_current_amount(self, claim_hash):
for k, v in self.db.iterator(prefix=DB_PREFIXES.claim_to_txo.value + claim_hash):
unpacked_k = Prefixes.claim_to_txo.unpack_key(k)
unpacked_v = Prefixes.claim_to_txo.unpack_value(v)
return unpacked_k, unpacked_v
def make_staged_claim_item(self, claim_hash: bytes) -> Optional[StagedClaimtrieItem]: def make_staged_claim_item(self, claim_hash: bytes) -> Optional[StagedClaimtrieItem]:
claim_info = self.get_root_claim_txo_and_current_amount(claim_hash) claim_info = self.get_claim_txo(claim_hash)
k, v = claim_info k, v = claim_info
root_tx_num = v.root_tx_num root_tx_num = v.root_tx_num
root_idx = v.root_position root_idx = v.root_position
@ -381,16 +374,14 @@ class LevelDB:
tx_num = k.tx_num tx_num = k.tx_num
idx = k.position idx = k.position
height = bisect_right(self.tx_counts, tx_num) height = bisect_right(self.tx_counts, tx_num)
effective_amount = self.get_support_amount(claim_hash) + value
signing_hash = self.get_channel_for_claim(claim_hash) signing_hash = self.get_channel_for_claim(claim_hash)
activation_height = v.activation # if signing_hash:
if signing_hash: # count = self.get_claims_in_channel_count(signing_hash)
count = self.get_claims_in_channel_count(signing_hash) # else:
else: # count = 0
count = 0
return StagedClaimtrieItem( return StagedClaimtrieItem(
name, claim_hash, value, effective_amount, activation_height, get_expiration_height(height), tx_num, idx, name, claim_hash, value, self.coin.get_expiration_height(height), tx_num, idx,
root_tx_num, root_idx, signing_hash, count root_tx_num, root_idx, signing_hash
) )
def get_claim_txo_amount(self, claim_hash: bytes, tx_num: int, position: int) -> Optional[int]: def get_claim_txo_amount(self, claim_hash: bytes, tx_num: int, position: int) -> Optional[int]:
@ -398,58 +389,57 @@ class LevelDB:
if v: if v:
return Prefixes.claim_to_txo.unpack_value(v).amount return Prefixes.claim_to_txo.unpack_value(v).amount
def get_claim_from_txo(self, claim_hash: bytes) -> Optional[ClaimToTXOValue]: def get_support_txo_amount(self, claim_hash: bytes, tx_num: int, position: int) -> Optional[int]:
v = self.db.get(Prefixes.claim_to_support.pack_key(claim_hash, tx_num, position))
if v:
return Prefixes.claim_to_support.unpack_value(v).amount
def get_claim_txo(self, claim_hash: bytes) -> Optional[Tuple[ClaimToTXOKey, ClaimToTXOValue]]:
assert claim_hash assert claim_hash
for v in self.db.iterator(prefix=DB_PREFIXES.claim_to_txo.value + claim_hash, include_key=False): for k, v in self.db.iterator(prefix=Prefixes.claim_to_txo.pack_partial_key(claim_hash)):
return Prefixes.claim_to_txo.unpack_value(v) return Prefixes.claim_to_txo.unpack_key(k), Prefixes.claim_to_txo.unpack_value(v)
def get_claim_amount(self, claim_hash: bytes) -> Optional[int]: def _get_active_amount(self, claim_hash: bytes, txo_type: int, height: int) -> int:
claim = self.get_claim_from_txo(claim_hash) return sum(
if claim: Prefixes.active_amount.unpack_value(v).amount
return claim.amount for v in self.db.iterator(start=Prefixes.active_amount.pack_partial_key(
claim_hash, txo_type, 0), stop=Prefixes.active_amount.pack_partial_key(
def get_effective_amount(self, claim_hash: bytes): claim_hash, txo_type, height), include_key=False)
return (self.get_claim_amount(claim_hash) or 0) + self.get_support_amount(claim_hash)
def get_update_effective_amount_ops(self, claim_hash: bytes, effective_amount: int):
claim_info = self.get_root_claim_txo_and_current_amount(claim_hash)
if not claim_info:
return []
root_tx_num = claim_info[1].root_tx_num
root_position = claim_info[1].root_position
amount = claim_info[1].amount
name = claim_info[1].name
tx_num = claim_info[0].tx_num
position = claim_info[0].position
activation = claim_info[1].activation
signing_hash = self.get_channel_for_claim(claim_hash)
claims_in_channel_count = None
if signing_hash:
claims_in_channel_count = self.get_claims_in_channel_count(signing_hash)
prev_effective_amount = self.get_effective_amount(claim_hash)
return get_update_effective_amount_ops(
name, effective_amount, prev_effective_amount, tx_num, position,
root_tx_num, root_position, claim_hash, activation, activation, signing_hash,
claims_in_channel_count
) )
def get_effective_amount(self, claim_hash: bytes, support_only=False) -> int:
support_amount = self._get_active_amount(claim_hash, ACTIVATED_SUPPORT_TXO_TYPE, self.db_height + 1)
if support_only:
return support_only
return support_amount + self._get_active_amount(claim_hash, ACTIVATED_CLAIM_TXO_TYPE, self.db_height + 1)
def get_claims_for_name(self, name):
claims = []
for _k, _v in self.db.iterator(prefix=Prefixes.claim_short_id.pack_partial_key(name)):
k, v = Prefixes.claim_short_id.unpack_key(_k), Prefixes.claim_short_id.unpack_value(_v)
# claims[v.claim_hash] = (k, v)
if k.claim_hash not in claims:
claims.append(k.claim_hash)
return claims
def get_claims_in_channel_count(self, channel_hash) -> int: def get_claims_in_channel_count(self, channel_hash) -> int:
for v in self.db.iterator(prefix=DB_PREFIXES.channel_to_claim.value + channel_hash, include_key=False): count = 0
return Prefixes.channel_to_claim.unpack_value(v).claims_in_channel for _ in self.db.iterator(prefix=Prefixes.channel_to_claim.pack_partial_key(channel_hash), include_key=False):
return 0 count += 1
return count
def get_channel_for_claim(self, claim_hash) -> Optional[bytes]: def get_channel_for_claim(self, claim_hash) -> Optional[bytes]:
return self.db.get(DB_PREFIXES.claim_to_channel.value + claim_hash) return self.db.get(Prefixes.claim_to_channel.pack_key(claim_hash))
def get_expired_by_height(self, height: int): def get_expired_by_height(self, height: int) -> Dict[bytes, Tuple[int, int, str, TxInput]]:
expired = {} expired = {}
for _k, _v in self.db.iterator(prefix=DB_PREFIXES.claim_expiration.value + struct.pack(b'>L', height)): for _k, _v in self.db.iterator(prefix=Prefixes.claim_expiration.pack_partial_key(height)):
k, v = Prefixes.claim_expiration.unpack_item(_k, _v) k, v = Prefixes.claim_expiration.unpack_item(_k, _v)
tx_hash = self.total_transactions[k.tx_num] tx_hash = self.total_transactions[k.tx_num]
tx = self.coin.transaction(self.db.get(DB_PREFIXES.TX_PREFIX.value + tx_hash)) tx = self.coin.transaction(self.db.get(DB_PREFIXES.TX_PREFIX.value + tx_hash))
# treat it like a claim spend so it will delete/abandon properly # treat it like a claim spend so it will delete/abandon properly
# the _spend_claim function this result is fed to expects a txi, so make a mock one # the _spend_claim function this result is fed to expects a txi, so make a mock one
print(f"\texpired lbry://{v.name} {v.claim_hash.hex()}")
expired[v.claim_hash] = ( expired[v.claim_hash] = (
k.tx_num, k.position, v.name, k.tx_num, k.position, v.name,
TxInput(prev_hash=tx_hash, prev_idx=k.position, script=tx.outputs[k.position].pk_script, sequence=0) TxInput(prev_hash=tx_hash, prev_idx=k.position, script=tx.outputs[k.position].pk_script, sequence=0)
@ -462,28 +452,21 @@ class LevelDB:
return return
return Prefixes.claim_takeover.unpack_value(controlling) return Prefixes.claim_takeover.unpack_value(controlling)
def get_claims_for_name(self, name: str): def get_claim_txos_for_name(self, name: str):
claim_hashes = set() txos = {}
for k in self.db.iterator(prefix=Prefixes.claim_short_id.prefix + length_encoded_name(name), for k, v in self.db.iterator(prefix=Prefixes.claim_short_id.pack_partial_key(name)):
include_value=False): claim_hash = Prefixes.claim_short_id.unpack_key(k).claim_hash
claim_hashes.add(Prefixes.claim_short_id.unpack_key(k).claim_hash) tx_num, nout = Prefixes.claim_short_id.unpack_value(v)
return claim_hashes txos[claim_hash] = tx_num, nout
return txos
def get_activated_claims_at_height(self, height: int) -> typing.Set[PendingActivationValue]: def get_activated_at_height(self, height: int) -> DefaultDict[PendingActivationValue, List[PendingActivationKey]]:
claims = set() activated = defaultdict(list)
prefix = Prefixes.pending_activation.prefix + height.to_bytes(4, byteorder='big') for _k, _v in self.db.iterator(prefix=Prefixes.pending_activation.pack_partial_key(height)):
for _v in self.db.iterator(prefix=prefix, include_key=False): k = Prefixes.pending_activation.unpack_key(_k)
v = Prefixes.pending_activation.unpack_value(_v) v = Prefixes.pending_activation.unpack_value(_v)
claims.add(v) activated[v].append(k)
return claims return activated
def get_activation_delay(self, claim_hash: bytes, name: str) -> int:
controlling = self.get_controlling_claim(name)
if not controlling:
return 0
if claim_hash == controlling.claim_hash:
return 0
return get_delay_for_name(self.db_height - controlling.height)
async def _read_tx_counts(self): async def _read_tx_counts(self):
if self.tx_counts is not None: if self.tx_counts is not None:
@ -494,7 +477,7 @@ class LevelDB:
def get_counts(): def get_counts():
return tuple( return tuple(
util.unpack_be_uint64(tx_count) util.unpack_be_uint64(tx_count)
for tx_count in self.db.iterator(prefix=TX_COUNT_PREFIX, include_key=False) for tx_count in self.db.iterator(prefix=DB_PREFIXES.TX_COUNT_PREFIX.value, include_key=False)
) )
tx_counts = await asyncio.get_event_loop().run_in_executor(self.executor, get_counts) tx_counts = await asyncio.get_event_loop().run_in_executor(self.executor, get_counts)
@ -509,7 +492,7 @@ class LevelDB:
async def _read_txids(self): async def _read_txids(self):
def get_txids(): def get_txids():
return list(self.db.iterator(prefix=TX_HASH_PREFIX, include_key=False)) return list(self.db.iterator(prefix=DB_PREFIXES.TX_HASH_PREFIX.value, include_key=False))
start = time.perf_counter() start = time.perf_counter()
self.logger.info("loading txids") self.logger.info("loading txids")
@ -528,7 +511,7 @@ class LevelDB:
def get_headers(): def get_headers():
return [ return [
header for header in self.db.iterator(prefix=HEADER_PREFIX, include_key=False) header for header in self.db.iterator(prefix=DB_PREFIXES.HEADER_PREFIX.value, include_key=False)
] ]
headers = await asyncio.get_event_loop().run_in_executor(self.executor, get_headers) headers = await asyncio.get_event_loop().run_in_executor(self.executor, get_headers)

View file

@ -1,6 +1,7 @@
import asyncio import asyncio
import json import json
import hashlib import hashlib
from bisect import bisect_right
from binascii import hexlify, unhexlify from binascii import hexlify, unhexlify
from lbry.testcase import CommandTestCase from lbry.testcase import CommandTestCase
from lbry.wallet.transaction import Transaction, Output from lbry.wallet.transaction import Transaction, Output
@ -43,35 +44,52 @@ class BaseResolveTestCase(CommandTestCase):
async def assertMatchClaim(self, claim_id): async def assertMatchClaim(self, claim_id):
expected = json.loads(await self.blockchain._cli_cmnd('getclaimbyid', claim_id)) expected = json.loads(await self.blockchain._cli_cmnd('getclaimbyid', claim_id))
resolved, _ = await self.conductor.spv_node.server.bp.db.fs_getclaimbyid(claim_id) claim = await self.conductor.spv_node.server.bp.db.fs_getclaimbyid(claim_id)
print(expected) if not expected:
print(resolved) self.assertIsNone(claim)
self.assertDictEqual({ return
'claim_id': expected['claimId'], self.assertEqual(expected['claimId'], claim.claim_hash.hex())
'activation_height': expected['validAtHeight'], self.assertEqual(expected['validAtHeight'], claim.activation_height)
'last_takeover_height': expected['lastTakeoverHeight'], self.assertEqual(expected['lastTakeoverHeight'], claim.last_takeover_height)
'txid': expected['txId'], self.assertEqual(expected['txId'], claim.tx_hash[::-1].hex())
'nout': expected['n'], self.assertEqual(expected['n'], claim.position)
'amount': expected['amount'], self.assertEqual(expected['amount'], claim.amount)
'effective_amount': expected['effectiveAmount'] self.assertEqual(expected['effectiveAmount'], claim.effective_amount)
}, { return claim
'claim_id': resolved.claim_hash.hex(),
'activation_height': resolved.activation_height,
'last_takeover_height': resolved.last_takeover_height,
'txid': resolved.tx_hash[::-1].hex(),
'nout': resolved.position,
'amount': resolved.amount,
'effective_amount': resolved.effective_amount
})
return resolved
async def assertMatchClaimIsWinning(self, name, claim_id): async def assertMatchClaimIsWinning(self, name, claim_id):
self.assertEqual(claim_id, (await self.assertMatchWinningClaim(name)).claim_hash.hex()) self.assertEqual(claim_id, (await self.assertMatchWinningClaim(name)).claim_hash.hex())
await self.assertMatchClaim(claim_id) await self.assertMatchClaim(claim_id)
await self.assertMatchClaimsForName(name)
async def assertMatchClaimsForName(self, name):
expected = json.loads(await self.blockchain._cli_cmnd('getclaimsforname', name))
print(len(expected['claims']), 'from lbrycrd for ', name)
db = self.conductor.spv_node.server.bp.db
def check_supports(claim_id, lbrycrd_supports):
for i, (tx_num, position, amount) in enumerate(db.get_supports(bytes.fromhex(claim_id))):
support = lbrycrd_supports[i]
self.assertEqual(support['txId'], db.total_transactions[tx_num][::-1].hex())
self.assertEqual(support['n'], position)
self.assertEqual(support['height'], bisect_right(db.tx_counts, tx_num))
self.assertEqual(support['validAtHeight'], db.get_activation(tx_num, position, is_support=True))
# self.assertEqual(len(expected['claims']), len(db_claims.claims))
# self.assertEqual(expected['lastTakeoverHeight'], db_claims.lastTakeoverHeight)
for c in expected['claims']:
check_supports(c['claimId'], c['supports'])
claim_hash = bytes.fromhex(c['claimId'])
self.assertEqual(c['validAtHeight'], db.get_activation(
db.total_transactions.index(bytes.fromhex(c['txId'])[::-1]), c['n']
))
self.assertEqual(c['effectiveAmount'], db.get_effective_amount(claim_hash))
class ResolveCommand(BaseResolveTestCase): class ResolveCommand(BaseResolveTestCase):
async def test_resolve_response(self): async def test_resolve_response(self):
channel_id = self.get_claim_id( channel_id = self.get_claim_id(
await self.channel_create('@abc', '0.01') await self.channel_create('@abc', '0.01')
@ -170,6 +188,7 @@ class ResolveCommand(BaseResolveTestCase):
await self.stream_create('foo', '0.9', allow_duplicate_name=True)) await self.stream_create('foo', '0.9', allow_duplicate_name=True))
# plain winning claim # plain winning claim
await self.assertResolvesToClaimId('foo', claim_id3) await self.assertResolvesToClaimId('foo', claim_id3)
# amount order resolution # amount order resolution
await self.assertResolvesToClaimId('foo$1', claim_id3) await self.assertResolvesToClaimId('foo$1', claim_id3)
await self.assertResolvesToClaimId('foo$2', claim_id2) await self.assertResolvesToClaimId('foo$2', claim_id2)
@ -275,9 +294,7 @@ class ResolveCommand(BaseResolveTestCase):
winner_id = self.get_claim_id(c) winner_id = self.get_claim_id(c)
# winning_one = await self.check_lbrycrd_winning(one) # winning_one = await self.check_lbrycrd_winning(one)
winning_two = await self.assertMatchWinningClaim(two) await self.assertMatchClaimIsWinning(two, winner_id)
self.assertEqual(winner_id, winning_two.claim_hash.hex())
r1 = await self.resolve(f'lbry://{one}') r1 = await self.resolve(f'lbry://{one}')
r2 = await self.resolve(f'lbry://{two}') r2 = await self.resolve(f'lbry://{two}')
@ -385,24 +402,37 @@ class ResolveCommand(BaseResolveTestCase):
class ResolveClaimTakeovers(BaseResolveTestCase): class ResolveClaimTakeovers(BaseResolveTestCase):
async def test_activation_delay(self): async def _test_activation_delay(self):
name = 'derp' name = 'derp'
# initially claim the name # initially claim the name
first_claim_id = (await self.stream_create(name, '0.1'))['outputs'][0]['claim_id'] first_claim_id = (await self.stream_create(name, '0.1', allow_duplicate_name=True))['outputs'][0]['claim_id']
self.assertEqual(first_claim_id, (await self.assertMatchWinningClaim(name)).claim_hash.hex()) await self.assertMatchClaimIsWinning(name, first_claim_id)
await self.generate(320) await self.generate(320)
# a claim of higher amount made now will have a takeover delay of 10 # a claim of higher amount made now will have a takeover delay of 10
second_claim_id = (await self.stream_create(name, '0.2', allow_duplicate_name=True))['outputs'][0]['claim_id'] second_claim_id = (await self.stream_create(name, '0.2', allow_duplicate_name=True))['outputs'][0]['claim_id']
# sanity check # sanity check
self.assertNotEqual(first_claim_id, second_claim_id) self.assertNotEqual(first_claim_id, second_claim_id)
# takeover should not have happened yet # takeover should not have happened yet
self.assertEqual(first_claim_id, (await self.assertMatchWinningClaim(name)).claim_hash.hex()) await self.assertMatchClaimIsWinning(name, first_claim_id)
await self.generate(9) await self.generate(9)
# not yet # not yet
self.assertEqual(first_claim_id, (await self.assertMatchWinningClaim(name)).claim_hash.hex()) await self.assertMatchClaimIsWinning(name, first_claim_id)
await self.generate(1) await self.generate(1)
# the new claim should have activated # the new claim should have activated
self.assertEqual(second_claim_id, (await self.assertMatchWinningClaim(name)).claim_hash.hex()) await self.assertMatchClaimIsWinning(name, second_claim_id)
return first_claim_id, second_claim_id
async def test_activation_delay(self):
await self._test_activation_delay()
async def test_activation_delay_then_abandon_then_reclaim(self):
name = 'derp'
first_claim_id, second_claim_id = await self._test_activation_delay()
await self.daemon.jsonrpc_txo_spend(type='stream', claim_id=first_claim_id)
await self.daemon.jsonrpc_txo_spend(type='stream', claim_id=second_claim_id)
await self.generate(1)
await self.assertNoClaimForName(name)
await self._test_activation_delay()
async def test_block_takeover_with_delay_1_support(self): async def test_block_takeover_with_delay_1_support(self):
name = 'derp' name = 'derp'
@ -415,46 +445,46 @@ class ResolveClaimTakeovers(BaseResolveTestCase):
# sanity check # sanity check
self.assertNotEqual(first_claim_id, second_claim_id) self.assertNotEqual(first_claim_id, second_claim_id)
# takeover should not have happened yet # takeover should not have happened yet
self.assertEqual(first_claim_id, (await self.assertMatchWinningClaim(name)).claim_hash.hex()) await self.assertMatchClaimIsWinning(name, first_claim_id)
await self.generate(8) await self.generate(8)
self.assertEqual(first_claim_id, (await self.assertMatchWinningClaim(name)).claim_hash.hex()) await self.assertMatchClaimIsWinning(name, first_claim_id)
# prevent the takeover by adding a support one block before the takeover happens # prevent the takeover by adding a support one block before the takeover happens
await self.support_create(first_claim_id, bid='1.0') await self.support_create(first_claim_id, bid='1.0')
# one more block until activation # one more block until activation
await self.generate(1) await self.generate(1)
self.assertEqual(first_claim_id, (await self.assertMatchWinningClaim(name)).claim_hash.hex()) await self.assertMatchClaimIsWinning(name, first_claim_id)
async def test_block_takeover_with_delay_0_support(self): async def test_block_takeover_with_delay_0_support(self):
name = 'derp' name = 'derp'
# initially claim the name # initially claim the name
first_claim_id = (await self.stream_create(name, '0.1'))['outputs'][0]['claim_id'] first_claim_id = (await self.stream_create(name, '0.1'))['outputs'][0]['claim_id']
self.assertEqual(first_claim_id, (await self.assertMatchWinningClaim(name)).claim_hash.hex()) await self.assertMatchClaimIsWinning(name, first_claim_id)
await self.generate(320) await self.generate(320)
# a claim of higher amount made now will have a takeover delay of 10 # a claim of higher amount made now will have a takeover delay of 10
second_claim_id = (await self.stream_create(name, '0.2', allow_duplicate_name=True))['outputs'][0]['claim_id'] second_claim_id = (await self.stream_create(name, '0.2', allow_duplicate_name=True))['outputs'][0]['claim_id']
# sanity check # sanity check
self.assertNotEqual(first_claim_id, second_claim_id) await self.assertMatchClaimIsWinning(name, first_claim_id)
# takeover should not have happened yet # takeover should not have happened yet
self.assertEqual(first_claim_id, (await self.assertMatchWinningClaim(name)).claim_hash.hex()) await self.assertMatchClaimIsWinning(name, first_claim_id)
await self.generate(9) await self.generate(9)
self.assertEqual(first_claim_id, (await self.assertMatchWinningClaim(name)).claim_hash.hex()) await self.assertMatchClaimIsWinning(name, first_claim_id)
# prevent the takeover by adding a support on the same block the takeover would happen # prevent the takeover by adding a support on the same block the takeover would happen
await self.support_create(first_claim_id, bid='1.0') await self.support_create(first_claim_id, bid='1.0')
self.assertEqual(first_claim_id, (await self.assertMatchWinningClaim(name)).claim_hash.hex()) await self.assertMatchClaimIsWinning(name, first_claim_id)
async def _test_almost_prevent_takeover(self, name: str, blocks: int = 9): async def _test_almost_prevent_takeover(self, name: str, blocks: int = 9):
# initially claim the name # initially claim the name
first_claim_id = (await self.stream_create(name, '0.1'))['outputs'][0]['claim_id'] first_claim_id = (await self.stream_create(name, '0.1'))['outputs'][0]['claim_id']
self.assertEqual(first_claim_id, (await self.assertMatchWinningClaim(name)).claim_hash.hex()) await self.assertMatchClaimIsWinning(name, first_claim_id)
await self.generate(320) await self.generate(320)
# a claim of higher amount made now will have a takeover delay of 10 # a claim of higher amount made now will have a takeover delay of 10
second_claim_id = (await self.stream_create(name, '0.2', allow_duplicate_name=True))['outputs'][0]['claim_id'] second_claim_id = (await self.stream_create(name, '0.2', allow_duplicate_name=True))['outputs'][0]['claim_id']
# sanity check # sanity check
self.assertNotEqual(first_claim_id, second_claim_id) self.assertNotEqual(first_claim_id, second_claim_id)
# takeover should not have happened yet # takeover should not have happened yet
self.assertEqual(first_claim_id, (await self.assertMatchWinningClaim(name)).claim_hash.hex()) await self.assertMatchClaimIsWinning(name, first_claim_id)
await self.generate(blocks) await self.generate(blocks)
self.assertEqual(first_claim_id, (await self.assertMatchWinningClaim(name)).claim_hash.hex()) await self.assertMatchClaimIsWinning(name, first_claim_id)
# prevent the takeover by adding a support on the same block the takeover would happen # prevent the takeover by adding a support on the same block the takeover would happen
tx = await self.daemon.jsonrpc_support_create(first_claim_id, '1.0') tx = await self.daemon.jsonrpc_support_create(first_claim_id, '1.0')
await self.ledger.wait(tx) await self.ledger.wait(tx)
@ -465,7 +495,7 @@ class ResolveClaimTakeovers(BaseResolveTestCase):
first_claim_id, second_claim_id, tx = await self._test_almost_prevent_takeover(name, 9) first_claim_id, second_claim_id, tx = await self._test_almost_prevent_takeover(name, 9)
await self.daemon.jsonrpc_txo_spend(type='support', txid=tx.id) await self.daemon.jsonrpc_txo_spend(type='support', txid=tx.id)
await self.generate(1) await self.generate(1)
self.assertEqual(second_claim_id, (await self.assertMatchWinningClaim(name)).claim_hash.hex()) await self.assertMatchClaimIsWinning(name, second_claim_id)
async def test_almost_prevent_takeover_remove_support_one_block_after_supported(self): async def test_almost_prevent_takeover_remove_support_one_block_after_supported(self):
name = 'derp' name = 'derp'
@ -473,35 +503,35 @@ class ResolveClaimTakeovers(BaseResolveTestCase):
await self.generate(1) await self.generate(1)
await self.daemon.jsonrpc_txo_spend(type='support', txid=tx.id) await self.daemon.jsonrpc_txo_spend(type='support', txid=tx.id)
await self.generate(1) await self.generate(1)
self.assertEqual(second_claim_id, (await self.assertMatchWinningClaim(name)).claim_hash.hex()) await self.assertMatchClaimIsWinning(name, second_claim_id)
async def test_abandon_before_takeover(self): async def test_abandon_before_takeover(self):
name = 'derp' name = 'derp'
# initially claim the name # initially claim the name
first_claim_id = (await self.stream_create(name, '0.1'))['outputs'][0]['claim_id'] first_claim_id = (await self.stream_create(name, '0.1'))['outputs'][0]['claim_id']
self.assertEqual(first_claim_id, (await self.assertMatchWinningClaim(name)).claim_hash.hex()) await self.assertMatchClaimIsWinning(name, first_claim_id)
await self.generate(320) await self.generate(320)
# a claim of higher amount made now will have a takeover delay of 10 # a claim of higher amount made now will have a takeover delay of 10
second_claim_id = (await self.stream_create(name, '0.2', allow_duplicate_name=True))['outputs'][0]['claim_id'] second_claim_id = (await self.stream_create(name, '0.2', allow_duplicate_name=True))['outputs'][0]['claim_id']
# sanity check # sanity check
self.assertNotEqual(first_claim_id, second_claim_id) self.assertNotEqual(first_claim_id, second_claim_id)
# takeover should not have happened yet # takeover should not have happened yet
self.assertEqual(first_claim_id, (await self.assertMatchWinningClaim(name)).claim_hash.hex()) await self.assertMatchClaimIsWinning(name, first_claim_id)
await self.generate(8) await self.generate(8)
self.assertEqual(first_claim_id, (await self.assertMatchWinningClaim(name)).claim_hash.hex()) await self.assertMatchClaimIsWinning(name, first_claim_id)
# abandon the winning claim # abandon the winning claim
await self.daemon.jsonrpc_txo_spend(type='stream', claim_id=first_claim_id) await self.daemon.jsonrpc_txo_spend(type='stream', claim_id=first_claim_id)
await self.generate(1) await self.generate(1)
# the takeover and activation should happen a block earlier than they would have absent the abandon # the takeover and activation should happen a block earlier than they would have absent the abandon
self.assertEqual(second_claim_id, (await self.assertMatchWinningClaim(name)).claim_hash.hex()) await self.assertMatchClaimIsWinning(name, second_claim_id)
await self.generate(1) await self.generate(1)
self.assertEqual(second_claim_id, (await self.assertMatchWinningClaim(name)).claim_hash.hex()) await self.assertMatchClaimIsWinning(name, second_claim_id)
async def test_abandon_before_takeover_no_delay_update(self): # TODO: fix race condition line 506 async def test_abandon_before_takeover_no_delay_update(self): # TODO: fix race condition line 506
name = 'derp' name = 'derp'
# initially claim the name # initially claim the name
first_claim_id = (await self.stream_create(name, '0.1'))['outputs'][0]['claim_id'] first_claim_id = (await self.stream_create(name, '0.1'))['outputs'][0]['claim_id']
self.assertEqual(first_claim_id, (await self.assertMatchWinningClaim(name)).claim_hash.hex()) await self.assertMatchClaimIsWinning(name, first_claim_id)
await self.generate(320) await self.generate(320)
# block 527 # block 527
# a claim of higher amount made now will have a takeover delay of 10 # a claim of higher amount made now will have a takeover delay of 10
@ -510,19 +540,23 @@ class ResolveClaimTakeovers(BaseResolveTestCase):
# sanity check # sanity check
self.assertNotEqual(first_claim_id, second_claim_id) self.assertNotEqual(first_claim_id, second_claim_id)
# takeover should not have happened yet # takeover should not have happened yet
self.assertEqual(first_claim_id, (await self.assertMatchWinningClaim(name)).claim_hash.hex()) await self.assertMatchClaimIsWinning(name, first_claim_id)
await self.assertMatchClaimsForName(name)
await self.generate(8) await self.generate(8)
self.assertEqual(first_claim_id, (await self.assertMatchWinningClaim(name)).claim_hash.hex()) await self.assertMatchClaimIsWinning(name, first_claim_id)
await self.assertMatchClaimsForName(name)
# abandon the winning claim # abandon the winning claim
await self.daemon.jsonrpc_txo_spend(type='stream', claim_id=first_claim_id) await self.daemon.jsonrpc_txo_spend(type='stream', claim_id=first_claim_id)
await self.daemon.jsonrpc_stream_update(second_claim_id, '0.1') await self.daemon.jsonrpc_stream_update(second_claim_id, '0.1')
await self.generate(1) await self.generate(1)
# the takeover and activation should happen a block earlier than they would have absent the abandon # the takeover and activation should happen a block earlier than they would have absent the abandon
self.assertEqual(second_claim_id, (await self.assertMatchWinningClaim(name)).claim_hash.hex()) await self.assertMatchClaimIsWinning(name, second_claim_id)
await self.assertMatchClaimsForName(name)
await self.generate(1) await self.generate(1)
# await self.ledger.on_header.where(lambda e: e.height == 537) # await self.ledger.on_header.where(lambda e: e.height == 537)
self.assertEqual(second_claim_id, (await self.assertMatchWinningClaim(name)).claim_hash.hex()) await self.assertMatchClaimIsWinning(name, second_claim_id)
await self.assertMatchClaimsForName(name)
async def test_abandon_controlling_support_before_pending_takeover(self): async def test_abandon_controlling_support_before_pending_takeover(self):
name = 'derp' name = 'derp'
@ -533,54 +567,78 @@ class ResolveClaimTakeovers(BaseResolveTestCase):
self.assertEqual(first_claim_id, (await self.assertMatchWinningClaim(name)).claim_hash.hex()) self.assertEqual(first_claim_id, (await self.assertMatchWinningClaim(name)).claim_hash.hex())
await self.generate(321) await self.generate(321)
second_claim_id = (await self.stream_create(name, '1.1', allow_duplicate_name=True))['outputs'][0]['claim_id'] second_claim_id = (await self.stream_create(name, '0.9', allow_duplicate_name=True))['outputs'][0]['claim_id']
self.assertNotEqual(first_claim_id, second_claim_id) self.assertNotEqual(first_claim_id, second_claim_id)
# takeover should not have happened yet # takeover should not have happened yet
self.assertEqual(first_claim_id, (await self.assertMatchWinningClaim(name)).claim_hash.hex()) await self.assertMatchClaimIsWinning(name, first_claim_id)
await self.generate(8) await self.generate(8)
self.assertEqual(first_claim_id, (await self.assertMatchWinningClaim(name)).claim_hash.hex()) await self.assertMatchClaimIsWinning(name, first_claim_id)
# abandon the support that causes the winning claim to have the highest staked # abandon the support that causes the winning claim to have the highest staked
tx = await self.daemon.jsonrpc_txo_spend(type='support', txid=controlling_support_tx.id) tx = await self.daemon.jsonrpc_txo_spend(type='support', txid=controlling_support_tx.id)
await self.generate(1) await self.generate(1)
self.assertEqual(first_claim_id, (await self.assertMatchWinningClaim(name)).claim_hash.hex()) await self.assertMatchClaimIsWinning(name, first_claim_id)
# await self.assertMatchClaim(second_claim_id)
await self.generate(1) await self.generate(1)
self.assertEqual(second_claim_id, (await self.assertMatchWinningClaim(name)).claim_hash.hex())
await self.assertMatchClaim(first_claim_id)
await self.assertMatchClaimIsWinning(name, second_claim_id)
async def test_remove_controlling_support(self): async def test_remove_controlling_support(self):
name = 'derp' name = 'derp'
# initially claim the name # initially claim the name
first_claim_id = (await self.stream_create(name, '0.1'))['outputs'][0]['claim_id'] first_claim_id = (await self.stream_create(name, '0.2'))['outputs'][0]['claim_id']
first_support_tx = await self.daemon.jsonrpc_support_create(first_claim_id, '0.9') first_support_tx = await self.daemon.jsonrpc_support_create(first_claim_id, '0.9')
await self.ledger.wait(first_support_tx) await self.ledger.wait(first_support_tx)
self.assertEqual(first_claim_id, (await self.assertMatchWinningClaim(name)).claim_hash.hex()) await self.assertMatchClaimIsWinning(name, first_claim_id)
await self.generate(321) # give the first claim long enough for a 10 block takeover delay await self.generate(320) # give the first claim long enough for a 10 block takeover delay
await self.assertMatchClaimIsWinning(name, first_claim_id)
# make a second claim which will take over the name # make a second claim which will take over the name
second_claim_id = (await self.stream_create(name, '0.2', allow_duplicate_name=True))['outputs'][0]['claim_id'] second_claim_id = (await self.stream_create(name, '0.1', allow_duplicate_name=True))['outputs'][0]['claim_id']
second_claim_support_tx = await self.daemon.jsonrpc_support_create(second_claim_id, '1.0')
await self.ledger.wait(second_claim_support_tx)
self.assertNotEqual(first_claim_id, second_claim_id) self.assertNotEqual(first_claim_id, second_claim_id)
second_claim_support_tx = await self.daemon.jsonrpc_support_create(second_claim_id, '1.5')
await self.ledger.wait(second_claim_support_tx)
await self.generate(1) # neither the second claim or its support have activated yet
await self.assertMatchClaimIsWinning(name, first_claim_id)
# the name resolves to the first claim await self.generate(9) # claim activates, but is not yet winning
self.assertEqual(first_claim_id, (await self.assertMatchWinningClaim(name)).claim_hash.hex()) await self.assertMatchClaimIsWinning(name, first_claim_id)
await self.generate(9)
# still resolves to the first claim await self.generate(1) # support activates, takeover happens
self.assertEqual(first_claim_id, (await self.assertMatchWinningClaim(name)).claim_hash.hex()) await self.assertMatchClaimIsWinning(name, second_claim_id)
await self.generate(1) # second claim takes over
self.assertEqual(second_claim_id, (await self.assertMatchWinningClaim(name)).claim_hash.hex()) await self.daemon.jsonrpc_txo_spend(type='support', claim_id=second_claim_id, blocking=True)
await self.generate(33) # give the second claim long enough for a 1 block takeover delay await self.generate(1) # support activates, takeover happens
self.assertEqual(second_claim_id, (await self.assertMatchWinningClaim(name)).claim_hash.hex()) await self.assertMatchClaimIsWinning(name, first_claim_id)
# abandon the support that causes the winning claim to have the highest staked
await self.daemon.jsonrpc_txo_spend(type='support', txid=second_claim_support_tx.id) async def test_claim_expiration(self):
name = 'derp'
# starts at height 206
vanishing_claim = (await self.stream_create('vanish', '0.1'))['outputs'][0]['claim_id']
await self.generate(493)
# in block 701 and 702
first_claim_id = (await self.stream_create(name, '0.3'))['outputs'][0]['claim_id']
await self.assertMatchClaimIsWinning('vanish', vanishing_claim)
await self.generate(100) # block 801, expiration fork happened
await self.assertNoClaimForName('vanish')
# second claim is in block 802
second_claim_id = (await self.stream_create(name, '0.2', allow_duplicate_name=True))['outputs'][0]['claim_id']
await self.assertMatchClaimIsWinning(name, first_claim_id)
await self.generate(498)
await self.assertMatchClaimIsWinning(name, first_claim_id)
await self.generate(1) await self.generate(1)
self.assertEqual(second_claim_id, (await self.assertMatchWinningClaim(name)).claim_hash.hex()) await self.assertMatchClaimIsWinning(name, second_claim_id)
await self.generate(1) # first claim takes over await self.generate(100)
self.assertEqual(first_claim_id, (await self.assertMatchWinningClaim(name)).claim_hash.hex()) await self.assertMatchClaimIsWinning(name, second_claim_id)
await self.generate(1)
await self.assertNoClaimForName(name)
class ResolveAfterReorg(BaseResolveTestCase): class ResolveAfterReorg(BaseResolveTestCase):
async def reorg(self, start): async def reorg(self, start):
blocks = self.ledger.headers.height - start blocks = self.ledger.headers.height - start
self.blockchain.block_expected = start - 1 self.blockchain.block_expected = start - 1