Skip to content

Commit dc637d4

Browse files
authored
Block ordering (#142)
* Look at current block additions before removals * Fix block ordering, and change state machine of wallet node * Fix infinite recursion * Fix some serious bugs with wallets/coinbases/reorgs and add testing
1 parent e9e2137 commit dc637d4

File tree

8 files changed

+683
-269
lines changed

8 files changed

+683
-269
lines changed

src/wallet/block_record.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ class BlockRecord(Streamable):
1919
prev_header_hash: bytes32
2020
height: uint32
2121
weight: uint128
22-
additions: List[Coin]
23-
removals: List[bytes32]
22+
additions: Optional[List[Coin]] # A block record without additions is not finished
23+
removals: Optional[List[bytes32]] # A block record without removals is not finished
2424
total_iters: Optional[uint64]
2525
new_challenge_hash: Optional[bytes32]

src/wallet/wallet.py

Lines changed: 5 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -129,15 +129,13 @@ async def get_keys(
129129

130130
async def select_coins(self, amount) -> Optional[Set[Coin]]:
131131
""" Returns a set of coins that can be used for generating a new transaction. """
132+
spendable_am = await self.wallet_state_manager.get_unconfirmed_spendable_for_wallet(
133+
self.wallet_info.id
134+
)
132135

133-
if (
134-
amount
135-
> await self.wallet_state_manager.get_unconfirmed_spendable_for_wallet(
136-
self.wallet_info.id
137-
)
138-
):
136+
if amount > spendable_am:
139137
self.log.warning(
140-
f"Can't select amount higher than our spendable balance {amount}"
138+
f"Can't select amount higher than our spendable balance {amount}, spendable {spendable_am}"
141139
)
142140
return None
143141

src/wallet/wallet_node.py

Lines changed: 313 additions & 230 deletions
Large diffs are not rendered by default.

src/wallet/wallet_state_manager.py

Lines changed: 84 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -240,7 +240,7 @@ async def get_confirmed_balance_for_wallet(self, wallet_id: int) -> uint64:
240240

241241
for record in record_list:
242242
amount = uint64(amount + record.coin.amount)
243-
self.log.info(f"amount is {amount}")
243+
self.log.info(f"Confirmed balance amount is {amount}")
244244
return uint64(amount)
245245

246246
async def get_unconfirmed_balance(self, wallet_id) -> uint64:
@@ -487,6 +487,30 @@ async def receive_block(
487487
fast sync). If validation succeeds, block is adedd to DB. If it's a new TIP, transactions are
488488
reorged accordingly.
489489
"""
490+
cb_and_fees_additions = []
491+
if header_block is not None:
492+
coinbase = header_block.header.data.coinbase
493+
fees_coin = header_block.header.data.fees_coin
494+
if await self.is_addition_relevant(coinbase):
495+
cb_and_fees_additions.append(coinbase)
496+
if await self.is_addition_relevant(fees_coin):
497+
cb_and_fees_additions.append(fees_coin)
498+
assert block.additions is not None
499+
if len(cb_and_fees_additions) > 0:
500+
block = BlockRecord(
501+
block.header_hash,
502+
block.prev_header_hash,
503+
block.height,
504+
block.weight,
505+
block.additions + cb_and_fees_additions,
506+
block.removals,
507+
block.total_iters,
508+
block.new_challenge_hash,
509+
)
510+
511+
assert block.additions is not None
512+
assert block.removals is not None
513+
490514
async with self.lock:
491515
if block.header_hash in self.block_records:
492516
return ReceiveBlockResult.ALREADY_HAVE_BLOCK
@@ -547,15 +571,18 @@ async def receive_block(
547571
for path_block in blocks_to_add:
548572
self.height_to_hash[path_block.height] = path_block.header_hash
549573
await self.wallet_store.add_block_to_path(path_block.header_hash)
550-
if header_block is not None:
551-
coinbase = header_block.header.data.coinbase
552-
fees_coin = header_block.header.data.fees_coin
553-
if await self.is_addition_relevant(coinbase):
554-
await self.coin_added(coinbase, path_block.height, True)
555-
if await self.is_addition_relevant(fees_coin):
556-
await self.coin_added(fees_coin, path_block.height, True)
574+
assert (
575+
path_block.additions is not None
576+
and path_block.removals is not None
577+
)
557578
for coin in path_block.additions:
558-
await self.coin_added(coin, path_block.height, False)
579+
is_coinbase = (
580+
True
581+
if bytes32((path_block.height).to_bytes(32, "big"))
582+
== coin.parent_coin_info
583+
else False
584+
)
585+
await self.coin_added(coin, path_block.height, is_coinbase)
559586
for coin_name in path_block.removals:
560587
await self.coin_removed(coin_name, path_block.height)
561588
self.lca = block.header_hash
@@ -810,7 +837,7 @@ def validate_select_proofs(
810837
self,
811838
all_proof_hashes: List[Tuple[bytes32, Optional[Tuple[uint64, uint64]]]],
812839
heights: List[uint32],
813-
cached_blocks: Dict[bytes32, Tuple[BlockRecord, HeaderBlock]],
840+
cached_blocks: Dict[bytes32, Tuple[BlockRecord, HeaderBlock, Optional[bytes]]],
814841
potential_header_hashes: Dict[uint32, bytes32],
815842
) -> bool:
816843
"""
@@ -825,7 +852,7 @@ def validate_select_proofs(
825852
prev_height = uint32(height - 1)
826853
# Get previous header block
827854
prev_hh = potential_header_hashes[prev_height]
828-
_, prev_header_block = cached_blocks[prev_hh]
855+
_, prev_header_block, _ = cached_blocks[prev_hh]
829856

830857
# Validate proof hash of previous header block
831858
if (
@@ -868,7 +895,7 @@ def validate_select_proofs(
868895

869896
# Get header block
870897
hh = potential_header_hashes[height]
871-
_, header_block = cached_blocks[hh]
898+
_, header_block, _ = cached_blocks[hh]
872899

873900
# Validate challenge hash is == pospace challenge hash
874901
if challenge_hash != header_block.proof_of_space.challenge_hash:
@@ -966,21 +993,59 @@ def validate_select_proofs(
966993
return True
967994

968995
async def get_filter_additions_removals(
969-
self, transactions_fitler: bytes
996+
self, new_block: BlockRecord, transactions_filter: bytes
970997
) -> Tuple[List[bytes32], List[bytes32]]:
971998
""" Returns a list of our coin ids, and a list of puzzle_hashes that positively match with provided filter. """
972-
tx_filter = PyBIP158([b for b in transactions_fitler])
973-
my_coin_records: Set[
999+
assert new_block.prev_header_hash in self.block_records
1000+
1001+
tx_filter = PyBIP158([b for b in transactions_filter])
1002+
1003+
# Find fork point
1004+
fork_h: uint32 = self._find_fork_point_in_chain(
1005+
self.block_records[self.lca], new_block
1006+
)
1007+
1008+
# Get all unspent coins
1009+
my_coin_records_lca: Set[
9741010
WalletCoinRecord
975-
] = await self.wallet_store.get_coin_records_by_spent(False)
1011+
] = await self.wallet_store.get_coin_records_by_spent(False, uint32(fork_h + 1))
1012+
1013+
# Filter coins up to and including fork point
1014+
unspent_coin_names: Set[bytes32] = set()
1015+
for coin in my_coin_records_lca:
1016+
if coin.confirmed_block_index <= fork_h:
1017+
unspent_coin_names.add(coin.name())
1018+
1019+
# Get all blocks after fork point up to but not including this block
1020+
curr: BlockRecord = self.block_records[new_block.prev_header_hash]
1021+
reorg_blocks: List[BlockRecord] = []
1022+
while curr.height > fork_h:
1023+
reorg_blocks.append(curr)
1024+
curr = self.block_records[curr.prev_header_hash]
1025+
reorg_blocks.reverse()
1026+
1027+
# For each block, process additions to get all Coins, then process removals to get unspent coins
1028+
for reorg_block in reorg_blocks:
1029+
assert (
1030+
reorg_block.additions is not None and reorg_block.removals is not None
1031+
)
1032+
for addition in reorg_block.additions:
1033+
unspent_coin_names.add(addition.name())
1034+
for removal in reorg_block.removals:
1035+
unspent_coin_names.remove(removal)
1036+
1037+
if new_block.additions is not None:
1038+
for addition in new_block.additions:
1039+
unspent_coin_names.add(addition.name())
1040+
9761041
my_puzzle_hashes = await self.puzzle_store.get_all_puzzle_hashes()
9771042

9781043
removals_of_interest: bytes32 = []
9791044
additions_of_interest: bytes32 = []
9801045

981-
for record in my_coin_records:
982-
if tx_filter.Match(bytearray(record.name())):
983-
removals_of_interest.append(record.name())
1046+
for coin_name in unspent_coin_names:
1047+
if tx_filter.Match(bytearray(coin_name)):
1048+
removals_of_interest.append(coin_name)
9841049

9851050
for puzzle_hash in my_puzzle_hashes:
9861051
if tx_filter.Match(bytearray(puzzle_hash)):

src/wallet/wallet_store.py

Lines changed: 14 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -143,13 +143,20 @@ async def get_coin_record(self, coin_name: bytes32) -> Optional[WalletCoinRecord
143143
)
144144
return None
145145

146-
async def get_coin_records_by_spent(self, spent: bool) -> Set[WalletCoinRecord]:
146+
async def get_coin_records_by_spent(
147+
self, spent: bool, spend_before_height: Optional[uint32] = None
148+
) -> Set[WalletCoinRecord]:
147149
""" Returns set of CoinRecords that have not been spent yet. """
148150
coins = set()
149-
150-
cursor = await self.db_connection.execute(
151-
"SELECT * from coin_record WHERE spent=?", (int(spent),)
152-
)
151+
if spend_before_height:
152+
cursor = await self.db_connection.execute(
153+
"SELECT * from coin_record WHERE spent=? OR spent_index>=?",
154+
(int(spent), spend_before_height),
155+
)
156+
else:
157+
cursor = await self.db_connection.execute(
158+
"SELECT * from coin_record WHERE spent=?", (int(spent),)
159+
)
153160
rows = await cursor.fetchall()
154161
await cursor.close()
155162
for row in rows:
@@ -191,9 +198,10 @@ async def get_spendable_for_index(
191198
) -> Set[WalletCoinRecord]:
192199
""" Returns set of CoinRecords that have been confirmed before index height. """
193200
coins = set()
201+
print("index,", index, "walelt id", wallet_id)
194202

195203
cursor_coinbase_coins = await self.db_connection.execute(
196-
"SELECT * from coin_record WHERE spent=? and confirmed_index<? and wallet_id=? and coinbase=?",
204+
"SELECT * from coin_record WHERE spent=? and confirmed_index<=? and wallet_id=? and coinbase=?",
197205
(0, int(index), wallet_id, 1),
198206
)
199207

tests/wallet/test_wallet.py

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -279,16 +279,18 @@ async def test_wallet_make_transaction_hop(self, two_wallet_nodes_five_freeze):
279279
spend_bundle = await wallet_0.generate_signed_transaction(
280280
10, await wallet_node_1.main_wallet.get_new_puzzlehash(), 0
281281
)
282+
282283
await wallet_0.push_transaction(spend_bundle)
283284

284285
await asyncio.sleep(1)
286+
# Full node height 11, wallet height 9
285287
confirmed_balance = await wallet_0.get_confirmed_balance()
286288
unconfirmed_balance = await wallet_0.get_unconfirmed_balance()
287289

288290
assert confirmed_balance == funds
289291
assert unconfirmed_balance == funds - 10
290292

291-
for i in range(0, 3):
293+
for i in range(0, 7):
292294
await full_node_0.farm_new_block(FarmNewBlockProtocol(token_bytes()))
293295

294296
await asyncio.sleep(1)
@@ -300,6 +302,7 @@ async def test_wallet_make_transaction_hop(self, two_wallet_nodes_five_freeze):
300302
]
301303
)
302304

305+
# Full node height 17, wallet height 15
303306
confirmed_balance = await wallet_0.get_confirmed_balance()
304307
unconfirmed_balance = await wallet_0.get_unconfirmed_balance()
305308
wallet_2_confirmed_balance = await wallet_1.get_confirmed_balance()
@@ -313,7 +316,7 @@ async def test_wallet_make_transaction_hop(self, two_wallet_nodes_five_freeze):
313316
)
314317
await wallet_1.push_transaction(spend_bundle)
315318

316-
for i in range(0, 3):
319+
for i in range(0, 7):
317320
await full_node_0.farm_new_block(FarmNewBlockProtocol(token_bytes()))
318321

319322
await asyncio.sleep(1)

0 commit comments

Comments
 (0)