Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

(WIP) Miscellaneous name_show Tor improvements #184

Open
wants to merge 8 commits into
base: master
Choose a base branch
from
117 changes: 63 additions & 54 deletions electrum_nmc/electrum/commands.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@
from .bip32 import BIP32Node
from .i18n import _
from .names import build_name_new, format_name_identifier, name_expires_in, name_identifier_to_scripthash, OP_NAME_FIRSTUPDATE, OP_NAME_UPDATE, validate_value_length
from .network import BestEffortRequestFailed
from .verifier import verify_tx_is_in_block
from .transaction import Transaction, multisig_script, TxOutput
from .paymentrequest import PR_PAID, PR_UNPAID, PR_UNKNOWN, PR_EXPIRED
Expand Down Expand Up @@ -303,12 +304,12 @@ async def make_seed(self, nbits=132, language=None, seed_type=None):
return s

@command('n')
async def getaddresshistory(self, address):
async def getaddresshistory(self, address, stream_id=None):
"""Return the transaction history of any address. Note: This is a
walletless server query, results are not checked by SPV.
"""
sh = bitcoin.address_to_scripthash(address)
return await self.network.get_history_for_scripthash(sh)
return await self.network.get_history_for_scripthash(sh, stream_id=stream_id)

@command('w')
async def listunspent(self, wallet: Abstract_Wallet = None):
Expand Down Expand Up @@ -372,12 +373,12 @@ async def name_list(self, identifier=None):
return result

@command('n')
async def getaddressunspent(self, address):
async def getaddressunspent(self, address, stream_id=None):
"""Returns the UTXO list of any address. Note: This
is a walletless server query, results are not checked by SPV.
"""
sh = bitcoin.address_to_scripthash(address)
return await self.network.listunspent_for_scripthash(sh)
return await self.network.listunspent_for_scripthash(sh, stream_id=stream_id)

@command('')
async def serialize(self, jsontx):
Expand Down Expand Up @@ -428,10 +429,10 @@ async def deserialize(self, tx):
return tx.deserialize(force_full_parse=True)

@command('n')
async def broadcast(self, tx):
async def broadcast(self, tx, stream_id=None):
"""Broadcast a transaction to the network. """
tx = Transaction(tx)
await self.network.broadcast_transaction(tx)
await self.network.broadcast_transaction(tx, stream_id=stream_id)
return tx.txid()

@command('')
Expand Down Expand Up @@ -497,21 +498,21 @@ async def getbalance(self, wallet: Abstract_Wallet = None):
return out

@command('n')
async def getaddressbalance(self, address):
async def getaddressbalance(self, address, stream_id=None):
"""Return the balance of any address. Note: This is a walletless
server query, results are not checked by SPV.
"""
sh = bitcoin.address_to_scripthash(address)
out = await self.network.get_balance_for_scripthash(sh)
out = await self.network.get_balance_for_scripthash(sh, stream_id=stream_id)
out["confirmed"] = str(Decimal(out["confirmed"])/COIN)
out["unconfirmed"] = str(Decimal(out["unconfirmed"])/COIN)
return out

@command('n')
async def getmerkle(self, txid, height):
async def getmerkle(self, txid, height, stream_id=None):
"""Get Merkle branch of a transaction included in a block. Electrum
uses this to verify transactions (Simple Payment Verification)."""
return await self.network.get_merkle_for_transaction(txid, int(height))
return await self.network.get_merkle_for_transaction(txid, int(height), stream_id=stream_id)

@command('n')
async def getservers(self):
Expand Down Expand Up @@ -689,12 +690,12 @@ async def paytomany(self, outputs, fee=None, feerate=None, from_addr=None, from_
return tx.as_dict()

@command('wp')
async def name_new(self, identifier, destination=None, amount=0.0, fee=None, from_addr=None, change_addr=None, nocheck=False, unsigned=False, rbf=None, password=None, locktime=None, allow_existing=False):
async def name_new(self, identifier, destination=None, amount=0.0, fee=None, from_addr=None, change_addr=None, nocheck=False, unsigned=False, rbf=None, password=None, locktime=None, allow_existing=False, stream_id=None):
"""Create a name_new transaction. """
if not allow_existing:
name_exists = True
try:
show = self.name_show(identifier)
show = self.name_show(identifier, stream_id=stream_id)
except NameNotFoundError:
name_exists = False
if name_exists:
Expand Down Expand Up @@ -772,7 +773,7 @@ async def name_update(self, identifier, value=None, destination=None, amount=0.0
return tx.as_dict()

@command('wpn')
async def name_autoregister(self, identifier, value, destination=None, amount=0.0, fee=None, from_addr=None, change_addr=None, nocheck=False, rbf=None, password=None, locktime=None, allow_existing=False):
async def name_autoregister(self, identifier, value, destination=None, amount=0.0, fee=None, from_addr=None, change_addr=None, nocheck=False, rbf=None, password=None, locktime=None, allow_existing=False, stream_id=None):
"""Creates a name_new transaction, broadcasts it, creates a corresponding name_firstupdate transaction, and queues it. """

# Validate the value before we try to pre-register the name. That way,
Expand All @@ -781,12 +782,12 @@ async def name_autoregister(self, identifier, value, destination=None, amount=0.
validate_value_length(value)

# TODO: Don't hardcode the 0.005 name_firstupdate fee
new_result = self.name_new(identifier, amount=amount+0.005, fee=fee, from_addr=from_addr, change_addr=change_addr, nocheck=nocheck, rbf=rbf, password=password, locktime=locktime, allow_existing=allow_existing)
new_result = self.name_new(identifier, amount=amount+0.005, fee=fee, from_addr=from_addr, change_addr=change_addr, nocheck=nocheck, rbf=rbf, password=password, locktime=locktime, allow_existing=allow_existing, stream_id=stream_id)
new_txid = new_result["txid"]
new_rand = new_result["rand"]
new_tx = new_result["tx"]["hex"]

self.broadcast(new_tx)
self.broadcast(new_tx, stream_id=stream_id)

# We add the name_new transaction to the wallet explicitly because
# otherwise, the wallet will only learn about the name_new once the
Expand Down Expand Up @@ -882,13 +883,13 @@ async def listaddresses(self, receiving=False, change=False, labels=False, froze
return out

@command('n')
async def gettransaction(self, txid, wallet: Abstract_Wallet = None):
async def gettransaction(self, txid, stream_id=None, wallet: Abstract_Wallet = None):
"""Retrieve a transaction. """
tx = None
if wallet:
tx = wallet.db.get_transaction(txid)
if tx is None:
raw = await self.network.get_transaction(txid)
raw = await self.network.get_transaction(txid, stream_id=stream_id)
if raw:
tx = Transaction(raw)
else:
Expand Down Expand Up @@ -1043,7 +1044,9 @@ async def updatequeuedtransactions(self):
if trigger_name is not None:
# TODO: handle non-ASCII trigger_name
try:
current_height = self.name_show(trigger_name)["height"]
# TODO: Store a stream ID in the queue, so that we can be
# more intelligent than using the txid.
current_height = self.name_show(trigger_name, stream_id="txid: " + txid)["height"]
current_depth = chain_height - current_height + 1
except NameNotFoundError:
current_depth = 36000
Expand All @@ -1056,7 +1059,9 @@ async def updatequeuedtransactions(self):
if current_depth >= trigger_depth:
tx = queue_item["tx"]
try:
self.broadcast(tx)
# TODO: Store a stream ID in the queue, so that we can be
# more intelligent than using the txid.
self.broadcast(tx, stream_id="txid: " + txid)
except Exception as e:
errors[txid] = str(e)

Expand Down Expand Up @@ -1128,12 +1133,45 @@ async def getfeerate(self, fee_method=None, fee_level=None):
return self.config.fee_per_kb(dyn=dyn, mempool=mempool, fee_level=fee_level)

@command('n')
async def name_show(self, identifier):
async def name_show(self, identifier, options=None, stream_id=None):
# Handle Namecoin-Core-style options
if options is not None:
if "streamID" in options:
if stream_id is None:
stream_id = options["streamID"]
else:
raise Exception("stream_id specified in both Electrum-NMC and Namecoin Core style")

if stream_id is None:
stream_id = ""

error_not_found = None
error_request_failed = None

# Try multiple times (with a different Tor circuit and different
# server) if the server claims that the name doesn't exist. This
# improves resilience against censorship attacks.
for i in range(3):
try:
return self.name_show_single_try(identifier, stream_id="Electrum-NMC name_show attempt "+str(i)+": "+stream_id)
except NameNotFoundError as e:
if error_not_found is None:
error_not_found = e
except BestEffortRequestFailed as e:
if error_request_failed is None:
error_request_failed = e

if error_not_found is not None:
raise error_not_found
if error_request_failed is not None:
raise error_request_failed

def name_show_single_try(self, identifier, stream_id=None):
# TODO: support non-ASCII encodings
identifier_bytes = identifier.encode("ascii")
sh = name_identifier_to_scripthash(identifier_bytes)

txs = self.network.run_from_another_thread(self.network.get_history_for_scripthash(sh))
txs = self.network.run_from_another_thread(self.network.get_history_for_scripthash(sh, stream_id=stream_id))

# Pick the most recent name op that's [12, 36000) confirmations.
chain_height = self.network.blockchain().height()
Expand All @@ -1152,39 +1190,8 @@ async def name_show(self, identifier):

# The height is now verified to be safe.

# (from verifier._request_proofs) if it's in the checkpoint region, we still might not have the header
header = self.network.blockchain().read_header(height)
if header is None:
if height < constants.net.max_checkpoint():
self.network.run_from_another_thread(self.network.request_chunk(height, None))

# (from verifier._request_and_verify_single_proof)
merkle = self.network.run_from_another_thread(self.network.get_merkle_for_transaction(txid, height))
if height != merkle.get('block_height'):
raise Exception('requested height {} differs from received height {} for txid {}'
.format(height, merkle.get('block_height'), txid))
pos = merkle.get('pos')
merkle_branch = merkle.get('merkle')
async def wait_for_header():
# we need to wait if header sync/reorg is still ongoing, hence lock:
async with self.network.bhi_lock:
return self.network.blockchain().read_header(height)
header = self.network.run_from_another_thread(wait_for_header())
verify_tx_is_in_block(txid, merkle_branch, pos, header, height)

# The txid is now verified to come from a safe height in the blockchain.

if self.wallet and txid in self.wallet.db.transactions:
tx = self.wallet.db.transactions[txid]
else:
raw = self.network.run_from_another_thread(self.network.get_transaction(txid))
if raw:
tx = Transaction(raw)
else:
raise Exception("Unknown transaction")

if tx.txid() != txid:
raise Exception("txid mismatch")
raw = self.gettransaction(txid, verify=True, height=height, stream_id=stream_id)['hex']
tx = Transaction(raw)

# the tx is now verified to come from a safe height in the blockchain

Expand Down Expand Up @@ -1390,6 +1397,7 @@ def eval_bool(x: str) -> bool:
'fee_level': (None, "Float between 0.0 and 1.0, representing fee slider position"),
'from_height': (None, "Only show transactions that confirmed after given block height"),
'to_height': (None, "Only show transactions that confirmed before given block height"),
'stream_id': (None, "Stream-isolate the network connection using this stream ID (only used with Tor)"),
'destination': (None, "Namecoin address, contact or alias"),
'amount': (None, "Amount to be sent (in NMC). Type \'!\' to send the maximum available."),
'allow_existing': (None, "Allow pre-registering a name that already is registered. Your registration fee will be forfeited until you can register the name after it expires."),
Expand All @@ -1398,6 +1406,7 @@ def eval_bool(x: str) -> bool:
'value': (None, "The value to assign to the name"),
'trigger_txid':(None, "Broadcast the transaction when this txid reaches the specified number of confirmations"),
'trigger_name':(None, "Broadcast the transaction when this name reaches the specified number of confirmations"),
'options': (None, "Options in Namecoin-Core-style dict"),
}


Expand Down
25 changes: 25 additions & 0 deletions electrum_nmc/electrum/interface.py
Original file line number Diff line number Diff line change
Expand Up @@ -709,6 +709,31 @@ def do_bucket():
return self._ipaddr_bucket


class InterfaceSecondary(Interface):
"""An Interface that doesn't try to fetch blocks, and instead stays idle
until it's explicitly used for something."""

async def ping(self):
# Since InterfaceSecondary doesn't ping periodically once it becomes
# dirty, it will time out if the user stops using it. That's good,
# since otherwise we'd accumulate a giant pile of secondary interfaces
# for stream ID's that aren't in use anymore.
while True:
await asyncio.sleep(300)
if self not in self.network.interfaces_clean.values():
break
await self.session.send_request('server.ping')

async def run_fetch_blocks(self):
if self.ready.cancelled():
raise GracefulDisconnect('conn establishment was too slow; *ready* future was cancelled')
if self.ready.done():
return

# Without this, the Interface will think the connection timed out.
self.ready.set_result(1)


def _assert_header_does_not_check_against_any_chain(header: dict) -> None:
chain_bad = blockchain.check_header(header) if 'mock' not in header else header['mock']['check'](header)
if chain_bad:
Expand Down
Loading