Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
57 changes: 25 additions & 32 deletions decred/decred/dcr/account.py
Original file line number Diff line number Diff line change
Expand Up @@ -139,19 +139,27 @@ def __init__(self, count=0, value=0):
self.value = value


def unblob_check(name, version, actual_pushes, expected_pushes):
def unblob_check(class_name, version, pushes, check_data):
"""
Check version and pushes to unblob.

Args:
class_name str: the class name that will appear in error messages.
version int: the version number that will be checked.
pushes int: the number of pushes that will be checked.
check_data dict: keys are version numbers, values are number of
expected pushes.

Raises:
NotImplementedError if version is not zero.
DecredError if actual_pushes and expected_pushes are not the same.
NotImplementedError if version is not in check_data keys.
DecredError if pushes is not the value in check_data keyed by version.
"""
if version != 0:
raise NotImplementedError(f"{name}: unsupported version {version}")
if actual_pushes != expected_pushes:
if version not in check_data.keys():
raise NotImplementedError(f"{class_name}: unsupported version {version}")
expected_pushes = check_data[version]
if pushes != expected_pushes:
raise DecredError(
f"{name}: expected {expected_pushes} pushes, got {actual_pushes}"
f"{class_name}: expected {expected_pushes} pushes, got {pushes}"
)


Expand Down Expand Up @@ -180,7 +188,7 @@ def blob(blk):
def unblob(b):
"""Satisfies the encode.Blobber API"""
ver, d = encode.decodeBlob(b)
unblob_check("TinyBlock", ver, len(d), 2)
unblob_check("TinyBlock", ver, len(d), {0: 2})
return TinyBlock(d[0], encode.intFromBytes(d[1]))

def serialize(self):
Expand Down Expand Up @@ -314,21 +322,7 @@ def blob(utxo):
def unblob(b):
"""Satisfies the encode.Blobber API"""
ver, d = encode.decodeBlob(b)
if ver == 0:
if len(d) != 7:
# TODO change to DecredError
raise AssertionError(
"wrong number of pushes for TicketInfo. wanted 7, got %d" % len(d)
)
d.extend([b"", b"", b"", b""])
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I suppose it's not so important at this stage, but this was intended to update the database. Once we have users, we can't expect them to delete their wallet and restore from the seed phrase every time we update something.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

made an issue #102

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh, I'm sorry, I missed that. Definitely an important feature that deserves its own consideration, and a clear and explicit implementation. 🙂 Thanks for making an issue about it.

elif ver == 1:
if len(d) != 11:
# TODO change to DecredError
raise AssertionError(
"wrong number of pushes for TicketInfo. wanted 11, got %d" % len(d)
)
else:
raise AssertionError("invalid TicketInfo version %d" % ver)
unblob_check("TicketInfo", ver, len(d), {0: 7, 1: 11})

iFunc = encode.intFromBytes
f = encode.extractNone
Expand Down Expand Up @@ -510,7 +504,7 @@ def blob(utxo):
def unblob(b):
"""Satisfies the encode.Blobber API"""
ver, d = encode.decodeBlob(b)
unblob_check("UTXO", ver, len(d), 9)
unblob_check("UTXO", ver, len(d), {0: 9})

iFunc = encode.intFromBytes
f = encode.extractNone
Expand Down Expand Up @@ -575,8 +569,8 @@ def ticketFromTx(tx, netParams, block=None, tinfo=None):
Args:
tx (msgtx.MsgTx): The ticket transaction.
netParams (obj): Network parameters.
block (msgblock.BlockHeader): Optional. Default=None. The block the ticket was
purchased in.
block (msgblock.BlockHeader): Optional. Default=None. The block
the ticket was purchased in.
tinfo (TicketInfo): Optional. Default of None is replaced with
immature status TicketInfo.

Expand Down Expand Up @@ -780,7 +774,7 @@ def blob(bal):
def unblob(b):
"""Satisfies the encode.Blobber API"""
ver, pushes = encode.decodeBlob(b)
unblob_check("Balance", ver, len(pushes), 3)
unblob_check("Balance", ver, len(pushes), {0: 3})
i = encode.intFromBytes
return Balance(i(pushes[0]), i(pushes[1]), i(pushes[2]))

Expand Down Expand Up @@ -892,7 +886,7 @@ def blob(acct):
def unblob(b):
"""Satisfies the encode.Blobber API"""
ver, d = encode.decodeBlob(b)
unblob_check("Account", ver, len(d), 7)
unblob_check("Account", ver, len(d), {0: 7})

iFunc = encode.intFromBytes

Expand Down Expand Up @@ -1060,15 +1054,14 @@ def calcBalance(self, tipHeight=None):

Args:
tipHeight (int): optional. The current tip height. If not provided,
the height from the current blockchain.tip will be used.
the height from the current blockchain tip will be used.

Returns:
Balance: The current balance. The balance is also assigned to the
Account.balance property.
"""
tipHeight = (
tipHeight if tipHeight is not None else self.blockchain.tip["height"]
)
if tipHeight is None:
tipHeight = self.blockchain.tipHeight
tot = 0
avail = 0
staked = 0
Expand Down
20 changes: 9 additions & 11 deletions decred/decred/dcr/dcrdata.py
Original file line number Diff line number Diff line change
Expand Up @@ -519,7 +519,7 @@ def __init__(self, db, params, datapath, skipConnect=False):
self.heightMap = db.child("height", datatypes=("INTEGER", "BLOB"))
self.headerDB = db.child("header", blobber=msgblock.BlockHeader)
self.txBlockMap = db.child("blocklink")
self.tip = None
self.tipHeight = None
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Only the height is ever used, so it makes sense to store just that.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Used here

tipHeight = (
tipHeight if tipHeight is not None else self.blockchain.tip["height"]
)

So could change there I guess. That's the only place I could find, but may be more.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed, thanks. Checked the codebase, no other uses.

self.subsidyCache = calc.SubsidyCache(params)
if not skipConnect:
self.connect()
Expand Down Expand Up @@ -865,7 +865,7 @@ def updateTip(self):
this can be used sparingly.
"""
try:
self.tip = self.bestBlock()
self.tipHeight = self.bestBlock()["height"]
except Exception as e:
log.error("failed to retrieve tip from blockchain: %s" % formatTraceback(e))
raise DecredError("no tip data retrieved")
Expand Down Expand Up @@ -928,23 +928,22 @@ def broadcast(self, txHex):

def pubsubSignal(self, sig):
"""
Process a notifictation from the block explorer.
Process a notification from the block explorer.

Arg:
sig (obj or string): The block explorer's notification, decoded.
"""
# log.debug("pubsub signal recieved: %s" % repr(sig))
# log.debug("pubsub signal received: %s" % repr(sig))
if "done" in sig:
return
sigType = sig["event"]
try:
if sigType == "address":
msg = sig["message"]
log.debug("signal received for %s" % msg["address"])
self.addressReceiver(msg["address"], msg["transaction"])
self.addressReceiver(sig)
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The receiver signature requires one obj argument.

elif sigType == "newblock":
self.tip = sig["message"]["block"]
self.tipHeight = self.tip["height"]
self.tipHeight = sig["message"]["block"]["height"]
self.blockReceiver(sig)
elif sigType == "subscribeResp":
# should check for error.
Expand All @@ -966,7 +965,7 @@ def changeScript(self, changeAddress):

def approveUTXO(self, utxo):
# If the UTXO appears unconfirmed, see if it can be confirmed.
if utxo.maturity and self.tip["height"] < utxo.maturity:
if utxo.maturity and self.tipHeight < utxo.maturity:
return False
if utxo.isTicket():
# Temporary until revocations implemented.
Expand Down Expand Up @@ -1172,8 +1171,7 @@ def purchaseTickets(self, keysource, utxosource, req):
raise DecredError("negative expiry")

# Perform a sanity check on expiry.
tipHeight = self.tip["height"]
if req.expiry <= tipHeight + 1 and req.expiry > 0:
if req.expiry <= self.tipHeight + 1 and req.expiry > 0:
raise DecredError("expiry height must be above next block height")

# Fetch a new address for creating a split transaction. Then,
Expand Down Expand Up @@ -1266,7 +1264,7 @@ def purchaseTickets(self, keysource, utxosource, req):
poolFeeAmt = txscript.stakePoolTicketFee(
ticketPrice,
ticketFee,
tipHeight,
self.tipHeight,
req.poolFees,
self.subsidyCache,
self.params,
Expand Down
2 changes: 1 addition & 1 deletion decred/tests/unit/dcr/test_account.py
Original file line number Diff line number Diff line change
Expand Up @@ -286,7 +286,7 @@ def test_account(tmp_path):
class Blockchain:
txsForAddr = lambda addr: []
UTXOs = lambda addrs: []
tip = {"height": 5}
tipHeight = 5

acct.blockchain = Blockchain

Expand Down
135 changes: 96 additions & 39 deletions decred/tests/unit/dcr/test_dcrdata_unit.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@

from decred import DecredError
from decred.crypto import opcode
from decred.dcr import agenda, txscript
from decred.dcr import account, agenda, txscript
from decred.dcr.dcrdata import (
DcrdataBlockchain,
DcrdataClient,
Expand Down Expand Up @@ -184,42 +184,6 @@ def test_static(self):


class TestDcrdataBlockchain:
def test_subscriptions(self, http_get_post, tmp_path):
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Moving this below the test data.


# Exception in updateTip.
preload_api_list(http_get_post)
with pytest.raises(DecredError):
DcrdataBlockchain(str(tmp_path / "test.db"), testnet, BASE_URL)

# Successful creation.
preload_api_list(http_get_post)
http_get_post(f"{BASE_URL}api/block/best", 1)
ddb = DcrdataBlockchain(str(tmp_path / "test.db"), testnet, BASE_URL)
assert ddb.tip == 1

# Set the mock WebsocketClient.
ddb.dcrdata.ps = MockWebsocketClient()

# Subscribes.
def receiver(obj):
print("msg: %s" % repr(obj))

ddb.subscribeBlocks(receiver)
assert ddb.dcrdata.ps.sent[0]["message"]["message"] == "newblock"

# Exception in subscribeAddresses.
with pytest.raises(DecredError):
ddb.subscribeAddresses([])

ddb.dcrdata.ps.sent = []
ddb.subscribeAddresses(["new_one"], receiver)
assert ddb.dcrdata.ps.sent[0]["message"]["message"] == "address:new_one"

# getAgendasInfo.
http_get_post(f"{BASE_URL}api/stake/vote/info", AGENDAS_INFO_RAW)
agsinfo = ddb.getAgendasInfo()
assert isinstance(agsinfo, agenda.AgendasInfo)

# Test data from block #427282.
utxos = (
# Coinbase.
Expand Down Expand Up @@ -302,9 +266,70 @@ def receiver(obj):
)
# fmt: on

def test_misc(self, http_get_post, tmp_path):
preload_api_list(http_get_post)
http_get_post(f"{BASE_URL}api/block/best", dict(height=1))
ddb = DcrdataBlockchain(str(tmp_path / "test.db"), testnet, BASE_URL)
assert ddb.tipHeight == 1

# getAgendasInfo
http_get_post(f"{BASE_URL}api/stake/vote/info", AGENDAS_INFO_RAW)
agsinfo = ddb.getAgendasInfo()
assert isinstance(agsinfo, agenda.AgendasInfo)

def test_subscriptions(self, http_get_post, tmp_path):
# Exception in updateTip.
preload_api_list(http_get_post)
with pytest.raises(DecredError):
DcrdataBlockchain(str(tmp_path / "test.db"), testnet, BASE_URL)

# Successful creation.
preload_api_list(http_get_post)
http_get_post(f"{BASE_URL}api/block/best", dict(height=1))
ddb = DcrdataBlockchain(str(tmp_path / "test.db"), testnet, BASE_URL)

# Set the mock WebsocketClient.
ddb.dcrdata.ps = MockWebsocketClient()

# Receiver
receive_queue = []

def receiver(obj):
receive_queue.append(obj)

# subscribeBlocks
ddb.subscribeBlocks(receiver)
assert ddb.dcrdata.ps.sent[0]["message"]["message"] == "newblock"
ddb.dcrdata.ps.sent = []

# subscribeAddresses
with pytest.raises(DecredError):
ddb.subscribeAddresses([])
ddb.subscribeAddresses(["new_one"], receiver)
assert ddb.dcrdata.ps.sent[0]["message"]["message"] == "address:new_one"

# pubsubSignal
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

New tests begin here.

assert ddb.pubsubSignal("done") is None
assert ddb.pubsubSignal(dict(event="subscribeResp")) is None
assert ddb.pubsubSignal(dict(event="ping")) is None
assert ddb.pubsubSignal(dict(event="unknown")) is None
# pubsubSignal address
sig = dict(
event="address",
message=dict(address="the_address", transaction="transaction",),
)
ddb.pubsubSignal(sig)
assert receive_queue[0] == sig
receive_queue.clear()
# pubsubSignal newblock
sig = dict(event="newblock", message=dict(block=dict(height=1)))
ddb.pubsubSignal(sig)
assert receive_queue[0] == sig
receive_queue.clear()

def test_utxos(self, http_get_post, tmp_path):
preload_api_list(http_get_post)
http_get_post(f"{BASE_URL}api/block/best", 1)
http_get_post(f"{BASE_URL}api/block/best", dict(height=1))
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This was incorrect. The best block call returns a whole block, not directly the height.

ddb = DcrdataBlockchain(str(tmp_path / "test.db"), testnet, BASE_URL)

# txVout error
Expand Down Expand Up @@ -366,9 +391,41 @@ def test_utxos(self, http_get_post, tmp_path):
# txVout success
assert ddb.txVout(self.txs[2][0], 0).satoshis == 14773017964

# approveUTXO
utxo = account.UTXO.parse(self.utxos[1])
utxo.maturity = 2
assert ddb.approveUTXO(utxo) is False
utxo.maturity = None
assert ddb.approveUTXO(utxo) is False
utxo = account.UTXO.parse(self.utxos[0])
assert ddb.approveUTXO(utxo) is True

# confirmUTXO
# No confirmation.
utxo = account.UTXO.parse(self.utxos[2])
assert ddb.confirmUTXO(utxo) is False
# Confirmation.
blockHash = "00000000000000002b197e4018b990efb85e6bd43ffb15f7ede97a78f806a3f8"
txURL = f"{BASE_URL}api/tx/{self.txs[2][0]}"
decodedTx = {"block": {"blockhash": blockHash}}
http_get_post(txURL, decodedTx)
headerURL = f"{BASE_URL}api/block/hash/{blockHash}/header/raw"
blockHeader = {
"hex": (
"07000000e00b3a83dc60f961d8f516ece63e6d009eff4c2af50139150000"
"000000000000873684038a5d384cf123ee39d39bdf9f65cf4051ec4d420f"
"e909c16344329aaa35879931c8695d9be6f9259fa7467c51d0c7e601d95c"
"c78fdd458210503865af0100721e0a6d2bf90500040091a40000e62f3418"
"c8518a700300000012850600213300003de0575e6e8b9d13e691326a1fd4"
"3a0000000000000000000000000000000000000000000000000007000000"
),
}
http_get_post(headerURL, blockHeader)
assert ddb.confirmUTXO(utxo) is True

def test_blocks(self, http_get_post, tmp_path):
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These are pre-existing tests.

preload_api_list(http_get_post)
http_get_post(f"{BASE_URL}api/block/best", 1)
http_get_post(f"{BASE_URL}api/block/best", dict(height=1))
ddb = DcrdataBlockchain(str(tmp_path / "test.db"), testnet, BASE_URL)

# blockHeader
Expand Down